Skip to content

Commit 58a7eb8

Browse files
committed
Add nested commands
1 parent 92ba898 commit 58a7eb8

File tree

2 files changed

+111
-24
lines changed

2 files changed

+111
-24
lines changed

acmd.go

+42-16
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ func RunnerOf(cmds []Command, cfg Config) *Runner {
7676
}
7777

7878
func (r *Runner) init() error {
79+
if len(r.cmds) == 0 {
80+
return errors.New("no cmds provided")
81+
}
7982
if r.cfg.AppName == "" {
8083
r.cfg.AppName = os.Args[0]
8184
}
@@ -105,7 +108,6 @@ func (r *Runner) init() error {
105108
cmds := r.cmds
106109
r.rootCmd = Command{
107110
Name: "root",
108-
Do: func(context.Context, []string) error { return nil },
109111
subcommands: cmds,
110112
}
111113
if err := validateCommand(r.rootCmd); err != nil {
@@ -136,6 +138,7 @@ func (r *Runner) init() error {
136138
})
137139

138140
r.rootCmd.subcommands = cmds
141+
r.rootCmd.Do = rootDo(r.cfg, cmds)
139142

140143
return nil
141144
}
@@ -145,7 +148,10 @@ func validateCommand(cmd Command) error {
145148

146149
switch {
147150
case cmd.Do == nil && len(cmds) == 0:
148-
return fmt.Errorf("command %q function cannot be nil", cmd.Name)
151+
return fmt.Errorf("command %q function cannot be nil or must have subcommands", cmd.Name)
152+
153+
case cmd.Do != nil && len(cmds) != 0:
154+
return fmt.Errorf("command %q function cannot be set and have subcommands", cmd.Name)
149155

150156
case cmd.Name == "help" || cmd.Name == "version":
151157
return fmt.Errorf("command %q is reserved", cmd.Name)
@@ -178,28 +184,48 @@ func (r *Runner) Run() error {
178184
if r.errInit != nil {
179185
return fmt.Errorf("acmd: cannot init runner: %w", r.errInit)
180186
}
181-
if err := run(r.ctx, r.cfg, r.rootCmd.subcommands, r.args); err != nil {
187+
if err := r.rootCmd.Do(r.ctx, r.args); err != nil {
182188
return fmt.Errorf("acmd: cannot run command: %w", err)
183189
}
184190
return nil
185191
}
186192

187-
func run(ctx context.Context, cfg Config, cmds []Command, args []string) error {
188-
if len(args) == 0 {
189-
return errors.New("no args provided")
190-
}
191-
selected, params := args[0], args[1:]
192-
193-
for _, c := range cmds {
194-
if c.Name == selected {
195-
return c.Do(ctx, params)
193+
func rootDo(cfg Config, cmds []Command) func(ctx context.Context, args []string) error {
194+
return func(ctx context.Context, args []string) error {
195+
if len(args) == 0 {
196+
return errors.New("no args provided")
196197
}
197-
}
198198

199-
if suggestion := suggestCommand(selected, cmds); suggestion != "" {
200-
fmt.Fprintf(cfg.Output, "%q is not a subcommand, did you mean %q?\n", selected, suggestion)
199+
cmds, args := cmds, args
200+
for {
201+
selected, params := args[0], args[1:]
202+
203+
var found bool
204+
for _, c := range cmds {
205+
if c.Name != selected {
206+
continue
207+
}
208+
209+
// go deeper into subcommands
210+
if c.Do == nil {
211+
if len(params) == 0 {
212+
return errors.New("no args for subcmd provided")
213+
}
214+
cmds, args = c.subcommands, params
215+
found = true
216+
break
217+
}
218+
return c.Do(ctx, params)
219+
}
220+
221+
if !found {
222+
if suggestion := suggestCommand(selected, cmds); suggestion != "" {
223+
fmt.Fprintf(cfg.Output, "%q is not a subcommand, did you mean %q?\n", selected, suggestion)
224+
}
225+
return fmt.Errorf("no such command %q", selected)
226+
}
227+
}
201228
}
202-
return fmt.Errorf("no such command %q", selected)
203229
}
204230

205231
// suggestCommand for not found earlier command.

acmd_test.go

+69-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package acmd
33
import (
44
"bytes"
55
"context"
6+
"fmt"
7+
"os"
8+
"strings"
69
"testing"
10+
"time"
711
)
812

913
var nopFunc = func(context.Context, []string) error { return nil }
@@ -19,8 +23,16 @@ func TestRunner_init(t *testing.T) {
1923
wantErrStr: `command "foo%" must contains only letters, digits, - and _`,
2024
},
2125
{
22-
cmds: []Command{{Name: "foo%", Do: nil}},
23-
wantErrStr: `command "foo%" function cannot be nil`,
26+
cmds: []Command{{Name: "foo", Do: nil}},
27+
wantErrStr: `command "foo" function cannot be nil or must have subcommands`,
28+
},
29+
{
30+
cmds: []Command{{
31+
Name: "foobar",
32+
Do: nopFunc,
33+
subcommands: []Command{{Name: "nested"}},
34+
}},
35+
wantErrStr: `command "foobar" function cannot be set and have subcommands`,
2436
},
2537
{
2638
cmds: []Command{{Name: "foo", Do: nopFunc}},
@@ -68,17 +80,17 @@ func TestRunner_suggestCommand(t *testing.T) {
6880
want: `"fooo" is not a subcommand, did you mean "foo"?` + "\n",
6981
},
7082
{
71-
cmds: []Command{},
83+
cmds: []Command{{Name: "for", Do: nopFunc}},
7284
args: []string{"hell"},
7385
want: `"hell" is not a subcommand, did you mean "help"?` + "\n",
7486
},
7587
{
76-
cmds: []Command{},
88+
cmds: []Command{{Name: "for", Do: nopFunc}},
7789
args: []string{"verZION"},
7890
want: "",
7991
},
8092
{
81-
cmds: []Command{},
93+
cmds: []Command{{Name: "for", Do: nopFunc}},
8294
args: []string{"verZion"},
8395
want: `"verZion" is not a subcommand, did you mean "version"?` + "\n",
8496
},
@@ -90,12 +102,61 @@ func TestRunner_suggestCommand(t *testing.T) {
90102
Args: tc.args,
91103
Output: buf,
92104
})
93-
if err := r.Run(); err == nil {
94-
t.Fatal()
105+
if err := r.Run(); err != nil && !strings.Contains(err.Error(), "no such command") {
106+
t.Fatal(err)
95107
}
96108

97109
if got := buf.String(); got != tc.want {
98-
t.Logf("want %q got %q", tc.want, got)
110+
t.Fatalf("want %q got %q", tc.want, got)
99111
}
100112
}
101113
}
114+
115+
func TestRunner(t *testing.T) {
116+
r := RunnerOf([]Command{
117+
{
118+
Name: "test",
119+
Description: "some test command",
120+
// Do: func(ctx context.Context, args []string) error {
121+
// return nil
122+
// },
123+
subcommands: []Command{
124+
{
125+
Name: "foo",
126+
// Do: func(ctx context.Context, args []string) error {
127+
// fmt.Fprint(os.Stderr, "foo")
128+
// return nil
129+
// },
130+
subcommands: []Command{
131+
{
132+
Name: "for", Do: func(ctx context.Context, args []string) error {
133+
fmt.Fprint(os.Stderr, "for")
134+
return nil
135+
},
136+
},
137+
},
138+
},
139+
{Name: "bar", Do: func(ctx context.Context, args []string) error {
140+
fmt.Fprint(os.Stderr, "bar")
141+
return nil
142+
}},
143+
},
144+
},
145+
{
146+
Name: "status",
147+
Description: "status command gives status of the state",
148+
Do: func(ctx context.Context, args []string) error {
149+
return nil
150+
},
151+
},
152+
}, Config{
153+
Args: []string{"test", "foo", "for"},
154+
AppName: "acmd_test_app",
155+
AppDescription: "acmd_test_app is a test application.",
156+
Version: time.Now().String(),
157+
})
158+
159+
if err := r.Run(); err != nil {
160+
t.Fatal(err)
161+
}
162+
}

0 commit comments

Comments
 (0)