From 1c81757e1f5188fface135d3c7d73ebb47202c87 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 1 Jun 2016 13:28:27 -0400 Subject: [PATCH 01/10] Add more v1 API examples --- README.md | 376 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 373 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e6701e5..f8361ce 100644 --- a/README.md +++ b/README.md @@ -752,14 +752,14 @@ func main() { } ``` -#### To Enable +#### Enabling Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while setting the `PROG` variable to the name of your program: `PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` -#### To Distribute +#### Distribution Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename it to the name of the program you wish to add autocomplete support for (or @@ -775,7 +775,48 @@ Alternatively, you can just document that users should source the generic `autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set to the name of their program (as above). -### Generated Help Text Customization +#### Customization + +The default bash completion flag (`--generate-bash-completion`) is defined as +`cli.BashCompletionFlag`, and may be redefined if desired, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.BashCompletionFlag = cli.BoolFlag{ + Name: "compgen", + Hidden: true, + } + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "wat", + }, + } + app.Run(os.Args) +} +``` + +### Generated Help Text + +The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked +by the cli internals in order to print generated help text for the app, command, +or subcommand, and break execution. + +#### Customization All of the help text generation may be customized, and at multiple levels. The templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and @@ -841,6 +882,335 @@ VERSION: } ``` +The default flag may be customized to something other than `-h/--help` by +setting `cli.HelpFlag`, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.HelpFlag = cli.BoolFlag{ + Name: "halp, haaaaalp", + Usage: "HALP", + EnvVar: "SHOW_HALP,HALPPLZ", + } + + cli.NewApp().Run(os.Args) +} +``` + +### Version Flag + +The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which +is checked by the cli internals in order to print the `App.Version` via +`cli.VersionPrinter` and break execution. + +#### Customization + +The default flag may be cusomized to something other than `-v/--version` by +setting `cli.VersionFlag`, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.VersionFlag = cli.BoolFlag{ + Name: "print-version, V", + Usage: "print only the version", + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "v19.99.0" + app.Run(os.Args) +} +``` + +Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: + + +``` go +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +var ( + Revision = "fafafaf" +) + +func main() { + cli.VersionPrinter = func(c *cli.Context) { + fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "v19.99.0" + app.Run(os.Args) +} +``` + +#### Full API Example + +**NOTE**: This is a contrived (functioning) example meant strictly for API +demonstration purposes. Use of one's imagination is encouraged. + +``` go +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "time" + + "github.com/urfave/cli" +) + +func init() { + cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" + cli.CommandHelpTemplate += "\nYMMV\n" + cli.SubcommandHelpTemplate += "\nor something\n" + + cli.HelpFlag = cli.BoolFlag{Name: "halp"} + cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} + cli.VersionFlag = cli.BoolFlag{Name: "print-version, V"} + + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Fprintf(w, "best of luck to you\n") + } + cli.VersionPrinter = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "version=%s\n", c.App.Version) + } + cli.OsExiter = func(c int) { + fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", c) + } + cli.ErrWriter = ioutil.Discard + cli.FlagStringer = func(fl cli.Flag) string { + return fmt.Sprintf("\t\t%s", fl.GetName()) + } +} + +type hexWriter struct{} + +func (w *hexWriter) Write(p []byte) (int, error) { + for _, b := range p { + fmt.Printf("%x", b) + } + fmt.Printf("\n") + + return len(p), nil +} + +func main() { + app := cli.NewApp() + app.Name = "kənˈtrīv" + app.Version = "v19.99.0" + app.Compiled = time.Now() + app.Authors = []cli.Author{ + { + Name: "Example Human", + Email: "human@example.com", + }, + } + app.Copyright = "(c) 1999 Serious Enterprise" + app.HelpName = "contrive" + app.Usage = "demonstrate available API" + app.UsageText = "contrive - demonstrating the available API" + app.ArgsUsage = "[args and such]" + app.Commands = []cli.Command{ + { + Name: "doo", + Aliases: []string{"do"}, + Category: "motion", + Usage: "do the doo", + UsageText: "doo - does the dooing", + Description: "no really, there is a lot of dooing to be done", + ArgsUsage: "[arrgh]", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "forever, forevvarr"}, + }, + Subcommands: cli.Commands{ + { + Name: "wop", + Action: wopAction, + }, + }, + SkipFlagParsing: false, + HideHelp: false, + Hidden: false, + HelpName: "doo!", + BashComplete: func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "--better\n") + }, + Before: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "brace for impact\n") + return nil + }, + After: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "did we lose anyone?\n") + return nil + }, + Action: func(c *cli.Context) error { + c.Command.FullName() + c.Command.HasName("wop") + c.Command.Names() + c.Command.VisibleFlags() + fmt.Fprintf(c.App.Writer, "dodododododoodododddooooododododooo\n") + if c.Bool("forever") { + c.Command.Run(c) + } + return nil + }, + OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error { + fmt.Fprintf(c.App.Writer, "for shame\n") + return err + }, + }, + } + app.Flags = []cli.Flag{ + cli.BoolFlag{Name: "fancy"}, + cli.BoolTFlag{Name: "fancy"}, + cli.StringFlag{Name: "dance-move, d"}, + } + app.EnableBashCompletion = true + app.HideHelp = false + app.HideVersion = false + app.BashComplete = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") + } + app.Before = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "HEEEERE GOES\n") + return nil + } + app.After = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "Phew!\n") + return nil + } + app.CommandNotFound = func(c *cli.Context, command string) { + fmt.Fprintf(c.App.Writer, "Thar be no %q here.\n", command) + } + app.OnUsageError = func(c *cli.Context, err error, isSubcommand bool) error { + if isSubcommand { + return err + } + + fmt.Fprintf(c.App.Writer, "WRONG: %#v\n", err) + return nil + } + app.Action = func(c *cli.Context) error { + cli.DefaultAppComplete(c) + cli.HandleExitCoder(errors.New("not an exit coder, though")) + cli.ShowAppHelp(c) + cli.ShowCommandCompletions(c, "nope") + cli.ShowCommandHelp(c, "also-nope") + cli.ShowCompletions(c) + cli.ShowSubcommandHelp(c) + cli.ShowVersion(c) + + categories := c.App.Categories() + categories.AddCommand("sounds", cli.Command{ + Name: "bloop", + }) + + for _, category := range categories { + fmt.Fprintf(c.App.Writer, "%s\n", category.Name) + fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) + fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) + } + + c.App.Command("doo") + c.App.Run([]string{"app", "doo", "wop"}) + c.App.RunAsSubcommand(c) + c.App.Setup() + c.App.VisibleCategories() + c.App.VisibleCommands() + c.App.VisibleFlags() + + c.Args().First() + c.Args().Get(1) + c.Args().Present() + c.Args().Tail() + + set := flag.NewFlagSet("contrive", 0) + nc := cli.NewContext(c.App, set, c) + nc.Args() + nc.Bool("nope") + nc.BoolT("nerp") + nc.Duration("howlong") + nc.Float64("hay") + nc.Generic("bloop") + nc.Int("bips") + nc.IntSlice("blups") + nc.String("snurt") + nc.StringSlice("snurkles") + nc.GlobalBool("global-nope") + nc.GlobalBoolT("global-nerp") + nc.GlobalDuration("global-howlong") + nc.GlobalFloat64("global-hay") + nc.GlobalGeneric("global-bloop") + nc.GlobalInt("global-bips") + nc.GlobalIntSlice("global-blups") + nc.GlobalString("global-snurt") + nc.GlobalStringSlice("global-snurkles") + + nc.FlagNames() + nc.GlobalFlagNames() + nc.GlobalIsSet("wat") + nc.GlobalSet("wat", "nope") + nc.NArg() + nc.NumFlags() + nc.Parent() + nc.Set("wat", "also-nope") + + ec := cli.NewExitError("ohwell", 86) + fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) + return ec + } + app.Writer = &hexWriter{} + app.ErrWriter = &hexWriter{} + app.Metadata = map[string]interface{}{ + "layers": "many", + "explicable": false, + } +} + +func wopAction(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, ":wave: over here, eh\n") + return nil +} +``` + ## Contribution Guidelines Feel free to put up a pull request to fix a bug or maybe add a feature. I will From b5c48311fae486d73bd59bf944f6b031b9451a98 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 1 Jun 2016 14:06:51 -0400 Subject: [PATCH 02/10] Ensure the full API example really runs --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f8361ce..3b34449 100644 --- a/README.md +++ b/README.md @@ -983,6 +983,9 @@ func main() { **NOTE**: This is a contrived (functioning) example meant strictly for API demonstration purposes. Use of one's imagination is encouraged. + ``` go package main @@ -992,6 +995,7 @@ import ( "fmt" "io" "io/ioutil" + "os" "time" "github.com/urfave/cli" @@ -1100,7 +1104,7 @@ func main() { } app.Flags = []cli.Flag{ cli.BoolFlag{Name: "fancy"}, - cli.BoolTFlag{Name: "fancy"}, + cli.BoolTFlag{Name: "fancier"}, cli.StringFlag{Name: "dance-move, d"}, } app.EnableBashCompletion = true @@ -1150,8 +1154,13 @@ func main() { } c.App.Command("doo") - c.App.Run([]string{"app", "doo", "wop"}) - c.App.RunAsSubcommand(c) + if c.Bool("infinite") { + c.App.Run([]string{"app", "doo", "wop"}) + } + + if c.Bool("forevar") { + c.App.RunAsSubcommand(c) + } c.App.Setup() c.App.VisibleCategories() c.App.VisibleCommands() @@ -1195,14 +1204,19 @@ func main() { ec := cli.NewExitError("ohwell", 86) fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) + fmt.Printf("made it!\n") return ec } app.Writer = &hexWriter{} + // just kidding there + app.Writer = os.Stdout app.ErrWriter = &hexWriter{} app.Metadata = map[string]interface{}{ "layers": "many", "explicable": false, } + + app.Run(os.Args) } func wopAction(c *cli.Context) error { From a121e978f7ddf85ec44f2b1780569f094562ba9c Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Tue, 31 May 2016 16:00:14 -0700 Subject: [PATCH 03/10] Switch "printHelp" tabwriter padchar to space Using tabs for alignment is troubling if the output is used for anything besides display in a terminal (or if the user's terminal allows for adjustment of the default tab size), as noted by the documentation for `tabwriter` (https://golang.org/pkg/text/tabwriter/#Writer.Init): > (for correct-looking results, tabwidth must correspond to the tab width in the viewer displaying the result) The safer solution is to use `' '` as the `padchar`, which only carries the assumption of a fixed-width font (which is a more reasonable assumption than a fixed, constant tab size). --- help.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/help.go b/help.go index a9e7327..f2ddc54 100644 --- a/help.go +++ b/help.go @@ -191,7 +191,7 @@ func printHelp(out io.Writer, templ string, data interface{}) { "join": strings.Join, } - w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0) + w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) err := t.Execute(w, data) if err != nil { From 288b636ba31cc99171170ed7fb57f07f72ec6c04 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 1 Jun 2016 22:27:36 -0400 Subject: [PATCH 04/10] Modifying full API example for more migrator exercise --- README.md | 93 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 3b34449..9bbfe83 100644 --- a/README.md +++ b/README.md @@ -1042,7 +1042,7 @@ func main() { app.Version = "v19.99.0" app.Compiled = time.Now() app.Authors = []cli.Author{ - { + cli.Author{ Name: "Example Human", Email: "human@example.com", }, @@ -1053,7 +1053,7 @@ func main() { app.UsageText = "contrive - demonstrating the available API" app.ArgsUsage = "[args and such]" app.Commands = []cli.Command{ - { + cli.Command{ Name: "doo", Aliases: []string{"do"}, Category: "motion", @@ -1065,7 +1065,7 @@ func main() { cli.BoolFlag{Name: "forever, forevvarr"}, }, Subcommands: cli.Commands{ - { + cli.Command{ Name: "wop", Action: wopAction, }, @@ -1147,13 +1147,13 @@ func main() { Name: "bloop", }) - for _, category := range categories { + for _, category := range c.App.Categories() { fmt.Fprintf(c.App.Writer, "%s\n", category.Name) fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) } - c.App.Command("doo") + fmt.Printf("%#v\n", c.App.Command("doo")) if c.Bool("infinite") { c.App.Run([]string{"app", "doo", "wop"}) } @@ -1162,44 +1162,48 @@ func main() { c.App.RunAsSubcommand(c) } c.App.Setup() - c.App.VisibleCategories() - c.App.VisibleCommands() - c.App.VisibleFlags() + fmt.Printf("%#v\n", c.App.VisibleCategories()) + fmt.Printf("%#v\n", c.App.VisibleCommands()) + fmt.Printf("%#v\n", c.App.VisibleFlags()) - c.Args().First() - c.Args().Get(1) - c.Args().Present() - c.Args().Tail() + fmt.Printf("%#v\n", c.Args().First()) + if len(c.Args()) > 0 { + fmt.Printf("%#v\n", c.Args()[1]) + } + fmt.Printf("%#v\n", c.Args().Present()) + fmt.Printf("%#v\n", c.Args().Tail()) set := flag.NewFlagSet("contrive", 0) nc := cli.NewContext(c.App, set, c) - nc.Args() - nc.Bool("nope") - nc.BoolT("nerp") - nc.Duration("howlong") - nc.Float64("hay") - nc.Generic("bloop") - nc.Int("bips") - nc.IntSlice("blups") - nc.String("snurt") - nc.StringSlice("snurkles") - nc.GlobalBool("global-nope") - nc.GlobalBoolT("global-nerp") - nc.GlobalDuration("global-howlong") - nc.GlobalFloat64("global-hay") - nc.GlobalGeneric("global-bloop") - nc.GlobalInt("global-bips") - nc.GlobalIntSlice("global-blups") - nc.GlobalString("global-snurt") - nc.GlobalStringSlice("global-snurkles") - - nc.FlagNames() - nc.GlobalFlagNames() - nc.GlobalIsSet("wat") - nc.GlobalSet("wat", "nope") - nc.NArg() - nc.NumFlags() - nc.Parent() + + fmt.Printf("%#v\n", nc.Args()) + fmt.Printf("%#v\n", nc.Bool("nope")) + fmt.Printf("%#v\n", nc.BoolT("nerp")) + fmt.Printf("%#v\n", nc.Duration("howlong")) + fmt.Printf("%#v\n", nc.Float64("hay")) + fmt.Printf("%#v\n", nc.Generic("bloop")) + fmt.Printf("%#v\n", nc.Int("bips")) + fmt.Printf("%#v\n", nc.IntSlice("blups")) + fmt.Printf("%#v\n", nc.String("snurt")) + fmt.Printf("%#v\n", nc.StringSlice("snurkles")) + fmt.Printf("%#v\n", nc.GlobalBool("global-nope")) + fmt.Printf("%#v\n", nc.GlobalBoolT("global-nerp")) + fmt.Printf("%#v\n", nc.GlobalDuration("global-howlong")) + fmt.Printf("%#v\n", nc.GlobalFloat64("global-hay")) + fmt.Printf("%#v\n", nc.GlobalGeneric("global-bloop")) + fmt.Printf("%#v\n", nc.GlobalInt("global-bips")) + fmt.Printf("%#v\n", nc.GlobalIntSlice("global-blups")) + fmt.Printf("%#v\n", nc.GlobalString("global-snurt")) + fmt.Printf("%#v\n", nc.GlobalStringSlice("global-snurkles")) + + fmt.Printf("%#v\n", nc.FlagNames()) + fmt.Printf("%#v\n", nc.GlobalFlagNames()) + fmt.Printf("%#v\n", nc.GlobalIsSet("wat")) + fmt.Printf("%#v\n", nc.GlobalSet("wat", "nope")) + fmt.Printf("%#v\n", nc.NArg()) + fmt.Printf("%#v\n", nc.NumFlags()) + fmt.Printf("%#v\n", nc.Parent()) + nc.Set("wat", "also-nope") ec := cli.NewExitError("ohwell", 86) @@ -1207,13 +1211,16 @@ func main() { fmt.Printf("made it!\n") return ec } - app.Writer = &hexWriter{} - // just kidding there - app.Writer = os.Stdout - app.ErrWriter = &hexWriter{} + + if os.Getenv("HEXY") != "" { + app.Writer = &hexWriter{} + app.ErrWriter = &hexWriter{} + } + app.Metadata = map[string]interface{}{ "layers": "many", "explicable": false, + "whatever-values": 19.99, } app.Run(os.Args) From dedc6e14746fb4b1a99dbf8630785c648c0aa437 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Wed, 1 Jun 2016 20:10:13 -0700 Subject: [PATCH 05/10] Add note about #441 (replace tabs with spaces for alignment) in CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d51d2a..ac6d73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - testing on OS X - testing on Windows +### Changed +- Use spaces for alignment in help/usage output instead of tabs, making the + output alignment consistent regardless of tab width + ### Fixed - Printing of command aliases in help text From e97f74a5708775999e80bcd0026238c62fca3ec2 Mon Sep 17 00:00:00 2001 From: James Cunningham Date: Tue, 7 Jun 2016 16:28:10 +0100 Subject: [PATCH 06/10] fix ActionFunc signature of ShowAppHelp action --- help.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/help.go b/help.go index f2ddc54..0f4cf14 100644 --- a/help.go +++ b/help.go @@ -117,8 +117,9 @@ var HelpPrinter helpPrinter = printHelp var VersionPrinter = printVersion // ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) { +func ShowAppHelp(c *Context) error { HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + return nil } // DefaultAppComplete prints the list of subcommands as the default app completion method From f621deee5a7eb89d23ef3e74c0bec74c65c854ea Mon Sep 17 00:00:00 2001 From: Joshua Rubin Date: Wed, 8 Jun 2016 04:27:57 -0600 Subject: [PATCH 07/10] fix panic getting visible flags --- flag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flag.go b/flag.go index 1e8112e..2ed875b 100644 --- a/flag.go +++ b/flag.go @@ -512,7 +512,7 @@ func (f Float64Flag) GetName() string { func visibleFlags(fl []Flag) []Flag { visible := []Flag{} for _, flag := range fl { - if !reflect.ValueOf(flag).FieldByName("Hidden").Bool() { + if !reflect.Indirect(reflect.ValueOf(flag)).FieldByName("Hidden").Bool() { visible = append(visible, flag) } } From 7cd5bed6cb6f7941dc875d61d1ba24a72a448f21 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 8 Jun 2016 10:34:44 -0400 Subject: [PATCH 08/10] Backporting flagValue func from v2 branch --- flag.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/flag.go b/flag.go index 2ed875b..b087e25 100644 --- a/flag.go +++ b/flag.go @@ -512,7 +512,7 @@ func (f Float64Flag) GetName() string { func visibleFlags(fl []Flag) []Flag { visible := []Flag{} for _, flag := range fl { - if !reflect.Indirect(reflect.ValueOf(flag)).FieldByName("Hidden").Bool() { + if !flagValue(flag).FieldByName("Hidden").Bool() { visible = append(visible, flag) } } @@ -578,8 +578,16 @@ func withEnvHint(envVar, str string) string { return str + envText } -func stringifyFlag(f Flag) string { +func flagValue(f Flag) reflect.Value { fv := reflect.ValueOf(f) + for fv.Kind() == reflect.Ptr { + fv = reflect.Indirect(fv) + } + return fv +} + +func stringifyFlag(f Flag) string { + fv := flagValue(f) switch f.(type) { case IntSliceFlag: From d645386c59ca8482ad37255caefe65ef3422dda2 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 8 Jun 2016 17:18:25 -0400 Subject: [PATCH 09/10] Mention #448 fix in change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac6d73a..b6da886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixed - Printing of command aliases in help text +- Printing of visible flags for both struct and struct pointer flags ## [1.17.0] - 2016-05-09 ### Added From 5a3515fdf81fdb1ca284dfbc130a8662a2759d73 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 9 Jun 2016 17:32:00 -0400 Subject: [PATCH 10/10] Focus on catching fewer panics in HandleAction so that "unknown" panics can still bubble up. --- app.go | 13 ++++++++----- app_test.go | 13 +++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 9c7f679..a17300d 100644 --- a/app.go +++ b/app.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "sort" + "strings" "time" ) @@ -464,11 +465,13 @@ func (a Author) String() string { 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) + // Try to detect a known reflection error from *this scope*, rather than + // swallowing all panics that may happen when calling an Action func. + s := fmt.Sprintf("%v", r) + if strings.HasPrefix(s, "reflect: ") && strings.Contains(s, "too many input arguments") { + err = NewExitError(fmt.Sprintf("ERROR unknown Action error: %v. See %s", r, appActionDeprecationURL), 2) + } else { + panic(r) } } }() diff --git a/app_test.go b/app_test.go index 42c852e..ee8cc35 100644 --- a/app_test.go +++ b/app_test.go @@ -1452,3 +1452,16 @@ func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) { t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) } } + +func TestHandleAction_WithUnknownPanic(t *testing.T) { + defer func() { refute(t, recover(), nil) }() + + var fn ActionFunc + + app := NewApp() + app.Action = func(ctx *Context) error { + fn(ctx) + return nil + } + HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil)) +}