diff --git a/acmd.go b/acmd.go index 3b22f0a..9a0fbd5 100644 --- a/acmd.go +++ b/acmd.go @@ -37,8 +37,17 @@ type Command struct { Description string // Do will be invoked. + // Deprecated: use ExecFunc or Exec. Do func(ctx context.Context, args []string) error + // ExecFunc represents the command function. + // Use Exec if you have struct implementing this function. + ExecFunc func(ctx context.Context, args []string) error + + // Exec represents the command function. + // Will be used only if ExecFunc is nil. + Exec Exec + // Subcommands of the command. Subcommands []Command @@ -46,6 +55,23 @@ type Command struct { IsHidden bool } +// simple way to get exec function +func (cmd *Command) getExec() func(ctx context.Context, args []string) error { + switch { + case cmd.ExecFunc != nil: + return cmd.ExecFunc + case cmd.Exec != nil: + return cmd.Exec.ExecCommand + default: + return nil + } +} + +// Exec represents a command to run. +type Exec interface { + ExecCommand(ctx context.Context, args []string) error +} + // Config for the runner. type Config struct { // AppName is an optional name for the app, if empty os.Args[0] will be used. @@ -158,7 +184,7 @@ func (r *Runner) init() error { Command{ Name: "help", Description: "shows help message", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { r.cfg.Usage(r.cfg, r.cmds) return nil }, @@ -166,7 +192,7 @@ func (r *Runner) init() error { Command{ Name: "version", Description: "shows version of the application", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { fmt.Fprintf(r.cfg.Output, "%s version: %s\n\n", r.cfg.AppName, r.cfg.Version) return nil }, @@ -183,11 +209,11 @@ func validateCommand(cmd Command) error { cmds := cmd.Subcommands switch { - case cmd.Do == nil && len(cmds) == 0: - return fmt.Errorf("command %q function cannot be nil or must have subcommands", cmd.Name) + case cmd.getExec() == nil && len(cmds) == 0: + return fmt.Errorf("command %q exec function cannot be nil OR must have subcommands", cmd.Name) - case cmd.Do != nil && len(cmds) != 0: - return fmt.Errorf("command %q function cannot be set and have subcommands", cmd.Name) + case cmd.getExec() != nil && len(cmds) != 0: + return fmt.Errorf("command %q exec function cannot be set AND have subcommands", cmd.Name) case cmd.Name == "help" || cmd.Name == "version": return fmt.Errorf("command %q is reserved", cmd.Name) @@ -271,7 +297,7 @@ func findCmd(cfg Config, cmds []Command, args []string) (func(ctx context.Contex } // go deeper into subcommands - if c.Do == nil { + if c.getExec() == nil { if len(params) == 0 { return nil, nil, errors.New("no args for command provided") } @@ -279,7 +305,7 @@ func findCmd(cfg Config, cmds []Command, args []string) (func(ctx context.Contex found = true break } - return c.Do, params, nil + return c.getExec(), params, nil } if !found { diff --git a/acmd_test.go b/acmd_test.go index a0be70c..a55fd3b 100644 --- a/acmd_test.go +++ b/acmd_test.go @@ -31,7 +31,7 @@ func TestRunner(t *testing.T) { Name: "foo", Subcommands: []Command{ { - Name: "for", Do: func(ctx context.Context, args []string) error { + Name: "for", ExecFunc: func(ctx context.Context, args []string) error { fmt.Fprint(buf, "for") return nil }, @@ -40,7 +40,7 @@ func TestRunner(t *testing.T) { }, { Name: "bar", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { fmt.Fprint(buf, "bar") return nil }, @@ -50,7 +50,7 @@ func TestRunner(t *testing.T) { { Name: "status", Description: "status command gives status of the state", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { return nil }, }, @@ -70,7 +70,7 @@ func TestRunner(t *testing.T) { func TestRunnerMustSetDefaults(t *testing.T) { app := "./someapp" args := append([]string{app, "runner"}, os.Args[1:]...) - cmds := []Command{{Name: "foo", Do: nopFunc}} + cmds := []Command{{Name: "foo", ExecFunc: nopFunc}} r := RunnerOf(cmds, Config{ Args: args, Output: io.Discard, @@ -105,7 +105,7 @@ func TestRunnerMustSetDefaults(t *testing.T) { } func TestRunnerWithoutArgs(t *testing.T) { - cmds := []Command{{Name: "foo", Do: nopFunc}} + cmds := []Command{{Name: "foo", ExecFunc: nopFunc}} r := RunnerOf(cmds, Config{ Args: []string{"./app"}, Output: io.Discard, @@ -119,15 +119,15 @@ func TestRunnerWithoutArgs(t *testing.T) { func TestRunnerMustSortCommands(t *testing.T) { cmds := []Command{ - {Name: "foo", Do: nopFunc}, + {Name: "foo", ExecFunc: nopFunc}, {Name: "xyz"}, - {Name: "cake", Do: nopFunc}, - {Name: "foo2", Do: nopFunc}, + {Name: "cake", ExecFunc: nopFunc}, + {Name: "foo2", ExecFunc: nopFunc}, } cmds[1].Subcommands = []Command{ - {Name: "a", Do: nopFunc}, - {Name: "c", Do: nopFunc}, - {Name: "b", Do: nopFunc}, + {Name: "a", ExecFunc: nopFunc}, + {Name: "c", ExecFunc: nopFunc}, + {Name: "b", ExecFunc: nopFunc}, } r := RunnerOf(cmds, Config{ @@ -164,7 +164,7 @@ func TestRunnerJustExit(t *testing.T) { doExitOld, doExit = doExit, doExitOld buf := &bytes.Buffer{} - r := RunnerOf([]Command{{Name: "foo", Do: nopFunc}}, Config{ + r := RunnerOf([]Command{{Name: "foo", ExecFunc: nopFunc}}, Config{ AppName: "exit-test", Output: buf, }) @@ -189,74 +189,74 @@ func TestRunnerInit(t *testing.T) { wantErrStr string }{ { - cmds: []Command{{Name: "app:cre.ate", Do: nopFunc}}, + cmds: []Command{{Name: "app:cre.ate", ExecFunc: nopFunc}}, wantErrStr: ``, }, { - cmds: []Command{{Name: "", Do: nopFunc}}, + cmds: []Command{{Name: "", ExecFunc: nopFunc}}, wantErrStr: `command "" must contains only letters, digits, - and _`, }, { - cmds: []Command{{Name: "foo%", Do: nopFunc}}, + cmds: []Command{{Name: "foo%", ExecFunc: nopFunc}}, wantErrStr: `command "foo%" must contains only letters, digits, - and _`, }, { - cmds: []Command{{Name: "foo", Alias: "%", Do: nopFunc}}, + cmds: []Command{{Name: "foo", Alias: "%", ExecFunc: nopFunc}}, wantErrStr: `command alias "%" must contains only letters, digits, - and _`, }, { - cmds: []Command{{Name: "foo%", Do: nil}}, - wantErrStr: `command "foo%" function cannot be nil`, + cmds: []Command{{Name: "foo%", ExecFunc: nil}}, + wantErrStr: `command "foo%" exec function cannot be nil OR must have subcommands`, }, { - cmds: []Command{{Name: "foo", Do: nil}}, - wantErrStr: `command "foo" function cannot be nil or must have subcommands`, + cmds: []Command{{Name: "foo", ExecFunc: nil}}, + wantErrStr: `command "foo" exec function cannot be nil OR must have subcommands`, }, { cmds: []Command{{ Name: "foobar", - Do: nopFunc, + ExecFunc: nopFunc, Subcommands: []Command{{Name: "nested"}}, }}, - wantErrStr: `command "foobar" function cannot be set and have subcommands`, + wantErrStr: `command "foobar" exec function cannot be set AND have subcommands`, }, { - cmds: []Command{{Name: "foo", Do: nopFunc}}, + cmds: []Command{{Name: "foo", ExecFunc: nopFunc}}, cfg: Config{ Args: []string{}, }, wantErrStr: `no args provided`, }, { - cmds: []Command{{Name: "help", Do: nopFunc}}, + cmds: []Command{{Name: "help", ExecFunc: nopFunc}}, wantErrStr: `command "help" is reserved`, }, { - cmds: []Command{{Name: "version", Do: nopFunc}}, + cmds: []Command{{Name: "version", ExecFunc: nopFunc}}, wantErrStr: `command "version" is reserved`, }, { - cmds: []Command{{Name: "foo", Alias: "help", Do: nopFunc}}, + cmds: []Command{{Name: "foo", Alias: "help", ExecFunc: nopFunc}}, wantErrStr: `command alias "help" is reserved`, }, { - cmds: []Command{{Name: "foo", Alias: "version", Do: nopFunc}}, + cmds: []Command{{Name: "foo", Alias: "version", ExecFunc: nopFunc}}, wantErrStr: `command alias "version" is reserved`, }, { - cmds: []Command{{Name: "a", Do: nopFunc}, {Name: "a", Do: nopFunc}}, + cmds: []Command{{Name: "a", ExecFunc: nopFunc}, {Name: "a", ExecFunc: nopFunc}}, wantErrStr: `duplicate command "a"`, }, { - cmds: []Command{{Name: "aaa", Do: nopFunc}, {Name: "b", Alias: "aaa", Do: nopFunc}}, + cmds: []Command{{Name: "aaa", ExecFunc: nopFunc}, {Name: "b", Alias: "aaa", ExecFunc: nopFunc}}, wantErrStr: `duplicate command alias "aaa"`, }, { - cmds: []Command{{Name: "aaa", Alias: "a", Do: nopFunc}, {Name: "bbb", Alias: "a", Do: nopFunc}}, + cmds: []Command{{Name: "aaa", Alias: "a", ExecFunc: nopFunc}, {Name: "bbb", Alias: "a", ExecFunc: nopFunc}}, wantErrStr: `duplicate command alias "a"`, }, { - cmds: []Command{{Name: "a", Do: nopFunc}, {Name: "b", Alias: "a", Do: nopFunc}}, + cmds: []Command{{Name: "a", ExecFunc: nopFunc}, {Name: "b", Alias: "a", ExecFunc: nopFunc}}, wantErrStr: `duplicate command alias "a"`, }, } @@ -264,9 +264,10 @@ func TestRunnerInit(t *testing.T) { for _, tc := range testCases { tc.cfg.Output = io.Discard err := RunnerOf(tc.cmds, tc.cfg).Run() + failIfOk(t, err) if got := err.Error(); tc.wantErrStr != "" && !strings.Contains(got, tc.wantErrStr) { - t.Fatalf("want %q got %q", tc.wantErrStr, got) + t.Fatalf("\nhave: %+v\nwant: %+v\n", tc.wantErrStr, got) } } } @@ -279,25 +280,25 @@ func TestRunner_suggestCommand(t *testing.T) { }{ { cmds: []Command{ - {Name: "for", Do: nopFunc}, - {Name: "foo", Do: nopFunc}, - {Name: "bar", Do: nopFunc}, + {Name: "for", ExecFunc: nopFunc}, + {Name: "foo", ExecFunc: nopFunc}, + {Name: "bar", ExecFunc: nopFunc}, }, args: []string{"./someapp", "fooo"}, want: `"fooo" unknown command, did you mean "foo"?` + "\n" + `Run "myapp help" for usage.` + "\n\n", }, { - cmds: []Command{{Name: "for", Do: nopFunc}}, + cmds: []Command{{Name: "for", ExecFunc: nopFunc}}, args: []string{"./someapp", "hell"}, want: `"hell" unknown command, did you mean "help"?` + "\n" + `Run "myapp help" for usage.` + "\n\n", }, { - cmds: []Command{{Name: "for", Do: nopFunc}}, + cmds: []Command{{Name: "for", ExecFunc: nopFunc}}, args: []string{"./someapp", "verZION"}, want: `"verZION" unknown command` + "\n" + `Run "myapp help" for usage.` + "\n\n", }, { - cmds: []Command{{Name: "for", Do: nopFunc}}, + cmds: []Command{{Name: "for", ExecFunc: nopFunc}}, args: []string{"./someapp", "verZion"}, want: `"verZion" unknown command, did you mean "version"?` + "\n" + `Run "myapp help" for usage.` + "\n\n", }, @@ -337,9 +338,9 @@ func TestHasHelpFlag(t *testing.T) { func TestCommand_IsHidden(t *testing.T) { buf := &bytes.Buffer{} cmds := []Command{ - {Name: "for", Do: nopFunc}, - {Name: "foo", Do: nopFunc, IsHidden: true}, - {Name: "bar", Do: nopFunc}, + {Name: "for", ExecFunc: nopFunc}, + {Name: "foo", ExecFunc: nopFunc, IsHidden: true}, + {Name: "bar", ExecFunc: nopFunc}, } r := RunnerOf(cmds, Config{ Args: []string{"./someapp", "help"}, @@ -360,7 +361,7 @@ func TestExit(t *testing.T) { cmds := []Command{ { Name: "for", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { return ErrCode(wantStatus) }, }, @@ -404,9 +405,9 @@ func failIfErr(t testing.TB, err error) { } } -func mustEqual(t testing.TB, got, want interface{}) { +func mustEqual(t testing.TB, have, want interface{}) { t.Helper() - if !reflect.DeepEqual(got, want) { - t.Fatalf("\nhave %+v\nwant %+v", got, want) + if !reflect.DeepEqual(have, want) { + t.Fatalf("\nhave: %+v\nwant: %+v\n", have, want) } } diff --git a/example_test.go b/example_test.go index 0917277..1ddf044 100644 --- a/example_test.go +++ b/example_test.go @@ -3,8 +3,10 @@ package acmd_test import ( "bytes" "context" + "errors" "flag" "fmt" + "io" "net/http" "os" "time" @@ -28,7 +30,7 @@ func ExampleRunner() { { Name: "now", Description: "prints current time", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { fs := flag.NewFlagSet("some name for help", flag.ContinueOnError) times := fs.Int("times", 1, "how many times to print time") if err := fs.Parse(args); err != nil { @@ -44,7 +46,7 @@ func ExampleRunner() { { Name: "status", Description: "prints status of the system", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.githubstatus.com/", http.NoBody) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -85,16 +87,16 @@ func ExampleHelp() { { Name: "now", Description: "prints current time", - Do: nopFunc, + ExecFunc: nopFunc, }, { Name: "status", Description: "prints status of the system", - Do: nopFunc, + ExecFunc: nopFunc, }, { - Name: "boom", - Do: nopFunc, + Name: "boom", + ExecFunc: nopFunc, }, } @@ -135,8 +137,8 @@ func ExampleVersion() { testArgs := []string{"someapp", "version"} cmds := []acmd.Command{ - {Name: "foo", Do: nopFunc}, - {Name: "bar", Do: nopFunc}, + {Name: "foo", ExecFunc: nopFunc}, + {Name: "bar", ExecFunc: nopFunc}, } r := acmd.RunnerOf(cmds, acmd.Config{ @@ -163,7 +165,7 @@ func ExampleAlias() { { Name: "foo", Alias: "f", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { fmt.Fprint(testOut, "foo") return nil }, @@ -171,7 +173,7 @@ func ExampleAlias() { { Name: "bar", Alias: "b", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { fmt.Fprint(testOut, "bar") return nil }, @@ -199,8 +201,8 @@ func ExampleAutosuggestion() { testArgs := []string{"someapp", "baz"} cmds := []acmd.Command{ - {Name: "foo", Do: nopFunc}, - {Name: "bar", Do: nopFunc}, + {Name: "foo", ExecFunc: nopFunc}, + {Name: "bar", ExecFunc: nopFunc}, } r := acmd.RunnerOf(cmds, acmd.Config{ @@ -229,18 +231,18 @@ func ExampleNestedCommands() { { Name: "foo", Subcommands: []acmd.Command{ - {Name: "bar", Do: nopFunc}, - {Name: "baz", Do: nopFunc}, + {Name: "bar", ExecFunc: nopFunc}, + {Name: "baz", ExecFunc: nopFunc}, { Name: "qux", - Do: func(ctx context.Context, args []string) error { + ExecFunc: func(ctx context.Context, args []string) error { fmt.Fprint(testOut, "qux") return nil }, }, }, }, - {Name: "boom", Do: nopFunc}, + {Name: "boom", ExecFunc: nopFunc}, } r := acmd.RunnerOf(cmds, acmd.Config{ @@ -258,6 +260,46 @@ func ExampleNestedCommands() { // Output: qux } +type myCommand struct { + ErrToReturn error +} + +func (mc *myCommand) ExecCommand(ctx context.Context, args []string) error { + return mc.ErrToReturn +} + +func ExampleExecStruct() { + myErr := errors.New("everything is ok") + myCmd := &myCommand{ErrToReturn: myErr} + + cmds := []acmd.Command{ + { + Name: "what", + Description: "does something", + + // ExecFunc: myCmd.ExecCommand, + // NOTE: line below is literally line above + Exec: myCmd, + }, + } + + r := acmd.RunnerOf(cmds, acmd.Config{ + AppName: "acmd-example", + AppDescription: "Example of acmd package", + PostDescription: "Best place to add examples", + Output: io.Discard, + Args: []string{"someapp", "what"}, + Usage: nopUsage, + }) + + err := r.Run() + if !errors.Is(err, myErr) { + panic(fmt.Sprintf("\ngot : %+v\nwant: %+v\n", err, myErr)) + } + + // Output: +} + func ExamplePropagateFlags() { testOut := os.Stdout testArgs := []string{"someapp", "foo", "-dir=test-dir", "--verbose"} @@ -265,7 +307,7 @@ func ExamplePropagateFlags() { cmds := []acmd.Command{ { - Name: "foo", Do: func(ctx context.Context, args []string) error { + Name: "foo", ExecFunc: func(ctx context.Context, args []string) error { fs := flag.NewFlagSet("foo", flag.ContinueOnError) isRecursive := fs.Bool("r", false, "should file list be recursive") common := withCommonFlags(fs) @@ -279,7 +321,7 @@ func ExamplePropagateFlags() { }, }, { - Name: "bar", Do: func(ctx context.Context, args []string) error { + Name: "bar", ExecFunc: func(ctx context.Context, args []string) error { fs := flag.NewFlagSet("bar", flag.ContinueOnError) common := withCommonFlags(fs) if err := fs.Parse(args); err != nil {