diff --git a/.travis.yml b/.travis.yml index c2b5c8d..133722f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,10 @@ matrix: allow_failures: - go: tip +before_script: +- go get github.com/meatballhat/gfmxr/... + script: - go vet ./... - go test -v ./... +- gfmxr -c $(grep -c 'package main' README.md) -s README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 77eb86a..87a55c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,33 @@ ## [Unreleased] ### Added +- This file! - Support for placeholders in flag usage strings +- `App.Metadata` map for arbitrary data/state management +- `Set` and `GlobalSet` methods on `*cli.Context` for altering values after +parsing. + +### Changed +- The `App.Action` and `Command.Action` now prefer a return signature of +`func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil +`error` is returned, there may be two outcomes: + - If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called + automatically + - Else the error is bubbled up and returned from `App.Run` +- Specifying an `Action` with the legacy return signature of +`func(*cli.Context)` will produce a deprecation message to stderr +- Specifying an `Action` that is not a `func` type will produce a non-zero exit +from `App.Run` +- Specifying an `Action` func that has an invalid (input) signature will +produce a non-zero exit from `App.Run` + +### Deprecated +- +`cli.App.RunAndExitOnError`, which should now be done by returning an error +that fulfills `cli.ExitCoder` to `cli.App.Run`. +- the legacy signature for +`cli.App.Action` of `func(*cli.Context)`, which should now have a return +signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. ### Fixed - Added missing `*cli.Context.GlobalFloat64` method diff --git a/README.md b/README.md index 524723e..777b0a8 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,9 @@ This app will run and show help text, but is not very useful. Let's give an acti package main import ( + "fmt" "os" + "github.com/codegangsta/cli" ) @@ -58,8 +60,9 @@ func main() { app := cli.NewApp() app.Name = "boom" app.Usage = "make an explosive entrance" - app.Action = func(c *cli.Context) { - println("boom! I say!") + app.Action = func(c *cli.Context) error { + fmt.Println("boom! I say!") + return nil } app.Run(os.Args) @@ -78,7 +81,9 @@ Start by creating a directory named `greet`, and within it, add a file, `greet.g package main import ( + "fmt" "os" + "github.com/codegangsta/cli" ) @@ -86,8 +91,9 @@ func main() { app := cli.NewApp() app.Name = "greet" app.Usage = "fight the loneliness!" - app.Action = func(c *cli.Context) { - println("Hello friend!") + app.Action = func(c *cli.Context) error { + fmt.Println("Hello friend!") + return nil } app.Run(os.Args) @@ -133,8 +139,9 @@ You can lookup arguments by calling the `Args` function on `cli.Context`. ``` go ... -app.Action = func(c *cli.Context) { - println("Hello", c.Args()[0]) +app.Action = func(c *cli.Context) error { + fmt.Println("Hello", c.Args()[0]) + return nil } ... ``` @@ -152,16 +159,17 @@ app.Flags = []cli.Flag { Usage: "language for the greeting", }, } -app.Action = func(c *cli.Context) { +app.Action = func(c *cli.Context) error { name := "someone" if c.NArg() > 0 { name = c.Args()[0] } if c.String("lang") == "spanish" { - println("Hola", name) + fmt.Println("Hola", name) } else { - println("Hello", name) + fmt.Println("Hello", name) } + return nil } ... ``` @@ -179,16 +187,17 @@ app.Flags = []cli.Flag { Destination: &language, }, } -app.Action = func(c *cli.Context) { +app.Action = func(c *cli.Context) error { name := "someone" if c.NArg() > 0 { name = c.Args()[0] } if language == "spanish" { - println("Hola", name) + fmt.Println("Hola", name) } else { - println("Hello", name) + fmt.Println("Hello", name) } + return nil } ... ``` @@ -214,7 +223,7 @@ Will result in help output like: --config FILE, -c FILE Load configuration from FILE ``` -Note that only the first placeholder is used. Subsequent back-quoted words will be left as-is. +Note that only the first placeholder is used. Subsequent back-quoted words will be left as-is. #### Alternate Names @@ -276,8 +285,8 @@ Initialization must also occur for these flags. Below is an example initializing command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) ``` -The code above will use the "load" string as a flag name to get the file name of a yaml file from the cli.Context. -It will then use that file name to initialize the yaml input source for any flags that are defined on that command. +The code above will use the "load" string as a flag name to get the file name of a yaml file from the cli.Context. +It will then use that file name to initialize the yaml input source for any flags that are defined on that command. As a note the "load" flag used would also have to be defined on the command flags in order for this code snipped to work. Currently only YAML files are supported but developers can add support for other input sources by implementing the @@ -286,20 +295,20 @@ altsrc.InputSourceContext for their given sources. Here is a more complete sample of a command using YAML support: ``` go - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) { - // Action to run - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) error { + // Action to run + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) + err := command.Run(c) ``` ### Subcommands @@ -314,7 +323,7 @@ app.Commands = []cli.Command{ Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(c *cli.Context) { - println("added task: ", c.Args().First()) + fmt.Println("added task: ", c.Args().First()) }, }, { @@ -322,7 +331,7 @@ app.Commands = []cli.Command{ Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) { - println("completed task: ", c.Args().First()) + fmt.Println("completed task: ", c.Args().First()) }, }, { @@ -334,14 +343,14 @@ app.Commands = []cli.Command{ Name: "add", Usage: "add a new template", Action: func(c *cli.Context) { - println("new task template: ", c.Args().First()) + fmt.Println("new task template: ", c.Args().First()) }, }, { Name: "remove", Usage: "remove an existing template", Action: func(c *cli.Context) { - println("removed task template: ", c.Args().First()) + fmt.Println("removed task template: ", c.Args().First()) }, }, }, @@ -360,19 +369,19 @@ E.g. ```go ... - app.Commands = []cli.Command{ - { - Name: "noop", - }, - { - Name: "add", - Category: "template", - }, - { - Name: "remove", - Category: "template", - }, - } + app.Commands = []cli.Command{ + { + Name: "noop", + }, + { + Name: "add", + Category: "template", + }, + { + Name: "remove", + Category: "template", + }, + } ... ``` @@ -389,6 +398,41 @@ COMMANDS: ... ``` +### Exit code + +Calling `App.Run` will not automatically call `os.Exit`, which means that by +default the exit code will "fall through" to being `0`. An explicit exit code +may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a +`cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: + +``` go +package main + +import ( + "os" + + "github.com/codegangsta/cli" +) + +func main() { + app := cli.NewApp() + app.Flags = []cli.Flag{ + cli.BoolTFlag{ + Name: "ginger-crouton", + Usage: "is it in the soup?", + }, + } + app.Action = func(ctx *cli.Context) error { + if !ctx.Bool("ginger-crouton") { + return cli.NewExitError("it is not in the soup", 86) + } + return nil + } + + app.Run(os.Args) +} +``` + ### Bash Completion You can enable completion commands by setting the `EnableBashCompletion` @@ -407,7 +451,7 @@ app.Commands = []cli.Command{ Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) { - println("completed task: ", c.Args().First()) + fmt.Println("completed task: ", c.Args().First()) }, BashComplete: func(c *cli.Context) { // This will complete if no args are passed diff --git a/altsrc/flag.go b/altsrc/flag.go index 2869905..d934e6a 100644 --- a/altsrc/flag.go +++ b/altsrc/flag.go @@ -38,7 +38,7 @@ func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSource // InitInputSource is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new // input source based on the func provided. If there is no error it will then apply the new input source to any flags // that are supported by the input source -func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) func(context *cli.Context) error { +func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc { return func(context *cli.Context) error { inputSource, err := createInputSource() if err != nil { @@ -52,7 +52,7 @@ func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceCont // InitInputSourceWithContext is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new // input source based on the func provided with potentially using existing cli.Context values to initialize itself. If there is // no error it will then apply the new input source to any flags that are supported by the input source -func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(context *cli.Context) (InputSourceContext, error)) func(context *cli.Context) error { +func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(context *cli.Context) (InputSourceContext, error)) cli.BeforeFunc { return func(context *cli.Context) error { inputSource, err := createInputSource(context) if err != nil { diff --git a/altsrc/yaml_command_test.go b/altsrc/yaml_command_test.go index 275bc64..39c36f6 100644 --- a/altsrc/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -29,9 +29,10 @@ func TestCommandYamlFileTest(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 15) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test"}), @@ -61,9 +62,10 @@ func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 10) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "THE_TEST"}), @@ -92,9 +94,10 @@ func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 7) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test"}), @@ -123,9 +126,10 @@ func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 15) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test", Value: 7}), @@ -157,9 +161,10 @@ func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 11) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test", Value: 7, EnvVar: "THE_TEST"}), diff --git a/app.go b/app.go index bd20a2d..6917fbc 100644 --- a/app.go +++ b/app.go @@ -6,10 +6,24 @@ import ( "io/ioutil" "os" "path/filepath" + "reflect" "sort" "time" ) +var ( + appActionDeprecationURL = "https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-action-signature" + + contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." + + errNonFuncAction = NewExitError("ERROR invalid Action type. "+ + fmt.Sprintf("Must be a func of type `cli.ActionFunc`. %s", contactSysadmin)+ + fmt.Sprintf("See %s", appActionDeprecationURL), 2) + errInvalidActionSignature = NewExitError("ERROR invalid Action signature. "+ + fmt.Sprintf("Must be `cli.ActionFunc`. %s", contactSysadmin)+ + fmt.Sprintf("See %s", appActionDeprecationURL), 2) +) + // App is the main structure of a cli application. It is recommended that // an app be created with the cli.NewApp() function type App struct { @@ -38,21 +52,22 @@ type App struct { // Populate on app startup, only gettable throught method Categories() categories CommandCategories // An action to execute when the bash-completion flag is set - BashComplete func(context *Context) + BashComplete BashCompleteFunc // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run - Before func(context *Context) error + Before BeforeFunc // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics - After func(context *Context) error + After AfterFunc // The action to execute when no subcommands are specified - Action func(context *Context) + Action interface{} + // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind + // of deprecation period has passed, maybe? + // Execute this function if the proper command cannot be found - CommandNotFound func(context *Context, command string) - // Execute this function, if an usage error occurs. This is useful for displaying customized usage error messages. - // This function is able to replace the original error messages. - // If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. - OnUsageError func(context *Context, err error, isSubcommand bool) error + CommandNotFound CommandNotFoundFunc + // Execute this function if an usage error occurs + OnUsageError OnUsageErrorFunc // Compilation date Compiled time.Time // List of all authors who contributed @@ -65,6 +80,8 @@ type App struct { Email string // Writer writer to write output to Writer io.Writer + // Other custom info + Metadata map[string]interface{} } // Tries to find out when this binary was compiled. @@ -149,6 +166,9 @@ func (a *App) Run(arguments []string) (err error) { if err != nil { if a.OnUsageError != nil { err := a.OnUsageError(context, err, false) + if err != nil { + HandleExitCoder(err) + } return err } else { fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") @@ -180,10 +200,12 @@ func (a *App) Run(arguments []string) (err error) { } if a.Before != nil { - err = a.Before(context) - if err != nil { - fmt.Fprintf(a.Writer, "%v\n\n", err) + beforeErr := a.Before(context) + if beforeErr != nil { + fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) ShowAppHelp(context) + HandleExitCoder(beforeErr) + err = beforeErr return err } } @@ -198,12 +220,19 @@ func (a *App) Run(arguments []string) (err error) { } // Run default Action - a.Action(context) - return nil + err = HandleAction(a.Action, context) + + if err != nil { + HandleExitCoder(err) + } + return err } -// Another entry point to the cli app, takes care of passing arguments and error handling +// DEPRECATED: Another entry point to the cli app, takes care of passing arguments and error handling func (a *App) RunAndExitOnError() { + fmt.Fprintln(os.Stderr, + "DEPRECATED cli.App.RunAndExitOnError. "+ + "See https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-runandexitonerror") if err := a.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -261,6 +290,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { if err != nil { if a.OnUsageError != nil { err = a.OnUsageError(context, err, true) + HandleExitCoder(err) return err } else { fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") @@ -283,6 +313,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { defer func() { afterErr := a.After(context) if afterErr != nil { + HandleExitCoder(err) if err != nil { err = NewMultiError(err, afterErr) } else { @@ -293,8 +324,10 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } if a.Before != nil { - err := a.Before(context) - if err != nil { + beforeErr := a.Before(context) + if beforeErr != nil { + HandleExitCoder(beforeErr) + err = beforeErr return err } } @@ -309,9 +342,12 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } // Run default Action - a.Action(context) + err = HandleAction(a.Action, context) - return nil + if err != nil { + HandleExitCoder(err) + } + return err } // Returns the named command on App. Returns nil if the command does not exist @@ -361,3 +397,42 @@ func (a Author) String() string { return fmt.Sprintf("%v %v", a.Name, e) } + +// HandleAction uses ✧✧✧reflection✧✧✧ to figure out if the given Action is an +// ActionFunc, a func with the legacy signature for Action, or some other +// invalid thing. If it's an ActionFunc or a func with the legacy signature for +// Action, the func is run! +func HandleAction(action interface{}, context *Context) (err error) { + defer func() { + if r := recover(); r != nil { + switch r.(type) { + case error: + err = r.(error) + default: + err = NewExitError(fmt.Sprintf("ERROR unknown Action error: %v. See %s", r, appActionDeprecationURL), 2) + } + } + }() + + if reflect.TypeOf(action).Kind() != reflect.Func { + return errNonFuncAction + } + + vals := reflect.ValueOf(action).Call([]reflect.Value{reflect.ValueOf(context)}) + + if len(vals) == 0 { + fmt.Fprintln(os.Stderr, + "DEPRECATED Action signature. Must be `cli.ActionFunc`") + return nil + } + + if len(vals) > 1 { + return errInvalidActionSignature + } + + if retErr, ok := reflect.ValueOf(vals[0]).Interface().(error); ok { + return retErr + } + + return err +} diff --git a/app_test.go b/app_test.go index 5b0ce2a..ca4abce 100644 --- a/app_test.go +++ b/app_test.go @@ -26,8 +26,9 @@ func ExampleApp_Run() { app.Flags = []Flag{ StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, } - app.Action = func(c *Context) { + app.Action = func(c *Context) error { fmt.Printf("Hello %v\n", c.String("name")) + return nil } app.UsageText = "app [first_arg] [second_arg]" app.Author = "Harrison" @@ -62,8 +63,9 @@ func ExampleApp_Run_subcommand() { Usage: "Name of the person to greet", }, }, - Action: func(c *Context) { + Action: func(c *Context) error { fmt.Println("Hello,", c.String("name")) + return nil }, }, }, @@ -90,8 +92,9 @@ func ExampleApp_Run_help() { Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", - Action: func(c *Context) { + Action: func(c *Context) error { fmt.Printf("i like to describe things") + return nil }, }, } @@ -120,15 +123,17 @@ func ExampleApp_Run_bashComplete() { Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", - Action: func(c *Context) { + Action: func(c *Context) error { fmt.Printf("i like to describe things") + return nil }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", - Action: func(c *Context) { + Action: func(c *Context) error { fmt.Printf("the next example") + return nil }, }, } @@ -146,8 +151,9 @@ func TestApp_Run(t *testing.T) { s := "" app := NewApp() - app.Action = func(c *Context) { + app.Action = func(c *Context) error { s = s + c.Args().First() + return nil } err := app.Run([]string{"command", "foo"}) @@ -192,9 +198,10 @@ func TestApp_CommandWithArgBeforeFlags(t *testing.T) { Flags: []Flag{ StringFlag{Name: "option", Value: "", Usage: "some option"}, }, - Action: func(c *Context) { + Action: func(c *Context) error { parsedOption = c.String("option") firstArg = c.Args().First() + return nil }, } app.Commands = []Command{command} @@ -212,8 +219,9 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) { a.Commands = []Command{ { Name: "foo", - Action: func(c *Context) { + Action: func(c *Context) error { context = c + return nil }, Flags: []Flag{ StringFlag{ @@ -241,9 +249,10 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { Flags: []Flag{ StringFlag{Name: "option", Value: "", Usage: "some option"}, }, - Action: func(c *Context) { + Action: func(c *Context) error { parsedOption = c.String("option") args = c.Args() + return nil }, } app.Commands = []Command{command} @@ -262,8 +271,9 @@ func TestApp_CommandWithDash(t *testing.T) { app := NewApp() command := Command{ Name: "cmd", - Action: func(c *Context) { + Action: func(c *Context) error { args = c.Args() + return nil }, } app.Commands = []Command{command} @@ -280,8 +290,9 @@ func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { app := NewApp() command := Command{ Name: "cmd", - Action: func(c *Context) { + Action: func(c *Context) error { args = c.Args() + return nil }, } app.Commands = []Command{command} @@ -300,8 +311,9 @@ func TestApp_Float64Flag(t *testing.T) { app.Flags = []Flag{ Float64Flag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"}, } - app.Action = func(c *Context) { + app.Action = func(c *Context) error { meters = c.Float64("height") + return nil } app.Run([]string{"", "--height", "1.93"}) @@ -320,11 +332,12 @@ func TestApp_ParseSliceFlags(t *testing.T) { IntSliceFlag{Name: "p", Value: NewIntSlice(), Usage: "set one or more ip addr"}, StringSliceFlag{Name: "ip", Value: NewStringSlice(), Usage: "set one or more ports to open"}, }, - Action: func(c *Context) { + Action: func(c *Context) error { parsedIntSlice = c.IntSlice("p") parsedStringSlice = c.StringSlice("ip") parsedOption = c.String("option") firstArg = c.Args().First() + return nil }, } app.Commands = []Command{command} @@ -377,9 +390,10 @@ func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) { IntSliceFlag{Name: "a", Usage: "set numbers"}, StringSliceFlag{Name: "str", Usage: "set strings"}, }, - Action: func(c *Context) { + Action: func(c *Context) error { parsedIntSlice = c.IntSlice("a") parsedStringSlice = c.StringSlice("str") + return nil }, } app.Commands = []Command{command} @@ -463,9 +477,10 @@ func TestApp_BeforeFunc(t *testing.T) { app.Commands = []Command{ Command{ Name: "sub", - Action: func(c *Context) { + Action: func(c *Context) error { counts.Total++ counts.SubCommand = counts.Total + return nil }, }, } @@ -508,6 +523,29 @@ func TestApp_BeforeFunc(t *testing.T) { t.Errorf("Subcommand executed when NOT expected") } + // reset + counts = &opCounts{} + + afterError := errors.New("fail again") + app.After = func(_ *Context) error { + return afterError + } + + // run with the Before() func failing, wrapped by After() + err = app.Run([]string{"command", "--opt", "fail", "sub"}) + + // should be the same error produced by the Before func + if _, ok := err.(MultiError); !ok { + t.Errorf("MultiError expected, but not received") + } + + if counts.Before != 1 { + t.Errorf("Before() not executed when expected") + } + + if counts.SubCommand != 0 { + t.Errorf("Subcommand executed when NOT expected") + } } func TestApp_AfterFunc(t *testing.T) { @@ -531,9 +569,10 @@ func TestApp_AfterFunc(t *testing.T) { app.Commands = []Command{ Command{ Name: "sub", - Action: func(c *Context) { + Action: func(c *Context) error { counts.Total++ counts.SubCommand = counts.Total + return nil }, }, } @@ -645,9 +684,10 @@ func TestApp_CommandNotFound(t *testing.T) { app.Commands = []Command{ Command{ Name: "bar", - Action: func(c *Context) { + Action: func(c *Context) error { counts.Total++ counts.SubCommand = counts.Total + return nil }, }, } @@ -670,6 +710,7 @@ func TestApp_OrderOfOperations(t *testing.T) { counts.Total++ counts.BashComplete = counts.Total } + app.OnUsageError = func(c *Context, err error, isSubcommand bool) error { counts.Total++ counts.OnUsageError = counts.Total @@ -710,16 +751,18 @@ func TestApp_OrderOfOperations(t *testing.T) { app.Commands = []Command{ Command{ Name: "bar", - Action: func(c *Context) { + Action: func(c *Context) error { counts.Total++ counts.SubCommand = counts.Total + return nil }, }, } - app.Action = func(c *Context) { + app.Action = func(c *Context) error { counts.Total++ counts.Action = counts.Total + return nil } _ = app.Run([]string{"command", "--nope"}) @@ -986,8 +1029,9 @@ func TestApp_Run_Help(t *testing.T) { app.Name = "boom" app.Usage = "make an explosive entrance" app.Writer = buf - app.Action = func(c *Context) { + app.Action = func(c *Context) error { buf.WriteString("boom I say!") + return nil } err := app.Run(args) @@ -1017,8 +1061,9 @@ func TestApp_Run_Version(t *testing.T) { app.Usage = "make an explosive entrance" app.Version = "0.1.0" app.Writer = buf - app.Action = func(c *Context) { + app.Action = func(c *Context) error { buf.WriteString("boom I say!") + return nil } err := app.Run(args) @@ -1086,7 +1131,7 @@ func TestApp_Run_Categories(t *testing.T) { func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() - app.Action = func(c *Context) {} + app.Action = func(c *Context) error { return nil } app.Before = func(c *Context) error { return fmt.Errorf("before error") } app.After = func(c *Context) error { return fmt.Errorf("after error") } @@ -1190,3 +1235,75 @@ func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) { t.Errorf("Expect an intercepted error, but got \"%v\"", err) } } + +func TestHandleAction_WithNonFuncAction(t *testing.T) { + app := NewApp() + app.Action = 42 + err := HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil)) + + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + exitErr, ok := err.(*ExitError) + + if !ok { + t.Fatalf("expected to receive a *ExitError") + } + + if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type") { + t.Fatalf("expected an unknown Action error, but got: %v", exitErr.Error()) + } + + if exitErr.ExitCode() != 2 { + t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) + } +} + +func TestHandleAction_WithInvalidFuncSignature(t *testing.T) { + app := NewApp() + app.Action = func() string { return "" } + err := HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil)) + + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + exitErr, ok := err.(*ExitError) + + if !ok { + t.Fatalf("expected to receive a *ExitError") + } + + if !strings.HasPrefix(exitErr.Error(), "ERROR unknown Action error") { + t.Fatalf("expected an unknown Action error, but got: %v", exitErr.Error()) + } + + if exitErr.ExitCode() != 2 { + t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) + } +} + +func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) { + app := NewApp() + app.Action = func(_ *Context) (int, error) { return 0, nil } + err := HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil)) + + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + exitErr, ok := err.(*ExitError) + + if !ok { + t.Fatalf("expected to receive a *ExitError") + } + + if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action signature") { + t.Fatalf("expected an invalid Action signature error, but got: %v", exitErr.Error()) + } + + if exitErr.ExitCode() != 2 { + t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) + } +} diff --git a/cli.go b/cli.go index 31dc912..b742545 100644 --- a/cli.go +++ b/cli.go @@ -17,24 +17,3 @@ // app.Run(os.Args) // } package cli - -import ( - "strings" -) - -type MultiError struct { - Errors []error -} - -func NewMultiError(err ...error) MultiError { - return MultiError{Errors: err} -} - -func (m MultiError) Error() string { - errs := make([]string, len(m.Errors)) - for i, err := range m.Errors { - errs[i] = err.Error() - } - - return strings.Join(errs, "\n") -} diff --git a/command.go b/command.go index 1a05b54..7f30932 100644 --- a/command.go +++ b/command.go @@ -26,19 +26,20 @@ type Command struct { // The category the command is part of Category string // The function to call when checking for bash command completions - BashComplete func(context *Context) + BashComplete BashCompleteFunc // An action to execute before any sub-subcommands are run, but after the context is ready // If a non-nil error is returned, no sub-subcommands are run - Before func(context *Context) error - // An action to execute after any subcommands are run, but before the subcommand has finished + Before BeforeFunc + // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics - After func(context *Context) error + After AfterFunc // The function to call when this command is invoked - Action func(context *Context) - // Execute this function, if an usage error occurs. This is useful for displaying customized usage error messages. - // This function is able to replace the original error messages. - // If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. - OnUsageError func(context *Context, err error) error + Action interface{} + // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind + // of deprecation period has passed, maybe? + + // Execute this function if a usage error occurs. + OnUsageError OnUsageErrorFunc // List of child commands Subcommands Commands // List of flags to parse @@ -125,7 +126,8 @@ func (c Command) Run(ctx *Context) (err error) { if err != nil { if c.OnUsageError != nil { - err := c.OnUsageError(ctx, err) + err := c.OnUsageError(ctx, err, false) + HandleExitCoder(err) return err } else { fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") @@ -142,6 +144,7 @@ func (c Command) Run(ctx *Context) (err error) { ShowCommandHelp(ctx, c.Name) return nerr } + context := NewContext(ctx.App, set, ctx) if checkCommandCompletions(context, c.Name) { @@ -156,6 +159,7 @@ func (c Command) Run(ctx *Context) (err error) { defer func() { afterErr := c.After(context) if afterErr != nil { + HandleExitCoder(err) if err != nil { err = NewMultiError(err, afterErr) } else { @@ -166,18 +170,23 @@ func (c Command) Run(ctx *Context) (err error) { } if c.Before != nil { - err := c.Before(context) + err = c.Before(context) if err != nil { fmt.Fprintln(ctx.App.Writer, err) fmt.Fprintln(ctx.App.Writer) ShowCommandHelp(ctx, c.Name) + HandleExitCoder(err) return err } } context.Command = c - c.Action(context) - return nil + err = HandleAction(c.Action, context) + + if err != nil { + HandleExitCoder(err) + } + return err } func (c Command) Names() []string { @@ -202,7 +211,7 @@ func (c Command) HasName(name string) bool { func (c Command) startApp(ctx *Context) error { app := NewApp() - + app.Metadata = ctx.App.Metadata // set the name and usage app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) if c.HelpName == "" { diff --git a/command_test.go b/command_test.go index 827da1d..2687212 100644 --- a/command_test.go +++ b/command_test.go @@ -34,7 +34,7 @@ func TestCommandFlagParsing(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(_ *Context) {}, + Action: func(_ *Context) error { return nil }, } command.SkipFlagParsing = c.skipFlagParsing @@ -50,9 +50,13 @@ func TestCommand_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() app.Commands = []Command{ Command{ - Name: "bar", - Before: func(c *Context) error { return fmt.Errorf("before error") }, - After: func(c *Context) error { return fmt.Errorf("after error") }, + Name: "bar", + Before: func(c *Context) error { + return fmt.Errorf("before error") + }, + After: func(c *Context) error { + return fmt.Errorf("after error") + }, }, } @@ -73,11 +77,11 @@ func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { app := NewApp() app.Commands = []Command{ Command{ - Name: "bar", + Name: "bar", Flags: []Flag{ IntFlag{Name: "flag"}, }, - OnUsageError: func(c *Context, err error) error { + OnUsageError: func(c *Context, err error, _ bool) error { if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { t.Errorf("Expect an invalid value error, but got \"%v\"", err) } diff --git a/context.go b/context.go index a4c3904..94cd085 100644 --- a/context.go +++ b/context.go @@ -141,6 +141,16 @@ func (c *Context) NumFlags() int { return c.flagSet.NFlag() } +// Set sets a context flag to a value. +func (c *Context) Set(name, value string) error { + return c.flagSet.Set(name, value) +} + +// GlobalSet sets a context flag to a value on the global flagset +func (c *Context) GlobalSet(name, value string) error { + return globalContext(c).flagSet.Set(name, value) +} + // Determines if the flag was actually set func (c *Context) IsSet(name string) bool { if c.setFlags == nil { @@ -247,6 +257,19 @@ func (a Args) Swap(from, to int) error { return nil } +func globalContext(ctx *Context) *Context { + if ctx == nil { + return nil + } + + for { + if ctx.parentContext == nil { + return ctx + } + ctx = ctx.parentContext + } +} + func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet { if ctx.parentContext != nil { ctx = ctx.parentContext diff --git a/context_test.go b/context_test.go index 20647b8..4c23271 100644 --- a/context_test.go +++ b/context_test.go @@ -154,9 +154,10 @@ func TestContext_GlobalFlag(t *testing.T) { app.Flags = []Flag{ StringFlag{Name: "global, g", Usage: "global"}, } - app.Action = func(c *Context) { + app.Action = func(c *Context) error { globalFlag = c.GlobalString("global") globalFlagSet = c.GlobalIsSet("global") + return nil } app.Run([]string{"command", "-g", "foo"}) expect(t, globalFlag, "foo") @@ -182,13 +183,14 @@ func TestContext_GlobalFlagsInSubcommands(t *testing.T) { Subcommands: []Command{ { Name: "bar", - Action: func(c *Context) { + Action: func(c *Context) error { if c.GlobalBool("debug") { subcommandRun = true } if c.GlobalBool("parent") { parentFlag = true } + return nil }, }, }, @@ -200,3 +202,31 @@ func TestContext_GlobalFlagsInSubcommands(t *testing.T) { expect(t, subcommandRun, true) expect(t, parentFlag, true) } + +func TestContext_Set(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Int("int", 5, "an int") + c := NewContext(nil, set, nil) + + c.Set("int", "1") + expect(t, c.Int("int"), 1) +} + +func TestContext_GlobalSet(t *testing.T) { + gSet := flag.NewFlagSet("test", 0) + gSet.Int("int", 5, "an int") + + set := flag.NewFlagSet("sub", 0) + set.Int("int", 3, "an int") + + pc := NewContext(nil, gSet, nil) + c := NewContext(nil, set, pc) + + c.Set("int", "1") + expect(t, c.Int("int"), 1) + expect(t, c.GlobalInt("int"), 5) + + c.GlobalSet("int", "1") + expect(t, c.Int("int"), 1) + expect(t, c.GlobalInt("int"), 1) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..1a6a8c7 --- /dev/null +++ b/errors.go @@ -0,0 +1,75 @@ +package cli + +import ( + "fmt" + "os" + "strings" +) + +type MultiError struct { + Errors []error +} + +func NewMultiError(err ...error) MultiError { + return MultiError{Errors: err} +} + +func (m MultiError) Error() string { + errs := make([]string, len(m.Errors)) + for i, err := range m.Errors { + errs[i] = err.Error() + } + + return strings.Join(errs, "\n") +} + +// ExitCoder is the interface checked by `App` and `Command` for a custom exit +// code +type ExitCoder interface { + ExitCode() int +} + +// ExitError fulfills both the builtin `error` interface and `ExitCoder` +type ExitError struct { + exitCode int + message string +} + +// NewExitError makes a new *ExitError +func NewExitError(message string, exitCode int) *ExitError { + return &ExitError{ + exitCode: exitCode, + message: message, + } +} + +// Error returns the string message, fulfilling the interface required by +// `error` +func (ee *ExitError) Error() string { + return ee.message +} + +// ExitCode returns the exit code, fulfilling the interface required by +// `ExitCoder` +func (ee *ExitError) ExitCode() int { + return ee.exitCode +} + +// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if +// so prints the error to stderr (if it is non-empty) and calls os.Exit with the +// given exit code. If the given error is a MultiError, then this func is +// called on all members of the Errors slice. +func HandleExitCoder(err error) { + if exitErr, ok := err.(ExitCoder); ok { + if err.Error() != "" { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(exitErr.ExitCode()) + } + + if multiErr, ok := err.(MultiError); ok { + for _, merr := range multiErr.Errors { + HandleExitCoder(merr) + } + } +} diff --git a/flag_test.go b/flag_test.go index 843f175..79c0ae0 100644 --- a/flag_test.go +++ b/flag_test.go @@ -304,13 +304,14 @@ func TestParseMultiString(t *testing.T) { Flags: []Flag{ StringFlag{Name: "serve, s"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.String("serve") != "10" { t.Errorf("main name not set") } if ctx.String("s") != "10" { t.Errorf("short name not set") } + return nil }, }).Run([]string{"run", "-s", "10"}) } @@ -324,10 +325,11 @@ func TestParseDestinationString(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if dest != "10" { t.Errorf("expected destination String 10") } + return nil }, } a.Run([]string{"run", "--dest", "10"}) @@ -340,13 +342,14 @@ func TestParseMultiStringFromEnv(t *testing.T) { Flags: []Flag{ StringFlag{Name: "count, c", EnvVar: "APP_COUNT"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.String("count") != "20" { t.Errorf("main name not set") } if ctx.String("c") != "20" { t.Errorf("short name not set") } + return nil }, }).Run([]string{"run"}) } @@ -358,13 +361,14 @@ func TestParseMultiStringFromEnvCascade(t *testing.T) { Flags: []Flag{ StringFlag{Name: "count, c", EnvVar: "COMPAT_COUNT,APP_COUNT"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.String("count") != "20" { t.Errorf("main name not set") } if ctx.String("c") != "20" { t.Errorf("short name not set") } + return nil }, }).Run([]string{"run"}) } @@ -374,7 +378,7 @@ func TestParseMultiStringSlice(t *testing.T) { Flags: []Flag{ StringSliceFlag{Name: "serve, s", Value: NewStringSlice()}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { expected := []string{"10", "20"} if !reflect.DeepEqual(ctx.StringSlice("serve"), expected) { t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) @@ -382,6 +386,7 @@ func TestParseMultiStringSlice(t *testing.T) { if !reflect.DeepEqual(ctx.StringSlice("s"), expected) { t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) } + return nil }, }).Run([]string{"run", "-s", "10", "-s", "20"}) } @@ -446,13 +451,14 @@ func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) { Flags: []Flag{ StringSliceFlag{Name: "intervals, i", Value: NewStringSlice("1", "2", "5"), EnvVar: "APP_INTERVALS"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { t.Errorf("short name not set from env") } + return nil }, }).Run([]string{"run"}) } @@ -484,13 +490,14 @@ func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) { Flags: []Flag{ StringSliceFlag{Name: "intervals, i", Value: NewStringSlice("1", "2", "5"), EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { t.Errorf("short name not set from env") } + return nil }, }).Run([]string{"run"}) } @@ -500,13 +507,14 @@ func TestParseMultiInt(t *testing.T) { Flags: []Flag{ IntFlag{Name: "serve, s"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Int("serve") != 10 { t.Errorf("main name not set") } if ctx.Int("s") != 10 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "-s", "10"}) @@ -521,10 +529,11 @@ func TestParseDestinationInt(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if dest != 10 { t.Errorf("expected destination Int 10") } + return nil }, } a.Run([]string{"run", "--dest", "10"}) @@ -537,13 +546,14 @@ func TestParseMultiIntFromEnv(t *testing.T) { Flags: []Flag{ IntFlag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Int("timeout") != 10 { t.Errorf("main name not set") } if ctx.Int("t") != 10 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run"}) @@ -556,13 +566,14 @@ func TestParseMultiIntFromEnvCascade(t *testing.T) { Flags: []Flag{ IntFlag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Int("timeout") != 10 { t.Errorf("main name not set") } if ctx.Int("t") != 10 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run"}) @@ -573,13 +584,14 @@ func TestParseMultiIntSlice(t *testing.T) { Flags: []Flag{ IntSliceFlag{Name: "serve, s", Value: NewIntSlice()}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.IntSlice("serve"), []int{10, 20}) { t.Errorf("main name not set") } if !reflect.DeepEqual(ctx.IntSlice("s"), []int{10, 20}) { t.Errorf("short name not set") } + return nil }, }).Run([]string{"run", "-s", "10", "-s", "20"}) } @@ -643,13 +655,14 @@ func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) { Flags: []Flag{ IntSliceFlag{Name: "intervals, i", Value: NewIntSlice(1, 2, 5), EnvVar: "APP_INTERVALS"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) { t.Errorf("short name not set from env") } + return nil }, }).Run([]string{"run"}) } @@ -662,13 +675,14 @@ func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { Flags: []Flag{ IntSliceFlag{Name: "intervals, i", Value: NewIntSlice(), EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) { t.Errorf("short name not set from env") } + return nil }, }).Run([]string{"run"}) } @@ -678,13 +692,14 @@ func TestParseMultiFloat64(t *testing.T) { Flags: []Flag{ Float64Flag{Name: "serve, s"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Float64("serve") != 10.2 { t.Errorf("main name not set") } if ctx.Float64("s") != 10.2 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "-s", "10.2"}) @@ -699,10 +714,11 @@ func TestParseDestinationFloat64(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if dest != 10.2 { t.Errorf("expected destination Float64 10.2") } + return nil }, } a.Run([]string{"run", "--dest", "10.2"}) @@ -715,13 +731,14 @@ func TestParseMultiFloat64FromEnv(t *testing.T) { Flags: []Flag{ Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Float64("timeout") != 15.5 { t.Errorf("main name not set") } if ctx.Float64("t") != 15.5 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run"}) @@ -734,13 +751,14 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) { Flags: []Flag{ Float64Flag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Float64("timeout") != 15.5 { t.Errorf("main name not set") } if ctx.Float64("t") != 15.5 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run"}) @@ -751,13 +769,14 @@ func TestParseMultiBool(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "serve, s"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Bool("serve") != true { t.Errorf("main name not set") } if ctx.Bool("s") != true { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "--serve"}) @@ -772,10 +791,11 @@ func TestParseDestinationBool(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if dest != true { t.Errorf("expected destination Bool true") } + return nil }, } a.Run([]string{"run", "--dest"}) @@ -788,13 +808,14 @@ func TestParseMultiBoolFromEnv(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Bool("debug") != true { t.Errorf("main name not set from env") } if ctx.Bool("d") != true { t.Errorf("short name not set from env") } + return nil }, } a.Run([]string{"run"}) @@ -807,13 +828,14 @@ func TestParseMultiBoolFromEnvCascade(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Bool("debug") != true { t.Errorf("main name not set from env") } if ctx.Bool("d") != true { t.Errorf("short name not set from env") } + return nil }, } a.Run([]string{"run"}) @@ -824,13 +846,14 @@ func TestParseMultiBoolT(t *testing.T) { Flags: []Flag{ BoolTFlag{Name: "serve, s"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.BoolT("serve") != true { t.Errorf("main name not set") } if ctx.BoolT("s") != true { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "--serve"}) @@ -845,10 +868,11 @@ func TestParseDestinationBoolT(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if dest != true { t.Errorf("expected destination BoolT true") } + return nil }, } a.Run([]string{"run", "--dest"}) @@ -861,13 +885,14 @@ func TestParseMultiBoolTFromEnv(t *testing.T) { Flags: []Flag{ BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.BoolT("debug") != false { t.Errorf("main name not set from env") } if ctx.BoolT("d") != false { t.Errorf("short name not set from env") } + return nil }, } a.Run([]string{"run"}) @@ -880,13 +905,14 @@ func TestParseMultiBoolTFromEnvCascade(t *testing.T) { Flags: []Flag{ BoolTFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.BoolT("debug") != false { t.Errorf("main name not set from env") } if ctx.BoolT("d") != false { t.Errorf("short name not set from env") } + return nil }, } a.Run([]string{"run"}) @@ -915,13 +941,14 @@ func TestParseGeneric(t *testing.T) { Flags: []Flag{ GenericFlag{Name: "serve, s", Value: &Parser{}}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"10", "20"}) { t.Errorf("main name not set") } if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"10", "20"}) { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "-s", "10,20"}) @@ -934,13 +961,14 @@ func TestParseGenericFromEnv(t *testing.T) { Flags: []Flag{ GenericFlag{Name: "serve, s", Value: &Parser{}, EnvVar: "APP_SERVE"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"20", "30"}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"20", "30"}) { t.Errorf("short name not set from env") } + return nil }, } a.Run([]string{"run"}) @@ -953,10 +981,11 @@ func TestParseGenericFromEnvCascade(t *testing.T) { Flags: []Flag{ GenericFlag{Name: "foos", Value: &Parser{}, EnvVar: "COMPAT_FOO,APP_FOO"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.Generic("foos"), &Parser{"99", "2000"}) { t.Errorf("value not set from env") } + return nil }, } a.Run([]string{"run"}) diff --git a/funcs.go b/funcs.go new file mode 100644 index 0000000..94640ea --- /dev/null +++ b/funcs.go @@ -0,0 +1,24 @@ +package cli + +// An action to execute when the bash-completion flag is set +type BashCompleteFunc func(*Context) + +// An action to execute before any subcommands are run, but after the context is ready +// If a non-nil error is returned, no subcommands are run +type BeforeFunc func(*Context) error + +// An action to execute after any subcommands are run, but after the subcommand has finished +// It is run even if Action() panics +type AfterFunc func(*Context) error + +// The action to execute when no subcommands are specified +type ActionFunc func(*Context) error + +// Execute this function if the proper command cannot be found +type CommandNotFoundFunc func(*Context, string) + +// Execute this function if an usage error occurs. This is useful for displaying +// customized usage error messages. This function is able to replace the +// original error messages. If this function is not set, the "Incorrect usage" +// is displayed and the execution is interrupted. +type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error diff --git a/help.go b/help.go index adf157d..a895e6c 100644 --- a/help.go +++ b/help.go @@ -78,13 +78,14 @@ var helpCommand = Command{ Aliases: []string{"h"}, Usage: "Shows a list of commands or help for one command", ArgsUsage: "[command]", - Action: func(c *Context) { + Action: func(c *Context) error { args := c.Args() if args.Present() { ShowCommandHelp(c, args.First()) } else { ShowAppHelp(c) } + return nil }, } @@ -93,13 +94,14 @@ var helpSubcommand = Command{ Aliases: []string{"h"}, Usage: "Shows a list of commands or help for one command", ArgsUsage: "[command]", - Action: func(c *Context) { + Action: func(c *Context) error { args := c.Args() if args.Present() { ShowCommandHelp(c, args.First()) } else { ShowSubcommandHelp(c) } + return nil }, } diff --git a/help_test.go b/help_test.go index 0821f48..ee5c25c 100644 --- a/help_test.go +++ b/help_test.go @@ -66,10 +66,11 @@ func Test_Help_Custom_Flags(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "foo, h"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Bool("h") != true { t.Errorf("custom help flag not set") } + return nil }, } output := new(bytes.Buffer) @@ -95,10 +96,11 @@ func Test_Version_Custom_Flags(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "foo, v"}, }, - Action: func(ctx *Context) { + Action: func(ctx *Context) error { if ctx.Bool("v") != true { t.Errorf("custom version flag not set") } + return nil }, } output := new(bytes.Buffer)