diff --git a/.travis.yml b/.travis.yml index 890e185..cf8d098 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,17 @@ language: go - sudo: false +dist: trusty +osx_image: xcode8.3 +go: 1.8.x + +os: +- linux +- osx cache: directories: - node_modules -go: -- 1.6.x -- 1.7.x -- 1.8.x -- master - -matrix: - allow_failures: - - go: master - include: - - go: 1.6.x - os: osx - - go: 1.7.x - os: osx - - go: 1.8.x - os: osx - before_script: - go get github.com/urfave/gfmrun/... || true - go get golang.org/x/tools/cmd/goimports diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f7546..401eae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,49 @@ ## [Unreleased] +## 1.20.0 - 2017-08-10 + +### Fixed + +* `HandleExitCoder` is now correctly iterates over all errors in + a `MultiError`. The exit code is the exit code of the last error or `1` if + there are no `ExitCoder`s in the `MultiError`. +* Fixed YAML file loading on Windows (previously would fail validate the file path) +* Subcommand `Usage`, `Description`, `ArgsUsage`, `OnUsageError` correctly + propogated +* `ErrWriter` is now passed downwards through command structure to avoid the + need to redefine it +* Pass `Command` context into `OnUsageError` rather than parent context so that + all fields are avaiable +* Errors occuring in `Before` funcs are no longer double printed +* Use `UsageText` in the help templates for commands and subcommands if + defined; otherwise build the usage as before (was previously ignoring this + field) +* `IsSet` and `GlobalIsSet` now correctly return whether a flag is set if + a program calls `Set` or `GlobalSet` directly after flag parsing (would + previously only return `true` if the flag was set during parsing) + +### Changed + +* No longer exit the program on command/subcommand error if the error raised is + not an `OsExiter`. This exiting behavior was introduced in 1.19.0, but was + determined to be a regression in functionality. See [the + PR](https://github.com/urfave/cli/pull/595) for discussion. + +### Added + +* `CommandsByName` type was added to make it easy to sort `Command`s by name, + alphabetically +* `altsrc` now handles loading of string and int arrays from TOML +* Support for definition of custom help templates for `App` via + `CustomAppHelpTemplate` +* Support for arbitrary key/value fields on `App` to be used with + `CustomAppHelpTemplate` via `ExtraInfo` +* `HelpFlag`, `VersionFlag`, and `BashCompletionFlag` changed to explictly be + `cli.Flag`s allowing for the use of custom flags satisfying the `cli.Flag` + interface to be used. + + ## [1.19.1] - 2016-11-21 ### Fixed diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..41ba294 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be +reviewed and investigated and will result in a response that is deemed necessary +and appropriate to the circumstances. The project team is obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..329195e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +## Contributing + +**NOTE**: the primary maintainer(s) may be found in +[./MAINTAINERS.md](./MAINTAINERS.md). + +Feel free to put up a pull request to fix a bug or maybe add a feature. I will +give it a code review and make sure that it does not break backwards +compatibility. If I or any other collaborators agree that it is in line with +the vision of the project, we will work with you to get the code into +a mergeable state and merge it into the master branch. + +If you have contributed something significant to the project, we will most +likely add you as a collaborator. As a collaborator you are given the ability +to merge others pull requests. It is very important that new code does not +break existing code, so be careful about what code you do choose to merge. + +If you feel like you have contributed to the project but have not yet been added +as a collaborator, we probably forgot to add you :sweat_smile:. Please open an +issue! diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..f6bdd99 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1 @@ +- @meatballhat diff --git a/README.md b/README.md index 2bbbd8e..7750de7 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ cli [![top level coverage](https://gocover.io/_badge/github.com/urfave/cli?0 "top level coverage")](http://gocover.io/github.com/urfave/cli) / [![altsrc coverage](https://gocover.io/_badge/github.com/urfave/cli/altsrc?0 "altsrc coverage")](http://gocover.io/github.com/urfave/cli/altsrc) -**Notice:** This is the library formerly known as -`github.com/codegangsta/cli` -- Github will automatically redirect requests -to this repository, but we recommend updating your references for clarity. +This is the library formerly known as `github.com/codegangsta/cli` -- Github +will automatically redirect requests to this repository, but we recommend +updating your references for clarity. cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line @@ -32,7 +32,9 @@ applications in an expressive way. + [Alternate Names](#alternate-names) + [Ordering](#ordering) + [Values from the Environment](#values-from-the-environment) + + [Values from files](#values-from-files) + [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others) + + [Precedence](#precedence) * [Subcommands](#subcommands) * [Subcommands categories](#subcommands-categories) * [Exit code](#exit-code) @@ -45,6 +47,7 @@ applications in an expressive way. * [Version Flag](#version-flag) + [Customization](#customization-2) + [Full API Example](#full-api-example) + * [Combining short Bool options](#combining-short-bool-options) - [Contribution Guidelines](#contribution-guidelines) @@ -586,6 +589,41 @@ func main() { } ``` +#### Values from files + +You can also have the default value set from file via `FilePath`. e.g. + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "password, p", + Usage: "password for the mysql database", + FilePath: "/etc/mysql/password", + }, + } + + app.Run(os.Args) +} +``` + +Note that default values set from file (e.g. `FilePath`) take precedence over +default values set from the enviornment (e.g. `EnvVar`). + #### Values from alternate input sources (YAML, TOML, and others) There is a separate package altsrc that adds support for getting flag values @@ -656,6 +694,15 @@ func main() { } ``` +#### Precedence + +The precedence for flag value sources is as follows (highest to lowest): + +0. Command line flag value from user +0. Environment variable (if specified) +0. Configuration file (if specified) +0. Default defined on the flag + ### Subcommands Subcommands can be defined for a more git-like command line app. @@ -751,11 +798,11 @@ func main() { }, { Name: "add", - Category: "template", + Category: "Template actions", }, { Name: "remove", - Category: "template", + Category: "Template actions", }, } @@ -1364,18 +1411,26 @@ func wopAction(c *cli.Context) error { } ``` -## Contribution Guidelines +### Combining short Bool options -Feel free to put up a pull request to fix a bug or maybe add a feature. I will -give it a code review and make sure that it does not break backwards -compatibility. If I or any other collaborators agree that it is in line with -the vision of the project, we will work with you to get the code into -a mergeable state and merge it into the master branch. +Traditional use of boolean options using their shortnames look like this: +``` +# cmd foobar -s -o +``` -If you have contributed something significant to the project, we will most -likely add you as a collaborator. As a collaborator you are given the ability -to merge others pull requests. It is very important that new code does not -break existing code, so be careful about what code you do choose to merge. +Suppose you want users to be able to combine your bool options with their shortname. This +can be done using the **UseShortOptionHandling** bool in your commands. Suppose your program +has a two bool flags such as *serve* and *option* with the short options of *-o* and +*-s* respectively. With **UseShortOptionHandling** set to *true*, a user can use a syntax +like: +``` +# cmd foobar -so +``` + +If you enable the **UseShortOptionHandling*, then you must not use any flags that have a single +leading *-* or this will result in failures. For example, **-option** can no longer be used. Flags +with two leading dashes (such as **--options**) are still valid. + +## Contribution Guidelines -If you feel like you have contributed to the project but have not yet been -added as a collaborator, we probably forgot to add you, please open an issue. +See [./CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go index b3169e0..a111eee 100644 --- a/altsrc/map_input_source.go +++ b/altsrc/map_input_source.go @@ -22,15 +22,15 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool if sections := strings.Split(name, "."); len(sections) > 1 { node := tree for _, section := range sections[:len(sections)-1] { - if child, ok := node[section]; !ok { + child, ok := node[section] + if !ok { return nil, false - } else { - if ctype, ok := child.(map[interface{}]interface{}); !ok { - return nil, false - } else { - node = ctype - } } + ctype, ok := child.(map[interface{}]interface{}) + if !ok { + return nil, false + } + node = ctype } if val, ok := node[sections[len(sections)-1]]; ok { return val, true diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go index 37870fc..87bb9d9 100644 --- a/altsrc/toml_file_loader.go +++ b/altsrc/toml_file_loader.go @@ -66,9 +66,9 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { return ret, nil } -func (self *tomlMap) UnmarshalTOML(i interface{}) error { +func (tm *tomlMap) UnmarshalTOML(i interface{}) error { if tmp, err := unmarshalMap(i); err == nil { - self.Map = tmp + tm.Map = tmp } else { return err } diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index dd808d5..202469f 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -86,7 +86,7 @@ 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 { - return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) } + + return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) } diff --git a/app.go b/app.go index 51fc45d..9add067 100644 --- a/app.go +++ b/app.go @@ -83,6 +83,9 @@ type App struct { Writer io.Writer // ErrWriter writes error output ErrWriter io.Writer + // Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to + // function as a default, so this is optional. + ExitErrHandler ExitErrHandlerFunc // Other custom info Metadata map[string]interface{} // Carries a function which returns app specific info. @@ -207,7 +210,7 @@ func (a *App) Run(arguments []string) (err error) { if err != nil { if a.OnUsageError != nil { err := a.OnUsageError(context, err, false) - HandleExitCoder(err) + a.handleExitCoder(context, err) return err } fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) @@ -240,8 +243,9 @@ 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) + a.handleExitCoder(context, beforeErr) err = beforeErr return err } @@ -263,7 +267,7 @@ func (a *App) Run(arguments []string) (err error) { // Run default Action err = HandleAction(a.Action, context) - HandleExitCoder(err) + a.handleExitCoder(context, err) return err } @@ -330,7 +334,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { if err != nil { if a.OnUsageError != nil { err = a.OnUsageError(context, err, true) - HandleExitCoder(err) + a.handleExitCoder(context, err) return err } fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) @@ -352,7 +356,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { defer func() { afterErr := a.After(context) if afterErr != nil { - HandleExitCoder(err) + a.handleExitCoder(context, err) if err != nil { err = NewMultiError(err, afterErr) } else { @@ -365,7 +369,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { if a.Before != nil { beforeErr := a.Before(context) if beforeErr != nil { - HandleExitCoder(beforeErr) + a.handleExitCoder(context, beforeErr) err = beforeErr return err } @@ -383,7 +387,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { // Run default Action err = HandleAction(a.Action, context) - HandleExitCoder(err) + a.handleExitCoder(context, err) return err } @@ -449,7 +453,6 @@ func (a *App) hasFlag(flag Flag) bool { } func (a *App) errWriter() io.Writer { - // When the app ErrWriter is nil use the package level one. if a.ErrWriter == nil { return ErrWriter @@ -464,6 +467,14 @@ func (a *App) appendFlag(flag Flag) { } } +func (a *App) handleExitCoder(context *Context, err error) { + if a.ExitErrHandler != nil { + a.ExitErrHandler(context, err) + } else { + HandleExitCoder(err) + } +} + // Author represents someone who has contributed to a cli project. type Author struct { Name string // The Authors name @@ -491,7 +502,7 @@ func HandleAction(action interface{}, context *Context) (err error) { } else if a, ok := action.(func(*Context)); ok { // deprecated function signature a(context) return nil - } else { - return errInvalidActionType } + + return errInvalidActionType } diff --git a/app_test.go b/app_test.go index 3e64361..629681e 100644 --- a/app_test.go +++ b/app_test.go @@ -367,6 +367,39 @@ func TestApp_CommandWithArgBeforeFlags(t *testing.T) { expect(t, firstArg, "my-arg") } +func TestApp_CommandWithArgBeforeBoolFlags(t *testing.T) { + var parsedOption, parsedSecondOption, firstArg string + var parsedBool, parsedSecondBool bool + + app := NewApp() + command := Command{ + Name: "cmd", + Flags: []Flag{ + StringFlag{Name: "option", Value: "", Usage: "some option"}, + StringFlag{Name: "secondOption", Value: "", Usage: "another option"}, + BoolFlag{Name: "boolflag", Usage: "some bool"}, + BoolFlag{Name: "b", Usage: "another bool"}, + }, + Action: func(c *Context) error { + parsedOption = c.String("option") + parsedSecondOption = c.String("secondOption") + parsedBool = c.Bool("boolflag") + parsedSecondBool = c.Bool("b") + firstArg = c.Args().First() + return nil + }, + } + app.Commands = []Command{command} + + app.Run([]string{"", "cmd", "my-arg", "--boolflag", "--option", "my-option", "-b", "--secondOption", "fancy-option"}) + + expect(t, parsedOption, "my-option") + expect(t, parsedSecondOption, "fancy-option") + expect(t, parsedBool, true) + expect(t, parsedSecondBool, true) + expect(t, firstArg, "my-arg") +} + func TestApp_RunAsSubcommandParseFlags(t *testing.T) { var context *Context @@ -535,7 +568,6 @@ func TestApp_Float64Flag(t *testing.T) { } func TestApp_ParseSliceFlags(t *testing.T) { - var parsedOption, firstArg string var parsedIntSlice []int var parsedStringSlice []string @@ -549,8 +581,6 @@ func TestApp_ParseSliceFlags(t *testing.T) { Action: func(c *Context) error { parsedIntSlice = c.IntSlice("p") parsedStringSlice = c.StringSlice("ip") - parsedOption = c.String("option") - firstArg = c.Args().First() return nil }, } @@ -1699,6 +1729,42 @@ func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) { } } +func TestHandleExitCoder_Default(t *testing.T) { + app := NewApp() + fs, err := flagSet(app.Name, app.Flags) + if err != nil { + t.Errorf("error creating FlagSet: %s", err) + } + + ctx := NewContext(app, fs, nil) + app.handleExitCoder(ctx, NewExitError("Default Behavior Error", 42)) + + output := fakeErrWriter.String() + if !strings.Contains(output, "Default") { + t.Fatalf("Expected Default Behavior from Error Handler but got: %s", output) + } +} + +func TestHandleExitCoder_Custom(t *testing.T) { + app := NewApp() + fs, err := flagSet(app.Name, app.Flags) + if err != nil { + t.Errorf("error creating FlagSet: %s", err) + } + + app.ExitErrHandler = func(_ *Context, _ error) { + fmt.Fprintln(ErrWriter, "I'm a Custom error handler, I print what I want!") + } + + ctx := NewContext(app, fs, nil) + app.handleExitCoder(ctx, NewExitError("Default Behavior Error", 42)) + + output := fakeErrWriter.String() + if !strings.Contains(output, "Custom") { + t.Fatalf("Expected Custom Behavior from Error Handler but got: %s", output) + } +} + func TestHandleAction_WithUnknownPanic(t *testing.T) { defer func() { refute(t, recover(), nil) }() diff --git a/appveyor.yml b/appveyor.yml index 698b188..1e1489c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,14 +1,16 @@ version: "{build}" -os: Windows Server 2012 R2 +os: Windows Server 2016 + +image: Visual Studio 2017 clone_folder: c:\gopath\src\github.com\urfave\cli 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/category.go b/category.go index 1a60550..bf3c73c 100644 --- a/category.go +++ b/category.go @@ -10,7 +10,7 @@ type CommandCategory struct { } func (c CommandCategories) Less(i, j int) bool { - return c[i].Name < c[j].Name + return lexicographicLess(c[i].Name, c[j].Name) } func (c CommandCategories) Len() int { diff --git a/command.go b/command.go index 23de294..2acb976 100644 --- a/command.go +++ b/command.go @@ -1,6 +1,7 @@ package cli import ( + "flag" "fmt" "io/ioutil" "sort" @@ -55,6 +56,10 @@ type Command struct { HideHelp bool // Boolean to hide this command from help or completion Hidden bool + // Boolean to enable short-option handling so user can combine several + // single-character bool arguements into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool // Full name of command for help, defaults to full command name, including parent commands. HelpName string @@ -73,7 +78,7 @@ func (c CommandsByName) Len() int { } func (c CommandsByName) Less(i, j int) bool { - return c[i].Name < c[j].Name + return lexicographicLess(c[i].Name, c[j].Name) } func (c CommandsByName) Swap(i, j int) { @@ -106,57 +111,7 @@ func (c Command) Run(ctx *Context) (err error) { ) } - set, err := flagSet(c.Name, c.Flags) - if err != nil { - return err - } - set.SetOutput(ioutil.Discard) - - if c.SkipFlagParsing { - err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) - } else if !c.SkipArgReorder { - firstFlagIndex := -1 - terminatorIndex := -1 - for index, arg := range ctx.Args() { - if arg == "--" { - terminatorIndex = index - break - } else if arg == "-" { - // Do nothing. A dash alone is not really a flag. - continue - } else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 { - firstFlagIndex = index - } - } - - if firstFlagIndex > -1 { - args := ctx.Args() - regularArgs := make([]string, len(args[1:firstFlagIndex])) - copy(regularArgs, args[1:firstFlagIndex]) - - var flagArgs []string - if terminatorIndex > -1 { - flagArgs = args[firstFlagIndex:terminatorIndex] - regularArgs = append(regularArgs, args[terminatorIndex:]...) - } else { - flagArgs = args[firstFlagIndex:] - } - - err = set.Parse(append(flagArgs, regularArgs...)) - } else { - err = set.Parse(ctx.Args().Tail()) - } - } else { - err = set.Parse(ctx.Args().Tail()) - } - - nerr := normalizeFlags(c.Flags, set) - if nerr != nil { - fmt.Fprintln(ctx.App.Writer, nerr) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) - return nerr - } + set, err := c.parseFlags(ctx.Args().Tail()) context := NewContext(ctx.App, set, ctx) context.Command = c @@ -167,7 +122,7 @@ func (c Command) Run(ctx *Context) (err error) { if err != nil { if c.OnUsageError != nil { err := c.OnUsageError(context, err, false) - HandleExitCoder(err) + context.App.handleExitCoder(context, err) return err } fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) @@ -184,7 +139,7 @@ func (c Command) Run(ctx *Context) (err error) { defer func() { afterErr := c.After(context) if afterErr != nil { - HandleExitCoder(err) + context.App.handleExitCoder(context, err) if err != nil { err = NewMultiError(err, afterErr) } else { @@ -198,7 +153,7 @@ func (c Command) Run(ctx *Context) (err error) { err = c.Before(context) if err != nil { ShowCommandHelp(context, c.Name) - HandleExitCoder(err) + context.App.handleExitCoder(context, err) return err } } @@ -210,11 +165,88 @@ func (c Command) Run(ctx *Context) (err error) { err = HandleAction(c.Action, context) if err != nil { - HandleExitCoder(err) + context.App.handleExitCoder(context, err) } return err } +func (c *Command) parseFlags(args Args) (*flag.FlagSet, error) { + set, err := flagSet(c.Name, c.Flags) + if err != nil { + return nil, err + } + set.SetOutput(ioutil.Discard) + + if c.SkipFlagParsing { + return set, set.Parse(append([]string{"--"}, args...)) + } + + if c.UseShortOptionHandling { + args = translateShortOptions(args) + } + + if !c.SkipArgReorder { + args = reorderArgs(args) + } + + err = set.Parse(args) + if err != nil { + return nil, err + } + + err = normalizeFlags(c.Flags, set) + if err != nil { + return nil, err + } + + return set, nil +} + +// reorderArgs moves all flags before arguments as this is what flag expects +func reorderArgs(args []string) []string { + var nonflags, flags []string + + readFlagValue := false + for i, arg := range args { + if arg == "--" { + nonflags = append(nonflags, args[i:]...) + break + } + + if readFlagValue && !strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { + readFlagValue = false + flags = append(flags, arg) + continue + } + readFlagValue = false + + if arg != "-" && strings.HasPrefix(arg, "-") { + flags = append(flags, arg) + + readFlagValue = !strings.Contains(arg, "=") + } else { + nonflags = append(nonflags, arg) + } + } + + return append(flags, nonflags...) +} + +func translateShortOptions(flagArgs Args) []string { + // separate combined flags + var flagArgsSeparated []string + for _, flagArg := range flagArgs { + if strings.HasPrefix(flagArg, "-") && strings.HasPrefix(flagArg, "--") == false && len(flagArg) > 2 { + for _, flagChar := range flagArg[1:] { + flagArgsSeparated = append(flagArgsSeparated, "-"+string(flagChar)) + } + } else { + flagArgsSeparated = append(flagArgsSeparated, flagArg) + } + } + return flagArgsSeparated +} + // Names returns the names including short names and aliases. func (c Command) Names() []string { names := []string{c.Name} diff --git a/command_test.go b/command_test.go index 4ad994c..c84b762 100644 --- a/command_test.go +++ b/command_test.go @@ -11,20 +11,24 @@ import ( func TestCommandFlagParsing(t *testing.T) { cases := []struct { - testArgs []string - skipFlagParsing bool - skipArgReorder bool - expectedErr error + testArgs []string + skipFlagParsing bool + skipArgReorder bool + expectedErr error + UseShortOptionHandling bool }{ // Test normal "not ignoring flags" flow - {[]string{"test-cmd", "blah", "blah", "-break"}, false, false, errors.New("flag provided but not defined: -break")}, + {[]string{"test-cmd", "blah", "blah", "-break"}, false, false, errors.New("flag provided but not defined: -break"), false}, // Test no arg reorder - {[]string{"test-cmd", "blah", "blah", "-break"}, false, true, nil}, + {[]string{"test-cmd", "blah", "blah", "-break"}, false, true, nil, false}, + {[]string{"test-cmd", "blah", "blah", "-break", "ls", "-l"}, false, true, nil, true}, + + {[]string{"test-cmd", "blah", "blah"}, true, false, nil, false}, // Test SkipFlagParsing without any args that look like flags + {[]string{"test-cmd", "blah", "-break"}, true, false, nil, false}, // Test SkipFlagParsing with random flag arg + {[]string{"test-cmd", "blah", "-help"}, true, false, nil, false}, // Test SkipFlagParsing with "special" help flag arg + {[]string{"test-cmd", "blah"}, false, false, nil, true}, // Test UseShortOptionHandling - {[]string{"test-cmd", "blah", "blah"}, true, false, nil}, // Test SkipFlagParsing without any args that look like flags - {[]string{"test-cmd", "blah", "-break"}, true, false, nil}, // Test SkipFlagParsing with random flag arg - {[]string{"test-cmd", "blah", "-help"}, true, false, nil}, // Test SkipFlagParsing with "special" help flag arg } for _, c := range cases { @@ -36,13 +40,14 @@ func TestCommandFlagParsing(t *testing.T) { context := NewContext(app, set, nil) command := Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(_ *Context) error { return nil }, - SkipFlagParsing: c.skipFlagParsing, - SkipArgReorder: c.skipArgReorder, + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(_ *Context) error { return nil }, + SkipFlagParsing: c.skipFlagParsing, + SkipArgReorder: c.skipArgReorder, + UseShortOptionHandling: c.UseShortOptionHandling, } err := command.Run(context) @@ -238,3 +243,77 @@ func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { t.Fatal(err) } } + +func TestCommandFlagReordering(t *testing.T) { + cases := []struct { + testArgs []string + expectedValue string + expectedArgs []string + expectedErr error + }{ + {[]string{"some-exec", "some-command", "some-arg", "--flag", "foo"}, "foo", []string{"some-arg"}, nil}, + {[]string{"some-exec", "some-command", "some-arg", "--flag=foo"}, "foo", []string{"some-arg"}, nil}, + {[]string{"some-exec", "some-command", "--flag=foo", "some-arg"}, "foo", []string{"some-arg"}, nil}, + } + + for _, c := range cases { + value := "" + args := []string{} + app := &App{ + Commands: []Command{ + { + Name: "some-command", + Flags: []Flag{ + StringFlag{Name: "flag"}, + }, + Action: func(c *Context) { + fmt.Printf("%+v\n", c.String("flag")) + value = c.String("flag") + args = c.Args() + }, + }, + }, + } + + err := app.Run(c.testArgs) + expect(t, err, c.expectedErr) + expect(t, value, c.expectedValue) + expect(t, args, c.expectedArgs) + } +} + +func TestCommandSkipFlagParsing(t *testing.T) { + cases := []struct { + testArgs []string + expectedArgs []string + expectedErr error + }{ + {[]string{"some-exec", "some-command", "some-arg", "--flag", "foo"}, []string{"some-arg", "--flag", "foo"}, nil}, + {[]string{"some-exec", "some-command", "some-arg", "--flag=foo"}, []string{"some-arg", "--flag=foo"}, nil}, + } + + for _, c := range cases { + value := "" + args := []string{} + app := &App{ + Commands: []Command{ + { + SkipFlagParsing: true, + Name: "some-command", + Flags: []Flag{ + StringFlag{Name: "flag"}, + }, + Action: func(c *Context) { + fmt.Printf("%+v\n", c.String("flag")) + value = c.String("flag") + args = c.Args() + }, + }, + }, + } + + err := app.Run(c.testArgs) + expect(t, err, c.expectedErr) + expect(t, args, c.expectedArgs) + } +} diff --git a/context.go b/context.go index db94191..552ee74 100644 --- a/context.go +++ b/context.go @@ -3,6 +3,7 @@ package cli import ( "errors" "flag" + "os" "reflect" "strings" "syscall" @@ -73,7 +74,7 @@ func (c *Context) IsSet(name string) bool { // change in version 2 to add `IsSet` to the Flag interface to push the // responsibility closer to where the information required to determine // whether a flag is set by non-standard means such as environment - // variables is avaliable. + // variables is available. // // See https://github.com/urfave/cli/issues/294 for additional discussion flags := c.Command.Flags @@ -93,18 +94,26 @@ func (c *Context) IsSet(name string) bool { val = val.Elem() } - envVarValue := val.FieldByName("EnvVar") - if !envVarValue.IsValid() { - return + filePathValue := val.FieldByName("FilePath") + if filePathValue.IsValid() { + eachName(filePathValue.String(), func(filePath string) { + if _, err := os.Stat(filePath); err == nil { + c.setFlags[name] = true + return + } + }) } - eachName(envVarValue.String(), func(envVar string) { - envVar = strings.TrimSpace(envVar) - if _, ok := syscall.Getenv(envVar); ok { - c.setFlags[name] = true - return - } - }) + envVarValue := val.FieldByName("EnvVar") + if envVarValue.IsValid() { + eachName(envVarValue.String(), func(envVar string) { + envVar = strings.TrimSpace(envVar) + if _, ok := syscall.Getenv(envVar); ok { + c.setFlags[name] = true + return + } + }) + } }) } } diff --git a/flag.go b/flag.go index 877ff35..b0cffc0 100644 --- a/flag.go +++ b/flag.go @@ -3,6 +3,7 @@ package cli import ( "flag" "fmt" + "io/ioutil" "reflect" "runtime" "strconv" @@ -37,6 +38,18 @@ var HelpFlag Flag = BoolFlag{ // to display a flag. var FlagStringer FlagStringFunc = stringifyFlag +// FlagNamePrefixer converts a full flag name and its placeholder into the help +// message flag prefix. This is used by the default FlagStringer. +var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames + +// FlagEnvHinter annotates flag help message with the environment variable +// details. This is used by the default FlagStringer. +var FlagEnvHinter FlagEnvHintFunc = withEnvHint + +// FlagFileHinter annotates flag help message with the environment variable +// details. This is used by the default FlagStringer. +var FlagFileHinter FlagFileHintFunc = withFileHint + // FlagsByName is a slice of Flag. type FlagsByName []Flag @@ -45,7 +58,7 @@ func (f FlagsByName) Len() int { } func (f FlagsByName) Less(i, j int) bool { - return f[i].GetName() < f[j].GetName() + return lexicographicLess(f[i].GetName(), f[j].GetName()) } func (f FlagsByName) Swap(i, j int) { @@ -112,15 +125,9 @@ func (f GenericFlag) Apply(set *flag.FlagSet) { // provided by the user for parsing by the flag func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error { val := f.Value - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - if err := val.Set(envVal); err != nil { - return fmt.Errorf("could not parse %s as value for flag %s: %s", envVal, f.Name, err) - } - break - } + if fileEnvVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + if err := val.Set(fileEnvVal); err != nil { + return fmt.Errorf("could not parse %s as value for flag %s: %s", fileEnvVal, f.Name, err) } } @@ -163,21 +170,19 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - newVal := &StringSlice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err) - } - } - f.Value = newVal - break + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + newVal := &StringSlice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err) } } + if f.Value == nil { + f.Value = newVal + } else { + *f.Value = *newVal + } } eachName(f.Name, func(name string) { @@ -226,21 +231,19 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - newVal := &IntSlice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err) - } - } - f.Value = newVal - break + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + newVal := &IntSlice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err) } } + if f.Value == nil { + f.Value = newVal + } else { + *f.Value = *newVal + } } eachName(f.Name, func(name string) { @@ -289,21 +292,19 @@ func (f Int64SliceFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - newVal := &Int64Slice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err) - } - } - f.Value = newVal - break + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + newVal := &Int64Slice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err) } } + if f.Value == nil { + f.Value = newVal + } else { + *f.Value = *newVal + } } eachName(f.Name, func(name string) { @@ -324,23 +325,15 @@ func (f BoolFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error { val := false - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - if envVal == "" { - val = false - break - } - - envValBool, err := strconv.ParseBool(envVal) - if err != nil { - return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) - } - - val = envValBool - break + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + if envVal == "" { + val = false + } else { + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) } + val = envValBool } } @@ -364,23 +357,16 @@ func (f BoolTFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error { val := true - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - if envVal == "" { - val = false - break - } - envValBool, err := strconv.ParseBool(envVal) - if err != nil { - return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) - } - - val = envValBool - break + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + if envVal == "" { + val = false + } else { + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) } + val = envValBool } } @@ -403,14 +389,8 @@ func (f StringFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f StringFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - f.Value = envVal - break - } - } + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + f.Value = envVal } eachName(f.Name, func(name string) { @@ -432,18 +412,12 @@ func (f IntFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f IntFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) - } - f.Value = int(envValInt) - break - } + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValInt, err := strconv.ParseInt(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) } + f.Value = int(envValInt) } eachName(f.Name, func(name string) { @@ -465,19 +439,13 @@ func (f Int64Flag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = envValInt - break - } + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValInt, err := strconv.ParseInt(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) } + + f.Value = envValInt } eachName(f.Name, func(name string) { @@ -499,19 +467,13 @@ func (f UintFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f UintFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - envValInt, err := strconv.ParseUint(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = uint(envValInt) - break - } + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValInt, err := strconv.ParseUint(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err) } + + f.Value = uint(envValInt) } eachName(f.Name, func(name string) { @@ -533,19 +495,13 @@ func (f Uint64Flag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - envValInt, err := strconv.ParseUint(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = uint64(envValInt) - break - } + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValInt, err := strconv.ParseUint(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err) } + + f.Value = uint64(envValInt) } eachName(f.Name, func(name string) { @@ -567,19 +523,13 @@ func (f DurationFlag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - envValDuration, err := time.ParseDuration(envVal) - if err != nil { - return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err) - } - - f.Value = envValDuration - break - } + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValDuration, err := time.ParseDuration(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err) } + + f.Value = envValDuration } eachName(f.Name, func(name string) { @@ -601,19 +551,13 @@ func (f Float64Flag) Apply(set *flag.FlagSet) { // ApplyWithError populates the flag given the flag set and environment func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - envValFloat, err := strconv.ParseFloat(envVal, 10) - if err != nil { - return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = float64(envValFloat) - break - } + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValFloat, err := strconv.ParseFloat(envVal, 10) + if err != nil { + return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err) } + + f.Value = float64(envValFloat) } eachName(f.Name, func(name string) { @@ -692,11 +636,19 @@ func withEnvHint(envVar, str string) string { suffix = "%" sep = "%, %" } - envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(strings.Split(envVar, ","), sep), suffix) + envText = " [" + prefix + strings.Join(strings.Split(envVar, ","), sep) + suffix + "]" } return str + envText } +func withFileHint(filePath, str string) string { + fileText := "" + if filePath != "" { + fileText = fmt.Sprintf(" [%s]", filePath) + } + return str + fileText +} + func flagValue(f Flag) reflect.Value { fv := reflect.ValueOf(f) for fv.Kind() == reflect.Ptr { @@ -710,14 +662,29 @@ func stringifyFlag(f Flag) string { switch f.(type) { case IntSliceFlag: - return withEnvHint(fv.FieldByName("EnvVar").String(), - stringifyIntSliceFlag(f.(IntSliceFlag))) + return FlagFileHinter( + fv.FieldByName("FilePath").String(), + FlagEnvHinter( + fv.FieldByName("EnvVar").String(), + stringifyIntSliceFlag(f.(IntSliceFlag)), + ), + ) case Int64SliceFlag: - return withEnvHint(fv.FieldByName("EnvVar").String(), - stringifyInt64SliceFlag(f.(Int64SliceFlag))) + return FlagFileHinter( + fv.FieldByName("FilePath").String(), + FlagEnvHinter( + fv.FieldByName("EnvVar").String(), + stringifyInt64SliceFlag(f.(Int64SliceFlag)), + ), + ) case StringSliceFlag: - return withEnvHint(fv.FieldByName("EnvVar").String(), - stringifyStringSliceFlag(f.(StringSliceFlag))) + return FlagFileHinter( + fv.FieldByName("FilePath").String(), + FlagEnvHinter( + fv.FieldByName("EnvVar").String(), + stringifyStringSliceFlag(f.(StringSliceFlag)), + ), + ) } placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) @@ -742,17 +709,22 @@ func stringifyFlag(f Flag) string { placeholder = defaultPlaceholder } - usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultValueString)) + usageWithDefault := strings.TrimSpace(usage + defaultValueString) - return withEnvHint(fv.FieldByName("EnvVar").String(), - fmt.Sprintf("%s\t%s", prefixedNames(fv.FieldByName("Name").String(), placeholder), usageWithDefault)) + return FlagFileHinter( + fv.FieldByName("FilePath").String(), + FlagEnvHinter( + fv.FieldByName("EnvVar").String(), + FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder)+"\t"+usageWithDefault, + ), + ) } func stringifyIntSliceFlag(f IntSliceFlag) string { defaultVals := []string{} if f.Value != nil && len(f.Value.Value()) > 0 { for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, fmt.Sprintf("%d", i)) + defaultVals = append(defaultVals, strconv.Itoa(i)) } } @@ -763,7 +735,7 @@ func stringifyInt64SliceFlag(f Int64SliceFlag) string { defaultVals := []string{} if f.Value != nil && len(f.Value.Value()) > 0 { for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, fmt.Sprintf("%d", i)) + defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) } } @@ -775,7 +747,7 @@ func stringifyStringSliceFlag(f StringSliceFlag) string { if f.Value != nil && len(f.Value.Value()) > 0 { for _, s := range f.Value.Value() { if len(s) > 0 { - defaultVals = append(defaultVals, fmt.Sprintf("%q", s)) + defaultVals = append(defaultVals, strconv.Quote(s)) } } } @@ -794,6 +766,21 @@ func stringifySliceFlag(usage, name string, defaultVals []string) string { defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", ")) } - usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) - return fmt.Sprintf("%s\t%s", prefixedNames(name, placeholder), usageWithDefault) + usageWithDefault := strings.TrimSpace(usage + defaultVal) + return FlagNamePrefixer(name, placeholder) + "\t" + usageWithDefault +} + +func flagFromFileEnv(filePath, envName string) (val string, ok bool) { + for _, envVar := range strings.Split(envName, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + return envVal, true + } + } + for _, fileVar := range strings.Split(filePath, ",") { + if data, err := ioutil.ReadFile(fileVar); err == nil { + return string(data), true + } + } + return "", false } diff --git a/flag_generated.go b/flag_generated.go index 491b619..001576c 100644 --- a/flag_generated.go +++ b/flag_generated.go @@ -13,6 +13,7 @@ type BoolFlag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Destination *bool } @@ -60,6 +61,7 @@ type BoolTFlag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Destination *bool } @@ -107,6 +109,7 @@ type DurationFlag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Value time.Duration Destination *time.Duration @@ -155,6 +158,7 @@ type Float64Flag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Value float64 Destination *float64 @@ -200,11 +204,12 @@ func lookupFloat64(name string, set *flag.FlagSet) float64 { // GenericFlag is a flag with type Generic type GenericFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value Generic + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value Generic } // String returns a readable representation of this value @@ -250,6 +255,7 @@ type Int64Flag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Value int64 Destination *int64 @@ -298,6 +304,7 @@ type IntFlag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Value int Destination *int @@ -343,11 +350,12 @@ func lookupInt(name string, set *flag.FlagSet) int { // IntSliceFlag is a flag with type *IntSlice type IntSliceFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value *IntSlice + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value *IntSlice } // String returns a readable representation of this value @@ -390,11 +398,12 @@ func lookupIntSlice(name string, set *flag.FlagSet) []int { // Int64SliceFlag is a flag with type *Int64Slice type Int64SliceFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value *Int64Slice + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value *Int64Slice } // String returns a readable representation of this value @@ -440,6 +449,7 @@ type StringFlag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Value string Destination *string @@ -485,11 +495,12 @@ func lookupString(name string, set *flag.FlagSet) string { // StringSliceFlag is a flag with type *StringSlice type StringSliceFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value *StringSlice + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value *StringSlice } // String returns a readable representation of this value @@ -535,6 +546,7 @@ type Uint64Flag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Value uint64 Destination *uint64 @@ -583,6 +595,7 @@ type UintFlag struct { Name string Usage string EnvVar string + FilePath string Hidden bool Value uint Destination *uint diff --git a/flag_test.go b/flag_test.go index 1ccb639..da9fd73 100644 --- a/flag_test.go +++ b/flag_test.go @@ -2,6 +2,8 @@ package cli import ( "fmt" + "io" + "io/ioutil" "os" "reflect" "regexp" @@ -158,6 +160,83 @@ func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { } } +var prefixStringFlagTests = []struct { + name string + usage string + value string + prefixer FlagNamePrefixFunc + expected string +}{ + {"foo", "", "", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: foo, ph: value\t"}, + {"f", "", "", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: f, ph: value\t"}, + {"f", "The total `foo` desired", "all", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: f, ph: foo\tThe total foo desired (default: \"all\")"}, + {"test", "", "Something", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: test, ph: value\t(default: \"Something\")"}, + {"config,c", "Load configuration from `FILE`", "", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: config,c, ph: FILE\tLoad configuration from FILE"}, + {"config,c", "Load configuration from `CONFIG`", "config.json", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: config,c, ph: CONFIG\tLoad configuration from CONFIG (default: \"config.json\")"}, +} + +func TestFlagNamePrefixer(t *testing.T) { + defer func() { + FlagNamePrefixer = prefixedNames + }() + + for _, test := range prefixStringFlagTests { + FlagNamePrefixer = test.prefixer + flag := StringFlag{Name: test.name, Usage: test.usage, Value: test.value} + output := flag.String() + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +var envHintFlagTests = []struct { + name string + env string + hinter FlagEnvHintFunc + expected string +}{ + {"foo", "", func(a, b string) string { + return fmt.Sprintf("env: %s, str: %s", a, b) + }, "env: , str: --foo value\t"}, + {"f", "", func(a, b string) string { + return fmt.Sprintf("env: %s, str: %s", a, b) + }, "env: , str: -f value\t"}, + {"foo", "ENV_VAR", func(a, b string) string { + return fmt.Sprintf("env: %s, str: %s", a, b) + }, "env: ENV_VAR, str: --foo value\t"}, + {"f", "ENV_VAR", func(a, b string) string { + return fmt.Sprintf("env: %s, str: %s", a, b) + }, "env: ENV_VAR, str: -f value\t"}, +} + +func TestFlagEnvHinter(t *testing.T) { + defer func() { + FlagEnvHinter = withEnvHint + }() + + for _, test := range envHintFlagTests { + FlagEnvHinter = test.hinter + flag := StringFlag{Name: test.name, EnvVar: test.env} + output := flag.String() + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + var stringSliceFlagTests = []struct { name string value *StringSlice @@ -969,6 +1048,31 @@ func TestParseMultiBool(t *testing.T) { a.Run([]string{"run", "--serve"}) } +func TestParseBoolShortOptionHandle(t *testing.T) { + a := App{ + Commands: []Command{ + { + Name: "foobar", + UseShortOptionHandling: true, + Action: func(ctx *Context) error { + if ctx.Bool("serve") != true { + t.Errorf("main name not set") + } + if ctx.Bool("option") != true { + t.Errorf("short name not set") + } + return nil + }, + Flags: []Flag{ + BoolFlag{Name: "serve, s"}, + BoolFlag{Name: "option, o"}, + }, + }, + }, + } + a.Run([]string{"run", "foobar", "-so"}) +} + func TestParseDestinationBool(t *testing.T) { var dest bool a := App{ @@ -1213,3 +1317,38 @@ func TestParseGenericFromEnvCascade(t *testing.T) { } a.Run([]string{"run"}) } + +func TestFlagFromFile(t *testing.T) { + os.Clearenv() + os.Setenv("APP_FOO", "123") + + temp, err := ioutil.TempFile("", "urfave_cli_test") + if err != nil { + t.Error(err) + return + } + io.WriteString(temp, "abc") + temp.Close() + defer func() { + os.Remove(temp.Name()) + }() + + var filePathTests = []struct { + path string + name string + expected string + }{ + {"file-does-not-exist", "APP_BAR", ""}, + {"file-does-not-exist", "APP_FOO", "123"}, + {"file-does-not-exist", "APP_FOO,APP_BAR", "123"}, + {temp.Name(), "APP_FOO", "123"}, + {temp.Name(), "APP_BAR", "abc"}, + } + + for _, filePathTest := range filePathTests { + got, _ := flagFromFileEnv(filePathTest.path, filePathTest.name) + if want := filePathTest.expected; got != want { + t.Errorf("Did not expect %v - Want %v", got, want) + } + } +} diff --git a/funcs.go b/funcs.go index cba5e6c..0036b11 100644 --- a/funcs.go +++ b/funcs.go @@ -23,6 +23,22 @@ type CommandNotFoundFunc func(*Context, string) // is displayed and the execution is interrupted. type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error +// ExitErrHandlerFunc is executed if provided in order to handle ExitError values +// returned by Actions and Before/After functions. +type ExitErrHandlerFunc func(context *Context, err error) + // FlagStringFunc is used by the help generation to display a flag, which is // expected to be a single line. type FlagStringFunc func(Flag) string + +// FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix +// text for a flag's full name. +type FlagNamePrefixFunc func(fullName, placeholder string) string + +// FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help +// with the environment variable details. +type FlagEnvHintFunc func(envVar, str string) string + +// FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help +// with the file path details. +type FlagFileHintFunc func(filePath, str string) string diff --git a/generate-flag-types b/generate-flag-types index 7147381..1358857 100755 --- a/generate-flag-types +++ b/generate-flag-types @@ -142,6 +142,7 @@ def _write_cli_flag_types(outfile, types): Name string Usage string EnvVar string + FilePath string Hidden bool """.format(**typedef)) diff --git a/help.go b/help.go index dda3c7c..65874fa 100644 --- a/help.go +++ b/help.go @@ -29,6 +29,7 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{end}}{{range .VisibleCommands}} {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} diff --git a/sort.go b/sort.go new file mode 100644 index 0000000..23d1c2f --- /dev/null +++ b/sort.go @@ -0,0 +1,29 @@ +package cli + +import "unicode" + +// lexicographicLess compares strings alphabetically considering case. +func lexicographicLess(i, j string) bool { + iRunes := []rune(i) + jRunes := []rune(j) + + lenShared := len(iRunes) + if lenShared > len(jRunes) { + lenShared = len(jRunes) + } + + for index := 0; index < lenShared; index++ { + ir := iRunes[index] + jr := jRunes[index] + + if lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr { + return lir < ljr + } + + if ir != jr { + return ir < jr + } + } + + return i < j +} diff --git a/sort_test.go b/sort_test.go new file mode 100644 index 0000000..662ef9b --- /dev/null +++ b/sort_test.go @@ -0,0 +1,30 @@ +package cli + +import "testing" + +var lexicographicLessTests = []struct { + i string + j string + expected bool +}{ + {"", "a", true}, + {"a", "", false}, + {"a", "a", false}, + {"a", "A", false}, + {"A", "a", true}, + {"aa", "a", false}, + {"a", "aa", true}, + {"a", "b", true}, + {"a", "B", true}, + {"A", "b", true}, + {"A", "B", true}, +} + +func TestLexicographicLess(t *testing.T) { + for _, test := range lexicographicLessTests { + actual := lexicographicLess(test.i, test.j) + if test.expected != actual { + t.Errorf(`expected string "%s" to come before "%s"`, test.i, test.j) + } + } +}