diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a55c9..ea6c483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,47 @@ **ATTN**: This project uses [semantic versioning](http://semver.org/). -## [Unreleased] +## 2.0.0 - (unreleased 2.x series) +### Added +- `NewStringSlice` and `NewIntSlice` for creating their related types + +### Removed +- the ability to specify `&StringSlice{...string}` or `&IntSlice{...int}`. +To migrate to the new API, you may choose to run [this helper +(python) script](./cli-migrate-slice-types). + +## [Unreleased] - (1.x series) +### 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. +- 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 +- `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 + +### Fixed +- handling of error values in `HandleAction` and `HandleExitCoder` + +## [1.15.0] - 2016-04-30 ### Added - This file! - Support for placeholders in flag usage strings - `App.Metadata` map for arbitrary data/state management - `Set` and `GlobalSet` methods on `*cli.Context` for altering values after parsing. +- 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 @@ -36,15 +69,6 @@ signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. ### Fixed - Added missing `*cli.Context.GlobalFloat64` method -## [2.0.0] -### Added -- `NewStringSlice` and `NewIntSlice` for creating their related types - -### Removed -- the ability to specify `&StringSlice{...string}` or `&IntSlice{...int}`. -To migrate to the new API, you may choose to run [this helper -(python) script](./cli-migrate-slice-types). - ## [1.14.0] - 2016-04-03 (backfilled 2016-04-25) ### Added - Codebeat badge @@ -257,8 +281,9 @@ To migrate to the new API, you may choose to run [this helper ### Added - Initial implementation. -[Unreleased]: https://github.com/codegangsta/cli/compare/v1.14.0...HEAD -[2.0.0]: https://github.com/codegangsta/cli/compare/v1.14.0...v2.0.0 +[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 [1.12.0]: https://github.com/codegangsta/cli/compare/v1.11.1...v1.12.0 diff --git a/README.md b/README.md index 777b0a8..06f7b84 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 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 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 @@ -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 @@ -490,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. 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 39c36f6..519bd81 100644 --- a/altsrc/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -78,6 +78,41 @@ 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) error { + val := c.Int("top.test") + expect(t, val, 10) + return nil + }, + 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) @@ -110,6 +145,39 @@ 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) error { + val := c.Int("top.test") + expect(t, val, 7) + return nil + }, + 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) @@ -142,6 +210,39 @@ 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) error { + val := c.Int("top.test") + expect(t, val, 15) + return nil + }, + 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) @@ -175,3 +276,38 @@ 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) 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"}), + 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()) diff --git a/app.go b/app.go index 6917fbc..89c741b 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." @@ -166,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.") @@ -222,20 +222,18 @@ 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 } // 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) + OsExiter(1) } } @@ -344,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 } @@ -366,6 +362,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 { @@ -421,8 +422,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 } @@ -430,7 +432,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); vals[0].IsValid() && ok { return retErr } 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/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) +} diff --git a/flag.go b/flag.go index a5c8d2c..c25ad25 100644 --- a/flag.go +++ b/flag.go @@ -5,19 +5,21 @@ import ( "flag" "fmt" "os" + "reflect" "runtime" "strconv" "strings" "time" ) -var ( - slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano()) -) +const defaultPlaceholder = "value" + +var slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano()) // This flag enables bash-completion for all commands and subcommands var BashCompletionFlag = BoolFlag{ - Name: "generate-bash-completion", + Name: "generate-bash-completion", + Hidden: true, } // This flag prints the version for the application @@ -34,6 +36,8 @@ var HelpFlag = BoolFlag{ Usage: "show help", } +var FlagStringer FlagStringFunc = stringifyFlag + // Serializeder is used to circumvent the limitations of flag.FlagSet.Set type Serializeder interface { Serialized() string @@ -78,25 +82,14 @@ type GenericFlag struct { Value Generic Usage string EnvVar string + Hidden bool } // String returns the string representation of the generic flag to display the // 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)) -} - -func (f GenericFlag) FormatValueHelp() string { - if f.Value == nil { - return "" - } - s := f.Value.String() - if len(s) == 0 { - return "" - } - return fmt.Sprintf("\"%s\"", s) + return FlagStringer(f) } // Apply takes the flagset and calls Set on the generic flag with the value @@ -174,14 +167,12 @@ type StringSliceFlag struct { Value *StringSlice Usage string EnvVar string + Hidden bool } // 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 FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -281,14 +272,12 @@ type IntSliceFlag struct { Value *IntSlice Usage string EnvVar string + Hidden bool } // 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 FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -330,12 +319,12 @@ type BoolFlag struct { Usage string EnvVar string Destination *bool + Hidden bool } // 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 FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -374,12 +363,12 @@ type BoolTFlag struct { Usage string EnvVar string Destination *bool + Hidden bool } // 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 FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -418,20 +407,12 @@ type StringFlag struct { Usage string EnvVar string Destination *string + Hidden bool } // 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)) -} - -func (f StringFlag) FormatValueHelp() string { - s := f.Value - if len(s) == 0 { - return "" - } - return fmt.Sprintf("\"%s\"", s) + return FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -467,12 +448,12 @@ type IntFlag struct { Usage string EnvVar string Destination *int + Hidden bool } // 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 FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -511,12 +492,12 @@ type DurationFlag struct { Usage string EnvVar string Destination *time.Duration + Hidden bool } // 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 FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -555,12 +536,12 @@ type Float64Flag struct { Usage string EnvVar string Destination *float64 + Hidden bool } // 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 FlagStringer(f) } // Apply populates the flag given the flag set and environment @@ -590,6 +571,16 @@ func (f Float64Flag) GetName() string { return f.Name } +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) { if len(name) == 1 { prefix = "-" @@ -648,3 +639,83 @@ func withEnvHint(envVar, str string) string { } return str + envText } + +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 := "" + val := fv.FieldByName("Value") + + if val.IsValid() { + needsPlaceholder = true + defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) + + if val.Kind() == reflect.String && val.String() != "" { + defaultValueString = fmt.Sprintf(" (default: %q)", val.String()) + } + } + + if defaultValueString == " (default: )" { + defaultValueString = "" + } + + if needsPlaceholder && placeholder == "" { + placeholder = defaultPlaceholder + } + + 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)) +} + +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 + } + + defaultVal := "" + if len(defaultVals) > 0 { + defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", ")) + } + + usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) + return fmt.Sprintf("%s\t%s", prefixedNames(name, placeholder), usageWithDefault) +} diff --git a/flag_test.go b/flag_test.go index 79c0ae0..f914da3 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,12 @@ var stringFlagTests = []struct { value string expected string }{ - {"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"}, + {"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) { @@ -48,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) } } } @@ -75,11 +76,11 @@ var stringSliceFlagTests = []struct { value *StringSlice expected string }{ - {"help", NewStringSlice(""), "--help [--help option --help option]\t"}, - {"h", NewStringSlice(""), "-h [-h option -h option]\t"}, - {"h", NewStringSlice(""), "-h [-h option -h option]\t"}, - {"test", NewStringSlice("Something"), "--test [--test option --test option]\t"}, - {"d, dee", NewStringSlice("Inka", "Dinka", "dooo"), "-d, --dee [-d option -d option]\t"}, + {"foo", NewStringSlice(""), "--foo value\t"}, + {"f", NewStringSlice(""), "-f value\t"}, + {"f", NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")"}, + {"test", NewStringSlice("Something"), "--test value\t(default: \"Something\")"}, + {"d, dee", NewStringSlice("Inka", "Dinka", "dooo"), "-d value, --dee value\t(default: \"Inka\", \"Dinka\", \"dooo\")"}, } func TestStringSliceFlagHelpOutput(t *testing.T) { @@ -114,14 +115,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 { @@ -151,18 +151,17 @@ var durationFlagTests = []struct { name string expected string }{ - {"help", "--help \"0\"\t"}, - {"h", "-h \"0\"\t"}, + {"hooting", "--hooting value\t(default: 1s)"}, + {"H", "-H value\t(default: 1s)"}, } 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) } } } @@ -189,14 +188,12 @@ var intSliceFlagTests = []struct { value *IntSlice expected string }{ - {"help", NewIntSlice(), "--help [--help option --help option]\t"}, - {"h", NewIntSlice(), "-h [-h option -h option]\t"}, - {"h", NewIntSlice(), "-h [-h option -h option]\t"}, - {"test", NewIntSlice(9), "--test [--test option --test option]\t"}, + {"heads", NewIntSlice(), "--heads value\t"}, + {"H", NewIntSlice(), "-H value\t"}, + {"H, heads", NewIntSlice(9, 3), "-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() @@ -228,18 +225,17 @@ var float64FlagTests = []struct { name string expected string }{ - {"help", "--help \"0\"\t"}, - {"h", "-h \"0\"\t"}, + {"hooting", "--hooting value\t(default: 0.1)"}, + {"H", "-H value\t(default: 0.1)"}, } 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) } } } @@ -266,12 +262,11 @@ 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) { - 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 diff --git a/help.go b/help.go index a895e6c..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}} `