From 683e911d61b0be794eec4200056f0451b3976622 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 31 Aug 2014 11:40:31 -0700 Subject: [PATCH 01/22] Return error from app and command actions. See #66. This introduces a significant breaking change, and I don't expect it to be merged off-hand. I do think that it's worth discussion, since it seems like a very idiomatic choice in terms of how errors would be handled. A similar backwards-incompatible change was introduced in e6e64114, allowing the app's Run method to return an error. --- app.go | 5 ++--- app_test.go | 36 ++++++++++++++++++++++++------------ cli_test.go | 18 ++++++++++++------ command.go | 5 ++--- command_test.go | 4 ++-- flag_test.go | 48 ++++++++++++++++++++++++++++++++---------------- help.go | 6 ++++-- 7 files changed, 78 insertions(+), 44 deletions(-) diff --git a/app.go b/app.go index 66e541c..145611b 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ type App struct { // If a non-nil error is returned, no subcommands are run Before func(context *Context) error // The action to execute when no subcommands are specified - Action func(context *Context) + Action func(context *Context) error // Execute this function if the proper command cannot be found CommandNotFound func(context *Context, command string) // Compilation date @@ -127,8 +127,7 @@ func (a *App) Run(arguments []string) error { } // Run default Action - a.Action(context) - return nil + return a.Action(context) } // Another entry point to the cli app, takes care of passing arguments and error handling diff --git a/app_test.go b/app_test.go index 81d1174..cefa5fd 100644 --- a/app_test.go +++ b/app_test.go @@ -17,8 +17,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) error { fmt.Printf("Hello %v\n", c.String("name")) + return nil } app.Run(os.Args) // Output: @@ -49,8 +50,9 @@ func ExampleAppSubcommand() { Usage: "Name of the person to greet", }, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { fmt.Println("Hello,", c.String("name")) + return nil }, }, }, @@ -77,8 +79,9 @@ func ExampleAppHelp() { ShortName: "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) error { fmt.Printf("i like to describe things") + return nil }, }, } @@ -107,15 +110,17 @@ func ExampleAppBashComplete() { ShortName: "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) error { fmt.Printf("i like to describe things") + return nil }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { fmt.Printf("the next example") + return nil }, }, } @@ -133,8 +138,9 @@ func TestApp_Run(t *testing.T) { s := "" app := cli.NewApp() - app.Action = func(c *cli.Context) { + app.Action = func(c *cli.Context) error { s = s + c.Args().First() + return nil } err := app.Run([]string{"command", "foo"}) @@ -179,9 +185,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) error { parsedOption = c.String("option") firstArg = c.Args().First() + return nil }, } app.Commands = []cli.Command{command} @@ -199,8 +206,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) error { meters = c.Float64("height") + return nil } app.Run([]string{"", "--height", "1.93"}) @@ -219,11 +227,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) error { parsedIntSlice = c.IntSlice("p") parsedStringSlice = c.StringSlice("ip") parsedOption = c.String("option") firstArg = c.Args().First() + return nil }, } app.Commands = []cli.Command{command} @@ -285,8 +294,9 @@ func TestApp_BeforeFunc(t *testing.T) { app.Commands = []cli.Command{ cli.Command{ Name: "sub", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { subcommandRun = true + return nil }, }, } @@ -381,8 +391,9 @@ func TestAppCommandNotFound(t *testing.T) { app.Commands = []cli.Command{ cli.Command{ Name: "bar", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { subcommandRun = true + return nil }, }, } @@ -407,10 +418,11 @@ func TestGlobalFlagsInSubcommands(t *testing.T) { Subcommands: []cli.Command{ { Name: "bar", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { if c.GlobalBool("debug") { subcommandRun = true } + return nil }, }, }, diff --git a/cli_test.go b/cli_test.go index 879a793..e330810 100644 --- a/cli_test.go +++ b/cli_test.go @@ -15,16 +15,18 @@ func Example() { Name: "add", ShortName: "a", Usage: "add a task to the list", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { println("added task: ", c.Args().First()) + return nil }, }, { Name: "complete", ShortName: "c", Usage: "complete a task on the list", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { println("completed task: ", c.Args().First()) + return nil }, }, } @@ -54,8 +56,9 @@ func ExampleSubcommand() { Usage: "Name of the person to greet", }, }, - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { println("Hello, ", c.String("name")) + return nil }, }, { 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) error { println("Hola, ", c.String("surname")) + return nil }, }, { 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) error { println("Bonjour, ", c.String("nickname")) + return nil }, }, }, }, { Name: "bye", Usage: "says goodbye", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { println("bye") + return nil }, }, } diff --git a/command.go b/command.go index 5622b38..362db59 100644 --- a/command.go +++ b/command.go @@ -22,7 +22,7 @@ type Command struct { // If a non-nil error is returned, no sub-subcommands are run Before func(context *Context) error // The function to call when this command is invoked - Action func(context *Context) + Action func(context *Context) error // List of child commands Subcommands []Command // List of flags to parse @@ -98,8 +98,7 @@ func (c Command) Run(ctx *Context) error { return nil } context.Command = c - c.Action(context) - return nil + return c.Action(context) } // Returns true if Command.Name or Command.ShortName matches given name diff --git a/command_test.go b/command_test.go index c0f556a..06c2203 100644 --- a/command_test.go +++ b/command_test.go @@ -20,7 +20,7 @@ func TestCommandDoNotIgnoreFlags(t *testing.T) { ShortName: "tc", Usage: "this is for testing", Description: "testing", - Action: func(_ *cli.Context) {}, + Action: func(_ *cli.Context) error { return nil }, } err := command.Run(c) @@ -40,7 +40,7 @@ func TestCommandIgnoreFlags(t *testing.T) { ShortName: "tc", Usage: "this is for testing", Description: "testing", - Action: func(_ *cli.Context) {}, + Action: func(_ *cli.Context) error { return nil }, SkipFlagParsing: true, } err := command.Run(c) diff --git a/flag_test.go b/flag_test.go index bc5059c..9aa1cff 100644 --- a/flag_test.go +++ b/flag_test.go @@ -297,13 +297,14 @@ func TestParseMultiString(t *testing.T) { Flags: []cli.Flag{ cli.StringFlag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) error { if ctx.String("serve") != "10" { t.Errorf("main name not set") } if ctx.String("s") != "10" { t.Errorf("short name not set") } + return nil }, }).Run([]string{"run", "-s", "10"}) } @@ -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) error { if ctx.String("count") != "20" { t.Errorf("main name not set") } if ctx.String("c") != "20" { t.Errorf("short name not set") } + return nil }, }).Run([]string{"run"}) } @@ -330,13 +332,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) 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 nil }, }).Run([]string{"run", "-s", "10", "-s", "20"}) } @@ -348,13 +351,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) error { if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { t.Errorf("short name not set from env") } + return nil }, }).Run([]string{"run"}) } @@ -364,13 +368,14 @@ func TestParseMultiInt(t *testing.T) { Flags: []cli.Flag{ cli.IntFlag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) error { if ctx.Int("serve") != 10 { t.Errorf("main name not set") } if ctx.Int("s") != 10 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "-s", "10"}) @@ -382,13 +387,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) error { if ctx.Int("timeout") != 10 { t.Errorf("main name not set") } if ctx.Int("t") != 10 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run"}) @@ -399,13 +405,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) error { if !reflect.DeepEqual(ctx.IntSlice("serve"), []int{10, 20}) { t.Errorf("main name not set") } if !reflect.DeepEqual(ctx.IntSlice("s"), []int{10, 20}) { t.Errorf("short name not set") } + return nil }, }).Run([]string{"run", "-s", "10", "-s", "20"}) } @@ -417,13 +424,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) error { if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) { t.Errorf("short name not set from env") } + return nil }, }).Run([]string{"run"}) } @@ -433,13 +441,14 @@ func TestParseMultiFloat64(t *testing.T) { Flags: []cli.Flag{ cli.Float64Flag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) error { if ctx.Float64("serve") != 10.2 { t.Errorf("main name not set") } if ctx.Float64("s") != 10.2 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "-s", "10.2"}) @@ -451,13 +460,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) error { if ctx.Float64("timeout") != 15.5 { t.Errorf("main name not set") } if ctx.Float64("t") != 15.5 { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run"}) @@ -468,13 +478,14 @@ func TestParseMultiBool(t *testing.T) { Flags: []cli.Flag{ cli.BoolFlag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) error { if ctx.Bool("serve") != true { t.Errorf("main name not set") } if ctx.Bool("s") != true { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "--serve"}) @@ -486,13 +497,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) error { if ctx.Bool("debug") != true { t.Errorf("main name not set from env") } if ctx.Bool("d") != true { t.Errorf("short name not set from env") } + return nil }, } a.Run([]string{"run"}) @@ -503,13 +515,14 @@ func TestParseMultiBoolT(t *testing.T) { Flags: []cli.Flag{ cli.BoolTFlag{Name: "serve, s"}, }, - Action: func(ctx *cli.Context) { + Action: func(ctx *cli.Context) error { if ctx.BoolT("serve") != true { t.Errorf("main name not set") } if ctx.BoolT("s") != true { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "--serve"}) @@ -521,13 +534,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) error { if ctx.BoolT("debug") != false { t.Errorf("main name not set from env") } if ctx.BoolT("d") != false { t.Errorf("short name not set from env") } + return nil }, } a.Run([]string{"run"}) @@ -556,13 +570,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) error { if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"10", "20"}) { t.Errorf("main name not set") } if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"10", "20"}) { t.Errorf("short name not set") } + return nil }, } a.Run([]string{"run", "-s", "10,20"}) @@ -574,13 +589,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) error { if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"20", "30"}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"20", "30"}) { t.Errorf("short name not set from env") } + return nil }, } a.Run([]string{"run"}) diff --git a/help.go b/help.go index 5020cb6..4997dde 100644 --- a/help.go +++ b/help.go @@ -69,13 +69,14 @@ var helpCommand = Command{ Name: "help", ShortName: "h", Usage: "Shows a list of commands or help for one command", - Action: func(c *Context) { + Action: func(c *Context) error { args := c.Args() if args.Present() { ShowCommandHelp(c, args.First()) } else { ShowAppHelp(c) } + return nil }, } @@ -83,13 +84,14 @@ var helpSubcommand = Command{ Name: "help", ShortName: "h", Usage: "Shows a list of commands or help for one command", - Action: func(c *Context) { + Action: func(c *Context) error { args := c.Args() if args.Present() { ShowCommandHelp(c, args.First()) } else { ShowSubcommandHelp(c) } + return nil }, } From 35ac968c9e1d4b82f46dbf9b42399f3049a5d8e5 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 12 May 2015 17:08:05 -0700 Subject: [PATCH 02/22] upate string slice usage Signed-off-by: Victor Vieux --- flag.go | 2 +- flag_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flag.go b/flag.go index 2511586..bac044b 100644 --- a/flag.go +++ b/flag.go @@ -124,7 +124,7 @@ type StringSliceFlag struct { func (f StringSliceFlag) String() string { firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") pref := prefixFor(firstName) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" ...", f.Usage)) } func (f StringSliceFlag) Apply(set *flag.FlagSet) { diff --git a/flag_test.go b/flag_test.go index f0f096a..4c7fd5d 100644 --- a/flag_test.go +++ b/flag_test.go @@ -75,22 +75,22 @@ var stringSliceFlagTests = []struct { s := &cli.StringSlice{} s.Set("") return s - }(), "--help [--help option --help option]\t"}, + }(), "--help [--help ...]\t"}, {"h", func() *cli.StringSlice { s := &cli.StringSlice{} s.Set("") return s - }(), "-h [-h option -h option]\t"}, + }(), "-h [-h ...]\t"}, {"h", func() *cli.StringSlice { s := &cli.StringSlice{} s.Set("") return s - }(), "-h [-h option -h option]\t"}, + }(), "-h [-h ...]\t"}, {"test", func() *cli.StringSlice { s := &cli.StringSlice{} s.Set("Something") return s - }(), "--test [--test option --test option]\t"}, + }(), "--test [--test ...]\t"}, } func TestStringSliceFlagHelpOutput(t *testing.T) { From 36a5323a47c4c22c23f035cf8aa1eadd48c56715 Mon Sep 17 00:00:00 2001 From: rob boll Date: Fri, 29 Apr 2016 12:33:59 -0400 Subject: [PATCH 03/22] altsrc: allow nested defaults in yaml files Previously, defaults specified as nested keys in a yaml file would not be recognized, i.e. `top: \n bottom: key` would not be accessible using the name `top.bottom`, but `top.bottom: key` would. These changes support using nested keys by traversing the configuration tree if the key name uses '.' as a delimiter. --- altsrc/flag_test.go | 2 +- altsrc/map_input_source.go | 100 ++++++++++++++++++++++++++- altsrc/yaml_command_test.go | 132 ++++++++++++++++++++++++++++++++++++ altsrc/yaml_file_loader.go | 2 +- 4 files changed, 232 insertions(+), 4 deletions(-) diff --git a/altsrc/flag_test.go b/altsrc/flag_test.go index ac4d1f5..4e25be6 100644 --- a/altsrc/flag_test.go +++ b/altsrc/flag_test.go @@ -296,7 +296,7 @@ func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) { } func runTest(t *testing.T, test testApplyInputSource) *cli.Context { - inputSource := &MapInputSource{valueMap: map[string]interface{}{test.FlagName: test.MapValue}} + inputSource := &MapInputSource{valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue}} set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError) c := cli.NewContext(nil, set, nil) if test.EnvVarName != "" && test.EnvVarValue != "" { diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go index f1670fb..19f87af 100644 --- a/altsrc/map_input_source.go +++ b/altsrc/map_input_source.go @@ -3,6 +3,7 @@ package altsrc import ( "fmt" "reflect" + "strings" "time" "github.com/codegangsta/cli" @@ -11,7 +12,31 @@ import ( // MapInputSource implements InputSourceContext to return // data from the map that is loaded. type MapInputSource struct { - valueMap map[string]interface{} + valueMap map[interface{}]interface{} +} + +// nestedVal checks if the name has '.' delimiters. +// If so, it tries to traverse the tree by the '.' delimited sections to find +// a nested value for the key. +func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool) { + if sections := strings.Split(name, "."); len(sections) > 1 { + node := tree + for _, section := range sections[:len(sections)-1] { + if child, ok := node[section]; !ok { + return nil, false + } else { + if ctype, ok := child.(map[interface{}]interface{}); !ok { + return nil, false + } else { + node = ctype + } + } + } + if val, ok := node[sections[len(sections)-1]]; ok { + return val, true + } + } + return nil, false } // Int returns an int from the map if it exists otherwise returns 0 @@ -22,7 +47,14 @@ func (fsm *MapInputSource) Int(name string) (int, error) { if !isType { return 0, incorrectTypeForFlagError(name, "int", otherGenericValue) } - + return otherValue, nil + } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.(int) + if !isType { + return 0, incorrectTypeForFlagError(name, "int", nestedGenericValue) + } return otherValue, nil } @@ -39,6 +71,14 @@ func (fsm *MapInputSource) Duration(name string) (time.Duration, error) { } return otherValue, nil } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.(time.Duration) + if !isType { + return 0, incorrectTypeForFlagError(name, "duration", nestedGenericValue) + } + return otherValue, nil + } return 0, nil } @@ -53,6 +93,14 @@ func (fsm *MapInputSource) Float64(name string) (float64, error) { } return otherValue, nil } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.(float64) + if !isType { + return 0, incorrectTypeForFlagError(name, "float64", nestedGenericValue) + } + return otherValue, nil + } return 0, nil } @@ -67,6 +115,14 @@ func (fsm *MapInputSource) String(name string) (string, error) { } return otherValue, nil } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.(string) + if !isType { + return "", incorrectTypeForFlagError(name, "string", nestedGenericValue) + } + return otherValue, nil + } return "", nil } @@ -81,6 +137,14 @@ func (fsm *MapInputSource) StringSlice(name string) ([]string, error) { } return otherValue, nil } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.([]string) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]string", nestedGenericValue) + } + return otherValue, nil + } return nil, nil } @@ -95,6 +159,14 @@ func (fsm *MapInputSource) IntSlice(name string) ([]int, error) { } return otherValue, nil } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.([]int) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]int", nestedGenericValue) + } + return otherValue, nil + } return nil, nil } @@ -109,6 +181,14 @@ func (fsm *MapInputSource) Generic(name string) (cli.Generic, error) { } return otherValue, nil } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.(cli.Generic) + if !isType { + return nil, incorrectTypeForFlagError(name, "cli.Generic", nestedGenericValue) + } + return otherValue, nil + } return nil, nil } @@ -123,6 +203,14 @@ func (fsm *MapInputSource) Bool(name string) (bool, error) { } return otherValue, nil } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.(bool) + if !isType { + return false, incorrectTypeForFlagError(name, "bool", nestedGenericValue) + } + return otherValue, nil + } return false, nil } @@ -137,6 +225,14 @@ func (fsm *MapInputSource) BoolT(name string) (bool, error) { } return otherValue, nil } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := nestedGenericValue.(bool) + if !isType { + return true, incorrectTypeForFlagError(name, "bool", nestedGenericValue) + } + return otherValue, nil + } return true, nil } diff --git a/altsrc/yaml_command_test.go b/altsrc/yaml_command_test.go index 275bc64..29ead8d 100644 --- a/altsrc/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -76,6 +76,40 @@ func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { expect(t, err, nil) } +func TestCommandYamlFileTestGlobalEnvVarWinsNested(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte(`top: + test: 15`), 0666) + defer os.Remove("current.yaml") + + os.Setenv("THE_TEST", "10") + defer os.Setenv("THE_TEST", "") + test := []string{"test-cmd", "--load", "current.yaml"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("top.test") + expect(t, val, 10) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "top.test", EnvVar: "THE_TEST"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) + + err := command.Run(c) + + expect(t, err, nil) +} + func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { app := cli.NewApp() set := flag.NewFlagSet("test", 0) @@ -107,6 +141,38 @@ func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { expect(t, err, nil) } +func TestCommandYamlFileTestSpecifiedFlagWinsNested(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte(`top: + test: 15`), 0666) + defer os.Remove("current.yaml") + + test := []string{"test-cmd", "--load", "current.yaml", "--top.test", "7"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("top.test") + expect(t, val, 7) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "top.test"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) + + err := command.Run(c) + + expect(t, err, nil) +} + func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { app := cli.NewApp() set := flag.NewFlagSet("test", 0) @@ -138,6 +204,38 @@ func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { expect(t, err, nil) } +func TestCommandYamlFileTestDefaultValueFileWinsNested(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte(`top: + test: 15`), 0666) + defer os.Remove("current.yaml") + + test := []string{"test-cmd", "--load", "current.yaml"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("top.test") + expect(t, val, 15) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) + + err := command.Run(c) + + expect(t, err, nil) +} + func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T) { app := cli.NewApp() set := flag.NewFlagSet("test", 0) @@ -170,3 +268,37 @@ func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T expect(t, err, nil) } + +func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWinsNested(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte(`top: + test: 15`), 0666) + defer os.Remove("current.yaml") + + os.Setenv("THE_TEST", "11") + defer os.Setenv("THE_TEST", "") + + test := []string{"test-cmd", "--load", "current.yaml"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("top.test") + expect(t, val, 11) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7, EnvVar: "THE_TEST"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) + err := command.Run(c) + + expect(t, err, nil) +} diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index 4fb0965..01797ad 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -24,7 +24,7 @@ type yamlSourceContext struct { // NewYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath. func NewYamlSourceFromFile(file string) (InputSourceContext, error) { ysc := &yamlSourceContext{FilePath: file} - var results map[string]interface{} + var results map[interface{}]interface{} err := readCommandYaml(ysc.FilePath, &results) if err != nil { return nil, fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", ysc.FilePath, err.Error()) From 896d2fd3c174b3bbd2c1f8587b98ef84d7cb8a29 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 30 Apr 2016 13:27:11 -0400 Subject: [PATCH 04/22] Add a note to CHANGELOG about dot-delimited YAML key lookup --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d88ae..50f4140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - `App.Metadata` map for arbitrary data/state management - `Set` and `GlobalSet` methods on `*cli.Context` for altering values after parsing. +- Support for nested lookup of dot-delimited keys in structures loaded from +YAML. ### Changed - The `App.Action` and `Command.Action` now prefer a return signature of From a9afed5b1522421b2d5f0c93b4b1d13349fd095d Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 30 Apr 2016 13:48:08 -0400 Subject: [PATCH 05/22] Prepping v1.15.0 release --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f4140..5760983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## [Unreleased] +## [1.15.0] - 2016-04-30 ### Added - This file! - Support for placeholders in flag usage strings @@ -250,7 +251,8 @@ signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. ### Added - Initial implementation. -[Unreleased]: https://github.com/codegangsta/cli/compare/v1.14.0...HEAD +[Unreleased]: https://github.com/codegangsta/cli/compare/v1.15.0...HEAD +[1.15.0]: https://github.com/codegangsta/cli/compare/v1.14.0...v1.15.0 [1.14.0]: https://github.com/codegangsta/cli/compare/v1.13.0...v1.14.0 [1.13.0]: https://github.com/codegangsta/cli/compare/v1.12.0...v1.13.0 [1.12.0]: https://github.com/codegangsta/cli/compare/v1.11.1...v1.12.0 From 99431669d05b813fd8ec3d922c0b440cf9c36705 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Sun, 15 Mar 2015 03:11:52 -0700 Subject: [PATCH 06/22] New ``Hide`` variable for all Flags This is a way to provide hidden flags for app, command and subcommands For example: --generate-bash-completion global flag shouldn't be printed along with other flags as it might generally confuse people into thinking that 'generate' in-fact would generate a bash completion file for them to be used along with their app. Also in general one would want to hide some flags for their apps. --- flag.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ help.go | 30 +++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/flag.go b/flag.go index 42f6a61..d18df0a 100644 --- a/flag.go +++ b/flag.go @@ -13,6 +13,7 @@ import ( // This flag enables bash-completion for all commands and subcommands var BashCompletionFlag = BoolFlag{ Name: "generate-bash-completion", + Hide: true, } // This flag prints the version for the application @@ -37,6 +38,7 @@ type Flag interface { // Apply Flag settings to the given flag set Apply(*flag.FlagSet) GetName() string + isNotHidden() bool } func flagSet(name string, flags []Flag) *flag.FlagSet { @@ -68,6 +70,7 @@ type GenericFlag struct { Value Generic Usage string EnvVar string + Hide bool } // String returns the string representation of the generic flag to display the @@ -112,6 +115,10 @@ func (f GenericFlag) GetName() string { return f.Name } +func (f GenericFlag) isNotHidden() bool { + return !f.Hide +} + // StringSlice is an opaque type for []string to satisfy flag.Value type StringSlice []string @@ -138,6 +145,7 @@ type StringSliceFlag struct { Value *StringSlice Usage string EnvVar string + Hide bool } // String returns the usage @@ -177,6 +185,10 @@ func (f StringSliceFlag) GetName() string { return f.Name } +func (f StringSliceFlag) isNotHidden() bool { + return !f.Hide +} + // StringSlice is an opaque type for []int to satisfy flag.Value type IntSlice []int @@ -208,6 +220,7 @@ type IntSliceFlag struct { Value *IntSlice Usage string EnvVar string + Hide bool } // String returns the usage @@ -250,12 +263,17 @@ func (f IntSliceFlag) GetName() string { return f.Name } +func (f IntSliceFlag) isNotHidden() bool { + return !f.Hide +} + // BoolFlag is a switch that defaults to false type BoolFlag struct { Name string Usage string EnvVar string Destination *bool + Hide bool } // String returns a readable representation of this value (for usage defaults) @@ -293,6 +311,10 @@ func (f BoolFlag) GetName() string { return f.Name } +func (f BoolFlag) isNotHidden() bool { + return !f.Hide +} + // BoolTFlag this represents a boolean flag that is true by default, but can // still be set to false by --some-flag=false type BoolTFlag struct { @@ -300,6 +322,7 @@ type BoolTFlag struct { Usage string EnvVar string Destination *bool + Hide bool } // String returns a readable representation of this value (for usage defaults) @@ -337,6 +360,10 @@ func (f BoolTFlag) GetName() string { return f.Name } +func (f BoolTFlag) isNotHidden() bool { + return !f.Hide +} + // StringFlag represents a flag that takes as string value type StringFlag struct { Name string @@ -344,6 +371,7 @@ type StringFlag struct { Usage string EnvVar string Destination *string + Hide bool } // String returns the usage @@ -385,6 +413,10 @@ func (f StringFlag) GetName() string { return f.Name } +func (f StringFlag) isNotHidden() bool { + return !f.Hide +} + // IntFlag is a flag that takes an integer // Errors if the value provided cannot be parsed type IntFlag struct { @@ -393,6 +425,7 @@ type IntFlag struct { Usage string EnvVar string Destination *int + Hide bool } // String returns the usage @@ -429,6 +462,10 @@ func (f IntFlag) GetName() string { return f.Name } +func (f IntFlag) isNotHidden() bool { + return !f.Hide +} + // DurationFlag is a flag that takes a duration specified in Go's duration // format: https://golang.org/pkg/time/#ParseDuration type DurationFlag struct { @@ -437,6 +474,7 @@ type DurationFlag struct { Usage string EnvVar string Destination *time.Duration + Hide bool } // String returns a readable representation of this value (for usage defaults) @@ -473,6 +511,10 @@ func (f DurationFlag) GetName() string { return f.Name } +func (f DurationFlag) isNotHidden() bool { + return !f.Hide +} + // Float64Flag is a flag that takes an float value // Errors if the value provided cannot be parsed type Float64Flag struct { @@ -481,6 +523,7 @@ type Float64Flag struct { Usage string EnvVar string Destination *float64 + Hide bool } // String returns the usage @@ -516,6 +559,10 @@ func (f Float64Flag) GetName() string { return f.Name } +func (f Float64Flag) isNotHidden() bool { + return !f.Hide +} + func prefixFor(name string) (prefix string) { if len(name) == 1 { prefix = "-" diff --git a/help.go b/help.go index a895e6c..507fbe5 100644 --- a/help.go +++ b/help.go @@ -114,7 +114,15 @@ var HelpPrinter helpPrinter = printHelp var VersionPrinter = printVersion func ShowAppHelp(c *Context) { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + // Make a copy of c.App context + app := *c.App + app.Flags = make([]Flag, 0) + for _, flag := range c.App.Flags { + if flag.isNotHidden() { + app.Flags = append(app.Flags, flag) + } + } + HelpPrinter(c.App.Writer, AppHelpTemplate, app) } // Prints the list of subcommands as the default app completion method @@ -130,13 +138,29 @@ func DefaultAppComplete(c *Context) { func ShowCommandHelp(ctx *Context, command string) { // show the subcommand help for a command with subcommands if command == "" { - HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) + // Make a copy of c.App context + app := *c.App + app.Flags = make([]Flag, 0) + for _, flag := range c.App.Flags { + if flag.isNotHidden() { + app.Flags = append(app.Flags, flag) + } + } + HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, app) return } for _, c := range ctx.App.Commands { if c.HasName(command) { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + // Make a copy of command context + c0 := c + c0.Flags = make([]Flag, 0) + for _, flag := range c.Flags { + if flag.isNotHidden() { + c0.Flags = append(c0.Flags, flag) + } + } + HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c0) return } } From fed78b8bab633ee554cfa229e9975ae731d40f5d Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 1 May 2016 08:36:17 -0400 Subject: [PATCH 07/22] Rework of hidden flag impl plus some Action func corrections --- README.md | 16 ++++++--- altsrc/yaml_command_test.go | 12 ++++--- app.go | 5 +++ cli.go | 2 +- command.go | 5 +++ flag.go | 66 +++++++++++-------------------------- help.go | 48 +++++++-------------------- 7 files changed, 62 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 777b0a8..5db1b6f 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,7 @@ Here is a more complete sample of a command using YAML support: Description: "testing", Action: func(c *cli.Context) error { // Action to run + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "test"}), @@ -322,16 +323,18 @@ app.Commands = []cli.Command{ Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { fmt.Println("added task: ", c.Args().First()) + return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { fmt.Println("completed task: ", c.Args().First()) + return nil }, }, { @@ -342,15 +345,17 @@ app.Commands = []cli.Command{ { Name: "add", Usage: "add a new template", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { fmt.Println("new task template: ", c.Args().First()) + return nil }, }, { Name: "remove", Usage: "remove an existing template", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { fmt.Println("removed task template: ", c.Args().First()) + return nil }, }, }, @@ -450,8 +455,9 @@ app.Commands = []cli.Command{ Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { fmt.Println("completed task: ", c.Args().First()) + return nil }, BashComplete: func(c *cli.Context) { // This will complete if no args are passed diff --git a/altsrc/yaml_command_test.go b/altsrc/yaml_command_test.go index 90d7823..519bd81 100644 --- a/altsrc/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -97,9 +97,10 @@ func TestCommandYamlFileTestGlobalEnvVarWinsNested(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("top.test") expect(t, val, 10) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "top.test", EnvVar: "THE_TEST"}), @@ -161,9 +162,10 @@ func TestCommandYamlFileTestSpecifiedFlagWinsNested(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("top.test") expect(t, val, 7) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "top.test"}), @@ -225,9 +227,10 @@ func TestCommandYamlFileTestDefaultValueFileWinsNested(t *testing.T) { Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("top.test") expect(t, val, 15) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7}), @@ -294,9 +297,10 @@ func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWinsNested(t *tes Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", - Action: func(c *cli.Context) { + Action: func(c *cli.Context) error { val := c.Int("top.test") expect(t, val, 11) + return nil }, Flags: []cli.Flag{ NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7, EnvVar: "THE_TEST"}), diff --git a/app.go b/app.go index 6917fbc..2b7c9c6 100644 --- a/app.go +++ b/app.go @@ -366,6 +366,11 @@ func (a *App) Categories() CommandCategories { return a.categories } +// VisibleFlags returns a slice of the Flags with Hidden=false +func (a *App) VisibleFlags() []Flag { + return visibleFlags(a.Flags) +} + func (a *App) hasFlag(flag Flag) bool { for _, f := range a.Flags { if flag == f { diff --git a/cli.go b/cli.go index b742545..f0440c5 100644 --- a/cli.go +++ b/cli.go @@ -10,7 +10,7 @@ // app := cli.NewApp() // app.Name = "greet" // app.Usage = "say a greeting" -// app.Action = func(c *cli.Context) { +// app.Action = func(c *cli.Context) error { // println("Greetings") // } // diff --git a/command.go b/command.go index 7f30932..9ca7e51 100644 --- a/command.go +++ b/command.go @@ -269,3 +269,8 @@ func (c Command) startApp(ctx *Context) error { return app.RunAsSubcommand(ctx) } + +// VisibleFlags returns a slice of the Flags with Hidden=false +func (c Command) VisibleFlags() []Flag { + return visibleFlags(c.Flags) +} diff --git a/flag.go b/flag.go index d18df0a..3c5c1b3 100644 --- a/flag.go +++ b/flag.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "os" + "reflect" "runtime" "strconv" "strings" @@ -12,8 +13,8 @@ import ( // This flag enables bash-completion for all commands and subcommands var BashCompletionFlag = BoolFlag{ - Name: "generate-bash-completion", - Hide: true, + Name: "generate-bash-completion", + Hidden: true, } // This flag prints the version for the application @@ -38,7 +39,6 @@ type Flag interface { // Apply Flag settings to the given flag set Apply(*flag.FlagSet) GetName() string - isNotHidden() bool } func flagSet(name string, flags []Flag) *flag.FlagSet { @@ -70,7 +70,7 @@ type GenericFlag struct { Value Generic Usage string EnvVar string - Hide bool + Hidden bool } // String returns the string representation of the generic flag to display the @@ -115,10 +115,6 @@ func (f GenericFlag) GetName() string { return f.Name } -func (f GenericFlag) isNotHidden() bool { - return !f.Hide -} - // StringSlice is an opaque type for []string to satisfy flag.Value type StringSlice []string @@ -145,7 +141,7 @@ type StringSliceFlag struct { Value *StringSlice Usage string EnvVar string - Hide bool + Hidden bool } // String returns the usage @@ -185,10 +181,6 @@ func (f StringSliceFlag) GetName() string { return f.Name } -func (f StringSliceFlag) isNotHidden() bool { - return !f.Hide -} - // StringSlice is an opaque type for []int to satisfy flag.Value type IntSlice []int @@ -220,7 +212,7 @@ type IntSliceFlag struct { Value *IntSlice Usage string EnvVar string - Hide bool + Hidden bool } // String returns the usage @@ -263,17 +255,13 @@ func (f IntSliceFlag) GetName() string { return f.Name } -func (f IntSliceFlag) isNotHidden() bool { - return !f.Hide -} - // BoolFlag is a switch that defaults to false type BoolFlag struct { Name string Usage string EnvVar string Destination *bool - Hide bool + Hidden bool } // String returns a readable representation of this value (for usage defaults) @@ -311,10 +299,6 @@ func (f BoolFlag) GetName() string { return f.Name } -func (f BoolFlag) isNotHidden() bool { - return !f.Hide -} - // BoolTFlag this represents a boolean flag that is true by default, but can // still be set to false by --some-flag=false type BoolTFlag struct { @@ -322,7 +306,7 @@ type BoolTFlag struct { Usage string EnvVar string Destination *bool - Hide bool + Hidden bool } // String returns a readable representation of this value (for usage defaults) @@ -360,10 +344,6 @@ func (f BoolTFlag) GetName() string { return f.Name } -func (f BoolTFlag) isNotHidden() bool { - return !f.Hide -} - // StringFlag represents a flag that takes as string value type StringFlag struct { Name string @@ -371,7 +351,7 @@ type StringFlag struct { Usage string EnvVar string Destination *string - Hide bool + Hidden bool } // String returns the usage @@ -413,10 +393,6 @@ func (f StringFlag) GetName() string { return f.Name } -func (f StringFlag) isNotHidden() bool { - return !f.Hide -} - // IntFlag is a flag that takes an integer // Errors if the value provided cannot be parsed type IntFlag struct { @@ -425,7 +401,7 @@ type IntFlag struct { Usage string EnvVar string Destination *int - Hide bool + Hidden bool } // String returns the usage @@ -462,10 +438,6 @@ func (f IntFlag) GetName() string { return f.Name } -func (f IntFlag) isNotHidden() bool { - return !f.Hide -} - // DurationFlag is a flag that takes a duration specified in Go's duration // format: https://golang.org/pkg/time/#ParseDuration type DurationFlag struct { @@ -474,7 +446,7 @@ type DurationFlag struct { Usage string EnvVar string Destination *time.Duration - Hide bool + Hidden bool } // String returns a readable representation of this value (for usage defaults) @@ -511,10 +483,6 @@ func (f DurationFlag) GetName() string { return f.Name } -func (f DurationFlag) isNotHidden() bool { - return !f.Hide -} - // Float64Flag is a flag that takes an float value // Errors if the value provided cannot be parsed type Float64Flag struct { @@ -523,7 +491,7 @@ type Float64Flag struct { Usage string EnvVar string Destination *float64 - Hide bool + Hidden bool } // String returns the usage @@ -559,8 +527,14 @@ func (f Float64Flag) GetName() string { return f.Name } -func (f Float64Flag) isNotHidden() bool { - return !f.Hide +func visibleFlags(fl []Flag) []Flag { + visible := []Flag{} + for _, flag := range fl { + if !reflect.ValueOf(flag).FieldByName("Hidden").Bool() { + visible = append(visible, flag) + } + } + return visible } func prefixFor(name string) (prefix string) { diff --git a/help.go b/help.go index 507fbe5..45e8603 100644 --- a/help.go +++ b/help.go @@ -15,7 +15,7 @@ var AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} {{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}} @@ -26,9 +26,9 @@ AUTHOR(S): COMMANDS:{{range .Categories}}{{if .Name}} {{.Name}}{{ ":" }}{{end}}{{range .Commands}} {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} -{{end}}{{end}}{{if .Flags}} +{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: - {{range .Flags}}{{.}} + {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} COPYRIGHT: {{.Copyright}} @@ -42,16 +42,16 @@ var CommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{.HelpName}}{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} + {{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description}}{{end}}{{if .Flags}} + {{.Description}}{{end}}{{if .VisibleFlags}} OPTIONS: - {{range .Flags}}{{.}} + {{range .VisibleFlags}}{{.}} {{end}}{{ end }} ` @@ -62,14 +62,14 @@ var SubcommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{.HelpName}} command{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} COMMANDS:{{range .Categories}}{{if .Name}} {{.Name}}{{ ":" }}{{end}}{{range .Commands}} {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} -{{end}}{{if .Flags}} +{{end}}{{if .VisibleFlags}} OPTIONS: - {{range .Flags}}{{.}} + {{range .VisibleFlags}}{{.}} {{end}}{{end}} ` @@ -114,15 +114,7 @@ var HelpPrinter helpPrinter = printHelp var VersionPrinter = printVersion func ShowAppHelp(c *Context) { - // Make a copy of c.App context - app := *c.App - app.Flags = make([]Flag, 0) - for _, flag := range c.App.Flags { - if flag.isNotHidden() { - app.Flags = append(app.Flags, flag) - } - } - HelpPrinter(c.App.Writer, AppHelpTemplate, app) + HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) } // Prints the list of subcommands as the default app completion method @@ -138,29 +130,13 @@ func DefaultAppComplete(c *Context) { func ShowCommandHelp(ctx *Context, command string) { // show the subcommand help for a command with subcommands if command == "" { - // Make a copy of c.App context - app := *c.App - app.Flags = make([]Flag, 0) - for _, flag := range c.App.Flags { - if flag.isNotHidden() { - app.Flags = append(app.Flags, flag) - } - } - HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, app) + HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) return } for _, c := range ctx.App.Commands { if c.HasName(command) { - // Make a copy of command context - c0 := c - c0.Flags = make([]Flag, 0) - for _, flag := range c.Flags { - if flag.isNotHidden() { - c0.Flags = append(c0.Flags, flag) - } - } - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c0) + HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) return } } From 95845551505a09fe45cf0681455d3e8313920c78 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 1 May 2016 08:44:01 -0400 Subject: [PATCH 08/22] Include details of hidden flag impl in CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5760983..7c865e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ **ATTN**: This project uses [semantic versioning](http://semver.org/). ## [Unreleased] +### Added +- `Hidden` field on all flag struct types to omit from generated help text + +### Changed +- `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from +generated help text via the `Hidden` field ## [1.15.0] - 2016-04-30 ### Added From 007295e509868d8dfbe52659c97ef04c059ed52b Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 1 May 2016 09:09:54 -0400 Subject: [PATCH 09/22] Cleaning up the recently-introduced deprecations to ensure the intent, responsibility, and migration path are all more clear. --- app.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app.go b/app.go index 6917fbc..6d0690e 100644 --- a/app.go +++ b/app.go @@ -12,7 +12,9 @@ import ( ) var ( - appActionDeprecationURL = "https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-action-signature" + changeLogURL = "https://github.com/codegangsta/cli/blob/master/CHANGELOG.md" + appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) + runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL) contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." @@ -230,9 +232,9 @@ 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 cli.App.RunAndExitOnError. "+ - "See https://github.com/codegangsta/cli/blob/master/CHANGELOG.md#deprecated-cli-app-runandexitonerror") + fmt.Fprintf(os.Stderr, + "DEPRECATED cli.App.RunAndExitOnError. %s See %s\n", + contactSysadmin, runAndExitOnErrorDeprecationURL) if err := a.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -421,8 +423,9 @@ func HandleAction(action interface{}, context *Context) (err error) { 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`") + fmt.Fprintf(os.Stderr, + "DEPRECATED Action signature. Must be `cli.ActionFunc`. %s See %s\n", + contactSysadmin, appActionDeprecationURL) return nil } From b738841df82b9a299ee3ebcd3e80a0b49836fa90 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 1 May 2016 10:06:12 -0400 Subject: [PATCH 10/22] Add some explicit docs about help text customization --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index 5db1b6f..064961d 100644 --- a/README.md +++ b/README.md @@ -496,6 +496,69 @@ 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 + +All of the help text generation may be customized, and at multiple levels. The +templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and +`SubcommandHelpTemplate` which may be reassigned or augmented, and full override +is possible by assigning a compatible func to the `cli.HelpPrinter` variable, +e.g.: + +``` go +package main + +import ( + "fmt" + "io" + "os" + + "github.com/codegangsta/cli" +) + +func main() { + // EXAMPLE: Append to an existing template + cli.AppHelpTemplate = fmt.Sprintf(`%s + +WEBSITE: http://awesometown.example.com + +SUPPORT: support@awesometown.example.com + +`, cli.AppHelpTemplate) + + // EXAMPLE: Override a template + cli.AppHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} +USAGE: + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command +[command options]{{end}} {{if +.ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if len .Authors}} +AUTHOR(S): + {{range .Authors}}{{ . }}{{end}} + {{end}}{{if .Commands}} +COMMANDS: +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t" +}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +GLOBAL OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}}{{if .Copyright }} +COPYRIGHT: + {{.Copyright}} + {{end}}{{if .Version}} +VERSION: + {{.Version}} + {{end}} +` + + // EXAMPLE: Replace the `HelpPrinter` func + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Println("Ha HA. I pwnd the help!!1") + } + + cli.NewApp().Run(os.Args) +} +``` + ## Contribution Guidelines Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. From f90241a6a37a14ae9d5fe678b6effc2bae462254 Mon Sep 17 00:00:00 2001 From: Paul Makepeace Date: Sun, 1 May 2016 20:58:59 -0700 Subject: [PATCH 11/22] Assert type against actual return val's interface. Exit code example produces now correctly, https://github.com/codegangsta/cli#exit-code ``` $ ./ec --ginger-crouton=false it is not in the soup $ echo $? 86 $ ``` --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index a5b0aa3..696c05e 100644 --- a/app.go +++ b/app.go @@ -438,7 +438,7 @@ func HandleAction(action interface{}, context *Context) (err error) { return errInvalidActionSignature } - if retErr, ok := reflect.ValueOf(vals[0]).Interface().(error); ok { + if retErr, ok := vals[0].Interface().(error); ok { return retErr } From a90e2e4ff1f110ff75b7a12d43df6328bf4cc134 Mon Sep 17 00:00:00 2001 From: Gert-Jan Timmer Date: Mon, 2 May 2016 17:06:14 +0200 Subject: [PATCH 12/22] Fix #376 NewExitError not working, reflect vals[0] cast to Interface() was missing --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index a5b0aa3..76ab2f2 100644 --- a/app.go +++ b/app.go @@ -438,7 +438,7 @@ func HandleAction(action interface{}, context *Context) (err error) { return errInvalidActionSignature } - if retErr, ok := reflect.ValueOf(vals[0]).Interface().(error); ok { + if retErr, ok := reflect.ValueOf(vals[0].Interface()).Interface().(error); ok { return retErr } From 4b4c07bd4fda8289a8c3d12d8feca4b2c9912868 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 11:25:37 -0400 Subject: [PATCH 13/22] Ensure HandleAction/HandleExitCoder work correctly with *ExitError Closes #376 --- app.go | 16 +++++--------- errors.go | 12 ++++++++-- errors_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 errors_test.go diff --git a/app.go b/app.go index 76ab2f2..89c741b 100644 --- a/app.go +++ b/app.go @@ -168,9 +168,7 @@ func (a *App) Run(arguments []string) (err error) { if err != nil { if a.OnUsageError != nil { err := a.OnUsageError(context, err, false) - if err != nil { - HandleExitCoder(err) - } + HandleExitCoder(err) return err } else { fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") @@ -224,9 +222,7 @@ func (a *App) Run(arguments []string) (err error) { // Run default Action err = HandleAction(a.Action, context) - if err != nil { - HandleExitCoder(err) - } + HandleExitCoder(err) return err } @@ -237,7 +233,7 @@ func (a *App) RunAndExitOnError() { contactSysadmin, runAndExitOnErrorDeprecationURL) if err := a.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) - os.Exit(1) + OsExiter(1) } } @@ -346,9 +342,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { // Run default Action err = HandleAction(a.Action, context) - if err != nil { - HandleExitCoder(err) - } + HandleExitCoder(err) return err } @@ -438,7 +432,7 @@ func HandleAction(action interface{}, context *Context) (err error) { return errInvalidActionSignature } - if retErr, ok := reflect.ValueOf(vals[0].Interface()).Interface().(error); ok { + if retErr, ok := vals[0].Interface().(error); vals[0].IsValid() && ok { return retErr } diff --git a/errors.go b/errors.go index 1a6a8c7..5f1e83b 100644 --- a/errors.go +++ b/errors.go @@ -6,6 +6,8 @@ import ( "strings" ) +var OsExiter = os.Exit + type MultiError struct { Errors []error } @@ -26,6 +28,7 @@ func (m MultiError) Error() string { // ExitCoder is the interface checked by `App` and `Command` for a custom exit // code type ExitCoder interface { + error ExitCode() int } @@ -56,15 +59,20 @@ func (ee *ExitError) ExitCode() int { } // HandleExitCoder checks if the error fulfills the ExitCoder interface, and if -// so prints the error to stderr (if it is non-empty) and calls os.Exit with the +// so prints the error to stderr (if it is non-empty) and calls OsExiter 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 err == nil { + return + } + if exitErr, ok := err.(ExitCoder); ok { if err.Error() != "" { fmt.Fprintln(os.Stderr, err) } - os.Exit(exitErr.ExitCode()) + OsExiter(exitErr.ExitCode()) + return } if multiErr, ok := err.(MultiError); ok { diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..6863105 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,60 @@ +package cli + +import ( + "errors" + "os" + "testing" +) + +func TestHandleExitCoder_nil(t *testing.T) { + exitCode := 0 + called := false + + OsExiter = func(rc int) { + exitCode = rc + called = true + } + + defer func() { OsExiter = os.Exit }() + + HandleExitCoder(nil) + + expect(t, exitCode, 0) + expect(t, called, false) +} + +func TestHandleExitCoder_ExitCoder(t *testing.T) { + exitCode := 0 + called := false + + OsExiter = func(rc int) { + exitCode = rc + called = true + } + + defer func() { OsExiter = os.Exit }() + + HandleExitCoder(NewExitError("galactic perimiter breach", 9)) + + expect(t, exitCode, 9) + expect(t, called, true) +} + +func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) { + exitCode := 0 + called := false + + OsExiter = func(rc int) { + exitCode = rc + called = true + } + + defer func() { OsExiter = os.Exit }() + + exitErr := NewExitError("galactic perimiter breach", 9) + err := NewMultiError(errors.New("wowsa"), errors.New("egad"), exitErr) + HandleExitCoder(err) + + expect(t, exitCode, 9) + expect(t, called, true) +} From fe67cb0f3ddab023e6ceec62c0d12ed0a414a83d Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 11:41:01 -0400 Subject: [PATCH 14/22] Add note about error handling fix, prep 1.16.0 section --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c865e9..39581e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ **ATTN**: This project uses [semantic versioning](http://semver.org/). ## [Unreleased] + +## [1.16.0] - 2016-05-02 ### Added - `Hidden` field on all flag struct types to omit from generated help text @@ -10,6 +12,9 @@ - `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from generated help text via the `Hidden` field +### Fixed +- handling of error values in `HandleAction` and `HandleExitCoder` + ## [1.15.0] - 2016-04-30 ### Added - This file! @@ -257,7 +262,8 @@ signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. ### Added - Initial implementation. -[Unreleased]: https://github.com/codegangsta/cli/compare/v1.15.0...HEAD +[Unreleased]: https://github.com/codegangsta/cli/compare/v1.16.0...HEAD +[1.16.0]: https://github.com/codegangsta/cli/compare/v1.15.0...v1.16.0 [1.15.0]: https://github.com/codegangsta/cli/compare/v1.14.0...v1.15.0 [1.14.0]: https://github.com/codegangsta/cli/compare/v1.13.0...v1.14.0 [1.13.0]: https://github.com/codegangsta/cli/compare/v1.12.0...v1.13.0 From 5c36fa18d949f5b9501668a58d85abe72ac1952d Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 11:51:54 -0400 Subject: [PATCH 15/22] Change refs from `cli.go` to cli since the days of this being a single-file library are long gone. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 064961d..4c61af9 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,21 @@ [![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli) [![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-codegangsta-cli) -# cli.go +# cli -`cli.go` is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. +cli is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. ## Overview Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. -**This is where `cli.go` comes into play.** `cli.go` makes command line programming fun, organized, and expressive! +**This is where cli comes into play.** cli makes command line programming fun, organized, and expressive! ## Installation Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html). -To install `cli.go`, simply run: +To install cli, simply run: ``` $ go get github.com/codegangsta/cli ``` @@ -29,7 +29,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 is that an API should be playful and full of discovery. So a cli app can be as little as one line of code in `main()`. ``` go package main @@ -113,7 +113,7 @@ $ greet Hello friend! ``` -`cli.go` also generates neat help text: +cli also generates neat help text: ``` $ greet help From 22773b14c1d7ddda94888c7797405237f547f1db Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 13:05:21 -0400 Subject: [PATCH 16/22] Allow for pluggable flag-level help text formatting by defining `cli.DefaultFlagStringFunc` with a default value that uses `withEnvHint`, conditionally running a given flag's `FormatValueHelp` if present. Closes #257 --- CHANGELOG.md | 6 +++++ flag.go | 69 +++++++++++++++++++++++++++++++++++----------------- flag_test.go | 37 +++++++++++++--------------- funcs.go | 4 +++ 4 files changed, 74 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39581e9..efdc548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ **ATTN**: This project uses [semantic versioning](http://semver.org/). ## [Unreleased] +### Added +- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` + +### Changed +- `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer +quoted in help text output. ## [1.16.0] - 2016-05-02 ### Added diff --git a/flag.go b/flag.go index 3c5c1b3..d21e018 100644 --- a/flag.go +++ b/flag.go @@ -31,6 +31,8 @@ var HelpFlag = BoolFlag{ Usage: "show help", } +var DefaultFlagStringFunc FlagStringFunc = flagStringer + // Flag is a common interface related to parsing flags in cli. // For more advanced flag parsing techniques, it is recommended that // this interface be implemented. @@ -77,8 +79,7 @@ type GenericFlag struct { // help text to the user (uses the String() method of the generic flag to show // the value) func (f GenericFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name, placeholder), f.FormatValueHelp(), usage)) + return DefaultFlagStringFunc(f) } func (f GenericFlag) FormatValueHelp() string { @@ -146,10 +147,7 @@ type StringSliceFlag struct { // String returns the usage func (f StringSliceFlag) String() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name, placeholder), pref+firstName+" option "+pref+firstName+" option", usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -181,6 +179,12 @@ func (f StringSliceFlag) GetName() string { return f.Name } +func (f StringSliceFlag) FormatValueHelp() string { + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return fmt.Sprintf("[%s%s option %s%s option]", pref, firstName, pref, firstName) +} + // StringSlice is an opaque type for []int to satisfy flag.Value type IntSlice []int @@ -217,10 +221,7 @@ type IntSliceFlag struct { // String returns the usage func (f IntSliceFlag) String() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name, placeholder), pref+firstName+" option "+pref+firstName+" option", usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -255,6 +256,12 @@ func (f IntSliceFlag) GetName() string { return f.Name } +func (f IntSliceFlag) FormatValueHelp() string { + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return fmt.Sprintf("[%s%s option %s%s option]", pref, firstName, pref, firstName) +} + // BoolFlag is a switch that defaults to false type BoolFlag struct { Name string @@ -266,8 +273,7 @@ type BoolFlag struct { // String returns a readable representation of this value (for usage defaults) func (f BoolFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name, placeholder), usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -311,8 +317,7 @@ type BoolTFlag struct { // String returns a readable representation of this value (for usage defaults) func (f BoolTFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name, placeholder), usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -356,8 +361,7 @@ type StringFlag struct { // String returns the usage func (f StringFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name, placeholder), f.FormatValueHelp(), usage)) + return DefaultFlagStringFunc(f) } func (f StringFlag) FormatValueHelp() string { @@ -406,8 +410,7 @@ type IntFlag struct { // String returns the usage func (f IntFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -451,8 +454,7 @@ type DurationFlag struct { // String returns a readable representation of this value (for usage defaults) func (f DurationFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -496,8 +498,7 @@ type Float64Flag struct { // String returns the usage func (f Float64Flag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -595,3 +596,27 @@ func withEnvHint(envVar, str string) string { } return str + envText } + +func flagStringer(f Flag) string { + fv := reflect.ValueOf(f) + placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) + + defaultValueString := "" + if val := fv.FieldByName("Value"); val.IsValid() { + defaultValueString = fmt.Sprintf("%v", val.Interface()) + } + + formatFunc := fv.MethodByName("FormatValueHelp") + if formatFunc.IsValid() { + defaultValueString = formatFunc.Call([]reflect.Value{})[0].String() + } + + if len(defaultValueString) > 0 { + defaultValueString = " " + defaultValueString + } + + return withEnvHint(fv.FieldByName("EnvVar").String(), + fmt.Sprintf("%s%v\t%v", + prefixedNames(fv.FieldByName("Name").String(), placeholder), + defaultValueString, usage)) +} diff --git a/flag_test.go b/flag_test.go index 48d920a..a49cac7 100644 --- a/flag_test.go +++ b/flag_test.go @@ -7,6 +7,7 @@ import ( "runtime" "strings" "testing" + "time" ) var boolFlagTests = []struct { @@ -18,13 +19,12 @@ var boolFlagTests = []struct { } func TestBoolFlagHelpOutput(t *testing.T) { - for _, test := range boolFlagTests { flag := BoolFlag{Name: test.name} output := flag.String() if output != test.expected { - t.Errorf("%s does not match %s", output, test.expected) + t.Errorf("%q does not match %q", output, test.expected) } } } @@ -35,11 +35,11 @@ var stringFlagTests = []struct { value string expected string }{ - {"help", "", "", "--help \t"}, - {"h", "", "", "-h \t"}, - {"h", "", "", "-h \t"}, + {"help", "", "", "--help\t"}, + {"h", "", "", "-h\t"}, + {"h", "", "", "-h\t"}, {"test", "", "Something", "--test \"Something\"\t"}, - {"config,c", "Load configuration from `FILE`", "", "--config FILE, -c FILE \tLoad configuration from FILE"}, + {"config,c", "Load configuration from `FILE`", "", "--config FILE, -c FILE\tLoad configuration from FILE"}, } func TestStringFlagHelpOutput(t *testing.T) { @@ -49,7 +49,7 @@ func TestStringFlagHelpOutput(t *testing.T) { output := flag.String() if output != test.expected { - t.Errorf("%s does not match %s", output, test.expected) + t.Errorf("%q does not match %q", output, test.expected) } } } @@ -131,8 +131,8 @@ var intFlagTests = []struct { name string expected string }{ - {"help", "--help \"0\"\t"}, - {"h", "-h \"0\"\t"}, + {"help", "--help 0\t"}, + {"h", "-h 0\t"}, } func TestIntFlagHelpOutput(t *testing.T) { @@ -168,18 +168,17 @@ var durationFlagTests = []struct { name string expected string }{ - {"help", "--help \"0\"\t"}, - {"h", "-h \"0\"\t"}, + {"help", "--help 1s\t"}, + {"h", "-h 1s\t"}, } func TestDurationFlagHelpOutput(t *testing.T) { - for _, test := range durationFlagTests { - flag := DurationFlag{Name: test.name} + flag := DurationFlag{Name: test.name, Value: 1 * time.Second} output := flag.String() if output != test.expected { - t.Errorf("%s does not match %s", output, test.expected) + t.Errorf("%q does not match %q", output, test.expected) } } } @@ -249,18 +248,17 @@ var float64FlagTests = []struct { name string expected string }{ - {"help", "--help \"0\"\t"}, - {"h", "-h \"0\"\t"}, + {"help", "--help 0.1\t"}, + {"h", "-h 0.1\t"}, } func TestFloat64FlagHelpOutput(t *testing.T) { - for _, test := range float64FlagTests { - flag := Float64Flag{Name: test.name} + flag := Float64Flag{Name: test.name, Value: float64(0.1)} output := flag.String() if output != test.expected { - t.Errorf("%s does not match %s", output, test.expected) + t.Errorf("%q does not match %q", output, test.expected) } } } @@ -292,7 +290,6 @@ var genericFlagTests = []struct { } func TestGenericFlagHelpOutput(t *testing.T) { - for _, test := range genericFlagTests { flag := GenericFlag{Name: test.name, Value: test.value, Usage: "test flag"} output := flag.String() diff --git a/funcs.go b/funcs.go index 94640ea..6b2a846 100644 --- a/funcs.go +++ b/funcs.go @@ -22,3 +22,7 @@ type CommandNotFoundFunc func(*Context, string) // 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 + +// FlagStringFunc is used by the help generation to display a flag, which is +// expected to be a single line. +type FlagStringFunc func(Flag) string From 23af5dd643c494dcc6cf2170734ef40fd87c0674 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 13:07:57 -0400 Subject: [PATCH 17/22] Rename flag stringer func bits for clarity, consistency --- flag.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/flag.go b/flag.go index d21e018..d00a984 100644 --- a/flag.go +++ b/flag.go @@ -31,7 +31,7 @@ var HelpFlag = BoolFlag{ Usage: "show help", } -var DefaultFlagStringFunc FlagStringFunc = flagStringer +var FlagStringer FlagStringFunc = stringifyFlag // Flag is a common interface related to parsing flags in cli. // For more advanced flag parsing techniques, it is recommended that @@ -79,7 +79,7 @@ type GenericFlag struct { // help text to the user (uses the String() method of the generic flag to show // the value) func (f GenericFlag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } func (f GenericFlag) FormatValueHelp() string { @@ -147,7 +147,7 @@ type StringSliceFlag struct { // String returns the usage func (f StringSliceFlag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -221,7 +221,7 @@ type IntSliceFlag struct { // String returns the usage func (f IntSliceFlag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -273,7 +273,7 @@ type BoolFlag struct { // String returns a readable representation of this value (for usage defaults) func (f BoolFlag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -317,7 +317,7 @@ type BoolTFlag struct { // String returns a readable representation of this value (for usage defaults) func (f BoolTFlag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -361,7 +361,7 @@ type StringFlag struct { // String returns the usage func (f StringFlag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } func (f StringFlag) FormatValueHelp() string { @@ -410,7 +410,7 @@ type IntFlag struct { // String returns the usage func (f IntFlag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -454,7 +454,7 @@ type DurationFlag struct { // String returns a readable representation of this value (for usage defaults) func (f DurationFlag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -498,7 +498,7 @@ type Float64Flag struct { // String returns the usage func (f Float64Flag) String() string { - return DefaultFlagStringFunc(f) + return FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -597,7 +597,7 @@ func withEnvHint(envVar, str string) string { return str + envText } -func flagStringer(f Flag) string { +func stringifyFlag(f Flag) string { fv := reflect.ValueOf(f) placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) From 69a8e25f3d7390cf45d81b130200b0a9448d3ca9 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 19:42:08 -0400 Subject: [PATCH 18/22] Make flag usage rendering more consistent; show default values --- flag.go | 128 ++++++++++++++++++++++++++++++++++----------------- flag_test.go | 57 +++++++++++------------ 2 files changed, 114 insertions(+), 71 deletions(-) diff --git a/flag.go b/flag.go index d00a984..4535b5d 100644 --- a/flag.go +++ b/flag.go @@ -11,6 +11,8 @@ import ( "time" ) +const defaultPlaceholder = "value" + // This flag enables bash-completion for all commands and subcommands var BashCompletionFlag = BoolFlag{ Name: "generate-bash-completion", @@ -82,17 +84,6 @@ func (f GenericFlag) String() string { return FlagStringer(f) } -func (f GenericFlag) FormatValueHelp() string { - if f.Value == nil { - return "" - } - s := f.Value.String() - if len(s) == 0 { - return "" - } - return fmt.Sprintf("\"%s\"", s) -} - // Apply takes the flagset and calls Set on the generic flag with the value // provided by the user for parsing by the flag func (f GenericFlag) Apply(set *flag.FlagSet) { @@ -179,12 +170,6 @@ func (f StringSliceFlag) GetName() string { return f.Name } -func (f StringSliceFlag) FormatValueHelp() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - return fmt.Sprintf("[%s%s option %s%s option]", pref, firstName, pref, firstName) -} - // StringSlice is an opaque type for []int to satisfy flag.Value type IntSlice []int @@ -256,12 +241,6 @@ func (f IntSliceFlag) GetName() string { return f.Name } -func (f IntSliceFlag) FormatValueHelp() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - return fmt.Sprintf("[%s%s option %s%s option]", pref, firstName, pref, firstName) -} - // BoolFlag is a switch that defaults to false type BoolFlag struct { Name string @@ -364,14 +343,6 @@ func (f StringFlag) String() string { return FlagStringer(f) } -func (f StringFlag) FormatValueHelp() string { - s := f.Value - if len(s) == 0 { - return "" - } - return fmt.Sprintf("\"%s\"", s) -} - // Apply populates the flag given the flag set and environment func (f StringFlag) Apply(set *flag.FlagSet) { if f.EnvVar != "" { @@ -599,24 +570,99 @@ func withEnvHint(envVar, str string) string { func stringifyFlag(f Flag) string { fv := reflect.ValueOf(f) + + switch f.(type) { + case IntSliceFlag: + return withEnvHint(fv.FieldByName("EnvVar").String(), + stringifyIntSliceFlag(f.(IntSliceFlag))) + case StringSliceFlag: + return withEnvHint(fv.FieldByName("EnvVar").String(), + stringifyStringSliceFlag(f.(StringSliceFlag))) + } + placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) + needsPlaceholder := false defaultValueString := "" - if val := fv.FieldByName("Value"); val.IsValid() { - defaultValueString = fmt.Sprintf("%v", val.Interface()) + val := fv.FieldByName("Value") + + valString := "" + if val.IsValid() { + needsPlaceholder = true + if val.Kind() == reflect.String && val.String() != "" { + valString = fmt.Sprintf("%q", val.String()) + } else { + valString = fmt.Sprintf("%v", val.Interface()) + } } - formatFunc := fv.MethodByName("FormatValueHelp") - if formatFunc.IsValid() { - defaultValueString = formatFunc.Call([]reflect.Value{})[0].String() + if valString != "" { + defaultValueString = valString } - if len(defaultValueString) > 0 { - defaultValueString = " " + defaultValueString + if needsPlaceholder && placeholder == "" { + placeholder = defaultPlaceholder } + formattedDefaultValueString := "" + if defaultValueString != "" { + formattedDefaultValueString = fmt.Sprintf(" (default: %v)", defaultValueString) + } + + usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, formattedDefaultValueString)) + return withEnvHint(fv.FieldByName("EnvVar").String(), - fmt.Sprintf("%s%v\t%v", - prefixedNames(fv.FieldByName("Name").String(), placeholder), - defaultValueString, usage)) + fmt.Sprintf("%s\t%s", prefixedNames(fv.FieldByName("Name").String(), placeholder), usageWithDefault)) +} + +func stringifyIntSliceFlag(f IntSliceFlag) string { + defaultVals := []string{} + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, fmt.Sprintf("%d", i)) + } + } + + return stringifySliceFlag(f.Usage, f.Name, defaultVals) +} + +func stringifyStringSliceFlag(f StringSliceFlag) string { + defaultVals := []string{} + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, s := range f.Value.Value() { + if len(s) > 0 { + defaultVals = append(defaultVals, fmt.Sprintf("%q", s)) + } + } + } + + return stringifySliceFlag(f.Usage, f.Name, defaultVals) +} + +func stringifySliceFlag(usage, name string, defaultVals []string) string { + placeholder, usage := unquoteUsage(usage) + if placeholder == "" { + placeholder = defaultPlaceholder + } + + nameParts := []string{} + for _, part := range strings.Split(name, ",") { + nameParts = append(nameParts, strings.TrimSpace(part)) + } + + defaultVal := "" + if len(defaultVals) > 0 { + defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", ")) + } + + usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) + + if len(nameParts) < 2 { + return fmt.Sprintf("%s%s %s\t%s", prefixFor(nameParts[0]), nameParts[0], + placeholder, usageWithDefault) + } + + return fmt.Sprintf("%s%s %s, %s%s %s\t%s", prefixFor(nameParts[0]), nameParts[0], + placeholder, prefixFor(nameParts[1]), nameParts[1], + placeholder, usageWithDefault) } diff --git a/flag_test.go b/flag_test.go index a49cac7..e0df23b 100644 --- a/flag_test.go +++ b/flag_test.go @@ -35,15 +35,15 @@ var stringFlagTests = []struct { value string expected string }{ - {"help", "", "", "--help\t"}, - {"h", "", "", "-h\t"}, - {"h", "", "", "-h\t"}, - {"test", "", "Something", "--test \"Something\"\t"}, + {"foo", "", "", "--foo value\t"}, + {"f", "", "", "-f value\t"}, + {"f", "The total `foo` desired", "all", "-f foo\tThe total foo desired (default: \"all\")"}, + {"test", "", "Something", "--test value\t(default: \"Something\")"}, {"config,c", "Load configuration from `FILE`", "", "--config FILE, -c FILE\tLoad configuration from FILE"}, + {"config,c", "Load configuration from `CONFIG`", "config.json", "--config CONFIG, -c CONFIG\tLoad configuration from CONFIG (default: \"config.json\")"}, } func TestStringFlagHelpOutput(t *testing.T) { - for _, test := range stringFlagTests { flag := StringFlag{Name: test.name, Usage: test.usage, Value: test.value} output := flag.String() @@ -76,30 +76,29 @@ var stringSliceFlagTests = []struct { value *StringSlice expected string }{ - {"help", func() *StringSlice { + {"foo", func() *StringSlice { s := &StringSlice{} s.Set("") return s - }(), "--help [--help option --help option]\t"}, - {"h", func() *StringSlice { + }(), "--foo value\t"}, + {"f", func() *StringSlice { s := &StringSlice{} s.Set("") return s - }(), "-h [-h option -h option]\t"}, - {"h", func() *StringSlice { + }(), "-f value\t"}, + {"f", func() *StringSlice { s := &StringSlice{} - s.Set("") + s.Set("Lipstick") return s - }(), "-h [-h option -h option]\t"}, + }(), "-f value\t(default: \"Lipstick\")"}, {"test", func() *StringSlice { s := &StringSlice{} s.Set("Something") return s - }(), "--test [--test option --test option]\t"}, + }(), "--test value\t(default: \"Something\")"}, } func TestStringSliceFlagHelpOutput(t *testing.T) { - for _, test := range stringSliceFlagTests { flag := StringSliceFlag{Name: test.name, Value: test.value} output := flag.String() @@ -131,14 +130,13 @@ var intFlagTests = []struct { name string expected string }{ - {"help", "--help 0\t"}, - {"h", "-h 0\t"}, + {"hats", "--hats value\t(default: 9)"}, + {"H", "-H value\t(default: 9)"}, } func TestIntFlagHelpOutput(t *testing.T) { - for _, test := range intFlagTests { - flag := IntFlag{Name: test.name} + flag := IntFlag{Name: test.name, Value: 9} output := flag.String() if output != test.expected { @@ -168,8 +166,8 @@ var durationFlagTests = []struct { name string expected string }{ - {"help", "--help 1s\t"}, - {"h", "-h 1s\t"}, + {"hooting", "--hooting value\t(default: 1s)"}, + {"H", "-H value\t(default: 1s)"}, } func TestDurationFlagHelpOutput(t *testing.T) { @@ -205,18 +203,17 @@ var intSliceFlagTests = []struct { value *IntSlice expected string }{ - {"help", &IntSlice{}, "--help [--help option --help option]\t"}, - {"h", &IntSlice{}, "-h [-h option -h option]\t"}, - {"h", &IntSlice{}, "-h [-h option -h option]\t"}, - {"test", func() *IntSlice { + {"heads", &IntSlice{}, "--heads value\t"}, + {"H", &IntSlice{}, "-H value\t"}, + {"H, heads", func() *IntSlice { i := &IntSlice{} i.Set("9") + i.Set("3") return i - }(), "--test [--test option --test option]\t"}, + }(), "-H value, --heads value\t(default: 9, 3)"}, } func TestIntSliceFlagHelpOutput(t *testing.T) { - for _, test := range intSliceFlagTests { flag := IntSliceFlag{Name: test.name, Value: test.value} output := flag.String() @@ -248,8 +245,8 @@ var float64FlagTests = []struct { name string expected string }{ - {"help", "--help 0.1\t"}, - {"h", "-h 0.1\t"}, + {"hooting", "--hooting value\t(default: 0.1)"}, + {"H", "-H value\t(default: 0.1)"}, } func TestFloat64FlagHelpOutput(t *testing.T) { @@ -285,8 +282,8 @@ var genericFlagTests = []struct { value Generic expected string }{ - {"test", &Parser{"abc", "def"}, "--test \"abc,def\"\ttest flag"}, - {"t", &Parser{"abc", "def"}, "-t \"abc,def\"\ttest flag"}, + {"toads", &Parser{"abc", "def"}, "--toads value\ttest flag (default: abc,def)"}, + {"t", &Parser{"abc", "def"}, "-t value\ttest flag (default: abc,def)"}, } func TestGenericFlagHelpOutput(t *testing.T) { From 7de151883c333cde1739a25ae112c89e959328d4 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 19:48:35 -0400 Subject: [PATCH 19/22] Added more notes about usage formatting changes --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efdc548..d7ea0a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ ### Changed - `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer quoted in help text output. +- All flag types now include `(default: {value})` strings following usage when a +default value can be (reasonably) detected. +- `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent +with non-slice flag types ## [1.16.0] - 2016-05-02 ### Added From 6089d723a81a9970f806ac7e198c6dcc1210596e Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 19:52:39 -0400 Subject: [PATCH 20/22] Use existing `prefixedNames` func --- flag.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/flag.go b/flag.go index 4535b5d..8f124d3 100644 --- a/flag.go +++ b/flag.go @@ -645,24 +645,11 @@ func stringifySliceFlag(usage, name string, defaultVals []string) string { placeholder = defaultPlaceholder } - nameParts := []string{} - for _, part := range strings.Split(name, ",") { - nameParts = append(nameParts, strings.TrimSpace(part)) - } - defaultVal := "" if len(defaultVals) > 0 { defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", ")) } usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) - - if len(nameParts) < 2 { - return fmt.Sprintf("%s%s %s\t%s", prefixFor(nameParts[0]), nameParts[0], - placeholder, usageWithDefault) - } - - return fmt.Sprintf("%s%s %s, %s%s %s\t%s", prefixFor(nameParts[0]), nameParts[0], - placeholder, prefixFor(nameParts[1]), nameParts[1], - placeholder, usageWithDefault) + return fmt.Sprintf("%s\t%s", prefixedNames(name, placeholder), usageWithDefault) } From cd92adcb7512ec11789a2b3bed9028367da1b175 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 19:58:16 -0400 Subject: [PATCH 21/22] Further simplifying default flag stringer func --- flag.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/flag.go b/flag.go index 8f124d3..3b6a2e1 100644 --- a/flag.go +++ b/flag.go @@ -581,35 +581,29 @@ func stringifyFlag(f Flag) string { } placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) - needsPlaceholder := false + needsPlaceholder := false defaultValueString := "" val := fv.FieldByName("Value") - valString := "" if val.IsValid() { needsPlaceholder = true + defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) + if val.Kind() == reflect.String && val.String() != "" { - valString = fmt.Sprintf("%q", val.String()) - } else { - valString = fmt.Sprintf("%v", val.Interface()) + defaultValueString = fmt.Sprintf(" (default: %q)", val.String()) } } - if valString != "" { - defaultValueString = valString + if defaultValueString == " (default: )" { + defaultValueString = "" } if needsPlaceholder && placeholder == "" { placeholder = defaultPlaceholder } - formattedDefaultValueString := "" - if defaultValueString != "" { - formattedDefaultValueString = fmt.Sprintf(" (default: %v)", defaultValueString) - } - - usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, formattedDefaultValueString)) + usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultValueString)) return withEnvHint(fv.FieldByName("EnvVar").String(), fmt.Sprintf("%s\t%s", prefixedNames(fv.FieldByName("Name").String(), placeholder), usageWithDefault)) From b14dcdb7b97e38d247bd6389cc2f64c8f253d216 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 21:20:23 -0400 Subject: [PATCH 22/22] TRIVIAL the letter "a" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c61af9..06f7b84 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # cli -cli is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. +cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. ## Overview