Skip to content

Commit 667a2c8

Browse files
authored
chore: better errors for missing positional args (#68)
1 parent be57cb9 commit 667a2c8

3 files changed

Lines changed: 162 additions & 1 deletion

File tree

cmd/args.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var requiredArgRe = regexp.MustCompile(`<(\w+)>`)
12+
13+
func wrapArgsWithNames(cmd *cobra.Command) {
14+
if cmd.Args != nil {
15+
v := cmd.Args
16+
cmd.Args = func(cmd *cobra.Command, args []string) error {
17+
err := v(cmd, args)
18+
if err == nil {
19+
return nil
20+
}
21+
required := requiredArgNames(cmd.Use)
22+
if len(args) < len(required) {
23+
missing := required[len(args):]
24+
return fmt.Errorf(`required argument(s) "%s" not set`, strings.Join(missing, `", "`))
25+
}
26+
return err
27+
}
28+
}
29+
for _, c := range cmd.Commands() {
30+
wrapArgsWithNames(c)
31+
}
32+
}
33+
34+
func requiredArgNames(use string) []string {
35+
matches := requiredArgRe.FindAllStringSubmatch(use, -1)
36+
names := make([]string, len(matches))
37+
for i, m := range matches {
38+
names[i] = m[1]
39+
}
40+
return names
41+
}

cmd/args_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package cmd
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func TestWrapArgsWithNames_LeavesNilArgsAlone(t *testing.T) {
11+
root := &cobra.Command{Use: "root"}
12+
bare := &cobra.Command{Use: "bare"}
13+
root.AddCommand(bare)
14+
15+
wrapArgsWithNames(root)
16+
17+
if bare.Args != nil {
18+
t.Fatal("bare command should not be wrapped")
19+
}
20+
}
21+
22+
func TestWrapArgsWithNames_PassesThroughOnSuccess(t *testing.T) {
23+
cmd := &cobra.Command{Use: "login <name>", Args: cobra.ExactArgs(1)}
24+
wrapArgsWithNames(cmd)
25+
26+
if err := cmd.Args(cmd, []string{"x"}); err != nil {
27+
t.Fatalf("expected nil, got %v", err)
28+
}
29+
}
30+
31+
func TestWrapArgsWithNames_NamesMissingArg(t *testing.T) {
32+
cmd := &cobra.Command{Use: "login <name>", Args: cobra.ExactArgs(1)}
33+
wrapArgsWithNames(cmd)
34+
35+
err := cmd.Args(cmd, nil)
36+
if err == nil {
37+
t.Fatal("expected error")
38+
}
39+
if got, want := err.Error(), `required argument(s) "name" not set`; got != want {
40+
t.Fatalf("error = %q, want %q", got, want)
41+
}
42+
}
43+
44+
func TestWrapArgsWithNames_NamesMultipleMissingArgs(t *testing.T) {
45+
cmd := &cobra.Command{Use: "thing <a> <b>", Args: cobra.ExactArgs(2)}
46+
wrapArgsWithNames(cmd)
47+
48+
err := cmd.Args(cmd, nil)
49+
if err == nil {
50+
t.Fatal("expected error")
51+
}
52+
if got, want := err.Error(), `required argument(s) "a", "b" not set`; got != want {
53+
t.Fatalf("error = %q, want %q", got, want)
54+
}
55+
}
56+
57+
func TestWrapArgsWithNames_NamesOnlyTrailingMissingArgs(t *testing.T) {
58+
cmd := &cobra.Command{Use: "thing <a> <b>", Args: cobra.ExactArgs(2)}
59+
wrapArgsWithNames(cmd)
60+
61+
err := cmd.Args(cmd, []string{"first"})
62+
if err == nil {
63+
t.Fatal("expected error")
64+
}
65+
if got, want := err.Error(), `required argument(s) "b" not set`; got != want {
66+
t.Fatalf("error = %q, want %q", got, want)
67+
}
68+
}
69+
70+
func TestWrapArgsWithNames_FallsBackOnTooManyArgs(t *testing.T) {
71+
cmd := &cobra.Command{Use: "login <name>", Args: cobra.ExactArgs(1)}
72+
wrapArgsWithNames(cmd)
73+
74+
err := cmd.Args(cmd, []string{"a", "b"})
75+
if err == nil {
76+
t.Fatal("expected error")
77+
}
78+
if got, want := err.Error(), "accepts 1 arg(s), received 2"; got != want {
79+
t.Fatalf("error = %q, want %q", got, want)
80+
}
81+
}
82+
83+
func TestWrapArgsWithNames_RecursesIntoChildren(t *testing.T) {
84+
root := &cobra.Command{Use: "root"}
85+
parent := &cobra.Command{Use: "parent"}
86+
grandchild := &cobra.Command{Use: "grandchild <event>", Args: cobra.ExactArgs(1)}
87+
parent.AddCommand(grandchild)
88+
root.AddCommand(parent)
89+
90+
wrapArgsWithNames(root)
91+
92+
err := grandchild.Args(grandchild, nil)
93+
if err == nil {
94+
t.Fatal("expected error")
95+
}
96+
if got, want := err.Error(), `required argument(s) "event" not set`; got != want {
97+
t.Fatalf("error = %q, want %q", got, want)
98+
}
99+
}
100+
101+
func TestRequiredArgNames(t *testing.T) {
102+
cases := []struct {
103+
use string
104+
want []string
105+
}{
106+
{"login <name>", []string{"name"}},
107+
{"thing <a> <b>", []string{"a", "b"}},
108+
{"use [name]", nil},
109+
{"mixed <id> [extra]", []string{"id"}},
110+
{"bare", nil},
111+
}
112+
for _, tc := range cases {
113+
got := requiredArgNames(tc.use)
114+
if !reflect.DeepEqual(got, tc.want) && !(len(got) == 0 && len(tc.want) == 0) {
115+
t.Errorf("requiredArgNames(%q) = %v, want %v", tc.use, got, tc.want)
116+
}
117+
}
118+
}

cmd/root.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
"time"
1010

1111
"charm.land/fang/v2"
12-
"github.com/loops-so/loops-go"
1312
"github.com/loops-so/cli/internal/config"
13+
"github.com/loops-so/loops-go"
1414
"github.com/spf13/cobra"
1515
)
1616

@@ -69,6 +69,8 @@ func Execute() {
6969
}
7070
}()
7171

72+
wrapArgsWithNames(rootCmd)
73+
7274
err := fang.Execute(
7375
context.Background(),
7476
rootCmd,

0 commit comments

Comments
 (0)