Skip to content

Commit e7f9e74

Browse files
authored
feat: improve missing argument error messages (#711)
This PR adds a custom Cobra command validator that outputs useful errors when positional arguments are missing during command execution. For example, such an error could look like this: ``` $ hcloud server describe hcloud server describe [options] <server> ^^^^^^ hcloud: expected argument server at position 1 ``` Where previously it would look like this: ``` $ hcloud server describe hcloud: accepts 1 arg(s), received 0 ``` Additionally, if there are more arguments provided than necessary, the following error message will appear: ``` $ hcloud server describe arg1 arg2 hcloud server describe [options] <server> ^ hcloud: expected exactly 1 positional argument(s), but got 2 ``` This is done by parsing the `Use` property of a Cobra command with a regular expression and then matching the corresponding missing arguments. Fixes #700 --------- Co-authored-by: pauhull <[email protected]>
1 parent 373287b commit e7f9e74

File tree

114 files changed

+270
-120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

114 files changed

+270
-120
lines changed

CONTRIBUTING.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,30 @@ Additional Commands:
139139
ssh Spawn an SSH connection for the server
140140
```
141141

142+
### Command validation
143+
144+
Please use the `util.Validate` method to make sure the command is validated against its usage string during runtime.
145+
This can be done by setting the `Args` field of the `cobra.Command` struct to `util.Validate`:
146+
147+
```go
148+
cmd := &cobra.Command{
149+
Use: "my-command [options]",
150+
Args: util.Validate,
151+
Run: func(cmd *cobra.Command, args []string) {
152+
// ...
153+
},
154+
}
155+
```
156+
157+
If you are using base commands like `base.Cmd` or `base.ListCmd` etc., this is already done for you.
158+
159+
**Note:** Your command usage needs to follow the [docopt](http://docopt.org/) syntax for this to work.
160+
If your command uses optional positional arguments or other complicated usage that necessitates to disable
161+
argument count checking, you use `util.ValidateLenient` instead. It will not throw an error if there are
162+
more arguments than expected.
163+
142164
### Generated files
143165

144-
Generated files (that are created by running `go generate`) should be prefixed with `zz_`. This is to group them
166+
Generated files (that are created by running `go generate`) should be prefixed with `zz_`. This is to group them
145167
together in the file list and to easily identify them as generated files. Also, it allows the CI to check if the
146168
generated files are up-to-date.

internal/cmd/all/all.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ package all
33
import (
44
"github.com/spf13/cobra"
55

6+
"github.com/hetznercloud/cli/internal/cmd/util"
67
"github.com/hetznercloud/cli/internal/state"
78
)
89

910
func NewCommand(s state.State) *cobra.Command {
1011
cmd := &cobra.Command{
1112
Use: "all",
1213
Short: "Commands that apply to all resources",
13-
Args: cobra.NoArgs,
14+
Args: util.Validate,
1415
TraverseChildren: true,
1516
DisableFlagsInUseLine: true,
1617
}

internal/cmd/base/cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func (gc *Cmd) CobraCommand(s state.State) *cobra.Command {
1919
cmd := gc.BaseCobraCommand(s.Client())
2020

2121
if cmd.Args == nil {
22-
cmd.Args = cobra.NoArgs
22+
cmd.Args = util.Validate
2323
}
2424

2525
cmd.TraverseChildren = true

internal/cmd/base/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (cc *CreateCmd) CobraCommand(s state.State) *cobra.Command {
2727
output.AddFlag(cmd, output.OptionJSON(), output.OptionYAML())
2828

2929
if cmd.Args == nil {
30-
cmd.Args = cobra.NoArgs
30+
cmd.Args = util.Validate
3131
}
3232

3333
cmd.TraverseChildren = true

internal/cmd/base/delete.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package base
33
import (
44
"fmt"
55
"reflect"
6-
"strings"
76

87
"github.com/spf13/cobra"
98

@@ -32,9 +31,9 @@ func (dc *DeleteCmd) CobraCommand(s state.State) *cobra.Command {
3231
}
3332

3433
cmd := &cobra.Command{
35-
Use: fmt.Sprintf("delete %s<%s>", opts, strings.ToLower(dc.ResourceNameSingular)),
34+
Use: fmt.Sprintf("delete %s<%s>", opts, util.ToKebabCase(dc.ResourceNameSingular)),
3635
Short: dc.ShortDescription,
37-
Args: cobra.ExactArgs(1),
36+
Args: util.Validate,
3837
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(dc.NameSuggestions(s.Client()))),
3938
TraverseChildren: true,
4039
DisableFlagsInUseLine: true,

internal/cmd/base/describe.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"os"
66
"reflect"
7-
"strings"
87

98
"github.com/spf13/cobra"
109

@@ -33,9 +32,9 @@ type DescribeCmd struct {
3332
// CobraCommand creates a command that can be registered with cobra.
3433
func (dc *DescribeCmd) CobraCommand(s state.State) *cobra.Command {
3534
cmd := &cobra.Command{
36-
Use: fmt.Sprintf("describe [options] <%s>", strings.ToLower(dc.ResourceNameSingular)),
35+
Use: fmt.Sprintf("describe [options] <%s>", util.ToKebabCase(dc.ResourceNameSingular)),
3736
Short: dc.ShortDescription,
38-
Args: cobra.ExactArgs(1),
37+
Args: util.Validate,
3938
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(dc.NameSuggestions(s.Client()))),
4039
TraverseChildren: true,
4140
DisableFlagsInUseLine: true,

internal/cmd/base/labels.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ type LabelCmds struct {
2727
// AddCobraCommand creates a command that can be registered with cobra.
2828
func (lc *LabelCmds) AddCobraCommand(s state.State) *cobra.Command {
2929
cmd := &cobra.Command{
30-
Use: fmt.Sprintf("add-label [--overwrite] <%s> <label>...", strings.ToLower(lc.ResourceNameSingular)),
30+
Use: fmt.Sprintf("add-label [--overwrite] <%s> <label>...", util.ToKebabCase(lc.ResourceNameSingular)),
3131
Short: lc.ShortDescriptionAdd,
32-
Args: cobra.MinimumNArgs(2),
32+
Args: util.Validate,
3333
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(lc.NameSuggestions(s.Client()))),
3434
TraverseChildren: true,
3535
DisableFlagsInUseLine: true,
@@ -89,9 +89,9 @@ func validateAddLabel(_ *cobra.Command, args []string) error {
8989
// RemoveCobraCommand creates a command that can be registered with cobra.
9090
func (lc *LabelCmds) RemoveCobraCommand(s state.State) *cobra.Command {
9191
cmd := &cobra.Command{
92-
Use: fmt.Sprintf("remove-label <%s> (--all | <label>...)", strings.ToLower(lc.ResourceNameSingular)),
92+
Use: fmt.Sprintf("remove-label <%s> (--all | <label>...)", util.ToKebabCase(lc.ResourceNameSingular)),
9393
Short: lc.ShortDescriptionRemove,
94-
Args: cobra.MinimumNArgs(1),
94+
Args: util.ValidateLenient,
9595
ValidArgsFunction: cmpl.SuggestArgs(
9696
cmpl.SuggestCandidatesF(lc.NameSuggestions(s.Client())),
9797
cmpl.SuggestCandidatesCtx(func(_ *cobra.Command, args []string) []string {

internal/cmd/base/list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func (lc *ListCmd) CobraCommand(s state.State) *cobra.Command {
3535
fmt.Sprintf("Displays a list of %s.", lc.ResourceNamePlural),
3636
outputColumns,
3737
),
38+
Args: util.Validate,
3839
TraverseChildren: true,
3940
DisableFlagsInUseLine: true,
4041
PreRunE: s.EnsureToken,

internal/cmd/base/set_rdns.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"net"
66
"reflect"
7-
"strings"
87

98
"github.com/spf13/cobra"
109

@@ -28,9 +27,9 @@ type SetRdnsCmd struct {
2827
// CobraCommand creates a command that can be registered with cobra.
2928
func (rc *SetRdnsCmd) CobraCommand(s state.State) *cobra.Command {
3029
cmd := &cobra.Command{
31-
Use: fmt.Sprintf("set-rdns [options] --hostname <hostname> <%s>", strings.ToLower(rc.ResourceNameSingular)),
30+
Use: fmt.Sprintf("set-rdns [options] --hostname <hostname> <%s>", util.ToKebabCase(rc.ResourceNameSingular)),
3231
Short: rc.ShortDescription,
33-
Args: cobra.ExactArgs(1),
32+
Args: util.Validate,
3433
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(rc.NameSuggestions(s.Client()))),
3534
TraverseChildren: true,
3635
DisableFlagsInUseLine: true,

internal/cmd/base/update.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package base
33
import (
44
"fmt"
55
"reflect"
6-
"strings"
76

87
"github.com/spf13/cobra"
98
"github.com/spf13/pflag"
@@ -28,9 +27,9 @@ type UpdateCmd struct {
2827
// CobraCommand creates a command that can be registered with cobra.
2928
func (uc *UpdateCmd) CobraCommand(s state.State) *cobra.Command {
3029
cmd := &cobra.Command{
31-
Use: fmt.Sprintf("update [options] <%s>", strings.ToLower(uc.ResourceNameSingular)),
30+
Use: fmt.Sprintf("update [options] <%s>", util.ToKebabCase(uc.ResourceNameSingular)),
3231
Short: uc.ShortDescription,
33-
Args: cobra.ExactArgs(1),
32+
Args: util.Validate,
3433
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(uc.NameSuggestions(s.Client()))),
3534
TraverseChildren: true,
3635
DisableFlagsInUseLine: true,

0 commit comments

Comments
 (0)