From a3b93076fffa81ce505c15d47e3f847d37a97382 Mon Sep 17 00:00:00 2001 From: bryanl Date: Mon, 8 Jun 2015 16:21:26 -0400 Subject: [PATCH 01/29] Allow context value to be set after parse This change allows a context value to be set after parsing. The use case is updating default settings in a Before func. An example usage: ``` f, err := os.Open(configPath) if err == nil { config, err := docli.NewConfig(f) if err != nil { panic(err) } c.Set("token", config.APIKey) } ``` --- context.go | 5 +++++ context_test.go | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/context.go b/context.go index 5b67129..3c8f468 100644 --- a/context.go +++ b/context.go @@ -132,6 +132,11 @@ 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) +} + // Determines if the flag was actually set func (c *Context) IsSet(name string) bool { if c.setFlags == nil { diff --git a/context_test.go b/context_test.go index 6c27d06..c3f2631 100644 --- a/context_test.go +++ b/context_test.go @@ -113,3 +113,12 @@ func TestContext_NumFlags(t *testing.T) { globalSet.Parse([]string{"--myflagGlobal"}) expect(t, c.NumFlags(), 2) } + +func TestContext_Set(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Int("int", 5, "an int") + c := cli.NewContext(nil, set, nil) + + c.Set("int", "1") + expect(t, c.Int("int"), 1) +} From 9c0db3f4ac44d769b59c5d9ca2c95ea822cc1b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarc=C3=ADsio=20Gruppi?= Date: Tue, 28 Jul 2015 20:02:18 +0200 Subject: [PATCH 02/29] Created types for functions The function used by BashComplete, Before, After, Action and CommandNotFound have their won type. This makes easier to change/update the API --- app.go | 10 +++++----- command.go | 8 ++++---- funcs.go | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 funcs.go diff --git a/app.go b/app.go index e7caec9..41b08a8 100644 --- a/app.go +++ b/app.go @@ -28,17 +28,17 @@ type App struct { // Boolean to hide built-in version flag HideVersion bool // An action to execute when the bash-completion flag is set - BashComplete func(context *Context) + BashComplete BashCompleteFn // 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 BeforeFn // 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 AfterFn // The action to execute when no subcommands are specified - Action func(context *Context) + Action ActionFn // Execute this function if the proper command cannot be found - CommandNotFound func(context *Context, command string) + CommandNotFound CommandNotFoundFn // Compilation date Compiled time.Time // List of all authors who contributed diff --git a/command.go b/command.go index 54617af..7022f8c 100644 --- a/command.go +++ b/command.go @@ -19,15 +19,15 @@ type Command struct { // A longer explanation of how the command works Description string // The function to call when checking for bash command completions - BashComplete func(context *Context) + BashComplete BashCompleteFn // 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 + Before BeforeFn // 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 AfterFn // The function to call when this command is invoked - Action func(context *Context) + Action ActionFn // List of child commands Subcommands []Command // List of flags to parse diff --git a/funcs.go b/funcs.go new file mode 100644 index 0000000..6807c98 --- /dev/null +++ b/funcs.go @@ -0,0 +1,18 @@ +package cli + +// An action to execute when the bash-completion flag is set +type BashCompleteFn 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 BeforeFn 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 AfterFn func(*Context) error + +// The action to execute when no subcommands are specified +type ActionFn func(*Context) + +// Execute this function if the proper command cannot be found +type CommandNotFoundFn func(*Context, string) From 49c1229409f2539e19561b11f973f426014d0c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarc=C3=ADsio=20Gruppi?= Date: Tue, 28 Jul 2015 20:05:14 +0200 Subject: [PATCH 03/29] Added exit code support Now the exit code can be returned by BeforeFn, ActionFn and AfterFn. The `os.Exit` function is not called by this packaged This closes #66 and closes #164 --- app.go | 54 +++++++++++++++++++++++++++++------------------------- command.go | 15 +++++++-------- funcs.go | 6 +++--- help.go | 6 ++++-- 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/app.go b/app.go index 41b08a8..46a699e 100644 --- a/app.go +++ b/app.go @@ -8,6 +8,11 @@ import ( "time" ) +var ( + // Set to 125 which is the highest number not used in most shells + DefaultExitCode int = 0 +) + // App is the main structure of a cli application. It is recomended that // an app be created with the cli.NewApp() function type App struct { @@ -77,7 +82,7 @@ func NewApp() *App { } // Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination -func (a *App) Run(arguments []string) (err error) { +func (a *App) Run(arguments []string) (ec int, err error) { if a.Author != "" || a.Email != "" { a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) } @@ -108,7 +113,7 @@ func (a *App) Run(arguments []string) (err error) { fmt.Fprintln(a.Writer, nerr) context := NewContext(a, set, nil) ShowAppHelp(context) - return nerr + return DefaultExitCode, nerr } context := NewContext(a, set, nil) @@ -116,24 +121,24 @@ func (a *App) Run(arguments []string) (err error) { fmt.Fprintln(a.Writer, "Incorrect Usage.") fmt.Fprintln(a.Writer) ShowAppHelp(context) - return err + return DefaultExitCode, err } if checkCompletions(context) { - return nil + return 0, nil } if checkHelp(context) { - return nil + return 0, nil } if checkVersion(context) { - return nil + return 0, nil } if a.After != nil { defer func() { - afterErr := a.After(context) + afterEc, afterErr := a.After(context) if afterErr != nil { if err != nil { err = NewMultiError(err, afterErr) @@ -141,13 +146,14 @@ func (a *App) Run(arguments []string) (err error) { err = afterErr } } + ec = afterEc }() } if a.Before != nil { - err := a.Before(context) + ec, err = a.Before(context) if err != nil { - return err + return ec, err } } @@ -161,20 +167,19 @@ func (a *App) Run(arguments []string) (err error) { } // Run default Action - a.Action(context) - return nil + return a.Action(context), nil } // Another entry point to the cli app, takes care of passing arguments and error handling func (a *App) RunAndExitOnError() { - if err := a.Run(os.Args); err != nil { + if exitCode, err := a.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) - os.Exit(1) + os.Exit(exitCode) } } // Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags -func (a *App) RunAsSubcommand(ctx *Context) (err error) { +func (a *App) RunAsSubcommand(ctx *Context) (ec int, err error) { // append help to commands if len(a.Commands) > 0 { if a.Command(helpCommand.Name) == nil && !a.HideHelp { @@ -205,33 +210,33 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } else { ShowCommandHelp(ctx, context.Args().First()) } - return nerr + return DefaultExitCode, nerr } if err != nil { fmt.Fprintln(a.Writer, "Incorrect Usage.") fmt.Fprintln(a.Writer) ShowSubcommandHelp(context) - return err + return DefaultExitCode, err } if checkCompletions(context) { - return nil + return 0, nil } if len(a.Commands) > 0 { if checkSubcommandHelp(context) { - return nil + return 0, nil } } else { if checkCommandHelp(ctx, context.Args().First()) { - return nil + return 0, nil } } if a.After != nil { defer func() { - afterErr := a.After(context) + afterEc, afterErr := a.After(context) if afterErr != nil { if err != nil { err = NewMultiError(err, afterErr) @@ -239,13 +244,14 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { err = afterErr } } + ec = afterEc }() } if a.Before != nil { - err := a.Before(context) + ec, err = a.Before(context) if err != nil { - return err + return ec, err } } @@ -259,9 +265,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } // Run default Action - a.Action(context) - - return nil + return a.Action(context), nil } // Returns the named command on App. Returns nil if the command does not exist diff --git a/command.go b/command.go index 7022f8c..9aa42db 100644 --- a/command.go +++ b/command.go @@ -50,7 +50,7 @@ func (c Command) FullName() string { } // Invokes the command given the context, parses ctx.Args() to generate command-specific flags -func (c Command) Run(ctx *Context) error { +func (c Command) Run(ctx *Context) (int, error) { if len(c.Subcommands) > 0 || c.Before != nil || c.After != nil { return c.startApp(ctx) } @@ -104,7 +104,7 @@ func (c Command) Run(ctx *Context) error { fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") fmt.Fprintln(ctx.App.Writer) ShowCommandHelp(ctx, c.Name) - return err + return DefaultExitCode, err } nerr := normalizeFlags(c.Flags, set) @@ -112,20 +112,19 @@ func (c Command) Run(ctx *Context) error { fmt.Fprintln(ctx.App.Writer, nerr) fmt.Fprintln(ctx.App.Writer) ShowCommandHelp(ctx, c.Name) - return nerr + return DefaultExitCode, nerr } context := NewContext(ctx.App, set, ctx) if checkCommandCompletions(context, c.Name) { - return nil + return 0, nil } if checkCommandHelp(context, c.Name) { - return nil + return 0, nil } context.Command = c - c.Action(context) - return nil + return c.Action(context), nil } func (c Command) Names() []string { @@ -148,7 +147,7 @@ func (c Command) HasName(name string) bool { return false } -func (c Command) startApp(ctx *Context) error { +func (c Command) startApp(ctx *Context) (int, error) { app := NewApp() // set the name and usage diff --git a/funcs.go b/funcs.go index 6807c98..48909c1 100644 --- a/funcs.go +++ b/funcs.go @@ -5,14 +5,14 @@ type BashCompleteFn 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 BeforeFn func(*Context) error +type BeforeFn func(*Context) (int, error) // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics -type AfterFn func(*Context) error +type AfterFn func(*Context) (int, error) // The action to execute when no subcommands are specified -type ActionFn func(*Context) +type ActionFn func(*Context) int // Execute this function if the proper command cannot be found type CommandNotFoundFn func(*Context, string) diff --git a/help.go b/help.go index 66ef2fb..e1faef6 100644 --- a/help.go +++ b/help.go @@ -72,13 +72,14 @@ var helpCommand = Command{ Name: "help", Aliases: []string{"h"}, Usage: "Shows a list of commands or help for one command", - Action: func(c *Context) { + Action: func(c *Context) int { args := c.Args() if args.Present() { ShowCommandHelp(c, args.First()) } else { ShowAppHelp(c) } + return 0 }, } @@ -86,13 +87,14 @@ var helpSubcommand = Command{ Name: "help", Aliases: []string{"h"}, Usage: "Shows a list of commands or help for one command", - Action: func(c *Context) { + Action: func(c *Context) int { args := c.Args() if args.Present() { ShowCommandHelp(c, args.First()) } else { ShowSubcommandHelp(c) } + return 0 }, } From b79f884410d3bed89d0346dd393791f715e1acc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarc=C3=ADsio=20Gruppi?= Date: Tue, 28 Jul 2015 20:06:46 +0200 Subject: [PATCH 04/29] Updated README.md with exit code sample --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85b9cda..7c161a6 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ export PATH=$PATH:$GOPATH/bin ``` ## Getting Started -One of the philosophies behind cli.go is that an API should be playful and full of discovery. So a cli.go app can be as little as one line of code in `main()`. +One of the philosophies behind cli.go is that an API should be playful and full of discovery. So a cli.go app can be as little as one line of code in `main()`. ``` go package main @@ -57,7 +57,7 @@ func main() { app.Action = func(c *cli.Context) { println("boom! I say!") } - + app.Run(os.Args) } ``` @@ -251,6 +251,28 @@ app.Commands = []cli.Command{ ... ``` +### Exit code + +It is your responsability to call `os.Exit` with the exit code returned by +`app.Run` + +```go +package main + +import ( + "os" + "github.com/codegangsta/cli" +) + +func main() { + exitCode, err := cli.NewApp().Run(os.Args) + if err != nil { + log.Println(err) + } + os.Exit(exitCode) +} +``` + ### Bash Completion You can enable completion commands by setting the `EnableBashCompletion` From 1510d7e722cceb8515d51786328b5a8df20ed27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarc=C3=ADsio=20Gruppi?= Date: Tue, 28 Jul 2015 20:28:37 +0200 Subject: [PATCH 05/29] Updated tests to support exit code --- app_test.go | 137 ++++++++++++++++++++++++++++++++---------------- cli_test.go | 18 ++++--- command_test.go | 8 +-- flag_test.go | 72 ++++++++++++++++--------- 4 files changed, 155 insertions(+), 80 deletions(-) diff --git a/app_test.go b/app_test.go index 2d52e88..a59d671 100644 --- a/app_test.go +++ b/app_test.go @@ -21,8 +21,9 @@ func ExampleApp() { app.Flags = []cli.Flag{ cli.StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, } - app.Action = func(c *cli.Context) { + app.Action = func(c *cli.Context) int { fmt.Printf("Hello %v\n", c.String("name")) + return 0 } app.Author = "Harrison" app.Email = "harrison@lolwut.com" @@ -56,8 +57,9 @@ func ExampleAppSubcommand() { Usage: "Name of the person to greet", }, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { fmt.Println("Hello,", c.String("name")) + return 0 }, }, }, @@ -84,8 +86,9 @@ func ExampleAppHelp() { Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { fmt.Printf("i like to describe things") + return 0 }, }, } @@ -114,15 +117,17 @@ func ExampleAppBashComplete() { Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { fmt.Printf("i like to describe things") + return 0 }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { fmt.Printf("the next example") + return 0 }, }, } @@ -140,14 +145,17 @@ func TestApp_Run(t *testing.T) { s := "" app := cli.NewApp() - app.Action = func(c *cli.Context) { + app.Action = func(c *cli.Context) int { s = s + c.Args().First() + return 0 } - err := app.Run([]string{"command", "foo"}) + ec, err := app.Run([]string{"command", "foo"}) expect(t, err, nil) - err = app.Run([]string{"command", "bar"}) + expect(t, ec, 0) + ec, err = app.Run([]string{"command", "bar"}) expect(t, err, nil) + expect(t, ec, 0) expect(t, s, "foobar") } @@ -186,9 +194,10 @@ func TestApp_CommandWithArgBeforeFlags(t *testing.T) { Flags: []cli.Flag{ cli.StringFlag{Name: "option", Value: "", Usage: "some option"}, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { parsedOption = c.String("option") firstArg = c.Args().First() + return 0 }, } app.Commands = []cli.Command{command} @@ -206,8 +215,9 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) { a.Commands = []cli.Command{ { Name: "foo", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { context = c + return 0 }, Flags: []cli.Flag{ cli.StringFlag{ @@ -216,7 +226,7 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) { Usage: "language for the greeting", }, }, - Before: func(_ *cli.Context) error { return nil }, + Before: func(_ *cli.Context) (int, error) { return 0, nil }, }, } a.Run([]string{"", "foo", "--lang", "spanish", "abcd"}) @@ -235,9 +245,10 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { Flags: []cli.Flag{ cli.StringFlag{Name: "option", Value: "", Usage: "some option"}, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { parsedOption = c.String("option") args = c.Args() + return 0 }, } app.Commands = []cli.Command{command} @@ -256,8 +267,9 @@ func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { app := cli.NewApp() command := cli.Command{ Name: "cmd", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { args = c.Args() + return 0 }, } app.Commands = []cli.Command{command} @@ -276,8 +288,9 @@ func TestApp_Float64Flag(t *testing.T) { app.Flags = []cli.Flag{ cli.Float64Flag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"}, } - app.Action = func(c *cli.Context) { + app.Action = func(c *cli.Context) int { meters = c.Float64("height") + return 0 } app.Run([]string{"", "--height", "1.93"}) @@ -296,11 +309,12 @@ func TestApp_ParseSliceFlags(t *testing.T) { cli.IntSliceFlag{Name: "p", Value: &cli.IntSlice{}, Usage: "set one or more ip addr"}, cli.StringSliceFlag{Name: "ip", Value: &cli.StringSlice{}, Usage: "set one or more ports to open"}, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { parsedIntSlice = c.IntSlice("p") parsedStringSlice = c.StringSlice("ip") parsedOption = c.String("option") firstArg = c.Args().First() + return 0 }, } app.Commands = []cli.Command{command} @@ -353,9 +367,10 @@ func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) { cli.IntSliceFlag{Name: "a", Usage: "set numbers"}, cli.StringSliceFlag{Name: "str", Usage: "set strings"}, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { parsedIntSlice = c.IntSlice("a") parsedStringSlice = c.StringSlice("str") + return 0 }, } app.Commands = []cli.Command{command} @@ -407,7 +422,7 @@ func TestApp_SetStdout(t *testing.T) { app.Name = "test" app.Writer = w - err := app.Run([]string{"help"}) + _, err := app.Run([]string{"help"}) if err != nil { t.Fatalf("Run error: %s", err) @@ -425,21 +440,22 @@ func TestApp_BeforeFunc(t *testing.T) { app := cli.NewApp() - app.Before = func(c *cli.Context) error { + app.Before = func(c *cli.Context) (int, error) { beforeRun = true s := c.String("opt") if s == "fail" { - return beforeError + return 1, beforeError } - return nil + return 0, nil } app.Commands = []cli.Command{ cli.Command{ Name: "sub", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { subcommandRun = true + return 0 }, }, } @@ -449,7 +465,7 @@ func TestApp_BeforeFunc(t *testing.T) { } // run with the Before() func succeeding - err = app.Run([]string{"command", "--opt", "succeed", "sub"}) + ec, err := app.Run([]string{"command", "--opt", "succeed", "sub"}) if err != nil { t.Fatalf("Run error: %s", err) @@ -463,11 +479,15 @@ func TestApp_BeforeFunc(t *testing.T) { t.Errorf("Subcommand not executed when expected") } + if ec != 0 { + t.Errorf("Expected exit code to be %d but got %d", 0, ec) + } + // reset beforeRun, subcommandRun = false, false // run with the Before() func failing - err = app.Run([]string{"command", "--opt", "fail", "sub"}) + ec, err = app.Run([]string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func if err != beforeError { @@ -482,6 +502,9 @@ func TestApp_BeforeFunc(t *testing.T) { t.Errorf("Subcommand executed when NOT expected") } + if ec != 1 { + t.Errorf("Expected exit code to be %d but got %d", 1, ec) + } } func TestApp_AfterFunc(t *testing.T) { @@ -491,21 +514,22 @@ func TestApp_AfterFunc(t *testing.T) { app := cli.NewApp() - app.After = func(c *cli.Context) error { + app.After = func(c *cli.Context) (int, error) { afterRun = true s := c.String("opt") if s == "fail" { - return afterError + return 1, afterError } - return nil + return 0, nil } app.Commands = []cli.Command{ cli.Command{ Name: "sub", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { subcommandRun = true + return 0 }, }, } @@ -515,7 +539,7 @@ func TestApp_AfterFunc(t *testing.T) { } // run with the After() func succeeding - err = app.Run([]string{"command", "--opt", "succeed", "sub"}) + ec, err := app.Run([]string{"command", "--opt", "succeed", "sub"}) if err != nil { t.Fatalf("Run error: %s", err) @@ -529,11 +553,15 @@ func TestApp_AfterFunc(t *testing.T) { t.Errorf("Subcommand not executed when expected") } + if ec != 0 { + t.Errorf("Expected exit code to be %d but got %d", 0, ec) + } + // reset afterRun, subcommandRun = false, false // run with the Before() func failing - err = app.Run([]string{"command", "--opt", "fail", "sub"}) + ec, err = app.Run([]string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func if err != afterError { @@ -547,6 +575,10 @@ func TestApp_AfterFunc(t *testing.T) { if subcommandRun == false { t.Errorf("Subcommand not executed when expected") } + + if ec != 1 { + t.Errorf("Expected exit code to be %d but got %d", 1, ec) + } } func TestAppNoHelpFlag(t *testing.T) { @@ -558,7 +590,7 @@ func TestAppNoHelpFlag(t *testing.T) { cli.HelpFlag = cli.BoolFlag{} app := cli.NewApp() - err := app.Run([]string{"test", "-h"}) + _, err := app.Run([]string{"test", "-h"}) if err != flag.ErrHelp { t.Errorf("expected error about missing help flag, but got: %s (%T)", err, err) @@ -615,8 +647,9 @@ func TestAppCommandNotFound(t *testing.T) { app.Commands = []cli.Command{ cli.Command{ Name: "bar", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { subcommandRun = true + return 0 }, }, } @@ -634,9 +667,10 @@ func TestGlobalFlag(t *testing.T) { app.Flags = []cli.Flag{ cli.StringFlag{Name: "global, g", Usage: "global"}, } - app.Action = func(c *cli.Context) { + app.Action = func(c *cli.Context) int { globalFlag = c.GlobalString("global") globalFlagSet = c.GlobalIsSet("global") + return 0 } app.Run([]string{"command", "-g", "foo"}) expect(t, globalFlag, "foo") @@ -662,13 +696,14 @@ func TestGlobalFlagsInSubcommands(t *testing.T) { Subcommands: []cli.Command{ { Name: "bar", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { if c.GlobalBool("debug") { subcommandRun = true } if c.GlobalBool("parent") { parentFlag = true } + return 0 }, }, }, @@ -710,7 +745,7 @@ func TestApp_Run_CommandWithSubcommandHasHelpTopic(t *testing.T) { } app.Commands = []cli.Command{cmd} - err := app.Run(flagSet) + _, err := app.Run(flagSet) if err != nil { t.Error(err) @@ -751,7 +786,7 @@ func TestApp_Run_SubcommandFullPath(t *testing.T) { } app.Commands = []cli.Command{cmd} - err := app.Run([]string{"command", "foo", "bar", "--help"}) + _, err := app.Run([]string{"command", "foo", "bar", "--help"}) if err != nil { t.Error(err) } @@ -777,11 +812,12 @@ func TestApp_Run_Help(t *testing.T) { app.Name = "boom" app.Usage = "make an explosive entrance" app.Writer = buf - app.Action = func(c *cli.Context) { + app.Action = func(c *cli.Context) int { buf.WriteString("boom I say!") + return 0 } - err := app.Run(args) + _, err := app.Run(args) if err != nil { t.Error(err) } @@ -808,11 +844,12 @@ 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 *cli.Context) { + app.Action = func(c *cli.Context) int { buf.WriteString("boom I say!") + return 0 } - err := app.Run(args) + _, err := app.Run(args) if err != nil { t.Error(err) } @@ -828,11 +865,11 @@ func TestApp_Run_Version(t *testing.T) { func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := cli.NewApp() - app.Action = func(c *cli.Context) {} - app.Before = func(c *cli.Context) error { return fmt.Errorf("before error") } - app.After = func(c *cli.Context) error { return fmt.Errorf("after error") } + app.Action = func(c *cli.Context) int { return 0 } + app.Before = func(c *cli.Context) (int, error) { return 1, fmt.Errorf("before error") } + app.After = func(c *cli.Context) (int, error) { return 2, fmt.Errorf("after error") } - err := app.Run([]string{"foo"}) + ec, err := app.Run([]string{"foo"}) if err == nil { t.Fatalf("expected to recieve error from Run, got none") } @@ -843,6 +880,10 @@ func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { if !strings.Contains(err.Error(), "after error") { t.Errorf("expected text of error from After method, but got none in \"%v\"", err) } + + if ec != 2 { + t.Errorf("Expected exit code to be %d but got %d", 2, ec) + } } func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { @@ -850,12 +891,12 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { app.Commands = []cli.Command{ cli.Command{ Name: "bar", - Before: func(c *cli.Context) error { return fmt.Errorf("before error") }, - After: func(c *cli.Context) error { return fmt.Errorf("after error") }, + Before: func(c *cli.Context) (int, error) { return 1, fmt.Errorf("before error") }, + After: func(c *cli.Context) (int, error) { return 2, fmt.Errorf("after error") }, }, } - err := app.Run([]string{"foo", "bar"}) + ec, err := app.Run([]string{"foo", "bar"}) if err == nil { t.Fatalf("expected to recieve error from Run, got none") } @@ -866,4 +907,8 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { if !strings.Contains(err.Error(), "after error") { t.Errorf("expected text of error from After method, but got none in \"%v\"", err) } + + if ec != 2 { + t.Errorf("Expected exit code to be %d but got %d", 2, ec) + } } diff --git a/cli_test.go b/cli_test.go index 8a8df97..f71f7ee 100644 --- a/cli_test.go +++ b/cli_test.go @@ -15,16 +15,18 @@ func Example() { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { println("added task: ", c.Args().First()) + return 0 }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { println("completed task: ", c.Args().First()) + return 0 }, }, } @@ -54,8 +56,9 @@ func ExampleSubcommand() { Usage: "Name of the person to greet", }, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { println("Hello, ", c.String("name")) + return 0 }, }, { Name: "spanish", @@ -68,8 +71,9 @@ func ExampleSubcommand() { Usage: "Surname of the person to greet", }, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { println("Hola, ", c.String("surname")) + return 0 }, }, { Name: "french", @@ -82,16 +86,18 @@ func ExampleSubcommand() { Usage: "Nickname of the person to greet", }, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { println("Bonjour, ", c.String("nickname")) + return 0 }, }, }, }, { Name: "bye", Usage: "says goodbye", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) int { println("bye") + return 0 }, }, } diff --git a/command_test.go b/command_test.go index db81db2..ba9091e 100644 --- a/command_test.go +++ b/command_test.go @@ -20,9 +20,9 @@ func TestCommandDoNotIgnoreFlags(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(_ *cli.Context) {}, + Action: func(_ *cli.Context) int { return 0 }, } - err := command.Run(c) + _, err := command.Run(c) expect(t, err.Error(), "flag provided but not defined: -break") } @@ -40,10 +40,10 @@ func TestCommandIgnoreFlags(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(_ *cli.Context) {}, + Action: func(_ *cli.Context) int { return 0 }, SkipFlagParsing: true, } - err := command.Run(c) + _, err := command.Run(c) expect(t, err, nil) } diff --git a/flag_test.go b/flag_test.go index f0f096a..6eb0c8e 100644 --- a/flag_test.go +++ b/flag_test.go @@ -296,13 +296,14 @@ func TestParseMultiString(t *testing.T) { Flags: []cli.Flag{ cli.StringFlag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if ctx.String("serve") != "10" { t.Errorf("main name not set") } if ctx.String("s") != "10" { t.Errorf("short name not set") } + return 0 }, }).Run([]string{"run", "-s", "10"}) } @@ -314,13 +315,14 @@ func TestParseMultiStringFromEnv(t *testing.T) { Flags: []cli.Flag{ cli.StringFlag{Name: "count, c", EnvVar: "APP_COUNT"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if ctx.String("count") != "20" { t.Errorf("main name not set") } if ctx.String("c") != "20" { t.Errorf("short name not set") } + return 0 }, }).Run([]string{"run"}) } @@ -332,13 +334,14 @@ func TestParseMultiStringFromEnvCascade(t *testing.T) { Flags: []cli.Flag{ cli.StringFlag{Name: "count, c", EnvVar: "COMPAT_COUNT,APP_COUNT"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if ctx.String("count") != "20" { t.Errorf("main name not set") } if ctx.String("c") != "20" { t.Errorf("short name not set") } + return 0 }, }).Run([]string{"run"}) } @@ -348,13 +351,14 @@ func TestParseMultiStringSlice(t *testing.T) { Flags: []cli.Flag{ cli.StringSliceFlag{Name: "serve, s", Value: &cli.StringSlice{}}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if !reflect.DeepEqual(ctx.StringSlice("serve"), []string{"10", "20"}) { t.Errorf("main name not set") } if !reflect.DeepEqual(ctx.StringSlice("s"), []string{"10", "20"}) { t.Errorf("short name not set") } + return 0 }, }).Run([]string{"run", "-s", "10", "-s", "20"}) } @@ -367,13 +371,14 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) { Flags: []cli.Flag{ cli.StringSliceFlag{Name: "intervals, i", Value: &cli.StringSlice{}, EnvVar: "APP_INTERVALS"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, }).Run([]string{"run"}) } @@ -386,13 +391,14 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { Flags: []cli.Flag{ cli.StringSliceFlag{Name: "intervals, i", Value: &cli.StringSlice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, }).Run([]string{"run"}) } @@ -402,13 +408,14 @@ func TestParseMultiInt(t *testing.T) { Flags: []cli.Flag{ cli.IntFlag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if ctx.Int("serve") != 10 { t.Errorf("main name not set") } if ctx.Int("s") != 10 { t.Errorf("short name not set") } + return 0 }, } a.Run([]string{"run", "-s", "10"}) @@ -421,13 +428,14 @@ func TestParseMultiIntFromEnv(t *testing.T) { Flags: []cli.Flag{ cli.IntFlag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if ctx.Int("timeout") != 10 { t.Errorf("main name not set") } if ctx.Int("t") != 10 { t.Errorf("short name not set") } + return 0 }, } a.Run([]string{"run"}) @@ -440,13 +448,14 @@ func TestParseMultiIntFromEnvCascade(t *testing.T) { Flags: []cli.Flag{ cli.IntFlag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if ctx.Int("timeout") != 10 { t.Errorf("main name not set") } if ctx.Int("t") != 10 { t.Errorf("short name not set") } + return 0 }, } a.Run([]string{"run"}) @@ -457,13 +466,14 @@ func TestParseMultiIntSlice(t *testing.T) { Flags: []cli.Flag{ cli.IntSliceFlag{Name: "serve, s", Value: &cli.IntSlice{}}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, }).Run([]string{"run", "-s", "10", "-s", "20"}) } @@ -476,13 +486,14 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) { Flags: []cli.Flag{ cli.IntSliceFlag{Name: "intervals, i", Value: &cli.IntSlice{}, EnvVar: "APP_INTERVALS"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, }).Run([]string{"run"}) } @@ -495,13 +506,14 @@ func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { Flags: []cli.Flag{ cli.IntSliceFlag{Name: "intervals, i", Value: &cli.IntSlice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, }).Run([]string{"run"}) } @@ -511,13 +523,14 @@ func TestParseMultiFloat64(t *testing.T) { Flags: []cli.Flag{ cli.Float64Flag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run", "-s", "10.2"}) @@ -530,13 +543,14 @@ func TestParseMultiFloat64FromEnv(t *testing.T) { Flags: []cli.Flag{ cli.Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run"}) @@ -549,13 +563,14 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) { Flags: []cli.Flag{ cli.Float64Flag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run"}) @@ -566,13 +581,14 @@ func TestParseMultiBool(t *testing.T) { Flags: []cli.Flag{ cli.BoolFlag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if ctx.Bool("serve") != true { t.Errorf("main name not set") } if ctx.Bool("s") != true { t.Errorf("short name not set") } + return 0 }, } a.Run([]string{"run", "--serve"}) @@ -585,13 +601,14 @@ func TestParseMultiBoolFromEnv(t *testing.T) { Flags: []cli.Flag{ cli.BoolFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run"}) @@ -604,13 +621,14 @@ func TestParseMultiBoolFromEnvCascade(t *testing.T) { Flags: []cli.Flag{ cli.BoolFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run"}) @@ -621,13 +639,14 @@ func TestParseMultiBoolT(t *testing.T) { Flags: []cli.Flag{ cli.BoolTFlag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if ctx.BoolT("serve") != true { t.Errorf("main name not set") } if ctx.BoolT("s") != true { t.Errorf("short name not set") } + return 0 }, } a.Run([]string{"run", "--serve"}) @@ -640,13 +659,14 @@ func TestParseMultiBoolTFromEnv(t *testing.T) { Flags: []cli.Flag{ cli.BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run"}) @@ -659,13 +679,14 @@ func TestParseMultiBoolTFromEnvCascade(t *testing.T) { Flags: []cli.Flag{ cli.BoolTFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run"}) @@ -694,13 +715,14 @@ func TestParseGeneric(t *testing.T) { Flags: []cli.Flag{ cli.GenericFlag{Name: "serve, s", Value: &Parser{}}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run", "-s", "10,20"}) @@ -713,13 +735,14 @@ func TestParseGenericFromEnv(t *testing.T) { Flags: []cli.Flag{ cli.GenericFlag{Name: "serve, s", Value: &Parser{}, EnvVar: "APP_SERVE"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { 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 0 }, } a.Run([]string{"run"}) @@ -732,10 +755,11 @@ func TestParseGenericFromEnvCascade(t *testing.T) { Flags: []cli.Flag{ cli.GenericFlag{Name: "foos", Value: &Parser{}, EnvVar: "COMPAT_FOO,APP_FOO"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) int { if !reflect.DeepEqual(ctx.Generic("foos"), &Parser{"99", "2000"}) { t.Errorf("value not set from env") } + return 0 }, } a.Run([]string{"run"}) From 88ea7cbec8b0c5e10ae30a6a597a6621b6613130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=8C=AF=E5=A8=81?= Date: Wed, 2 Mar 2016 10:45:13 +0800 Subject: [PATCH 06/29] Add App extras info --- app.go | 2 ++ command.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 1ea3fd0..503f838 100644 --- a/app.go +++ b/app.go @@ -62,6 +62,8 @@ type App struct { Email string // Writer writer to write output to Writer io.Writer + // Other custom info + Extras map[string]interface{} } // Tries to find out when this binary was compiled. diff --git a/command.go b/command.go index 0153713..0ccffa2 100644 --- a/command.go +++ b/command.go @@ -197,7 +197,7 @@ func (c Command) HasName(name string) bool { func (c Command) startApp(ctx *Context) error { app := NewApp() - + app.Extras = ctx.App.Extras // set the name and usage app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) if c.HelpName == "" { From a17c8cf1d8cce58eb467b8310ba684091d10a14c Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 25 Apr 2016 18:29:05 -0400 Subject: [PATCH 07/29] Rename func type suffixes `Fn`->`Func` and add `OnUsageErrorFunc` --- altsrc/flag.go | 4 ++-- app.go | 16 +++++++--------- command.go | 16 +++++++--------- command_test.go | 2 +- funcs.go | 16 +++++++++++----- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/altsrc/flag.go b/altsrc/flag.go index 36ffa57..9aee544 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)) cli.BeforeFn { +func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc { return func(context *cli.Context) (int, 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)) cli.BeforeFn { +func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(context *cli.Context) (InputSourceContext, error)) cli.BeforeFunc { return func(context *cli.Context) (int, error) { inputSource, err := createInputSource(context) if err != nil { diff --git a/app.go b/app.go index 267387c..107bb2c 100644 --- a/app.go +++ b/app.go @@ -47,21 +47,19 @@ 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 BashCompleteFn + 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 BeforeFn + 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 AfterFn + After AfterFunc // The action to execute when no subcommands are specified - Action ActionFn + Action ActionFunc // Execute this function if the proper command cannot be found - CommandNotFound CommandNotFoundFn - // 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 diff --git a/command.go b/command.go index b9db123..7615c15 100644 --- a/command.go +++ b/command.go @@ -26,19 +26,17 @@ type Command struct { // The category the command is part of Category string // The function to call when checking for bash command completions - BashComplete BashCompleteFn + 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 BeforeFn + 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 AfterFn + After AfterFunc // The function to call when this command is invoked - Action ActionFn - // 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 ActionFunc + // Execute this function if a usage error occurs. + OnUsageError OnUsageErrorFunc // List of child commands Subcommands Commands // List of flags to parse @@ -125,7 +123,7 @@ func (c Command) Run(ctx *Context) (ec int, err error) { if err != nil { if c.OnUsageError != nil { - err := c.OnUsageError(ctx, err) + err := c.OnUsageError(ctx, err, false) if err != nil { return DefaultErrorExitCode, err } diff --git a/command_test.go b/command_test.go index 96b20de..80dc5cd 100644 --- a/command_test.go +++ b/command_test.go @@ -81,7 +81,7 @@ func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { 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/funcs.go b/funcs.go index 48909c1..f375fd9 100644 --- a/funcs.go +++ b/funcs.go @@ -1,18 +1,24 @@ package cli // An action to execute when the bash-completion flag is set -type BashCompleteFn func(*Context) +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 BeforeFn func(*Context) (int, error) +type BeforeFunc func(*Context) (int, error) // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics -type AfterFn func(*Context) (int, error) +type AfterFunc func(*Context) (int, error) // The action to execute when no subcommands are specified -type ActionFn func(*Context) int +type ActionFunc func(*Context) int // Execute this function if the proper command cannot be found -type CommandNotFoundFn func(*Context, string) +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 From b40b62794df47edfe7551f05f8f1635c0e3a2b9a Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Tue, 26 Apr 2016 07:05:50 -0400 Subject: [PATCH 08/29] Ensure README examples are runnable --- README.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 214e543..d0c3236 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) int { + fmt.Println("boom! I say!") + return 0 } 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) int { + fmt.Println("Hello friend!") + return 0 } app.Run(os.Args) @@ -370,8 +376,9 @@ COMMANDS: ### Exit code -It is your responsibility to call `os.Exit` with the exit code returned by -`app.Run`, e.g.: +Calling `App.Run` will not automatically call `os.Exit`, which means that by +default the exit code will "fall through" to being `0`. Proper exit code +propagation is the responsibility of the code that calls `App.Run`, e.g.: ```go package main @@ -382,10 +389,7 @@ import ( ) func main() { - exitCode, err := cli.NewApp().Run(os.Args) - if err != nil { - log.Println(err) - } + exitCode, _ := cli.NewApp().Run(os.Args) os.Exit(exitCode) } ``` From b7329f4968356c4747e41d88f73b999641b86c41 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 09:12:34 -0400 Subject: [PATCH 09/29] Switch from multi-return with exit codes to ExitCoder check --- altsrc/flag.go | 12 +-- altsrc/yaml_command_test.go | 30 +++---- app.go | 91 ++++++++++++------- app_test.go | 172 +++++++++++++++--------------------- cli.go | 29 ++++++ command.go | 46 ++++++---- command_test.go | 16 ++-- flag_test.go | 116 ++++++++++++------------ funcs.go | 6 +- help.go | 8 +- help_test.go | 8 +- 11 files changed, 291 insertions(+), 243 deletions(-) diff --git a/altsrc/flag.go b/altsrc/flag.go index 9aee544..0a11ad5 100644 --- a/altsrc/flag.go +++ b/altsrc/flag.go @@ -39,13 +39,13 @@ func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSource // 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)) cli.BeforeFunc { - return func(context *cli.Context) (int, error) { + return func(context *cli.Context) error { inputSource, err := createInputSource() if err != nil { - return cli.DefaultErrorExitCode, fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error()) + return fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error()) } - return cli.DefaultSuccessExitCode, ApplyInputSourceValues(context, inputSource, flags) + return ApplyInputSourceValues(context, inputSource, flags) } } @@ -53,13 +53,13 @@ func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceCont // 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)) cli.BeforeFunc { - return func(context *cli.Context) (int, error) { + return func(context *cli.Context) error { inputSource, err := createInputSource(context) if err != nil { - return cli.DefaultErrorExitCode, fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error()) + return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error()) } - return cli.DefaultSuccessExitCode, ApplyInputSourceValues(context, inputSource, flags) + return ApplyInputSourceValues(context, inputSource, flags) } } diff --git a/altsrc/yaml_command_test.go b/altsrc/yaml_command_test.go index 6909729..39c36f6 100644 --- a/altsrc/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -29,17 +29,17 @@ func TestCommandYamlFileTest(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) int { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 15) - return 0 + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test"}), cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - _, err := command.Run(c) + err := command.Run(c) expect(t, err, nil) } @@ -62,10 +62,10 @@ func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) int { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 10) - return 0 + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "THE_TEST"}), @@ -73,7 +73,7 @@ func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - _, err := command.Run(c) + err := command.Run(c) expect(t, err, nil) } @@ -94,10 +94,10 @@ func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) int { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 7) - return 0 + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test"}), @@ -105,7 +105,7 @@ func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - _, err := command.Run(c) + err := command.Run(c) expect(t, err, nil) } @@ -126,10 +126,10 @@ func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) int { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 15) - return 0 + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test", Value: 7}), @@ -137,7 +137,7 @@ func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - _, err := command.Run(c) + err := command.Run(c) expect(t, err, nil) } @@ -161,17 +161,17 @@ func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) int { + Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 11) - return 0 + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test", Value: 7, EnvVar: "THE_TEST"}), cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - _, err := command.Run(c) + err := command.Run(c) expect(t, err, nil) } diff --git a/app.go b/app.go index 107bb2c..70442f9 100644 --- a/app.go +++ b/app.go @@ -100,7 +100,7 @@ func NewApp() *App { } // Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination -func (a *App) Run(arguments []string) (ec int, err error) { +func (a *App) Run(arguments []string) (err error) { if a.Author != "" || a.Email != "" { a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) } @@ -146,40 +146,43 @@ func (a *App) Run(arguments []string) (ec int, err error) { if nerr != nil { fmt.Fprintln(a.Writer, nerr) ShowAppHelp(context) - return DefaultErrorExitCode, nerr + return nerr } if checkCompletions(context) { - return DefaultSuccessExitCode, nil + return nil } if err != nil { if a.OnUsageError != nil { err := a.OnUsageError(context, err, false) if err != nil { - return DefaultErrorExitCode, err + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") + } } - return DefaultSuccessExitCode, err + return err } else { fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") ShowAppHelp(context) - return DefaultErrorExitCode, err + return err } } if !a.HideHelp && checkHelp(context) { ShowAppHelp(context) - return DefaultSuccessExitCode, nil + return nil } if !a.HideVersion && checkVersion(context) { ShowVersion(context) - return DefaultSuccessExitCode, nil + return nil } if a.After != nil { defer func() { - afterEc, afterErr := a.After(context) + afterErr := a.After(context) if afterErr != nil { if err != nil { err = NewMultiError(err, afterErr) @@ -187,16 +190,21 @@ func (a *App) Run(arguments []string) (ec int, err error) { err = afterErr } } - ec = afterEc }() } if a.Before != nil { - ec, err = a.Before(context) + err = a.Before(context) if err != nil { fmt.Fprintf(a.Writer, "%v\n\n", err) ShowAppHelp(context) - return ec, err + + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") + } + + return err } } @@ -210,19 +218,34 @@ func (a *App) Run(arguments []string) (ec int, err error) { } // Run default Action - return a.Action(context), nil + err = a.Action(context) + + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") + } + + return err } // Another entry point to the cli app, takes care of passing arguments and error handling func (a *App) RunAndExitOnError() { - if exitCode, err := a.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(exitCode) + err := a.Run(os.Args) + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") } + + if err != nil { + os.Exit(DefaultErrorExitCode) + panic("unreachable") + } + + os.Exit(DefaultSuccessExitCode) } // Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags -func (a *App) RunAsSubcommand(ctx *Context) (ec int, err error) { +func (a *App) RunAsSubcommand(ctx *Context) (err error) { // append help to commands if len(a.Commands) > 0 { if a.Command(helpCommand.Name) == nil && !a.HideHelp { @@ -262,55 +285,63 @@ func (a *App) RunAsSubcommand(ctx *Context) (ec int, err error) { } else { ShowCommandHelp(ctx, context.Args().First()) } - return DefaultErrorExitCode, nerr + return nerr } if checkCompletions(context) { - return DefaultSuccessExitCode, nil + return nil } if err != nil { if a.OnUsageError != nil { err = a.OnUsageError(context, err, true) - if err != nil { - return DefaultErrorExitCode, err + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") } - return DefaultSuccessExitCode, err + return nil } else { fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") ShowSubcommandHelp(context) - return DefaultErrorExitCode, err + return err } } if len(a.Commands) > 0 { if checkSubcommandHelp(context) { - return DefaultSuccessExitCode, nil + return nil } } else { if checkCommandHelp(ctx, context.Args().First()) { - return DefaultSuccessExitCode, nil + return nil } } if a.After != nil { defer func() { - afterEc, afterErr := a.After(context) + afterErr := a.After(context) if afterErr != nil { + if exitErr, ok := afterErr.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") + } if err != nil { err = NewMultiError(err, afterErr) } else { err = afterErr } } - ec = afterEc }() } if a.Before != nil { - ec, err = a.Before(context) + err = a.Before(context) if err != nil { - return ec, err + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") + } + return err } } @@ -324,7 +355,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (ec int, err error) { } // Run default Action - return a.Action(context), nil + return a.Action(context) } // Returns the named command on App. Returns nil if the command does not exist diff --git a/app_test.go b/app_test.go index d0f06f0..0241ad3 100644 --- a/app_test.go +++ b/app_test.go @@ -22,9 +22,9 @@ func ExampleApp_Run() { app.Flags = []Flag{ StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, } - app.Action = func(c *Context) int { + app.Action = func(c *Context) error { fmt.Printf("Hello %v\n", c.String("name")) - return 0 + return nil } app.UsageText = "app [first_arg] [second_arg]" app.Author = "Harrison" @@ -59,9 +59,9 @@ func ExampleApp_Run_subcommand() { Usage: "Name of the person to greet", }, }, - Action: func(c *Context) int { + Action: func(c *Context) error { fmt.Println("Hello,", c.String("name")) - return 0 + return nil }, }, }, @@ -88,9 +88,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) int { + Action: func(c *Context) error { fmt.Printf("i like to describe things") - return 0 + return nil }, }, } @@ -119,17 +119,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) int { + Action: func(c *Context) error { fmt.Printf("i like to describe things") - return 0 + return nil }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", - Action: func(c *Context) int { + Action: func(c *Context) error { fmt.Printf("the next example") - return 0 + return nil }, }, } @@ -147,17 +147,15 @@ func TestApp_Run(t *testing.T) { s := "" app := NewApp() - app.Action = func(c *Context) int { + app.Action = func(c *Context) error { s = s + c.Args().First() - return 0 + return nil } - ec, err := app.Run([]string{"command", "foo"}) + err := app.Run([]string{"command", "foo"}) expect(t, err, nil) - expect(t, ec, 0) - ec, err = app.Run([]string{"command", "bar"}) + err = app.Run([]string{"command", "bar"}) expect(t, err, nil) - expect(t, ec, 0) expect(t, s, "foobar") } @@ -196,10 +194,10 @@ func TestApp_CommandWithArgBeforeFlags(t *testing.T) { Flags: []Flag{ StringFlag{Name: "option", Value: "", Usage: "some option"}, }, - Action: func(c *Context) int { + Action: func(c *Context) error { parsedOption = c.String("option") firstArg = c.Args().First() - return 0 + return nil }, } app.Commands = []Command{command} @@ -217,9 +215,9 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) { a.Commands = []Command{ { Name: "foo", - Action: func(c *Context) int { + Action: func(c *Context) error { context = c - return 0 + return nil }, Flags: []Flag{ StringFlag{ @@ -228,7 +226,7 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) { Usage: "language for the greeting", }, }, - Before: func(_ *Context) (int, error) { return 0, nil }, + Before: func(_ *Context) error { return nil }, }, } a.Run([]string{"", "foo", "--lang", "spanish", "abcd"}) @@ -247,10 +245,10 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { Flags: []Flag{ StringFlag{Name: "option", Value: "", Usage: "some option"}, }, - Action: func(c *Context) int { + Action: func(c *Context) error { parsedOption = c.String("option") args = c.Args() - return 0 + return nil }, } app.Commands = []Command{command} @@ -269,9 +267,9 @@ func TestApp_CommandWithDash(t *testing.T) { app := NewApp() command := Command{ Name: "cmd", - Action: func(c *Context) int { + Action: func(c *Context) error { args = c.Args() - return 0 + return nil }, } app.Commands = []Command{command} @@ -288,9 +286,9 @@ func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { app := NewApp() command := Command{ Name: "cmd", - Action: func(c *Context) int { + Action: func(c *Context) error { args = c.Args() - return 0 + return nil }, } app.Commands = []Command{command} @@ -309,9 +307,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) int { + app.Action = func(c *Context) error { meters = c.Float64("height") - return 0 + return nil } app.Run([]string{"", "--height", "1.93"}) @@ -330,12 +328,12 @@ func TestApp_ParseSliceFlags(t *testing.T) { IntSliceFlag{Name: "p", Value: &IntSlice{}, Usage: "set one or more ip addr"}, StringSliceFlag{Name: "ip", Value: &StringSlice{}, Usage: "set one or more ports to open"}, }, - Action: func(c *Context) int { + Action: func(c *Context) error { parsedIntSlice = c.IntSlice("p") parsedStringSlice = c.StringSlice("ip") parsedOption = c.String("option") firstArg = c.Args().First() - return 0 + return nil }, } app.Commands = []Command{command} @@ -388,10 +386,10 @@ func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) { IntSliceFlag{Name: "a", Usage: "set numbers"}, StringSliceFlag{Name: "str", Usage: "set strings"}, }, - Action: func(c *Context) int { + Action: func(c *Context) error { parsedIntSlice = c.IntSlice("a") parsedStringSlice = c.StringSlice("str") - return 0 + return nil }, } app.Commands = []Command{command} @@ -443,7 +441,7 @@ func TestApp_SetStdout(t *testing.T) { app.Name = "test" app.Writer = w - _, err := app.Run([]string{"help"}) + err := app.Run([]string{"help"}) if err != nil { t.Fatalf("Run error: %s", err) @@ -461,22 +459,22 @@ func TestApp_BeforeFunc(t *testing.T) { app := NewApp() - app.Before = func(c *Context) (int, error) { + app.Before = func(c *Context) error { beforeRun = true s := c.String("opt") if s == "fail" { - return DefaultErrorExitCode, beforeError + return beforeError } - return DefaultSuccessExitCode, nil + return nil } app.Commands = []Command{ Command{ Name: "sub", - Action: func(c *Context) int { + Action: func(c *Context) error { subcommandRun = true - return DefaultSuccessExitCode + return nil }, }, } @@ -486,7 +484,7 @@ func TestApp_BeforeFunc(t *testing.T) { } // run with the Before() func succeeding - ec, err := app.Run([]string{"command", "--opt", "succeed", "sub"}) + err = app.Run([]string{"command", "--opt", "succeed", "sub"}) if err != nil { t.Fatalf("Run error: %s", err) @@ -500,15 +498,11 @@ func TestApp_BeforeFunc(t *testing.T) { t.Errorf("Subcommand not executed when expected") } - if ec != DefaultSuccessExitCode { - t.Errorf("Expected exit code to be %d but got %d", DefaultSuccessExitCode, ec) - } - // reset beforeRun, subcommandRun = false, false // run with the Before() func failing - ec, err = app.Run([]string{"command", "--opt", "fail", "sub"}) + err = app.Run([]string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func if err != beforeError { @@ -522,10 +516,6 @@ func TestApp_BeforeFunc(t *testing.T) { if subcommandRun == true { t.Errorf("Subcommand executed when NOT expected") } - - if ec != DefaultErrorExitCode { - t.Errorf("Expected exit code to be %d but got %d", DefaultErrorExitCode, ec) - } } func TestApp_AfterFunc(t *testing.T) { @@ -535,22 +525,22 @@ func TestApp_AfterFunc(t *testing.T) { app := NewApp() - app.After = func(c *Context) (int, error) { + app.After = func(c *Context) error { afterRun = true s := c.String("opt") if s == "fail" { - return DefaultErrorExitCode, afterError + return afterError } - return DefaultSuccessExitCode, nil + return nil } app.Commands = []Command{ Command{ Name: "sub", - Action: func(c *Context) int { + Action: func(c *Context) error { subcommandRun = true - return DefaultSuccessExitCode + return nil }, }, } @@ -560,7 +550,7 @@ func TestApp_AfterFunc(t *testing.T) { } // run with the After() func succeeding - ec, err := app.Run([]string{"command", "--opt", "succeed", "sub"}) + err = app.Run([]string{"command", "--opt", "succeed", "sub"}) if err != nil { t.Fatalf("Run error: %s", err) @@ -574,15 +564,11 @@ func TestApp_AfterFunc(t *testing.T) { t.Errorf("Subcommand not executed when expected") } - if ec != DefaultSuccessExitCode { - t.Errorf("Expected exit code to be %d but got %d", DefaultSuccessExitCode, ec) - } - // reset afterRun, subcommandRun = false, false // run with the Before() func failing - ec, err = app.Run([]string{"command", "--opt", "fail", "sub"}) + err = app.Run([]string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func if err != afterError { @@ -596,10 +582,6 @@ func TestApp_AfterFunc(t *testing.T) { if subcommandRun == false { t.Errorf("Subcommand not executed when expected") } - - if ec != DefaultErrorExitCode { - t.Errorf("Expected exit code to be %d but got %d", DefaultErrorExitCode, ec) - } } func TestAppNoHelpFlag(t *testing.T) { @@ -612,7 +594,7 @@ func TestAppNoHelpFlag(t *testing.T) { app := NewApp() app.Writer = ioutil.Discard - _, err := app.Run([]string{"test", "-h"}) + err := app.Run([]string{"test", "-h"}) if err != flag.ErrHelp { t.Errorf("expected error about missing help flag, but got: %s (%T)", err, err) @@ -669,9 +651,9 @@ func TestAppCommandNotFound(t *testing.T) { app.Commands = []Command{ Command{ Name: "bar", - Action: func(c *Context) int { + Action: func(c *Context) error { subcommandRun = true - return 0 + return nil }, }, } @@ -689,10 +671,10 @@ func TestGlobalFlag(t *testing.T) { app.Flags = []Flag{ StringFlag{Name: "global, g", Usage: "global"}, } - app.Action = func(c *Context) int { + app.Action = func(c *Context) error { globalFlag = c.GlobalString("global") globalFlagSet = c.GlobalIsSet("global") - return 0 + return nil } app.Run([]string{"command", "-g", "foo"}) expect(t, globalFlag, "foo") @@ -718,14 +700,14 @@ func TestGlobalFlagsInSubcommands(t *testing.T) { Subcommands: []Command{ { Name: "bar", - Action: func(c *Context) int { + Action: func(c *Context) error { if c.GlobalBool("debug") { subcommandRun = true } if c.GlobalBool("parent") { parentFlag = true } - return 0 + return nil }, }, }, @@ -767,7 +749,7 @@ func TestApp_Run_CommandWithSubcommandHasHelpTopic(t *testing.T) { } app.Commands = []Command{cmd} - _, err := app.Run(flagSet) + err := app.Run(flagSet) if err != nil { t.Error(err) @@ -808,7 +790,7 @@ func TestApp_Run_SubcommandFullPath(t *testing.T) { } app.Commands = []Command{cmd} - _, err := app.Run([]string{"command", "foo", "bar", "--help"}) + err := app.Run([]string{"command", "foo", "bar", "--help"}) if err != nil { t.Error(err) } @@ -839,7 +821,7 @@ func TestApp_Run_SubcommandHelpName(t *testing.T) { } app.Commands = []Command{cmd} - _, err := app.Run([]string{"command", "foo", "bar", "--help"}) + err := app.Run([]string{"command", "foo", "bar", "--help"}) if err != nil { t.Error(err) } @@ -870,7 +852,7 @@ func TestApp_Run_CommandHelpName(t *testing.T) { } app.Commands = []Command{cmd} - _, err := app.Run([]string{"command", "foo", "bar", "--help"}) + err := app.Run([]string{"command", "foo", "bar", "--help"}) if err != nil { t.Error(err) } @@ -901,7 +883,7 @@ func TestApp_Run_CommandSubcommandHelpName(t *testing.T) { } app.Commands = []Command{cmd} - _, err := app.Run([]string{"command", "foo", "--help"}) + err := app.Run([]string{"command", "foo", "--help"}) if err != nil { t.Error(err) } @@ -927,12 +909,12 @@ func TestApp_Run_Help(t *testing.T) { app.Name = "boom" app.Usage = "make an explosive entrance" app.Writer = buf - app.Action = func(c *Context) int { + app.Action = func(c *Context) error { buf.WriteString("boom I say!") - return 0 + return nil } - _, err := app.Run(args) + err := app.Run(args) if err != nil { t.Error(err) } @@ -959,12 +941,12 @@ 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) int { + app.Action = func(c *Context) error { buf.WriteString("boom I say!") - return 0 + return nil } - _, err := app.Run(args) + err := app.Run(args) if err != nil { t.Error(err) } @@ -1029,11 +1011,11 @@ func TestApp_Run_Categories(t *testing.T) { func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() - app.Action = func(c *Context) int { return 0 } - app.Before = func(c *Context) (int, error) { return 1, fmt.Errorf("before error") } - app.After = func(c *Context) (int, error) { return 2, fmt.Errorf("after error") } + 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") } - ec, err := app.Run([]string{"foo"}) + err := app.Run([]string{"foo"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } @@ -1044,10 +1026,6 @@ func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { if !strings.Contains(err.Error(), "after error") { t.Errorf("expected text of error from After method, but got none in \"%v\"", err) } - - if ec != 2 { - t.Errorf("Expected exit code to be %d but got %d", 2, ec) - } } func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { @@ -1060,12 +1038,12 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { }, }, Name: "bar", - Before: func(c *Context) (int, error) { return 1, fmt.Errorf("before error") }, - After: func(c *Context) (int, error) { return 2, fmt.Errorf("after error") }, + Before: func(c *Context) error { return fmt.Errorf("before error") }, + After: func(c *Context) error { return fmt.Errorf("after error") }, }, } - ec, err := app.Run([]string{"foo", "bar"}) + err := app.Run([]string{"foo", "bar"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } @@ -1076,10 +1054,6 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { if !strings.Contains(err.Error(), "after error") { t.Errorf("expected text of error from After method, but got none in \"%v\"", err) } - - if ec != 2 { - t.Errorf("Expected exit code to be %d but got %d", 2, ec) - } } func TestApp_OnUsageError_WithWrongFlagValue(t *testing.T) { @@ -1102,7 +1076,7 @@ func TestApp_OnUsageError_WithWrongFlagValue(t *testing.T) { }, } - _, err := app.Run([]string{"foo", "--flag=wrong"}) + err := app.Run([]string{"foo", "--flag=wrong"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } @@ -1132,7 +1106,7 @@ func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) { }, } - _, err := app.Run([]string{"foo", "--flag=wrong", "bar"}) + err := app.Run([]string{"foo", "--flag=wrong", "bar"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } diff --git a/cli.go b/cli.go index 31dc912..f8476d2 100644 --- a/cli.go +++ b/cli.go @@ -19,6 +19,7 @@ package cli import ( + "fmt" "strings" ) @@ -38,3 +39,31 @@ func (m MultiError) Error() string { return strings.Join(errs, "\n") } + +type ExitCoder interface { + ExitCode() int +} + +type ExitError struct { + exitCode int + message string +} + +func NewExitError(message string, exitCode int) *ExitError { + return &ExitError{ + exitCode: exitCode, + message: message, + } +} + +func (ee *ExitError) Error() string { + return ee.message +} + +func (ee *ExitError) String() string { + return fmt.Sprintf("%s exitcode=%v", ee.message, ee.exitCode) +} + +func (ee *ExitError) ExitCode() int { + return ee.exitCode +} diff --git a/command.go b/command.go index 7615c15..1137267 100644 --- a/command.go +++ b/command.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "io/ioutil" + "os" "sort" "strings" ) @@ -63,7 +64,7 @@ func (c Command) FullName() string { type Commands []Command // Invokes the command given the context, parses ctx.Args() to generate command-specific flags -func (c Command) Run(ctx *Context) (ec int, err error) { +func (c Command) Run(ctx *Context) (err error) { if len(c.Subcommands) > 0 { return c.startApp(ctx) } @@ -124,15 +125,16 @@ func (c Command) Run(ctx *Context) (ec int, err error) { if err != nil { if c.OnUsageError != nil { err := c.OnUsageError(ctx, err, false) - if err != nil { - return DefaultErrorExitCode, err + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") } - return DefaultSuccessExitCode, err + return err } else { fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") fmt.Fprintln(ctx.App.Writer) ShowCommandHelp(ctx, c.Name) - return DefaultErrorExitCode, err + return err } } @@ -141,47 +143,59 @@ func (c Command) Run(ctx *Context) (ec int, err error) { fmt.Fprintln(ctx.App.Writer, nerr) fmt.Fprintln(ctx.App.Writer) ShowCommandHelp(ctx, c.Name) - return DefaultErrorExitCode, nerr + return nerr } context := NewContext(ctx.App, set, ctx) if checkCommandCompletions(context, c.Name) { - return DefaultSuccessExitCode, nil + return nil } if checkCommandHelp(context, c.Name) { - return DefaultSuccessExitCode, nil + return nil } if c.After != nil { defer func() { - afterEc, afterErr := c.After(context) + afterErr := c.After(context) if afterErr != nil { + if exitErr, ok := afterErr.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") + } if err != nil { err = NewMultiError(err, afterErr) } else { err = afterErr } - - ec = afterEc } }() } if c.Before != nil { - ec, 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) - return ec, err + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") + } + return err } } context.Command = c - ec = c.Action(context) - return ec, err + err = c.Action(context) + if err != nil { + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + panic("unreachable") + } + } + return err } func (c Command) Names() []string { @@ -204,7 +218,7 @@ func (c Command) HasName(name string) bool { return false } -func (c Command) startApp(ctx *Context) (int, error) { +func (c Command) startApp(ctx *Context) error { app := NewApp() // set the name and usage diff --git a/command_test.go b/command_test.go index 80dc5cd..2687212 100644 --- a/command_test.go +++ b/command_test.go @@ -34,12 +34,12 @@ func TestCommandFlagParsing(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(_ *Context) int { return 0 }, + Action: func(_ *Context) error { return nil }, } command.SkipFlagParsing = c.skipFlagParsing - _, err := command.Run(context) + err := command.Run(context) expect(t, err, c.expectedErr) expect(t, []string(context.Args()), c.testArgs) @@ -51,16 +51,16 @@ func TestCommand_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app.Commands = []Command{ Command{ Name: "bar", - Before: func(c *Context) (int, error) { - return 1, fmt.Errorf("before error") + Before: func(c *Context) error { + return fmt.Errorf("before error") }, - After: func(c *Context) (int, error) { - return 1, fmt.Errorf("after error") + After: func(c *Context) error { + return fmt.Errorf("after error") }, }, } - _, err := app.Run([]string{"foo", "bar"}) + err := app.Run([]string{"foo", "bar"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } @@ -90,7 +90,7 @@ func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { }, } - _, err := app.Run([]string{"foo", "bar", "--flag=wrong"}) + err := app.Run([]string{"foo", "bar", "--flag=wrong"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } diff --git a/flag_test.go b/flag_test.go index a68e543..3dac482 100644 --- a/flag_test.go +++ b/flag_test.go @@ -323,14 +323,14 @@ func TestParseMultiString(t *testing.T) { Flags: []Flag{ StringFlag{Name: "serve, s"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, }).Run([]string{"run", "-s", "10"}) } @@ -344,11 +344,11 @@ func TestParseDestinationString(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if dest != "10" { t.Errorf("expected destination String 10") } - return 0 + return nil }, } a.Run([]string{"run", "--dest", "10"}) @@ -361,14 +361,14 @@ func TestParseMultiStringFromEnv(t *testing.T) { Flags: []Flag{ StringFlag{Name: "count, c", EnvVar: "APP_COUNT"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, }).Run([]string{"run"}) } @@ -380,14 +380,14 @@ func TestParseMultiStringFromEnvCascade(t *testing.T) { Flags: []Flag{ StringFlag{Name: "count, c", EnvVar: "COMPAT_COUNT,APP_COUNT"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, }).Run([]string{"run"}) } @@ -397,14 +397,14 @@ func TestParseMultiStringSlice(t *testing.T) { Flags: []Flag{ StringSliceFlag{Name: "serve, s", Value: &StringSlice{}}, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.StringSlice("serve"), []string{"10", "20"}) { t.Errorf("main name not set") } if !reflect.DeepEqual(ctx.StringSlice("s"), []string{"10", "20"}) { t.Errorf("short name not set") } - return 0 + return nil }, }).Run([]string{"run", "-s", "10", "-s", "20"}) } @@ -417,14 +417,14 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) { Flags: []Flag{ StringSliceFlag{Name: "intervals, i", Value: &StringSlice{}, EnvVar: "APP_INTERVALS"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, }).Run([]string{"run"}) } @@ -437,14 +437,14 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { Flags: []Flag{ StringSliceFlag{Name: "intervals, i", Value: &StringSlice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, }).Run([]string{"run"}) } @@ -454,14 +454,14 @@ func TestParseMultiInt(t *testing.T) { Flags: []Flag{ IntFlag{Name: "serve, s"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run", "-s", "10"}) @@ -476,11 +476,11 @@ func TestParseDestinationInt(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if dest != 10 { t.Errorf("expected destination Int 10") } - return 0 + return nil }, } a.Run([]string{"run", "--dest", "10"}) @@ -493,14 +493,14 @@ func TestParseMultiIntFromEnv(t *testing.T) { Flags: []Flag{ IntFlag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -513,14 +513,14 @@ func TestParseMultiIntFromEnvCascade(t *testing.T) { Flags: []Flag{ IntFlag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -531,14 +531,14 @@ func TestParseMultiIntSlice(t *testing.T) { Flags: []Flag{ IntSliceFlag{Name: "serve, s", Value: &IntSlice{}}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, }).Run([]string{"run", "-s", "10", "-s", "20"}) } @@ -551,14 +551,14 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) { Flags: []Flag{ IntSliceFlag{Name: "intervals, i", Value: &IntSlice{}, EnvVar: "APP_INTERVALS"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, }).Run([]string{"run"}) } @@ -571,14 +571,14 @@ func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { Flags: []Flag{ IntSliceFlag{Name: "intervals, i", Value: &IntSlice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, }).Run([]string{"run"}) } @@ -588,14 +588,14 @@ func TestParseMultiFloat64(t *testing.T) { Flags: []Flag{ Float64Flag{Name: "serve, s"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run", "-s", "10.2"}) @@ -610,11 +610,11 @@ func TestParseDestinationFloat64(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if dest != 10.2 { t.Errorf("expected destination Float64 10.2") } - return 0 + return nil }, } a.Run([]string{"run", "--dest", "10.2"}) @@ -627,14 +627,14 @@ func TestParseMultiFloat64FromEnv(t *testing.T) { Flags: []Flag{ Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -647,14 +647,14 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) { Flags: []Flag{ Float64Flag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -665,14 +665,14 @@ func TestParseMultiBool(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "serve, s"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run", "--serve"}) @@ -687,11 +687,11 @@ func TestParseDestinationBool(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if dest != true { t.Errorf("expected destination Bool true") } - return 0 + return nil }, } a.Run([]string{"run", "--dest"}) @@ -704,14 +704,14 @@ func TestParseMultiBoolFromEnv(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -724,14 +724,14 @@ func TestParseMultiBoolFromEnvCascade(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -742,14 +742,14 @@ func TestParseMultiBoolT(t *testing.T) { Flags: []Flag{ BoolTFlag{Name: "serve, s"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run", "--serve"}) @@ -764,11 +764,11 @@ func TestParseDestinationBoolT(t *testing.T) { Destination: &dest, }, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if dest != true { t.Errorf("expected destination BoolT true") } - return 0 + return nil }, } a.Run([]string{"run", "--dest"}) @@ -781,14 +781,14 @@ func TestParseMultiBoolTFromEnv(t *testing.T) { Flags: []Flag{ BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -801,14 +801,14 @@ func TestParseMultiBoolTFromEnvCascade(t *testing.T) { Flags: []Flag{ BoolTFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -837,14 +837,14 @@ func TestParseGeneric(t *testing.T) { Flags: []Flag{ GenericFlag{Name: "serve, s", Value: &Parser{}}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run", "-s", "10,20"}) @@ -857,14 +857,14 @@ func TestParseGenericFromEnv(t *testing.T) { Flags: []Flag{ GenericFlag{Name: "serve, s", Value: &Parser{}, EnvVar: "APP_SERVE"}, }, - Action: func(ctx *Context) int { + 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 0 + return nil }, } a.Run([]string{"run"}) @@ -877,11 +877,11 @@ func TestParseGenericFromEnvCascade(t *testing.T) { Flags: []Flag{ GenericFlag{Name: "foos", Value: &Parser{}, EnvVar: "COMPAT_FOO,APP_FOO"}, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if !reflect.DeepEqual(ctx.Generic("foos"), &Parser{"99", "2000"}) { t.Errorf("value not set from env") } - return 0 + return nil }, } a.Run([]string{"run"}) diff --git a/funcs.go b/funcs.go index f375fd9..94640ea 100644 --- a/funcs.go +++ b/funcs.go @@ -5,14 +5,14 @@ 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) (int, error) +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) (int, error) +type AfterFunc func(*Context) error // The action to execute when no subcommands are specified -type ActionFunc func(*Context) int +type ActionFunc func(*Context) error // Execute this function if the proper command cannot be found type CommandNotFoundFunc func(*Context, string) diff --git a/help.go b/help.go index b6a190d..a895e6c 100644 --- a/help.go +++ b/help.go @@ -78,14 +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) int { + Action: func(c *Context) error { args := c.Args() if args.Present() { ShowCommandHelp(c, args.First()) } else { ShowAppHelp(c) } - return 0 + return nil }, } @@ -94,14 +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) int { + Action: func(c *Context) error { args := c.Args() if args.Present() { ShowCommandHelp(c, args.First()) } else { ShowSubcommandHelp(c) } - return 0 + return nil }, } diff --git a/help_test.go b/help_test.go index 260800d..ee5c25c 100644 --- a/help_test.go +++ b/help_test.go @@ -66,11 +66,11 @@ func Test_Help_Custom_Flags(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "foo, h"}, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if ctx.Bool("h") != true { t.Errorf("custom help flag not set") } - return 0 + return nil }, } output := new(bytes.Buffer) @@ -96,11 +96,11 @@ func Test_Version_Custom_Flags(t *testing.T) { Flags: []Flag{ BoolFlag{Name: "foo, v"}, }, - Action: func(ctx *Context) int { + Action: func(ctx *Context) error { if ctx.Bool("v") != true { t.Errorf("custom version flag not set") } - return 0 + return nil }, } output := new(bytes.Buffer) From f3e55a07831afc7b4ccaf13f31b2baa1bc55fac3 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 09:13:52 -0400 Subject: [PATCH 10/29] Move error types to errors.go --- cli.go | 50 -------------------------------------------------- errors.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 errors.go diff --git a/cli.go b/cli.go index f8476d2..b742545 100644 --- a/cli.go +++ b/cli.go @@ -17,53 +17,3 @@ // app.Run(os.Args) // } package cli - -import ( - "fmt" - "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") -} - -type ExitCoder interface { - ExitCode() int -} - -type ExitError struct { - exitCode int - message string -} - -func NewExitError(message string, exitCode int) *ExitError { - return &ExitError{ - exitCode: exitCode, - message: message, - } -} - -func (ee *ExitError) Error() string { - return ee.message -} - -func (ee *ExitError) String() string { - return fmt.Sprintf("%s exitcode=%v", ee.message, ee.exitCode) -} - -func (ee *ExitError) ExitCode() int { - return ee.exitCode -} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..0c6b44d --- /dev/null +++ b/errors.go @@ -0,0 +1,51 @@ +package cli + +import ( + "fmt" + "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") +} + +type ExitCoder interface { + ExitCode() int +} + +type ExitError struct { + exitCode int + message string +} + +func NewExitError(message string, exitCode int) *ExitError { + return &ExitError{ + exitCode: exitCode, + message: message, + } +} + +func (ee *ExitError) Error() string { + return ee.message +} + +func (ee *ExitError) String() string { + return fmt.Sprintf("%s exitcode=%v", ee.message, ee.exitCode) +} + +func (ee *ExitError) ExitCode() int { + return ee.exitCode +} From f688d474157f3dfc2c5295bbff2b1bc88a8492fb Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 09:18:42 -0400 Subject: [PATCH 11/29] Encapsulate ExitCoder check into a lil func --- app.go | 49 ++++++------------------------------------------- command.go | 21 ++++----------------- errors.go | 9 +++++++++ 3 files changed, 19 insertions(+), 60 deletions(-) diff --git a/app.go b/app.go index 70442f9..22f6bd6 100644 --- a/app.go +++ b/app.go @@ -157,10 +157,7 @@ func (a *App) Run(arguments []string) (err error) { if a.OnUsageError != nil { err := a.OnUsageError(context, err, false) if err != nil { - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) } return err } else { @@ -198,12 +195,7 @@ func (a *App) Run(arguments []string) (err error) { if err != nil { fmt.Fprintf(a.Writer, "%v\n\n", err) ShowAppHelp(context) - - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } - + HandleExitCoder(err) return err } } @@ -219,31 +211,11 @@ func (a *App) Run(arguments []string) (err error) { // Run default Action err = a.Action(context) - - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) return err } -// Another entry point to the cli app, takes care of passing arguments and error handling -func (a *App) RunAndExitOnError() { - err := a.Run(os.Args) - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } - - if err != nil { - os.Exit(DefaultErrorExitCode) - panic("unreachable") - } - - os.Exit(DefaultSuccessExitCode) -} - // Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags func (a *App) RunAsSubcommand(ctx *Context) (err error) { // append help to commands @@ -295,10 +267,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { if err != nil { if a.OnUsageError != nil { err = a.OnUsageError(context, err, true) - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) return nil } else { fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") @@ -321,10 +290,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { defer func() { afterErr := a.After(context) if afterErr != nil { - if exitErr, ok := afterErr.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) if err != nil { err = NewMultiError(err, afterErr) } else { @@ -337,10 +303,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { if a.Before != nil { err = a.Before(context) if err != nil { - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) return err } } diff --git a/command.go b/command.go index 1137267..0fe3223 100644 --- a/command.go +++ b/command.go @@ -3,7 +3,6 @@ package cli import ( "fmt" "io/ioutil" - "os" "sort" "strings" ) @@ -125,10 +124,7 @@ func (c Command) Run(ctx *Context) (err error) { if err != nil { if c.OnUsageError != nil { err := c.OnUsageError(ctx, err, false) - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) return err } else { fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") @@ -160,10 +156,7 @@ func (c Command) Run(ctx *Context) (err error) { defer func() { afterErr := c.After(context) if afterErr != nil { - if exitErr, ok := afterErr.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) if err != nil { err = NewMultiError(err, afterErr) } else { @@ -179,10 +172,7 @@ func (c Command) Run(ctx *Context) (err error) { fmt.Fprintln(ctx.App.Writer, err) fmt.Fprintln(ctx.App.Writer) ShowCommandHelp(ctx, c.Name) - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) return err } } @@ -190,10 +180,7 @@ func (c Command) Run(ctx *Context) (err error) { context.Command = c err = c.Action(context) if err != nil { - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - panic("unreachable") - } + HandleExitCoder(err) } return err } diff --git a/errors.go b/errors.go index 0c6b44d..da2d3de 100644 --- a/errors.go +++ b/errors.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "os" "strings" ) @@ -49,3 +50,11 @@ func (ee *ExitError) String() string { func (ee *ExitError) ExitCode() int { return ee.exitCode } + +// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if +// so calls os.Exit with the given exit code. +func HandleExitCoder(err error) { + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + } +} From 882dd2cc6bf6154b31444b7b49ff1be91f37b4e7 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 09:21:56 -0400 Subject: [PATCH 12/29] Making sure examples in README are valid --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d0c3236..cc09a3f 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,9 @@ func main() { app := cli.NewApp() app.Name = "boom" app.Usage = "make an explosive entrance" - app.Action = func(c *cli.Context) int { + app.Action = func(c *cli.Context) error { fmt.Println("boom! I say!") - return 0 + return nil } app.Run(os.Args) @@ -91,9 +91,9 @@ func main() { app := cli.NewApp() app.Name = "greet" app.Usage = "fight the loneliness!" - app.Action = func(c *cli.Context) int { + app.Action = func(c *cli.Context) error { fmt.Println("Hello friend!") - return 0 + return nil } app.Run(os.Args) @@ -139,8 +139,9 @@ You can lookup arguments by calling the `Args` function on `cli.Context`. ``` go ... -app.Action = func(c *cli.Context) { +app.Action = func(c *cli.Context) error { println("Hello", c.Args()[0]) + return nil } ... ``` @@ -158,7 +159,7 @@ 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] @@ -168,6 +169,7 @@ app.Action = func(c *cli.Context) { } else { println("Hello", name) } + return nil } ... ``` @@ -185,7 +187,7 @@ 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] @@ -195,6 +197,7 @@ app.Action = func(c *cli.Context) { } else { println("Hello", name) } + return nil } ... ``` @@ -276,7 +279,7 @@ Here is a more complete sample of a command using YAML support: Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { // Action to run }, Flags: []cli.Flag{ From b2ac0616d1cfe79ae14152a038b1c99e6a64267b Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 09:31:43 -0400 Subject: [PATCH 13/29] TRIVIAL Remove trailing whitespace --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1be55fd..53265d0 100644 --- a/README.md +++ b/README.md @@ -223,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 From d48e22a9dd36cdabce676e518bdc9d182d8b5399 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 09:54:08 -0400 Subject: [PATCH 14/29] Docs around exit error behavior, + handling MultiError in HandleExitCoder --- README.md | 45 ++++++++++++++++++++++++++++++--------------- errors.go | 25 ++++++++++++++++++++----- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 53265d0..777b0a8 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ You can lookup arguments by calling the `Args` function on `cli.Context`. ``` go ... app.Action = func(c *cli.Context) error { - println("Hello", c.Args()[0]) + fmt.Println("Hello", c.Args()[0]) return nil } ... @@ -165,9 +165,9 @@ app.Action = func(c *cli.Context) error { 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 } @@ -193,9 +193,9 @@ app.Action = func(c *cli.Context) error { name = c.Args()[0] } if language == "spanish" { - println("Hola", name) + fmt.Println("Hola", name) } else { - println("Hello", name) + fmt.Println("Hello", name) } return nil } @@ -323,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()) }, }, { @@ -331,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()) }, }, { @@ -343,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()) }, }, }, @@ -401,20 +401,35 @@ 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`. Proper exit code -propagation is the responsibility of the code that calls `App.Run`, e.g.: +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 +``` go package main import ( "os" + "github.com/codegangsta/cli" ) func main() { - exitCode, _ := cli.NewApp().Run(os.Args) - os.Exit(exitCode) + 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) } ``` @@ -436,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/errors.go b/errors.go index da2d3de..1a6a8c7 100644 --- a/errors.go +++ b/errors.go @@ -23,15 +23,19 @@ func (m MultiError) Error() string { 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, @@ -39,22 +43,33 @@ func NewExitError(message string, exitCode int) *ExitError { } } +// Error returns the string message, fulfilling the interface required by +// `error` func (ee *ExitError) Error() string { return ee.message } -func (ee *ExitError) String() string { - return fmt.Sprintf("%s exitcode=%v", ee.message, ee.exitCode) -} - +// 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 calls os.Exit with the given exit code. +// 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) + } + } } From d45f7c1fe2bacacbfc6a324dcbbc71be469cb43d Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 11:34:01 -0400 Subject: [PATCH 15/29] Allow for legacy and newer Action func signatures --- app.go | 51 +++++++++++++++++++++++++++++++++++++++++++++------ command.go | 8 ++++++-- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index 22f6bd6..d839f39 100644 --- a/app.go +++ b/app.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "reflect" "sort" "time" ) @@ -55,7 +56,10 @@ type App struct { // It is run even if Action() panics After AfterFunc // The action to execute when no subcommands are specified - Action ActionFunc + 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 CommandNotFoundFunc // Execute this function if an usage error occurs @@ -179,8 +183,7 @@ func (a *App) Run(arguments []string) (err error) { if a.After != nil { defer func() { - afterErr := a.After(context) - if afterErr != nil { + if afterErr := a.After(context); afterErr != nil { if err != nil { err = NewMultiError(err, afterErr) } else { @@ -210,9 +213,11 @@ func (a *App) Run(arguments []string) (err error) { } // Run default Action - err = a.Action(context) - HandleExitCoder(err) + err = HandleAction(a.Action, context) + if err != nil { + HandleExitCoder(err) + } return err } @@ -318,7 +323,12 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } // Run default Action - return a.Action(context) + err = HandleAction(a.Action, context) + + if err != nil { + HandleExitCoder(err) + } + return err } // Returns the named command on App. Returns nil if the command does not exist @@ -368,3 +378,32 @@ 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, LegacyActionFunc, or some other invalid thing. If it's an +// ActionFunc or LegacyActionFunc, the func is run! +func HandleAction(action interface{}, context *Context) error { + if reflect.TypeOf(action).Kind() != reflect.Func { + panic("given Action must be a func") + } + + 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 { + fmt.Fprintln(os.Stderr, + "ERROR invalid Action signature. Must be `cli.ActionFunc`") + panic("given Action has invalid return signature") + } + + if err, ok := reflect.ValueOf(vals[0]).Interface().(error); ok { + return err + } + + return nil +} diff --git a/command.go b/command.go index 0fe3223..2932d53 100644 --- a/command.go +++ b/command.go @@ -34,7 +34,10 @@ type Command struct { // It is run even if Action() panics After AfterFunc // The function to call when this command is invoked - Action ActionFunc + 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 @@ -178,7 +181,8 @@ func (c Command) Run(ctx *Context) (err error) { } context.Command = c - err = c.Action(context) + err = HandleAction(c.Action, context) + if err != nil { HandleExitCoder(err) } From 3b5133fbb1470779f6b0a81545be1c91d992167a Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 11:38:49 -0400 Subject: [PATCH 16/29] Add gfmxr integration for checking examples in README.md now that we support the legacy Action func signature. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index c2b5c8d..9d262b4 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 From 9e8ead512a239b79c58da4d97f9aa5de526a467e Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 11:40:46 -0400 Subject: [PATCH 17/29] Making gfmxr assertion stronger --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9d262b4..133722f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,4 +19,4 @@ before_script: script: - go vet ./... - go test -v ./... -- gfmxr +- gfmxr -c $(grep -c 'package main' README.md) -s README.md From 02924293ff31d413df30e70de754f62d63680055 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 11:51:41 -0400 Subject: [PATCH 18/29] Removing unused vars --- app.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app.go b/app.go index d839f39..b87ab9c 100644 --- a/app.go +++ b/app.go @@ -11,15 +11,6 @@ import ( "time" ) -var ( - // DefaultSuccessExitCode is the default for use with os.Exit intended to - // indicate success - DefaultSuccessExitCode = 0 - // DefaultErrorExitCode is the default for use with os.Exit intended to - // indicate an error - DefaultErrorExitCode = 1 -) - // 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 { From 7371138edbce6a2ace08433e37a0a71f13c0491d Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 27 Apr 2016 12:23:09 -0400 Subject: [PATCH 19/29] Add back App.RunAndExitOnError with deprecation message --- app.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app.go b/app.go index b87ab9c..fac5fd9 100644 --- a/app.go +++ b/app.go @@ -212,6 +212,17 @@ func (a *App) Run(arguments []string) (err error) { return err } +// 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 method `*cli.App.RunAndExitOnError`. "+ + "Use `*cli.App.Run` with an error type that fulfills `cli.ExitCoder`.") + if err := a.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + // Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags func (a *App) RunAsSubcommand(ctx *Context) (err error) { // append help to commands From 271b56c71b46d26543a729132cb7a0ecae5d2211 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 28 Apr 2016 10:15:04 -0400 Subject: [PATCH 20/29] Cleanups based on feedback in #361 --- CHANGELOG.md | 11 +++++++++++ app.go | 24 ++++++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074bfb2..b03b52c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ ### Added - Support for placeholders in flag usage strings +- Support for custom exit code by returning an error that fulfills +`cli.ExitCoder` to `cli.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 signature of `func(*cli.Context) error`, as defined by +`cli.ActionFunc`. ## [1.14.0] - 2016-04-03 (backfilled 2016-04-25) ### Added diff --git a/app.go b/app.go index fac5fd9..c9830d4 100644 --- a/app.go +++ b/app.go @@ -11,6 +11,15 @@ import ( "time" ) +var ( + errNonFuncAction = NewExitError("ERROR invalid Action type. "+ + "Must be a func of type `cli.ActionFunc`. "+ + "See https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-action-signature", 2) + errInvalidActionSignature = NewExitError("ERROR invalid Action signature. "+ + "Must be `cli.ActionFunc`. "+ + "See https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-action-signature", 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 { @@ -215,8 +224,8 @@ func (a *App) Run(arguments []string) (err error) { // 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 method `*cli.App.RunAndExitOnError`. "+ - "Use `*cli.App.Run` with an error type that fulfills `cli.ExitCoder`.") + "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) @@ -382,11 +391,12 @@ func (a Author) String() string { } // HandleAction uses ✧✧✧reflection✧✧✧ to figure out if the given Action is an -// ActionFunc, LegacyActionFunc, or some other invalid thing. If it's an -// ActionFunc or LegacyActionFunc, the func is run! +// 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) error { if reflect.TypeOf(action).Kind() != reflect.Func { - panic("given Action must be a func") + return errNonFuncAction } vals := reflect.ValueOf(action).Call([]reflect.Value{reflect.ValueOf(context)}) @@ -398,9 +408,7 @@ func HandleAction(action interface{}, context *Context) error { } if len(vals) > 1 { - fmt.Fprintln(os.Stderr, - "ERROR invalid Action signature. Must be `cli.ActionFunc`") - panic("given Action has invalid return signature") + return errInvalidActionSignature } if err, ok := reflect.ValueOf(vals[0]).Interface().(error); ok { From b453bf5940c0b5e469050dfe17b144a605f6d199 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 28 Apr 2016 11:03:10 -0400 Subject: [PATCH 21/29] Clarifying errors returned from HandleAction + tests --- app.go | 27 ++++++++++++++------ app_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/app.go b/app.go index c9830d4..0e78695 100644 --- a/app.go +++ b/app.go @@ -12,12 +12,14 @@ import ( ) var ( + appActionDeprecationURL = "https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-action-signature" + errNonFuncAction = NewExitError("ERROR invalid Action type. "+ "Must be a func of type `cli.ActionFunc`. "+ - "See https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-action-signature", 2) + fmt.Sprintf("See %s", appActionDeprecationURL), 2) errInvalidActionSignature = NewExitError("ERROR invalid Action signature. "+ "Must be `cli.ActionFunc`. "+ - "See https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-action-signature", 2) + fmt.Sprintf("See %s", appActionDeprecationURL), 2) ) // App is the main structure of a cli application. It is recommended that @@ -394,7 +396,18 @@ func (a Author) String() string { // 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) error { +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 } @@ -404,16 +417,16 @@ func HandleAction(action interface{}, context *Context) error { if len(vals) == 0 { fmt.Fprintln(os.Stderr, "DEPRECATED Action signature. Must be `cli.ActionFunc`") - return nil + return err } if len(vals) > 1 { return errInvalidActionSignature } - if err, ok := reflect.ValueOf(vals[0]).Interface().(error); ok { - return err + if retErr, ok := reflect.ValueOf(vals[0]).Interface().(error); ok { + return retErr } - return nil + return err } diff --git a/app_test.go b/app_test.go index 0241ad3..f947728 100644 --- a/app_test.go +++ b/app_test.go @@ -1115,3 +1115,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()) + } +} From c3a637061608c0680770d6465c6bf1118a331322 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 28 Apr 2016 11:07:53 -0400 Subject: [PATCH 22/29] Moving remaining details from #361 description to CHANGELOG.md --- CHANGELOG.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b03b52c..4830a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,28 @@ ### Added - Support for placeholders in flag usage strings -- Support for custom exit code by returning an error that fulfills -`cli.ExitCoder` to `cli.App.Run`. + +### 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 signature of `func(*cli.Context) error`, as defined by -`cli.ActionFunc`. +- the legacy signature for +`cli.App.Action` of `func(*cli.Context)`, which should now have a signature of +`func(*cli.Context) error`, as defined by `cli.ActionFunc`. ## [1.14.0] - 2016-04-03 (backfilled 2016-04-25) ### Added From 7651aa05a63f72d59f84211ce7382aad3cdb7139 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 28 Apr 2016 11:09:33 -0400 Subject: [PATCH 23/29] TRIVIAL specify *return* signature in deprecation message --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4830a80..1bf2212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,8 @@ produce a non-zero exit from `App.Run` `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 signature of -`func(*cli.Context) error`, as defined by `cli.ActionFunc`. +`cli.App.Action` of `func(*cli.Context)`, which should now have a return +signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. ## [1.14.0] - 2016-04-03 (backfilled 2016-04-25) ### Added From 4cae17cfe1ef51b3c2abaab464de3b08f6122855 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 28 Apr 2016 17:15:16 -0400 Subject: [PATCH 24/29] Ensure MultiError returned when both Before and After funcs given --- app.go | 18 ++++++++++-------- app_test.go | 27 ++++++++++++++++++++++++++- context_test.go | 3 ++- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app.go b/app.go index 0e78695..2bdbf4c 100644 --- a/app.go +++ b/app.go @@ -196,11 +196,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(err) + HandleExitCoder(beforeErr) + err = beforeErr return err } } @@ -286,7 +287,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { if a.OnUsageError != nil { err = a.OnUsageError(context, err, true) HandleExitCoder(err) - return nil + return err } else { fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") ShowSubcommandHelp(context) @@ -319,9 +320,10 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } if a.Before != nil { - err = a.Before(context) - if err != nil { - HandleExitCoder(err) + beforeErr := a.Before(context) + if beforeErr != nil { + HandleExitCoder(beforeErr) + err = beforeErr return err } } diff --git a/app_test.go b/app_test.go index a176de3..bf2887e 100644 --- a/app_test.go +++ b/app_test.go @@ -522,6 +522,30 @@ func TestApp_BeforeFunc(t *testing.T) { if counts.SubCommand != 0 { 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) { @@ -735,9 +759,10 @@ func TestApp_OrderOfOperations(t *testing.T) { }, } - app.Action = func(c *Context) { + app.Action = func(c *Context) error { counts.Total++ counts.Action = counts.Total + return nil } _ = app.Run([]string{"command", "--nope"}) diff --git a/context_test.go b/context_test.go index 836306c..cbad304 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") From 06c01a4bba6e9fd20b999096951b83469586d0c4 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 29 Apr 2016 03:01:57 -0400 Subject: [PATCH 25/29] A few tweaks based on feedback in #361 --- app.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app.go b/app.go index 2bdbf4c..4097a58 100644 --- a/app.go +++ b/app.go @@ -14,11 +14,13 @@ import ( 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. "+ - "Must be a func of type `cli.ActionFunc`. "+ + fmt.Sprintf("Must be a func of type `cli.ActionFunc`. %s", contactSysadmin)+ fmt.Sprintf("See %s", appActionDeprecationURL), 2) errInvalidActionSignature = NewExitError("ERROR invalid Action signature. "+ - "Must be `cli.ActionFunc`. "+ + fmt.Sprintf("Must be `cli.ActionFunc`. %s", contactSysadmin)+ fmt.Sprintf("See %s", appActionDeprecationURL), 2) ) @@ -419,7 +421,7 @@ func HandleAction(action interface{}, context *Context) (err error) { if len(vals) == 0 { fmt.Fprintln(os.Stderr, "DEPRECATED Action signature. Must be `cli.ActionFunc`") - return err + return nil } if len(vals) > 1 { From f72d40510730a83b18d501db0b6e2ec289d5529d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=8C=AF=E5=A8=81?= Date: Sat, 30 Apr 2016 10:42:07 +0800 Subject: [PATCH 26/29] Change Extras to Metadata --- app.go | 2 +- command.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index 503f838..93bbb04 100644 --- a/app.go +++ b/app.go @@ -63,7 +63,7 @@ type App struct { // Writer writer to write output to Writer io.Writer // Other custom info - Extras map[string]interface{} + Metadata map[string]interface{} } // Tries to find out when this binary was compiled. diff --git a/command.go b/command.go index 0ccffa2..9e713df 100644 --- a/command.go +++ b/command.go @@ -197,7 +197,7 @@ func (c Command) HasName(name string) bool { func (c Command) startApp(ctx *Context) error { app := NewApp() - app.Extras = ctx.App.Extras + app.Metadata = ctx.App.Metadata // set the name and usage app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) if c.HelpName == "" { From e2161d7f64270bd1ce2bfc7b6fabdfd41fd542e8 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 30 Apr 2016 09:54:44 -0400 Subject: [PATCH 27/29] Minor changelog update bits --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ade961f..434c88c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ ## [Unreleased] ### Added +- This file! - Support for placeholders in flag usage strings +- `App.Metadata` map for arbitrary data/state management ### Changed - The `App.Action` and `Command.Action` now prefer a return signature of From e059dc81880375ca5efca91549d2361a87df35ae Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 30 Apr 2016 11:46:47 -0400 Subject: [PATCH 28/29] Implement *Context.GlobalSet + relevant CHANGELOG entry --- CHANGELOG.md | 2 ++ context.go | 20 ++++++++++++++++++++ context_test.go | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 434c88c..a4d88ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - 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 diff --git a/context.go b/context.go index 93c000c..45013ac 100644 --- a/context.go +++ b/context.go @@ -146,6 +146,11 @@ 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 { @@ -252,6 +257,21 @@ 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 + } + + return nil +} + 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 20970b2..4c23271 100644 --- a/context_test.go +++ b/context_test.go @@ -211,3 +211,22 @@ func TestContext_Set(t *testing.T) { 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) +} From f3b589e89239925ddf57fda31d049dd31ce3f495 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 30 Apr 2016 12:22:32 -0400 Subject: [PATCH 29/29] Remove unreachable code --- context.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/context.go b/context.go index 45013ac..ef3d2fc 100644 --- a/context.go +++ b/context.go @@ -268,8 +268,6 @@ func globalContext(ctx *Context) *Context { } ctx = ctx.parentContext } - - return nil } func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet {