diff --git a/.travis.yml b/.travis.yml index 27c4910..e395617 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,35 +1,23 @@ language: go - sudo: false +dist: trusty +osx_image: xcode8.3 +go: 1.8.x + +os: +- linux +- osx cache: directories: - node_modules -go: -- 1.2.x -- 1.3.x -- 1.4.2 -- 1.5.x -- 1.6.x -- 1.7.x -- master - -env: pip_install="pip install --user" - -matrix: - allow_failures: - - go: master - include: - - go: 1.6.x - os: osx - env: pip_install="sudo pip install" - - go: 1.7.x - os: osx - env: pip_install="sudo pip install" - before_script: -- $pip_install flake8 +- if [[ $(uname) == Darwin ]]; then + sudo pip install flake8; + else + pip install --user flake8; + fi - mkdir -p ${GOPATH%%:*}/src/gopkg.in/urfave - rm -rvf ${GOPATH%%:*}/src/gopkg.in/urfave/cli.v2 - rm -rvf ${GOPATH%%:*}/pkg/*/gopkg.in/urfave/cli.v2.a diff --git a/CHANGELOG.md b/CHANGELOG.md index f5fc7f0..5fa0ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,13 +34,70 @@ ## [Unreleased] - (1.x series) ### Added + +### Changed + +### Removed + +### Fixed + +### Deprecated + +### Security + +## [1.19.1] - 2016-11-21 +### Fixed +- Fixes regression introduced in 1.19.0 where using an `ActionFunc` as + the `Action` for a command would cause it to error rather than calling the + function. Should not have a affected declarative cases using `func(c + *cli.Context) err)`. +- Shell completion now handles the case where the user specifies + `--generate-bash-completion` immediately after a flag that takes an argument. + Previously it call the application with `--generate-bash-completion` as the + flag value. + +## [1.19.0] - 2016-11-19 +### Added +- `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`) +- A `Description` field was added to `App` for a more detailed description of + the application (similar to the existing `Description` field on `Command`) - Flag type code generation via `go generate` - Write to stderr and exit 1 if action returns non-nil error - Added support for TOML to the `altsrc` loader +- `SkipArgReorder` was added to allow users to skip the argument reordering. + This is useful if you want to consider all "flags" after an argument as + arguments rather than flags (the default behavior of the stdlib `flag` + library). This is backported functionality from the [removal of the flag + reordering](https://github.com/urfave/cli/pull/398) in the unreleased version + 2 +- For formatted errors (those implementing `ErrorFormatter`), the errors will + be formatted during output. Compatible with `pkg/errors`. ### Changed - Raise minimum tested/supported Go version to 1.2+ +### Fixed +- Consider empty environment variables as set (previously environment variables + with the equivalent of `""` would be skipped rather than their value used). +- Return an error if the value in a given environment variable cannot be parsed + as the flag type. Previously these errors were silently swallowed. +- Print full error when an invalid flag is specified (which includes the invalid flag) +- `App.Writer` defaults to `stdout` when `nil` +- If no action is specified on a command or app, the help is now printed instead of `panic`ing +- `App.Metadata` is initialized automatically now (previously was `nil` unless initialized) +- Correctly show help message if `-h` is provided to a subcommand +- `context.(Global)IsSet` now respects environment variables. Previously it + would return `false` if a flag was specified in the environment rather than + as an argument +- Removed deprecation warnings to STDERR to avoid them leaking to the end-user +- `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This + fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well + as `altsrc` where Go would complain that the types didn't match + +## [1.18.1] - 2016-08-28 +### Fixed +- Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported) + ## [1.18.0] - 2016-06-27 ### Added - `./runtests` test runner with coverage tracking by default @@ -59,6 +116,10 @@ - No longer swallows `panic`s that occur within the `Action`s themselves when detecting the signature of the `Action` field +## [1.17.1] - 2016-08-28 +### Fixed +- Removed deprecation warnings to STDERR to avoid them leaking to the end-user + ## [1.17.0] - 2016-05-09 ### Added - Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` @@ -80,6 +141,10 @@ - cleanups based on [Go Report Card feedback](https://goreportcard.com/report/github.com/urfave/cli) +## [1.16.1] - 2016-08-28 +### Fixed +- Removed deprecation warnings to STDERR to avoid them leaking to the end-user + ## [1.16.0] - 2016-05-02 ### Added - `Hidden` field on all flag struct types to omit from generated help text diff --git a/README.md b/README.md index d46de8d..dbe1bf3 100644 --- a/README.md +++ b/README.md @@ -461,13 +461,13 @@ error. Flags for the application and commands are shown in the order they are defined. However, it's possible to sort them from outside this library by using `FlagsByName` -with `sort`. +or `CommandsByName` with `sort`. For example this: ``` go package main @@ -492,9 +492,28 @@ func main() { Usage: "Load configuration from `FILE`", }, }, + Commands: []*cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + }, } sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) app.Run(os.Args) } @@ -1001,16 +1020,13 @@ SUPPORT: support@awesometown.example.com cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: - {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command -[command options]{{end}} {{if -.ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} -AUTHOR(S): +AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: -{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t" -}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} diff --git a/altsrc/flag.go b/altsrc/flag.go index 879708c..c1fb669 100644 --- a/altsrc/flag.go +++ b/altsrc/flag.go @@ -2,8 +2,8 @@ package altsrc import ( "fmt" - "os" "strconv" + "syscall" "gopkg.in/urfave/cli.v2" ) @@ -217,13 +217,11 @@ func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourc func isEnvVarSet(envVars []string) bool { for _, envVar := range envVars { - if envVal := os.Getenv(envVar); envVal != "" { + if _, ok := syscall.Getenv(envVar); ok { // TODO: Can't use this for bools as // set means that it was true or false based on // Bool flag type, should work for other types - if len(envVal) > 0 { - return true - } + return true } } diff --git a/altsrc/flag_generated.go b/altsrc/flag_generated.go index 7ca8dac..ee2231c 100644 --- a/altsrc/flag_generated.go +++ b/altsrc/flag_generated.go @@ -27,6 +27,13 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) { f.BoolFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped BoolFlag.ApplyWithError +func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.BoolFlag.ApplyWithError(set) +} + // DurationFlag is the flag type that wraps cli.DurationFlag to allow // for other values to be specified type DurationFlag struct { @@ -46,6 +53,13 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) { f.DurationFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped DurationFlag.ApplyWithError +func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.DurationFlag.ApplyWithError(set) +} + // Float64Flag is the flag type that wraps cli.Float64Flag to allow // for other values to be specified type Float64Flag struct { @@ -65,6 +79,13 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) { f.Float64Flag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped Float64Flag.ApplyWithError +func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.Float64Flag.ApplyWithError(set) +} + // GenericFlag is the flag type that wraps cli.GenericFlag to allow // for other values to be specified type GenericFlag struct { @@ -84,6 +105,13 @@ func (f *GenericFlag) Apply(set *flag.FlagSet) { f.GenericFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped GenericFlag.ApplyWithError +func (f *GenericFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.GenericFlag.ApplyWithError(set) +} + // Int64Flag is the flag type that wraps cli.Int64Flag to allow // for other values to be specified type Int64Flag struct { @@ -103,6 +131,13 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) { f.Int64Flag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped Int64Flag.ApplyWithError +func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.Int64Flag.ApplyWithError(set) +} + // IntFlag is the flag type that wraps cli.IntFlag to allow // for other values to be specified type IntFlag struct { @@ -122,6 +157,13 @@ func (f *IntFlag) Apply(set *flag.FlagSet) { f.IntFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped IntFlag.ApplyWithError +func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.IntFlag.ApplyWithError(set) +} + // IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow // for other values to be specified type IntSliceFlag struct { @@ -141,6 +183,13 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) { f.IntSliceFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped IntSliceFlag.ApplyWithError +func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.IntSliceFlag.ApplyWithError(set) +} + // Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow // for other values to be specified type Int64SliceFlag struct { @@ -160,6 +209,13 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) { f.Int64SliceFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped Int64SliceFlag.ApplyWithError +func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.Int64SliceFlag.ApplyWithError(set) +} + // Float64SliceFlag is the flag type that wraps cli.Float64SliceFlag to allow // for other values to be specified type Float64SliceFlag struct { @@ -179,6 +235,13 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) { f.Float64SliceFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped Float64SliceFlag.ApplyWithError +func (f *Float64SliceFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.Float64SliceFlag.ApplyWithError(set) +} + // StringFlag is the flag type that wraps cli.StringFlag to allow // for other values to be specified type StringFlag struct { @@ -198,6 +261,13 @@ func (f *StringFlag) Apply(set *flag.FlagSet) { f.StringFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped StringFlag.ApplyWithError +func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.StringFlag.ApplyWithError(set) +} + // StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow // for other values to be specified type StringSliceFlag struct { @@ -217,6 +287,13 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) { f.StringSliceFlag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped StringSliceFlag.ApplyWithError +func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.StringSliceFlag.ApplyWithError(set) +} + // Uint64Flag is the flag type that wraps cli.Uint64Flag to allow // for other values to be specified type Uint64Flag struct { @@ -236,6 +313,13 @@ func (f *Uint64Flag) Apply(set *flag.FlagSet) { f.Uint64Flag.Apply(set) } +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped Uint64Flag.ApplyWithError +func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.Uint64Flag.ApplyWithError(set) +} + // UintFlag is the flag type that wraps cli.UintFlag to allow // for other values to be specified type UintFlag struct { @@ -254,3 +338,10 @@ func (f *UintFlag) Apply(set *flag.FlagSet) { f.set = set f.UintFlag.Apply(set) } + +// ApplyWithError saves the flagSet for later usage calls, then calls the +// wrapped UintFlag.ApplyWithError +func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error { + f.set = set + return f.UintFlag.ApplyWithError(set) +} diff --git a/altsrc/flag_test.go b/altsrc/flag_test.go index f077487..a4d1d59 100644 --- a/altsrc/flag_test.go +++ b/altsrc/flag_test.go @@ -63,7 +63,7 @@ func TestStringSliceApplyInputSourceValue(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}), FlagName: "test", - MapValue: []string{"hello", "world"}, + MapValue: []interface{}{"hello", "world"}, }) expect(t, c.StringSlice("test"), []string{"hello", "world"}) } @@ -72,7 +72,7 @@ func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}), FlagName: "test", - MapValue: []string{"hello", "world"}, + MapValue: []interface{}{"hello", "world"}, ContextValueString: "ohno", }) expect(t, c.StringSlice("test"), []string{"ohno"}) @@ -82,7 +82,7 @@ func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", EnvVars: []string{"TEST"}}), FlagName: "test", - MapValue: []string{"hello", "world"}, + MapValue: []interface{}{"hello", "world"}, EnvVarName: "TEST", EnvVarValue: "oh,no", }) @@ -93,7 +93,7 @@ func TestIntSliceApplyInputSourceValue(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}), FlagName: "test", - MapValue: []int{1, 2}, + MapValue: []interface{}{1, 2}, }) expect(t, c.IntSlice("test"), []int{1, 2}) } @@ -102,7 +102,7 @@ func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}), FlagName: "test", - MapValue: []int{1, 2}, + MapValue: []interface{}{1, 2}, ContextValueString: "3", }) expect(t, c.IntSlice("test"), []int{3}) @@ -112,7 +112,7 @@ func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", EnvVars: []string{"TEST"}}), FlagName: "test", - MapValue: []int{1, 2}, + MapValue: []interface{}{1, 2}, EnvVarName: "TEST", EnvVarValue: "3,4", }) diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go index 26d6e81..66a29b6 100644 --- a/altsrc/map_input_source.go +++ b/altsrc/map_input_source.go @@ -130,45 +130,59 @@ func (fsm *MapInputSource) String(name string) (string, error) { // StringSlice returns an []string from the map if it exists otherwise returns nil func (fsm *MapInputSource) StringSlice(name string) ([]string, error) { otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.([]string) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]string", otherGenericValue) + if !exists { + otherGenericValue, exists = nestedVal(name, fsm.valueMap) + if !exists { + return nil, nil } - 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 + otherValue, isType := otherGenericValue.([]interface{}) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) + } + + var stringSlice = make([]string, 0, len(otherValue)) + for i, v := range otherValue { + stringValue, isType := v.(string) + + if !isType { + return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "string", v) + } + + stringSlice = append(stringSlice, stringValue) + } + + return stringSlice, nil } // IntSlice returns an []int from the map if it exists otherwise returns nil func (fsm *MapInputSource) IntSlice(name string) ([]int, error) { otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.([]int) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]int", otherGenericValue) + if !exists { + otherGenericValue, exists = nestedVal(name, fsm.valueMap) + if !exists { + return nil, nil } - 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 + otherValue, isType := otherGenericValue.([]interface{}) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) + } + + var intSlice = make([]int, 0, len(otherValue)) + for i, v := range otherValue { + intValue, isType := v.(int) + + if !isType { + return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v) + } + + intSlice = append(intSlice, intValue) + } + + return intSlice, nil } // Generic returns an cli.Generic from the map if it exists otherwise returns nil diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go index 0fa2dbb..423e5ff 100644 --- a/altsrc/toml_file_loader.go +++ b/altsrc/toml_file_loader.go @@ -57,8 +57,8 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { } else { return nil, err } - case reflect.Array: - fallthrough // [todo] - Support array type + case reflect.Array, reflect.Slice: + ret[key] = val.([]interface{}) default: return nil, fmt.Errorf("Unsupported: type = %#v", v.Kind()) } diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index ada90a8..4c0060b 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -11,6 +11,8 @@ import ( "net/http" "net/url" "os" + "runtime" + "strings" "gopkg.in/urfave/cli.v2" @@ -78,6 +80,12 @@ func loadDataFrom(filePath string) ([]byte, error) { return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) } return ioutil.ReadFile(filePath) + } else if runtime.GOOS == "windows" && strings.Contains(u.String(), "\\") { + // on Windows systems u.Path is always empty, so we need to check the string directly. + if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil { + return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) + } + return ioutil.ReadFile(filePath) } else { return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) } diff --git a/app.go b/app.go index 0a529ad..fca28c7 100644 --- a/app.go +++ b/app.go @@ -65,6 +65,12 @@ type App struct { ErrWriter io.Writer // Other custom info Metadata map[string]interface{} + // Carries a function which returns app specific info. + ExtraInfo func() map[string]string + // CustomAppHelpTemplate the text template for app help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomAppHelpTemplate string didSetup bool } @@ -156,6 +162,10 @@ func (a *App) Setup() { if a.Metadata == nil { a.Metadata = make(map[string]interface{}) } + + if a.Writer == nil { + a.Writer = os.Stdout + } } // Run is the entry point to the cli app. Parses the arguments slice and routes @@ -163,8 +173,20 @@ func (a *App) Setup() { func (a *App) Run(arguments []string) (err error) { a.Setup() + // handle the completion flag separately from the flagset since + // completion could be attempted after a flag, but before its value was put + // on the command line. this causes the flagset to interpret the completion + // flag name as the value of the flag before it which is undesirable + // note that we can only do this because the shell autocomplete function + // always appends the completion flag at the end of the command + shellComplete, arguments := checkShellCompleteFlag(a, arguments) + // parse flags - set := flagSet(a.Name, a.Flags) + set, err := flagSet(a.Name, a.Flags) + if err != nil { + return err + } + set.SetOutput(ioutil.Discard) err = set.Parse(arguments[1:]) nerr := normalizeFlags(a.Flags, set) @@ -174,6 +196,7 @@ func (a *App) Run(arguments []string) (err error) { ShowAppHelp(context) return nerr } + context.shellComplete = shellComplete if checkCompletions(context) { return nil @@ -223,7 +246,6 @@ func (a *App) Run(arguments []string) (err error) { if a.Before != nil { beforeErr := a.Before(context) if beforeErr != nil { - fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) ShowAppHelp(context) HandleExitCoder(beforeErr) err = beforeErr @@ -240,6 +262,10 @@ func (a *App) Run(arguments []string) (err error) { } } + if a.Action == nil { + a.Action = helpCommand.Action + } + // Run default Action err = a.Action(context) @@ -278,7 +304,11 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } // parse flags - set := flagSet(a.Name, a.Flags) + set, err := flagSet(a.Name, a.Flags) + if err != nil { + return err + } + set.SetOutput(ioutil.Discard) err = set.Parse(ctx.Args().Tail()) nerr := normalizeFlags(a.Flags, set) diff --git a/app_test.go b/app_test.go index 3b3424a..796f665 100644 --- a/app_test.go +++ b/app_test.go @@ -181,9 +181,59 @@ func ExampleApp_Run_commandHelp() { // This is how we describe describeit the function } +func ExampleApp_Run_noAction() { + app := App{} + app.Name = "greet" + app.Run([]string{"greet"}) + // Output: + // NAME: + // greet - A new cli application + // + // USAGE: + // greet [global options] command [command options] [arguments...] + // + // VERSION: + // 0.0.0 + // + // COMMANDS: + // help, h Shows a list of commands or help for one command + // + // GLOBAL OPTIONS: + // --help, -h show help (default: false) + // --version, -v print the version (default: false) +} + +func ExampleApp_Run_subcommandNoAction() { + app := &App{ + Name: "greet", + Commands: []*Command{ + { + Name: "describeit", + Aliases: []string{"d"}, + Usage: "use it to see a description", + Description: "This is how we describe describeit the function", + }, + }, + } + app.Run([]string{"greet", "describeit"}) + // Output: + // NAME: + // greet describeit - use it to see a description + // + // USAGE: + // greet describeit [command options] [arguments...] + // + // DESCRIPTION: + // This is how we describe describeit the function + // + // OPTIONS: + // --help, -h show help (default: false) + +} + func ExampleApp_Run_shellComplete() { // set args for examples sake - os.Args = []string{"greet", "--generate-completion"} + os.Args = []string{"greet", fmt.Sprintf("--%s", genCompName())} app := &App{ Name: "greet", @@ -249,12 +299,11 @@ var commandAppTests = []struct { } func TestApp_Command(t *testing.T) { - app := &App{} - fooCommand := &Command{Name: "foobar", Aliases: []string{"f"}} - batCommand := &Command{Name: "batbaz", Aliases: []string{"b"}} - app.Commands = []*Command{ - fooCommand, - batCommand, + app := &App{ + Commands: []*Command{ + {Name: "foobar", Aliases: []string{"f"}}, + {Name: "batbaz", Aliases: []string{"b"}}, + }, } for _, test := range commandAppTests { @@ -262,6 +311,12 @@ func TestApp_Command(t *testing.T) { } } +func TestApp_Setup_defaultsWriter(t *testing.T) { + app := &App{} + app.Setup() + expect(t, app.Writer, os.Stdout) +} + func TestApp_RunAsSubcommandParseFlags(t *testing.T) { var context *Context @@ -312,19 +367,21 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { var parsedOption string var args Args - app := &App{} - command := &Command{ - Name: "cmd", - Flags: []Flag{ - &StringFlag{Name: "option", Value: "", Usage: "some option"}, - }, - Action: func(c *Context) error { - parsedOption = c.String("option") - args = c.Args() - return nil + app := &App{ + Commands: []*Command{ + { + Name: "cmd", + Flags: []Flag{ + &StringFlag{Name: "option", Value: "", Usage: "some option"}, + }, + Action: func(c *Context) error { + parsedOption = c.String("option") + args = c.Args() + return nil + }, + }, }, } - app.Commands = []*Command{command} app.Run([]string{"", "cmd", "--option", "my-option", "my-arg", "--", "--notARealFlag"}) @@ -337,15 +394,17 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { func TestApp_CommandWithDash(t *testing.T) { var args Args - app := &App{} - command := &Command{ - Name: "cmd", - Action: func(c *Context) error { - args = c.Args() - return nil + app := &App{ + Commands: []*Command{ + { + Name: "cmd", + Action: func(c *Context) error { + args = c.Args() + return nil + }, + }, }, } - app.Commands = []*Command{command} app.Run([]string{"", "cmd", "my-arg", "-"}) @@ -356,15 +415,17 @@ func TestApp_CommandWithDash(t *testing.T) { func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { var args Args - app := &App{} - command := &Command{ - Name: "cmd", - Action: func(c *Context) error { - args = c.Args() - return nil + app := &App{ + Commands: []*Command{ + { + Name: "cmd", + Action: func(c *Context) error { + args = c.Args() + return nil + }, + }, }, } - app.Commands = []*Command{command} app.Run([]string{"", "cmd", "my-arg", "--", "notAFlagAtAll"}) @@ -446,22 +507,24 @@ func TestApp_ParseSliceFlags(t *testing.T) { var parsedIntSlice []int var parsedStringSlice []string - app := &App{} - command := &Command{ - Name: "cmd", - Flags: []Flag{ - &IntSliceFlag{Name: "p", Value: NewIntSlice(), Usage: "set one or more ip addr"}, - &StringSliceFlag{Name: "ip", Value: NewStringSlice(), Usage: "set one or more ports to open"}, - }, - Action: func(c *Context) error { - parsedIntSlice = c.IntSlice("p") - parsedStringSlice = c.StringSlice("ip") - parsedOption = c.String("option") - firstArg = c.Args().First() - return nil + app := &App{ + Commands: []*Command{ + { + Name: "cmd", + Flags: []Flag{ + &IntSliceFlag{Name: "p", Value: NewIntSlice(), Usage: "set one or more ip addr"}, + &StringSliceFlag{Name: "ip", Value: NewStringSlice(), Usage: "set one or more ports to open"}, + }, + Action: func(c *Context) error { + parsedIntSlice = c.IntSlice("p") + parsedStringSlice = c.StringSlice("ip") + parsedOption = c.String("option") + firstArg = c.Args().First() + return nil + }, + }, }, } - app.Commands = []*Command{command} app.Run([]string{"", "cmd", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4", "my-arg"}) @@ -504,20 +567,22 @@ func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) { var parsedIntSlice []int var parsedStringSlice []string - app := &App{} - command := &Command{ - Name: "cmd", - Flags: []Flag{ - &IntSliceFlag{Name: "a", Usage: "set numbers"}, - &StringSliceFlag{Name: "str", Usage: "set strings"}, - }, - Action: func(c *Context) error { - parsedIntSlice = c.IntSlice("a") - parsedStringSlice = c.StringSlice("str") - return nil + app := &App{ + Commands: []*Command{ + { + Name: "cmd", + Flags: []Flag{ + &IntSliceFlag{Name: "a", Usage: "set numbers"}, + &StringSliceFlag{Name: "str", Usage: "set strings"}, + }, + Action: func(c *Context) error { + parsedIntSlice = c.IntSlice("a") + parsedStringSlice = c.StringSlice("str") + return nil + }, + }, }, } - app.Commands = []*Command{command} app.Run([]string{"", "cmd", "-a", "2", "-str", "A", "my-arg"}) @@ -824,6 +889,7 @@ func TestApp_OrderOfOperations(t *testing.T) { app := &App{ EnableShellCompletion: true, ShellComplete: func(c *Context) { + fmt.Fprintf(os.Stderr, "---> ShellComplete(%#v)\n", c) counts.Total++ counts.ShellComplete = counts.Total }, @@ -888,7 +954,7 @@ func TestApp_OrderOfOperations(t *testing.T) { resetCounts() - _ = app.Run([]string{"command", "--generate-completion"}) + _ = app.Run([]string{"command", fmt.Sprintf("--%s", genCompName())}) expect(t, counts.ShellComplete, 1) expect(t, counts.Total, 1) @@ -1485,3 +1551,107 @@ func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) { t.Errorf("Expect an intercepted error, but got \"%v\"", err) } } + +// A custom flag that conforms to the relevant interfaces, but has none of the +// fields that the other flag types do. +type customBoolFlag struct { + Nombre string +} + +// Don't use the normal FlagStringer +func (c *customBoolFlag) String() string { + return "***" + c.Nombre + "***" +} + +func (c *customBoolFlag) Names() []string { + return []string{c.Nombre} +} + +func (c *customBoolFlag) Apply(set *flag.FlagSet) { + set.String(c.Nombre, c.Nombre, "") +} + +func TestCustomFlagsUnused(t *testing.T) { + app := &App{ + Flags: []Flag{&customBoolFlag{"custom"}}, + } + + err := app.Run([]string{"foo"}) + if err != nil { + t.Errorf("Run returned unexpected error: %v", err) + } +} + +func TestCustomFlagsUsed(t *testing.T) { + app := &App{ + Flags: []Flag{&customBoolFlag{"custom"}}, + } + + err := app.Run([]string{"foo", "--custom=bar"}) + if err != nil { + t.Errorf("Run returned unexpected error: %v", err) + } +} + +func TestCustomHelpVersionFlags(t *testing.T) { + app := &App{} + + // Be sure to reset the global flags + defer func(helpFlag Flag, versionFlag Flag) { + HelpFlag = helpFlag.(*BoolFlag) + VersionFlag = versionFlag.(*BoolFlag) + }(HelpFlag, VersionFlag) + + HelpFlag = &customBoolFlag{"help-custom"} + VersionFlag = &customBoolFlag{"version-custom"} + + err := app.Run([]string{"foo", "--help-custom=bar"}) + if err != nil { + t.Errorf("Run returned unexpected error: %v", err) + } +} + +func TestShellCompletionForIncompleteFlags(t *testing.T) { + app := &App{ + Flags: []Flag{ + &IntFlag{ + Name: "test-completion", + }, + }, + EnableShellCompletion: true, + ShellComplete: func(ctx *Context) { + for _, command := range ctx.App.Commands { + if command.Hidden { + continue + } + + for _, name := range command.Names() { + fmt.Fprintln(ctx.App.Writer, name) + } + } + + for _, flag := range ctx.App.Flags { + for _, name := range flag.Names() { + if name == genCompName() { + continue + } + + switch name = strings.TrimSpace(name); len(name) { + case 0: + case 1: + fmt.Fprintln(ctx.App.Writer, "-"+name) + default: + fmt.Fprintln(ctx.App.Writer, "--"+name) + } + } + } + }, + Action: func(ctx *Context) error { + return fmt.Errorf("should not get here") + }, + } + err := app.Run([]string{"", "--test-completion", "--" + genCompName()}) + if err != nil { + t.Errorf("app should not return an error: %s", err) + } +} diff --git a/appveyor.yml b/appveyor.yml index ddc249e..886f0e9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,8 @@ version: "{build}" -os: Windows Server 2012 R2 +os: Windows Server 2016 + +image: Visual Studio 2017 clone_folder: c:\gopath\src\github.com\urfave\cli @@ -9,9 +11,9 @@ cache: environment: GOPATH: C:\gopath - GOVERSION: 1.6 - PYTHON: C:\Python27-x64 - PYTHON_VERSION: 2.7.x + GOVERSION: 1.8.x + PYTHON: C:\Python36-x64 + PYTHON_VERSION: 3.6.x PYTHON_ARCH: 64 install: diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete old mode 100644 new mode 100755 index 21a232f..37d9c14 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -3,12 +3,14 @@ : ${PROG:=$(basename ${BASH_SOURCE})} _cli_bash_autocomplete() { - local cur opts base - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - } - - complete -F _cli_bash_autocomplete $PROG + local cur opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} + +complete -F _cli_bash_autocomplete $PROG + +unset PROG diff --git a/cli-v1-to-v2 b/cli-v1-to-v2 index aa83461..b815295 100755 --- a/cli-v1-to-v2 +++ b/cli-v1-to-v2 @@ -236,7 +236,11 @@ def _flag_name_stringly_repl(match): @_migrator def _commands_opaque_type(source): - return re.sub('cli\\.Commands', '[]*cli.Command', source, flags=re.UNICODE) + return _subfmt( + 'cli\\.Commands(?P[^B])', + '[]*cli.Command{suffix}', + source + ) @_migrator diff --git a/cli.go b/cli.go index d5e3d54..81fc7ab 100644 --- a/cli.go +++ b/cli.go @@ -11,7 +11,8 @@ // Name: "greet", // Usage: "say a greeting", // Action: func(c *cli.Context) error { -// println("Greetings") +// fmt.Println("Greetings") +// return nil // }, // } // diff --git a/command.go b/command.go index 34b6152..4f0d142 100644 --- a/command.go +++ b/command.go @@ -49,6 +49,25 @@ type Command struct { // Full name of command for help, defaults to full command name, including parent commands. HelpName string commandNamePath []string + + // CustomHelpTemplate the text template for the command help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomHelpTemplate string +} + +type CommandsByName []*Command + +func (c CommandsByName) Len() int { + return len(c) +} + +func (c CommandsByName) Less(i, j int) bool { + return c[i].Name < c[j].Name +} + +func (c CommandsByName) Swap(i, j int) { + c[i], c[j] = c[j], c[i] } // FullName returns the full name of the command. @@ -75,7 +94,10 @@ func (c *Command) Run(ctx *Context) (err error) { c.appendFlag(GenerateCompletionFlag) } - set := flagSet(c.Name, c.Flags) + set, err := flagSet(c.Name, c.Flags) + if err != nil { + return err + } set.SetOutput(ioutil.Discard) if c.SkipFlagParsing { @@ -84,18 +106,6 @@ func (c *Command) Run(ctx *Context) (err error) { err = set.Parse(ctx.Args().Tail()) } - if err != nil { - if c.OnUsageError != nil { - err := c.OnUsageError(ctx, err, false) - HandleExitCoder(err) - return err - } - fmt.Fprintln(ctx.App.Writer, "Incorrect Usage:", err.Error()) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) - return err - } - nerr := normalizeFlags(c.Flags, set) if nerr != nil { fmt.Fprintln(ctx.App.Writer, nerr) @@ -105,11 +115,23 @@ func (c *Command) Run(ctx *Context) (err error) { } context := NewContext(ctx.App, set, ctx) - + context.Command = c if checkCommandCompletions(context, c.Name) { return nil } + if err != nil { + if c.OnUsageError != nil { + err := c.OnUsageError(context, err, false) + HandleExitCoder(err) + return err + } + fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) + fmt.Fprintln(context.App.Writer) + ShowCommandHelp(context, c.Name) + return err + } + if checkCommandHelp(context, c.Name) { return nil } @@ -131,14 +153,16 @@ func (c *Command) Run(ctx *Context) (err error) { if c.Before != nil { err = c.Before(context) if err != nil { - fmt.Fprintln(ctx.App.Writer, err) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) + ShowCommandHelp(context, c.Name) HandleExitCoder(err) return err } } + if c.Action == nil { + c.Action = helpSubcommand.Action + } + context.Command = c err = c.Action(context) @@ -175,14 +199,13 @@ func (c *Command) startApp(ctx *Context) error { app.HelpName = app.Name } - if c.Description != "" { - app.Usage = c.Description - } else { - app.Usage = c.Usage - } + app.Usage = c.Usage + app.Description = c.Description + app.ArgsUsage = c.ArgsUsage // set CommandNotFound app.CommandNotFound = ctx.App.CommandNotFound + app.CustomAppHelpTemplate = c.CustomHelpTemplate // set the flags and commands app.Commands = c.Subcommands @@ -193,6 +216,7 @@ func (c *Command) startApp(ctx *Context) error { app.HideVersion = ctx.App.HideVersion app.Compiled = ctx.App.Compiled app.Writer = ctx.App.Writer + app.ErrWriter = ctx.App.ErrWriter app.Categories = newCommandCategories() for _, command := range c.Subcommands { @@ -215,6 +239,7 @@ func (c *Command) startApp(ctx *Context) error { } else { app.Action = helpSubcommand.Action } + app.OnUsageError = c.OnUsageError for index, cc := range app.Commands { app.Commands[index].commandNamePath = []string{c.Name, cc.Name} diff --git a/command_test.go b/command_test.go index 6cd4d04..7fb3980 100644 --- a/command_test.go +++ b/command_test.go @@ -123,6 +123,31 @@ func TestCommand_Run_BeforeSavesMetadata(t *testing.T) { } } +func TestCommand_OnUsageError_hasCommandContext(t *testing.T) { + app := &App{ + Commands: []*Command{ + { + Name: "bar", + Flags: []Flag{ + &IntFlag{Name: "flag"}, + }, + OnUsageError: func(c *Context, err error, _ bool) error { + return fmt.Errorf("intercepted in %s: %s", c.Command.Name, err.Error()) + }, + }, + }, + } + + err := app.Run([]string{"foo", "bar", "--flag=wrong"}) + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + if !strings.HasPrefix(err.Error(), "intercepted in bar") { + t.Errorf("Expect an intercepted error, but got \"%v\"", err) + } +} + func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { app := &App{ Commands: []*Command{ @@ -150,3 +175,66 @@ func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { t.Errorf("Expect an intercepted error, but got \"%v\"", err) } } + +func TestCommand_OnUsageError_WithSubcommand(t *testing.T) { + app := &App{ + Commands: []*Command{ + { + Name: "bar", + Subcommands: []*Command{ + { + Name: "baz", + }, + }, + Flags: []Flag{ + &IntFlag{Name: "flag"}, + }, + OnUsageError: func(c *Context, err error, _ bool) error { + if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { + t.Errorf("Expect an invalid value error, but got \"%v\"", err) + } + return errors.New("intercepted: " + err.Error()) + }, + }, + }, + } + + err := app.Run([]string{"foo", "bar", "--flag=wrong"}) + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + if !strings.HasPrefix(err.Error(), "intercepted: invalid value") { + t.Errorf("Expect an intercepted error, but got \"%v\"", err) + } +} + +func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { + app := &App{ + ErrWriter: ioutil.Discard, + Commands: []*Command{ + { + Name: "bar", + Usage: "this is for testing", + Subcommands: []*Command{ + { + Name: "baz", + Usage: "this is for testing", + Action: func(c *Context) error { + if c.App.ErrWriter != ioutil.Discard { + return fmt.Errorf("ErrWriter not passed") + } + + return nil + }, + }, + }, + }, + }, + } + + err := app.Run([]string{"foo", "bar", "baz"}) + if err != nil { + t.Fatal(err) + } +} diff --git a/context.go b/context.go index e6e86b0..9802594 100644 --- a/context.go +++ b/context.go @@ -13,8 +13,9 @@ import ( // can be used to retrieve context-specific args and // parsed command-line options. type Context struct { - App *App - Command *Command + App *App + Command *Command + shellComplete bool flagSet *flag.FlagSet parentContext *Context @@ -22,7 +23,13 @@ type Context struct { // NewContext creates a new context. For use in when invoking an App or Command action. func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { - return &Context{App: app, flagSet: set, parentContext: parentCtx} + c := &Context{App: app, flagSet: set, parentContext: parentCtx} + + if parentCtx != nil { + c.shellComplete = parentCtx.shellComplete + } + + return c } // NumFlags returns the number of flags set @@ -114,6 +121,11 @@ func (c *Context) Lineage() []*Context { return lineage } +// value returns the value of the flag corresponding to `name` +func (c *Context) value(name string) interface{} { + return c.flagSet.Lookup(name).Value.(flag.Getter).Get() +} + // Args returns the command line arguments associated with the context. func (c *Context) Args() Args { ret := args(c.flagSet.Args()) diff --git a/context_test.go b/context_test.go index 870d4cd..edfbaee 100644 --- a/context_test.go +++ b/context_test.go @@ -2,7 +2,6 @@ package cli import ( "flag" - "os" "sort" "testing" "time" @@ -158,68 +157,6 @@ func TestContext_IsSet(t *testing.T) { expect(t, ctx.IsSet("bogus"), false) } -// XXX Corresponds to hack in context.IsSet for flags with EnvVar field -// Should be moved to `flag_test` in v2 -func TestContext_IsSet_fromEnv(t *testing.T) { - var ( - timeoutIsSet, tIsSet, noEnvVarIsSet, nIsSet bool - globalTimeoutIsSet, TIsSet, globalNoEnvVarIsSet, NIsSet bool - ) - - os.Clearenv() - os.Setenv("GLOBAL_APP_TIMEOUT_SECONDS", "15.5") - os.Setenv("APP_TIMEOUT_SECONDS", "15.5") - a := App{ - Flags: []Flag{ - &Float64Flag{ - Name: "global-timeout", - Aliases: []string{"T"}, - EnvVars: []string{"GLOBAL_APP_TIMEOUT_SECONDS"}, - }, - &Float64Flag{ - Name: "global-no-env-var", - Aliases: []string{"N"}, - }, - }, - Commands: []*Command{ - { - Name: "hello", - Flags: []Flag{ - &Float64Flag{ - Name: "timeout", - Aliases: []string{"t"}, - EnvVars: []string{"APP_TIMEOUT_SECONDS"}, - }, - &Float64Flag{ - Name: "no-env-var", - Aliases: []string{"n"}, - }, - }, - Action: func(ctx *Context) error { - globalTimeoutIsSet = ctx.IsSet("global-timeout") - TIsSet = ctx.IsSet("T") - globalNoEnvVarIsSet = ctx.IsSet("global-no-env-var") - NIsSet = ctx.IsSet("N") - timeoutIsSet = ctx.IsSet("timeout") - tIsSet = ctx.IsSet("t") - noEnvVarIsSet = ctx.IsSet("no-env-var") - nIsSet = ctx.IsSet("n") - return nil - }, - }, - }, - } - a.Run([]string{"run", "hello"}) - expect(t, globalTimeoutIsSet, true) - expect(t, TIsSet, true) - expect(t, globalNoEnvVarIsSet, false) - expect(t, NIsSet, false) - expect(t, timeoutIsSet, true) - expect(t, tIsSet, true) - expect(t, noEnvVarIsSet, false) - expect(t, nIsSet, false) -} - func TestContext_NumFlags(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") @@ -238,8 +175,10 @@ func TestContext_Set(t *testing.T) { set.Int("int", 5, "an int") c := NewContext(nil, set, nil) + expect(t, c.IsSet("int"), false) c.Set("int", "1") expect(t, c.Int("int"), 1) + expect(t, c.IsSet("int"), true) } func TestContext_LocalFlagNames(t *testing.T) { diff --git a/errors.go b/errors.go index 549e1d3..6259699 100644 --- a/errors.go +++ b/errors.go @@ -48,6 +48,10 @@ func (m *multiError) Errors() []error { return errs } +type ErrorFormatter interface { + Format(s fmt.State, verb rune) +} + // ExitCoder is the interface checked by `App` and `Command` for a custom exit // code type ExitCoder interface { @@ -57,12 +61,12 @@ type ExitCoder interface { type exitError struct { exitCode int - message string + message interface{} } // Exit wraps a message and exit code into an ExitCoder suitable for handling by // HandleExitCoder -func Exit(message string, exitCode int) ExitCoder { +func Exit(message interface{}, exitCode int) ExitCoder { return &exitError{ exitCode: exitCode, message: message, @@ -70,7 +74,7 @@ func Exit(message string, exitCode int) ExitCoder { } func (ee *exitError) Error() string { - return ee.message + return fmt.Sprintf("%v", ee.message) } func (ee *exitError) ExitCode() int { @@ -80,7 +84,7 @@ 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 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. +// called on all members of the Errors slice and calls OsExiter with the last exit code. func HandleExitCoder(err error) { if err == nil { return @@ -88,21 +92,34 @@ func HandleExitCoder(err error) { if exitErr, ok := err.(ExitCoder); ok { if err.Error() != "" { - fmt.Fprintln(ErrWriter, err) + if _, ok := exitErr.(ErrorFormatter); ok { + fmt.Fprintf(ErrWriter, "%+v\n", err) + } else { + fmt.Fprintln(ErrWriter, err) + } } OsExiter(exitErr.ExitCode()) return } if multiErr, ok := err.(MultiError); ok { - for _, merr := range multiErr.Errors() { - HandleExitCoder(merr) - } + code := handleMultiError(multiErr) + OsExiter(code) return } - - if err.Error() != "" { - fmt.Fprintln(ErrWriter, err) - } - OsExiter(1) +} + +func handleMultiError(multiErr MultiError) int { + code := 1 + for _, merr := range multiErr.Errors() { + if multiErr2, ok := merr.(MultiError); ok { + code = handleMultiError(multiErr2) + } else if merr != nil { + fmt.Fprintln(ErrWriter, merr) + if exitErr, ok := merr.(ExitCoder); ok { + code = exitErr.ExitCode() + } + } + } + return code } diff --git a/errors_test.go b/errors_test.go index 69f2b48..9ed3be3 100644 --- a/errors_test.go +++ b/errors_test.go @@ -3,6 +3,7 @@ package cli import ( "bytes" "errors" + "fmt" "testing" ) @@ -11,8 +12,10 @@ func TestHandleExitCoder_nil(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + exitCode = rc + called = true + } } defer func() { OsExiter = fakeOsExiter }() @@ -28,8 +31,10 @@ func TestHandleExitCoder_ExitCoder(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + exitCode = rc + called = true + } } defer func() { OsExiter = fakeOsExiter }() @@ -45,27 +50,43 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + exitCode = rc + called = true + } } defer func() { OsExiter = fakeOsExiter }() exitErr := Exit("galactic perimeter breach", 9) - err := newMultiError(errors.New("wowsa"), errors.New("egad"), exitErr) + exitErr2 := Exit("last ExitCoder", 11) + err := newMultiError(errors.New("wowsa"), errors.New("egad"), exitErr, exitErr2) HandleExitCoder(err) - expect(t, exitCode, 9) + expect(t, exitCode, 11) expect(t, called, true) } -func TestHandleExitCoder_ErrorWithMessage(t *testing.T) { - exitCode := 0 +// make a stub to not import pkg/errors +type ErrorWithFormat struct { + error +} + +func NewErrorWithFormat(m string) *ErrorWithFormat { + return &ErrorWithFormat{error: errors.New(m)} +} + +func (f *ErrorWithFormat) Format(s fmt.State, verb rune) { + fmt.Fprintf(s, "This the format: %v", f.error) +} + +func TestHandleExitCoder_ErrorWithFormat(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + called = true + } } ErrWriter = &bytes.Buffer{} @@ -74,33 +95,28 @@ func TestHandleExitCoder_ErrorWithMessage(t *testing.T) { ErrWriter = fakeErrWriter }() - err := errors.New("gourd havens") + err := Exit(NewErrorWithFormat("I am formatted"), 1) HandleExitCoder(err) - expect(t, exitCode, 1) expect(t, called, true) - expect(t, ErrWriter.(*bytes.Buffer).String(), "gourd havens\n") + expect(t, ErrWriter.(*bytes.Buffer).String(), "This the format: I am formatted\n") } -func TestHandleExitCoder_ErrorWithoutMessage(t *testing.T) { - exitCode := 0 +func TestHandleExitCoder_MultiErrorWithFormat(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + called = true + } } ErrWriter = &bytes.Buffer{} - defer func() { - OsExiter = fakeOsExiter - ErrWriter = fakeErrWriter - }() + defer func() { OsExiter = fakeOsExiter }() - err := errors.New("") + err := newMultiError(NewErrorWithFormat("err1"), NewErrorWithFormat("err2")) HandleExitCoder(err) - expect(t, exitCode, 1) expect(t, called, true) - expect(t, ErrWriter.(*bytes.Buffer).String(), "") + expect(t, ErrWriter.(*bytes.Buffer).String(), "This the format: err1\nThis the format: err2\n") } diff --git a/flag.go b/flag.go index 582944f..d5ab42e 100644 --- a/flag.go +++ b/flag.go @@ -4,12 +4,12 @@ import ( "encoding/json" "flag" "fmt" - "os" "reflect" "regexp" "runtime" "strconv" "strings" + "syscall" "time" ) @@ -22,11 +22,19 @@ var ( ) // GenerateCompletionFlag enables completion for all commands and subcommands -var GenerateCompletionFlag = &BoolFlag{ +var GenerateCompletionFlag Flag = &BoolFlag{ Name: "generate-completion", Hidden: true, } +func genCompName() string { + names := GenerateCompletionFlag.Names() + if len(names) == 0 { + return "generate-completion" + } + return names[0] +} + // InitCompletionFlag generates completion code var InitCompletionFlag = &StringFlag{ Name: "init-completion", @@ -34,7 +42,7 @@ var InitCompletionFlag = &StringFlag{ } // VersionFlag prints the version for the application -var VersionFlag = &BoolFlag{ +var VersionFlag Flag = &BoolFlag{ Name: "version", Aliases: []string{"v"}, Usage: "print the version", @@ -43,7 +51,7 @@ var VersionFlag = &BoolFlag{ // HelpFlag prints the help for all commands and subcommands. // Set to nil to disable the flag. The subcommand // will still be added unless HideHelp is set to true. -var HelpFlag = &BoolFlag{ +var HelpFlag Flag = &BoolFlag{ Name: "help", Aliases: []string{"h"}, Usage: "show help", @@ -88,13 +96,29 @@ type Flag interface { Names() []string } -func flagSet(name string, flags []Flag) *flag.FlagSet { +// errorableFlag is an interface that allows us to return errors during apply +// it allows flags defined in this library to return errors in a fashion backwards compatible +// TODO remove in v2 and modify the existing Flag interface to return errors +type errorableFlag interface { + Flag + + ApplyWithError(*flag.FlagSet) error +} + +func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) for _, f := range flags { - f.Apply(set) + //TODO remove in v2 when errorableFlag is removed + if ef, ok := f.(errorableFlag); ok { + if err := ef.ApplyWithError(set); err != nil { + return nil, err + } + } else { + f.Apply(set) + } } - return set + return set, nil } // Generic is a generic parseable type identified by a specific flag @@ -105,11 +129,18 @@ type Generic interface { // Apply takes the flagset and calls Set on the generic flag with the value // provided by the user for parsing by the flag +// Ignores parsing errors func (f *GenericFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError 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) ApplyWithError(set *flag.FlagSet) error { val := f.Value if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { val.Set(envVal) break } @@ -119,6 +150,7 @@ func (f *GenericFlag) Apply(set *flag.FlagSet) { for _, name := range f.Names() { set.Var(val, name, f.Usage) } + return nil } // StringSlice wraps a []string to satisfy flag.Value @@ -166,15 +198,28 @@ func (f *StringSlice) Value() []string { return f.slice } +// Get returns the slice of strings set by this flag +func (f *StringSlice) Get() interface{} { + return *f +} + // Apply populates the flag given the flag set and environment +// Ignores errors func (f *StringSliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { newVal := NewStringSlice() for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) - newVal.Set(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %q as string value for flag %s: %s", envVal, f.Name, err) + } } f.Value = newVal break @@ -189,6 +234,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) { for _, name := range f.Names() { set.Var(f.Value, name, f.Usage) } + return nil } // IntSlice wraps an []int to satisfy flag.Value @@ -256,17 +302,27 @@ func (i *IntSlice) Value() []int { return i.slice } +// Get returns the slice of ints set by this flag +func (f *IntSlice) Get() interface{} { + return *f +} + // Apply populates the flag given the flag set and environment +// Ignores errors func (f *IntSliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { newVal := NewIntSlice() for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) - err := newVal.Set(s) - if err != nil { - fmt.Fprintf(ErrWriter, err.Error()) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", envVal, f.Name, err) } } f.Value = newVal @@ -282,6 +338,7 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) { for _, name := range f.Names() { set.Var(f.Value, name, f.Usage) } + return nil } // Int64Slice is an opaque type for []int to satisfy flag.Value @@ -329,17 +386,27 @@ func (f *Int64Slice) Value() []int64 { return f.slice } +// Get returns the slice of ints set by this flag +func (f *Int64Slice) Get() interface{} { + return *f +} + // Apply populates the flag given the flag set and environment +// Ignores errors func (f *Int64SliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { newVal := NewInt64Slice() for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) - err := newVal.Set(s) - if err != nil { - fmt.Fprintf(ErrWriter, err.Error()) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", envVal, f.Name, err) } } f.Value = newVal @@ -355,17 +422,30 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) { for _, name := range f.Names() { set.Var(f.Value, name, f.Usage) } + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *BoolFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - f.Value = envValBool + if envVal, ok := syscall.Getenv(envVar); ok { + if envVal == "" { + f.Value = false + break } + + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %q as bool value for flag %s: %s", envVal, f.Name, err) + } + f.Value = envValBool break } } @@ -378,13 +458,20 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) { } set.Bool(name, f.Value, f.Usage) } + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *StringFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { f.Value = envVal break } @@ -398,18 +485,26 @@ func (f *StringFlag) Apply(set *flag.FlagSet) { } set.String(name, f.Value, f.Usage) } + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *IntFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err == nil { - f.Value = int(envValInt) - break + if err != nil { + return fmt.Errorf("could not parse %q as int value for flag %s: %s", envVal, f.Name, err) } + f.Value = int(envValInt) + break } } } @@ -421,18 +516,27 @@ func (f *IntFlag) Apply(set *flag.FlagSet) { } set.Int(name, f.Value, f.Usage) } + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *Int64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err == nil { - f.Value = envValInt - break + if err != nil { + return fmt.Errorf("could not parse %q as int value for flag %s: %s", envVal, f.Name, err) } + + f.Value = envValInt + break } } } @@ -440,22 +544,31 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) { for _, name := range f.Names() { if f.Destination != nil { set.Int64Var(f.Destination, name, f.Value, f.Usage) - return + return nil } set.Int64(name, f.Value, f.Usage) } + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *UintFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValInt, err := strconv.ParseUint(envVal, 0, 64) - if err == nil { - f.Value = uint(envValInt) - break + if err != nil { + return fmt.Errorf("could not parse %q as uint value for flag %s: %s", envVal, f.Name, err) } + + f.Value = uint(envValInt) + break } } } @@ -463,22 +576,31 @@ func (f *UintFlag) Apply(set *flag.FlagSet) { for _, name := range f.Names() { if f.Destination != nil { set.UintVar(f.Destination, name, f.Value, f.Usage) - return + return nil } set.Uint(name, f.Value, f.Usage) } + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *Uint64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValInt, err := strconv.ParseUint(envVal, 0, 64) - if err == nil { - f.Value = uint64(envValInt) - break + if err != nil { + return fmt.Errorf("could not parse %q as uint64 value for flag %s: %s", envVal, f.Name, err) } + + f.Value = uint64(envValInt) + break } } } @@ -486,22 +608,31 @@ func (f *Uint64Flag) Apply(set *flag.FlagSet) { for _, name := range f.Names() { if f.Destination != nil { set.Uint64Var(f.Destination, name, f.Value, f.Usage) - return + return nil } set.Uint64(name, f.Value, f.Usage) } + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *DurationFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValDuration, err := time.ParseDuration(envVal) - if err == nil { - f.Value = envValDuration - break + if err != nil { + return fmt.Errorf("could not parse %q as duration for flag %s: %s", envVal, f.Name, err) } + + f.Value = envValDuration + break } } } @@ -513,17 +644,27 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) { } set.Duration(name, f.Value, f.Usage) } + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *Float64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValFloat, err := strconv.ParseFloat(envVal, 10) - if err == nil { - f.Value = float64(envValFloat) + if err != nil { + return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", envVal, f.Name, err) } + + f.Value = float64(envValFloat) + break } } } @@ -535,6 +676,7 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) { } set.Float64(name, f.Value, f.Usage) } + return nil } // NewFloat64Slice makes a *Float64Slice with default values @@ -588,10 +730,16 @@ func (f *Float64Slice) Value() []float64 { } // Apply populates the flag given the flag set and environment +// Ignores errors func (f *Float64SliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f *Float64SliceFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVars != nil { for _, envVar := range f.EnvVars { - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { newVal := NewFloat64Slice() for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) @@ -613,12 +761,14 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) { for _, name := range f.Names() { set.Var(f.Value, name, f.Usage) } + return nil } func visibleFlags(fl []Flag) []Flag { visible := []Flag{} for _, flag := range fl { - if !flagValue(flag).FieldByName("Hidden").Bool() { + field := flagValue(flag).FieldByName("Hidden") + if !field.IsValid() || !field.Bool() { visible = append(visible, flag) } } diff --git a/flag_generated.go b/flag_generated.go index 1f48d9f..187a6ca 100644 --- a/flag_generated.go +++ b/flag_generated.go @@ -17,6 +17,7 @@ type BoolFlag struct { Hidden bool Value bool DefaultText string + Destination *bool } @@ -61,6 +62,7 @@ type DurationFlag struct { Hidden bool Value time.Duration DefaultText string + Destination *time.Duration } @@ -105,6 +107,7 @@ type Float64Flag struct { Hidden bool Value float64 DefaultText string + Destination *float64 } @@ -192,6 +195,7 @@ type Int64Flag struct { Hidden bool Value int64 DefaultText string + Destination *int64 } @@ -236,6 +240,7 @@ type IntFlag struct { Hidden bool Value int DefaultText string + Destination *int } @@ -409,6 +414,7 @@ type StringFlag struct { Hidden bool Value string DefaultText string + Destination *string } @@ -496,6 +502,7 @@ type Uint64Flag struct { Hidden bool Value uint64 DefaultText string + Destination *uint64 } @@ -540,6 +547,7 @@ type UintFlag struct { Hidden bool Value uint DefaultText string + Destination *uint } diff --git a/flag_test.go b/flag_test.go index ccb1d45..2c42176 100644 --- a/flag_test.go +++ b/flag_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "reflect" + "regexp" "runtime" "strings" "testing" @@ -41,6 +42,106 @@ func TestBoolFlagApply_SetsAllNames(t *testing.T) { expect(t, v, true) } +func TestFlagsFromEnv(t *testing.T) { + newSetIntSlice := func(defaults ...int) IntSlice { + s := NewIntSlice(defaults...) + s.hasBeenSet = true + return *s + } + + newSetInt64Slice := func(defaults ...int64) Int64Slice { + s := NewInt64Slice(defaults...) + s.hasBeenSet = true + return *s + } + + newSetStringSlice := func(defaults ...string) StringSlice { + s := NewStringSlice(defaults...) + s.hasBeenSet = true + return *s + } + + var flagTests = []struct { + input string + output interface{} + flag Flag + errRegexp string + }{ + {"", false, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""}, + {"1", true, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""}, + {"false", false, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""}, + {"foobar", true, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, `could not parse "foobar" as bool value for flag debug: .*`}, + + {"1s", 1 * time.Second, &DurationFlag{Name: "time", EnvVars: []string{"TIME"}}, ""}, + {"foobar", false, &DurationFlag{Name: "time", EnvVars: []string{"TIME"}}, `could not parse "foobar" as duration for flag time: .*`}, + + {"1.2", 1.2, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1", 1.0, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"foobar", 0, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as float64 value for flag seconds: .*`}, + + {"1", int64(1), &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1.2", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`}, + {"foobar", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`}, + + {"1", 1, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`}, + {"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`}, + + {"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value for flag seconds: .*`}, + {"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value for flag seconds: .*`}, + + {"1,2", newSetInt64Slice(1, 2), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1.2,2", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int64 slice value for flag seconds: .*`}, + {"foobar", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int64 slice value for flag seconds: .*`}, + + {"foo", "foo", &StringFlag{Name: "name", EnvVars: []string{"NAME"}}, ""}, + + {"foo,bar", newSetStringSlice("foo", "bar"), &StringSliceFlag{Name: "names", EnvVars: []string{"NAMES"}}, ""}, + + {"1", uint(1), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1.2", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint value for flag seconds: .*`}, + {"foobar", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint value for flag seconds: .*`}, + + {"1", uint64(1), &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1.2", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint64 value for flag seconds: .*`}, + {"foobar", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint64 value for flag seconds: .*`}, + + {"foo,bar", &Parser{"foo", "bar"}, &GenericFlag{Name: "names", Value: &Parser{}, EnvVars: []string{"NAMES"}}, ""}, + } + + for i, test := range flagTests { + clearenv() + envVarSlice := reflect.Indirect(reflect.ValueOf(test.flag)).FieldByName("EnvVars").Slice(0, 1) + os.Setenv(envVarSlice.Index(0).String(), test.input) + a := App{ + Flags: []Flag{test.flag}, + Action: func(ctx *Context) error { + if !reflect.DeepEqual(ctx.value(test.flag.Names()[0]), test.output) { + t.Errorf("ex:%01d expected %q to be parsed as %#v, instead was %#v", i, test.input, test.output, ctx.value(test.flag.Names()[0])) + } + return nil + }, + } + + err := a.Run([]string{"run"}) + + if test.errRegexp != "" { + if err == nil { + t.Errorf("expected error to match %q, got none", test.errRegexp) + } else { + if matched, _ := regexp.MatchString(test.errRegexp, err.Error()); !matched { + t.Errorf("expected error to match %q, got error %s", test.errRegexp, err) + } + } + } else { + if err != nil && test.errRegexp == "" { + t.Errorf("expected no error got %q", err) + } + } + } +} + var stringFlagTests = []struct { name string aliases []string @@ -78,7 +179,7 @@ func TestStringFlagDefaultText(t *testing.T) { } func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_FOO", "derp") for _, test := range stringFlagTests { flag := &StringFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_FOO"}} @@ -130,7 +231,7 @@ func TestStringSliceFlagHelpOutput(t *testing.T) { } func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_QWWX", "11,4") for _, test := range stringSliceFlagTests { flag := &StringSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_QWWX"}} @@ -175,7 +276,7 @@ func TestIntFlagHelpOutput(t *testing.T) { } func TestIntFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_BAR", "2") for _, test := range intFlagTests { flag := &IntFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} @@ -222,7 +323,7 @@ func TestInt64FlagHelpOutput(t *testing.T) { } func TestInt64FlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_BAR", "2") for _, test := range int64FlagTests { flag := IntFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} @@ -258,7 +359,7 @@ func TestUintFlagHelpOutput(t *testing.T) { } func TestUintFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_BAR", "2") for _, test := range uintFlagTests { flag := UintFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} @@ -294,7 +395,7 @@ func TestUint64FlagHelpOutput(t *testing.T) { } func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_BAR", "2") for _, test := range uint64FlagTests { flag := UintFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} @@ -330,7 +431,7 @@ func TestDurationFlagHelpOutput(t *testing.T) { } func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_BAR", "2h3m6s") for _, test := range durationFlagTests { flag := &DurationFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} @@ -380,7 +481,7 @@ func TestIntSliceFlagHelpOutput(t *testing.T) { } func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_SMURF", "42,3") for _, test := range intSliceFlagTests { flag := &IntSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_SMURF"}} @@ -429,7 +530,7 @@ func TestInt64SliceFlagHelpOutput(t *testing.T) { } func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_SMURF", "42,17179869184") for _, test := range int64SliceFlagTests { flag := Int64SliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}} @@ -465,7 +566,7 @@ func TestFloat64FlagHelpOutput(t *testing.T) { } func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_BAZ", "99.4") for _, test := range float64FlagTests { flag := &Float64Flag{Name: test.name, EnvVars: []string{"APP_BAZ"}} @@ -516,7 +617,7 @@ func TestFloat64SliceFlagHelpOutput(t *testing.T) { } func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_SMURF", "0.1234,-10.5") for _, test := range float64SliceFlagTests { flag := Float64SliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}} @@ -553,7 +654,7 @@ func TestGenericFlagHelpOutput(t *testing.T) { } func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_ZAP", "3") for _, test := range genericFlagTests { flag := &GenericFlag{Name: test.name, EnvVars: []string{"APP_ZAP"}} @@ -615,7 +716,7 @@ func TestParseDestinationString(t *testing.T) { } func TestParseMultiStringFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_COUNT", "20") (&App{ Flags: []Flag{ @@ -634,7 +735,7 @@ func TestParseMultiStringFromEnv(t *testing.T) { } func TestParseMultiStringFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_COUNT", "20") (&App{ Flags: []Flag{ @@ -706,7 +807,7 @@ func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) { } func TestParseMultiStringSliceFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&App{ @@ -726,7 +827,7 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) { } func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&App{ @@ -746,7 +847,7 @@ func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) { } func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&App{ @@ -766,7 +867,7 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { } func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&App{ @@ -823,7 +924,7 @@ func TestParseDestinationInt(t *testing.T) { } func TestParseMultiIntFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_TIMEOUT_SECONDS", "10") a := App{ Flags: []Flag{ @@ -843,7 +944,7 @@ func TestParseMultiIntFromEnv(t *testing.T) { } func TestParseMultiIntFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_TIMEOUT_SECONDS", "10") a := App{ Flags: []Flag{ @@ -914,7 +1015,7 @@ func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) { } func TestParseMultiIntSliceFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&App{ @@ -934,7 +1035,7 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) { } func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&App{ @@ -954,7 +1055,7 @@ func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) { } func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&App{ @@ -991,7 +1092,7 @@ func TestParseMultiInt64Slice(t *testing.T) { } func TestParseMultiInt64SliceFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,17179869184") (&App{ @@ -1011,7 +1112,7 @@ func TestParseMultiInt64SliceFromEnv(t *testing.T) { } func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "20,30,17179869184") (&App{ @@ -1068,7 +1169,7 @@ func TestParseDestinationFloat64(t *testing.T) { } func TestParseMultiFloat64FromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_TIMEOUT_SECONDS", "15.5") a := App{ Flags: []Flag{ @@ -1088,7 +1189,7 @@ func TestParseMultiFloat64FromEnv(t *testing.T) { } func TestParseMultiFloat64FromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_TIMEOUT_SECONDS", "15.5") a := App{ Flags: []Flag{ @@ -1108,7 +1209,7 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) { } func TestParseMultiFloat64SliceFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "0.1,-10.5") (&App{ @@ -1128,7 +1229,7 @@ func TestParseMultiFloat64SliceFromEnv(t *testing.T) { } func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_INTERVALS", "0.1234,-10.5") (&App{ @@ -1185,7 +1286,7 @@ func TestParseDestinationBool(t *testing.T) { } func TestParseMultiBoolFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_DEBUG", "1") a := App{ Flags: []Flag{ @@ -1205,7 +1306,7 @@ func TestParseMultiBoolFromEnv(t *testing.T) { } func TestParseMultiBoolFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_DEBUG", "1") a := App{ Flags: []Flag{ @@ -1264,7 +1365,7 @@ func TestParseDestinationBoolTrue(t *testing.T) { } func TestParseMultiBoolTrueFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_DEBUG", "0") a := App{ Flags: []Flag{ @@ -1289,7 +1390,7 @@ func TestParseMultiBoolTrueFromEnv(t *testing.T) { } func TestParseMultiBoolTrueFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_DEBUG", "0") a := App{ Flags: []Flag{ @@ -1331,6 +1432,10 @@ func (p *Parser) String() string { return fmt.Sprintf("%s,%s", p[0], p[1]) } +func (p *Parser) Get() interface{} { + return p +} + func TestParseGeneric(t *testing.T) { a := App{ Flags: []Flag{ @@ -1350,7 +1455,7 @@ func TestParseGeneric(t *testing.T) { } func TestParseGenericFromEnv(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_SERVE", "20,30") a := App{ Flags: []Flag{ @@ -1375,7 +1480,7 @@ func TestParseGenericFromEnv(t *testing.T) { } func TestParseGenericFromEnvCascade(t *testing.T) { - os.Clearenv() + clearenv() os.Setenv("APP_FOO", "99,2000") a := App{ Flags: []Flag{ diff --git a/generate-flag-types b/generate-flag-types index 3213dd0..d77d470 100755 --- a/generate-flag-types +++ b/generate-flag-types @@ -68,6 +68,9 @@ import tempfile import textwrap +_PY3 = sys.version_info.major == 3 + + class _FancyFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass @@ -104,7 +107,7 @@ def main(sysargs=sys.argv[:]): def _generate_flag_types(writefunc, output_go, input_json): types = json.load(input_json) - tmp = tempfile.NamedTemporaryFile(suffix='.go', delete=False) + tmp = _get_named_tmp_go() writefunc(tmp, types) tmp.close() @@ -117,6 +120,13 @@ def _generate_flag_types(writefunc, output_go, input_json): os.remove(tmp.name) +def _get_named_tmp_go(): + tmp_args = dict(suffix='.go', mode='w', delete=False) + if _PY3: + tmp_args['encoding'] = 'utf-8' + return tempfile.NamedTemporaryFile(**tmp_args) + + def _set_typedef_defaults(typedef): typedef.setdefault('doctail', '') typedef.setdefault('context_type', typedef['type']) @@ -222,11 +232,18 @@ def _write_altsrc_flag_types(outfile, types): f.set = set f.{name}Flag.Apply(set) }} + + // ApplyWithError saves the flagSet for later usage calls, then calls the + // wrapped {name}Flag.ApplyWithError + func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{ + f.set = set + return f.{name}Flag.ApplyWithError(set) + }} """.format(**typedef)) def _fwrite(outfile, text): - print(textwrap.dedent(text), end='', file=outfile) + print(textwrap.dedent(text), end=None, file=outfile) _WRITEFUNCS = { diff --git a/help.go b/help.go index 188366f..02fcf33 100644 --- a/help.go +++ b/help.go @@ -13,7 +13,7 @@ import ( // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var AppHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} USAGE: {{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}} @@ -47,7 +47,7 @@ var CommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} @@ -64,10 +64,10 @@ OPTIONS: // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var SubcommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} + {{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}} USAGE: - {{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{end}}{{range .VisibleCommands}} @@ -112,17 +112,43 @@ var helpSubcommand = &Command{ // Prints help for the App or Command type helpPrinter func(w io.Writer, templ string, data interface{}) +// Prints help for the App or Command with custom template function. +type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) + // HelpPrinter is a function that writes the help output. If not set a default // is used. The function signature is: // func(w io.Writer, templ string, data interface{}) var HelpPrinter helpPrinter = printHelp +// HelpPrinterCustom is same as HelpPrinter but +// takes a custom function for template function map. +var HelpPrinterCustom helpPrinterCustom = printHelpCustom + // VersionPrinter prints the version for the App var VersionPrinter = printVersion +// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. +func ShowAppHelpAndExit(c *Context, exitCode int) { + ShowAppHelp(c) + os.Exit(exitCode) +} + // ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) +func ShowAppHelp(c *Context) (err error) { + if c.App.CustomAppHelpTemplate == "" { + HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + return + } + customAppData := func() map[string]interface{} { + if c.App.ExtraInfo == nil { + return nil + } + return map[string]interface{}{ + "ExtraInfo": c.App.ExtraInfo, + } + } + HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData()) + return nil } // DefaultAppComplete prints the list of subcommands as the default app completion method @@ -137,6 +163,12 @@ func DefaultAppComplete(c *Context) { } } +// ShowCommandHelpAndExit - exits with code after showing help +func ShowCommandHelpAndExit(c *Context, command string, code int) { + ShowCommandHelp(c, command) + os.Exit(code) +} + // ShowCommandHelp prints help for the given command func ShowCommandHelp(ctx *Context, command string) error { // show the subcommand help for a command with subcommands @@ -147,7 +179,11 @@ func ShowCommandHelp(ctx *Context, command string) error { for _, c := range ctx.App.Commands { if c.HasName(command) { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + if c.CustomHelpTemplate != "" { + HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil) + } else { + HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + } return nil } } @@ -198,10 +234,15 @@ func ShowCommandCompletions(ctx *Context, command string) { } } -func printHelp(out io.Writer, templ string, data interface{}) { +func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { funcMap := template.FuncMap{ "join": strings.Join, } + if customFunc != nil { + for key, value := range customFunc { + funcMap[key] = value + } + } w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) @@ -219,13 +260,15 @@ func printHelp(out io.Writer, templ string, data interface{}) { w.Flush() } +func printHelp(out io.Writer, templ string, data interface{}) { + printHelpCustom(out, templ, data, nil) +} + func checkVersion(c *Context) bool { found := false - if VersionFlag.Name != "" { - for _, name := range VersionFlag.Names() { - if c.Bool(name) { - found = true - } + for _, name := range VersionFlag.Names() { + if c.Bool(name) { + found = true } } return found @@ -233,11 +276,9 @@ func checkVersion(c *Context) bool { func checkHelp(c *Context) bool { found := false - if HelpFlag.Name != "" { - for _, name := range HelpFlag.Names() { - if c.Bool(name) { - found = true - } + for _, name := range HelpFlag.Names() { + if c.Bool(name) { + found = true } } return found @@ -261,22 +302,45 @@ func checkSubcommandHelp(c *Context) bool { return false } -func checkCompletions(c *Context) bool { - if c.Bool(GenerateCompletionFlag.Name) && c.App.EnableShellCompletion { - ShowCompletions(c) - return true +func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { + if !a.EnableShellCompletion { + return false, arguments } - return false + pos := len(arguments) - 1 + lastArg := arguments[pos] + + if lastArg != "--"+genCompName() { + return false, arguments + } + + return true, arguments[:pos] +} + +func checkCompletions(c *Context) bool { + if !c.shellComplete { + return false + } + + if args := c.Args(); args.Present() { + name := args.First() + if cmd := c.App.Command(name); cmd != nil { + // let the command handle the completion + return false + } + } + + ShowCompletions(c) + return true } func checkCommandCompletions(c *Context, name string) bool { - if c.Bool(GenerateCompletionFlag.Name) && c.App.EnableShellCompletion { - ShowCommandCompletions(c, name) - return true + if !c.shellComplete { + return false } - return false + ShowCommandCompletions(c, name) + return true } func checkInitCompletion(c *Context) (bool, error) { @@ -302,12 +366,12 @@ func bashCompletionCode(progName string) string { local cur opts base; COMPREPLY=(); cur="${COMP_WORDS[COMP_CWORD]}"; - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-completion ); + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --%s ); COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ); return 0; }; complete -F _cli_bash_autocomplete %s` - return fmt.Sprintf(template, progName) + return fmt.Sprintf(template, genCompName(), progName) } func zshCompletionCode(progName string) string { diff --git a/help_test.go b/help_test.go index 579e6ab..375b28e 100644 --- a/help_test.go +++ b/help_test.go @@ -3,6 +3,8 @@ package cli import ( "bytes" "flag" + "fmt" + "runtime" "strings" "testing" ) @@ -255,6 +257,92 @@ func TestShowSubcommandHelp_CommandAliases(t *testing.T) { } } +func TestShowCommandHelp_Customtemplate(t *testing.T) { + app := &App{ + Commands: []*Command{ + { + Name: "frobbly", + Action: func(ctx *Context) error { + return nil + }, + HelpName: "foo frobbly", + CustomHelpTemplate: `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{.HelpName}} [FLAGS] TARGET [TARGET ...] + +FLAGS: + {{range .VisibleFlags}}{{.}} + {{end}} +EXAMPLES: + 1. Frobbly runs with this param locally. + $ {{.HelpName}} wobbly +`, + }, + }, + } + output := &bytes.Buffer{} + app.Writer = output + app.Run([]string{"foo", "help", "frobbly"}) + + if strings.Contains(output.String(), "2. Frobbly runs without this param locally.") { + t.Errorf("expected output to exclude \"2. Frobbly runs without this param locally.\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "1. Frobbly runs with this param locally.") { + t.Errorf("expected output to include \"1. Frobbly runs with this param locally.\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "$ foo frobbly wobbly") { + t.Errorf("expected output to include \"$ foo frobbly wobbly\"; got: %q", output.String()) + } +} + +func TestShowSubcommandHelp_CommandUsageText(t *testing.T) { + app := &App{ + Commands: []*Command{ + { + Name: "frobbly", + UsageText: "this is usage text", + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + + app.Run([]string{"foo", "frobbly", "--help"}) + + if !strings.Contains(output.String(), "this is usage text") { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + +func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) { + app := &App{ + Commands: []*Command{ + { + Name: "frobbly", + Subcommands: []*Command{ + { + Name: "bobbly", + UsageText: "this is usage text", + }, + }, + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + app.Run([]string{"foo", "frobbly", "bobbly", "--help"}) + + if !strings.Contains(output.String(), "this is usage text") { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + func TestShowAppHelp_HiddenCommand(t *testing.T) { app := &App{ Commands: []*Command{ @@ -286,3 +374,78 @@ func TestShowAppHelp_HiddenCommand(t *testing.T) { t.Errorf("expected output to include \"frobbly\"; got: %q", output.String()) } } + +func TestShowAppHelp_CustomAppTemplate(t *testing.T) { + app := &App{ + Commands: []*Command{ + { + Name: "frobbly", + Action: func(ctx *Context) error { + return nil + }, + }, + { + Name: "secretfrob", + Hidden: true, + Action: func(ctx *Context) error { + return nil + }, + }, + }, + ExtraInfo: func() map[string]string { + platform := fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH) + goruntime := fmt.Sprintf("Version: %s | CPUs: %d", runtime.Version(), runtime.NumCPU()) + return map[string]string{ + "PLATFORM": platform, + "RUNTIME": goruntime, + } + }, + CustomAppHelpTemplate: `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + {{.Name}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...] + +COMMANDS: + {{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} + {{end}}{{if .VisibleFlags}} +GLOBAL FLAGS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +VERSION: + 2.0.0 +{{"\n"}}{{range $key, $value := ExtraInfo}} +{{$key}}: + {{$value}} +{{end}}`, + } + + output := &bytes.Buffer{} + app.Writer = output + app.Run([]string{"app", "--help"}) + + if strings.Contains(output.String(), "secretfrob") { + t.Errorf("expected output to exclude \"secretfrob\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "frobbly") { + t.Errorf("expected output to include \"frobbly\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "PLATFORM:") || + !strings.Contains(output.String(), "OS:") || + !strings.Contains(output.String(), "Arch:") { + t.Errorf("expected output to include \"PLATFORM:, OS: and Arch:\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "RUNTIME:") || + !strings.Contains(output.String(), "Version:") || + !strings.Contains(output.String(), "CPUs:") { + t.Errorf("expected output to include \"RUNTIME:, Version: and CPUs:\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "VERSION:") || + !strings.Contains(output.String(), "2.0.0") { + t.Errorf("expected output to include \"VERSION:, 2.0.0\"; got: %q", output.String()) + } +} diff --git a/helpers_unix_test.go b/helpers_unix_test.go new file mode 100644 index 0000000..ae27fc5 --- /dev/null +++ b/helpers_unix_test.go @@ -0,0 +1,9 @@ +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package cli + +import "os" + +func clearenv() { + os.Clearenv() +} diff --git a/helpers_windows_test.go b/helpers_windows_test.go new file mode 100644 index 0000000..4eb84f9 --- /dev/null +++ b/helpers_windows_test.go @@ -0,0 +1,20 @@ +package cli + +import ( + "os" + "syscall" +) + +// os.Clearenv() doesn't actually unset variables on Windows +// See: https://github.com/golang/go/issues/17902 +func clearenv() { + for _, s := range os.Environ() { + for j := 1; j < len(s); j++ { + if s[j] == '=' { + keyp, _ := syscall.UTF16PtrFromString(s[0:j]) + syscall.SetEnvironmentVariable(keyp, nil) + break + } + } + } +} diff --git a/runtests b/runtests index bcd8f4c..d2c5ea6 100755 --- a/runtests +++ b/runtests @@ -1,7 +1,8 @@ #!/usr/bin/env python -from __future__ import print_function +from __future__ import print_function, unicode_literals import argparse +import codecs import glob import os import platform @@ -12,6 +13,7 @@ import tempfile from subprocess import check_call, check_output +_PY3 = sys.version_info.major == 3 _WINDOWS = platform.system().lower() == 'windows' _PACKAGE_NAME = os.environ.get( 'CLI_PACKAGE_NAME', 'github.com/urfave/cli' @@ -37,7 +39,7 @@ def _target(func): @_target def _test(): - if check_output('go version'.split()).split()[2] < 'go1.2': + if _go_version() < 'go1.2': _run('go test -v .') return @@ -61,7 +63,7 @@ def _test(): @_target def _gfmrun(): - go_version = check_output('go version'.split()).split()[2] + go_version = _go_version() if go_version < 'go1.3': print('runtests: skip on {}'.format(go_version), file=sys.stderr) return @@ -75,7 +77,7 @@ def _vet(): @_target def _migrations(): - go_version = check_output('go version'.split()).split()[2] + go_version = _go_version() if go_version < 'go1.3': print('runtests: skip on {}'.format(go_version), file=sys.stderr) return @@ -115,7 +117,7 @@ def _toc(): @_target def _gen(): - go_version = check_output('go version'.split()).split()[2] + go_version = _go_version() if go_version < 'go1.5': print('runtests: skip on {}'.format(go_version), file=sys.stderr) return @@ -133,23 +135,29 @@ def _run(command): def _gfmrun_count(): - with open('README.md') as infile: + with codecs.open('README.md', 'r', 'utf-8') as infile: lines = infile.read().splitlines() - return len(filter(_is_go_runnable, lines)) + return len(list(filter(_is_go_runnable, lines))) def _is_go_runnable(line): return line.startswith('package main') +def _go_version(): + return check_output('go version'.split()).decode('utf-8').split()[2] + + def _combine_coverprofiles(coverprofiles): - combined = tempfile.NamedTemporaryFile( - suffix='.coverprofile', delete=False - ) + tmp_args = dict(suffix='.coverprofile', mode='w', delete=False) + if _PY3: + tmp_args['encoding'] = 'utf-8' + + combined = tempfile.NamedTemporaryFile(**tmp_args) combined.write('mode: set\n') for coverprofile in coverprofiles: - with open(coverprofile, 'r') as infile: + with codecs.open(coverprofile, 'r', 'utf-8') as infile: for line in infile.readlines(): if not line.startswith('mode: '): combined.write(line)