Merge branch 'master' into merging-jereksel-zsh

main
Dan Buch 6 years ago committed by GitHub
commit 5fc8124af1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,28 +1,17 @@
language: go language: go
sudo: false sudo: false
dist: trusty
osx_image: xcode8.3
go: 1.8.x
os:
- linux
- osx
cache: cache:
directories: directories:
- node_modules - 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: before_script:
- go get github.com/urfave/gfmrun/... || true - go get github.com/urfave/gfmrun/... || true
- go get golang.org/x/tools/cmd/goimports - go get golang.org/x/tools/cmd/goimports

@ -4,6 +4,49 @@
## [Unreleased] ## [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 ## [1.19.1] - 2016-11-21
### Fixed ### Fixed

@ -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

@ -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!

@ -0,0 +1 @@
- @meatballhat

@ -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) / [![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) [![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 This is the library formerly known as `github.com/codegangsta/cli` -- Github
`github.com/codegangsta/cli` -- Github will automatically redirect requests will automatically redirect requests to this repository, but we recommend
to this repository, but we recommend updating your references for clarity. updating your references for clarity.
cli is a simple, fast, and fun package for building command line apps in Go. The 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 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) + [Alternate Names](#alternate-names)
+ [Ordering](#ordering) + [Ordering](#ordering)
+ [Values from the Environment](#values-from-the-environment) + [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) + [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others)
+ [Precedence](#precedence)
* [Subcommands](#subcommands) * [Subcommands](#subcommands)
* [Subcommands categories](#subcommands-categories) * [Subcommands categories](#subcommands-categories)
* [Exit code](#exit-code) * [Exit code](#exit-code)
@ -45,6 +47,7 @@ applications in an expressive way.
* [Version Flag](#version-flag) * [Version Flag](#version-flag)
+ [Customization](#customization-2) + [Customization](#customization-2)
+ [Full API Example](#full-api-example) + [Full API Example](#full-api-example)
* [Combining short Bool options](#combining-short-bool-options)
- [Contribution Guidelines](#contribution-guidelines) - [Contribution Guidelines](#contribution-guidelines)
<!-- tocstop --> <!-- tocstop -->
@ -586,6 +589,41 @@ func main() {
} }
``` ```
#### Values from files
You can also have the default value set from file via `FilePath`. e.g.
<!-- {
"args": ["&#45;&#45;help"],
"output": "password for the mysql database"
} -->
``` 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) #### Values from alternate input sources (YAML, TOML, and others)
There is a separate package altsrc that adds support for getting flag values 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
Subcommands can be defined for a more git-like command line app. Subcommands can be defined for a more git-like command line app.
@ -751,11 +798,11 @@ func main() {
}, },
{ {
Name: "add", Name: "add",
Category: "template", Category: "Template actions",
}, },
{ {
Name: "remove", 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 Traditional use of boolean options using their shortnames look like this:
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 # cmd foobar -s -o
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 Suppose you want users to be able to combine your bool options with their shortname. This
likely add you as a collaborator. As a collaborator you are given the ability can be done using the **UseShortOptionHandling** bool in your commands. Suppose your program
to merge others pull requests. It is very important that new code does not has a two bool flags such as *serve* and *option* with the short options of *-o* and
break existing code, so be careful about what code you do choose to merge. *-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 See [./CONTRIBUTING.md](./CONTRIBUTING.md)
added as a collaborator, we probably forgot to add you, please open an issue.

@ -22,15 +22,15 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool
if sections := strings.Split(name, "."); len(sections) > 1 { if sections := strings.Split(name, "."); len(sections) > 1 {
node := tree node := tree
for _, section := range sections[:len(sections)-1] { for _, section := range sections[:len(sections)-1] {
if child, ok := node[section]; !ok { child, ok := node[section]
if !ok {
return nil, false 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 { if val, ok := node[sections[len(sections)-1]]; ok {
return val, true return val, true

@ -66,9 +66,9 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) {
return ret, nil return ret, nil
} }
func (self *tomlMap) UnmarshalTOML(i interface{}) error { func (tm *tomlMap) UnmarshalTOML(i interface{}) error {
if tmp, err := unmarshalMap(i); err == nil { if tmp, err := unmarshalMap(i); err == nil {
self.Map = tmp tm.Map = tmp
} else { } else {
return err return err
} }

@ -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 nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath)
} }
return ioutil.ReadFile(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)
} }

@ -83,6 +83,9 @@ type App struct {
Writer io.Writer Writer io.Writer
// ErrWriter writes error output // ErrWriter writes error output
ErrWriter io.Writer 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 // Other custom info
Metadata map[string]interface{} Metadata map[string]interface{}
// Carries a function which returns app specific info. // Carries a function which returns app specific info.
@ -207,7 +210,7 @@ func (a *App) Run(arguments []string) (err error) {
if err != nil { if err != nil {
if a.OnUsageError != nil { if a.OnUsageError != nil {
err := a.OnUsageError(context, err, false) err := a.OnUsageError(context, err, false)
HandleExitCoder(err) a.handleExitCoder(context, err)
return err return err
} }
fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) 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 { if a.Before != nil {
beforeErr := a.Before(context) beforeErr := a.Before(context)
if beforeErr != nil { if beforeErr != nil {
fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
ShowAppHelp(context) ShowAppHelp(context)
HandleExitCoder(beforeErr) a.handleExitCoder(context, beforeErr)
err = beforeErr err = beforeErr
return err return err
} }
@ -263,7 +267,7 @@ func (a *App) Run(arguments []string) (err error) {
// Run default Action // Run default Action
err = HandleAction(a.Action, context) err = HandleAction(a.Action, context)
HandleExitCoder(err) a.handleExitCoder(context, err)
return err return err
} }
@ -330,7 +334,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
if err != nil { if err != nil {
if a.OnUsageError != nil { if a.OnUsageError != nil {
err = a.OnUsageError(context, err, true) err = a.OnUsageError(context, err, true)
HandleExitCoder(err) a.handleExitCoder(context, err)
return err return err
} }
fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) 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() { defer func() {
afterErr := a.After(context) afterErr := a.After(context)
if afterErr != nil { if afterErr != nil {
HandleExitCoder(err) a.handleExitCoder(context, err)
if err != nil { if err != nil {
err = NewMultiError(err, afterErr) err = NewMultiError(err, afterErr)
} else { } else {
@ -365,7 +369,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
if a.Before != nil { if a.Before != nil {
beforeErr := a.Before(context) beforeErr := a.Before(context)
if beforeErr != nil { if beforeErr != nil {
HandleExitCoder(beforeErr) a.handleExitCoder(context, beforeErr)
err = beforeErr err = beforeErr
return err return err
} }
@ -383,7 +387,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
// Run default Action // Run default Action
err = HandleAction(a.Action, context) err = HandleAction(a.Action, context)
HandleExitCoder(err) a.handleExitCoder(context, err)
return err return err
} }
@ -449,7 +453,6 @@ func (a *App) hasFlag(flag Flag) bool {
} }
func (a *App) errWriter() io.Writer { func (a *App) errWriter() io.Writer {
// When the app ErrWriter is nil use the package level one. // When the app ErrWriter is nil use the package level one.
if a.ErrWriter == nil { if a.ErrWriter == nil {
return ErrWriter 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. // Author represents someone who has contributed to a cli project.
type Author struct { type Author struct {
Name string // The Authors name 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 } else if a, ok := action.(func(*Context)); ok { // deprecated function signature
a(context) a(context)
return nil return nil
} else {
return errInvalidActionType
} }
return errInvalidActionType
} }

@ -367,6 +367,39 @@ func TestApp_CommandWithArgBeforeFlags(t *testing.T) {
expect(t, firstArg, "my-arg") 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) { func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
var context *Context var context *Context
@ -535,7 +568,6 @@ func TestApp_Float64Flag(t *testing.T) {
} }
func TestApp_ParseSliceFlags(t *testing.T) { func TestApp_ParseSliceFlags(t *testing.T) {
var parsedOption, firstArg string
var parsedIntSlice []int var parsedIntSlice []int
var parsedStringSlice []string var parsedStringSlice []string
@ -549,8 +581,6 @@ func TestApp_ParseSliceFlags(t *testing.T) {
Action: func(c *Context) error { Action: func(c *Context) error {
parsedIntSlice = c.IntSlice("p") parsedIntSlice = c.IntSlice("p")
parsedStringSlice = c.StringSlice("ip") parsedStringSlice = c.StringSlice("ip")
parsedOption = c.String("option")
firstArg = c.Args().First()
return nil 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) { func TestHandleAction_WithUnknownPanic(t *testing.T) {
defer func() { refute(t, recover(), nil) }() defer func() { refute(t, recover(), nil) }()

@ -1,14 +1,16 @@
version: "{build}" version: "{build}"
os: Windows Server 2012 R2 os: Windows Server 2016
image: Visual Studio 2017
clone_folder: c:\gopath\src\github.com\urfave\cli clone_folder: c:\gopath\src\github.com\urfave\cli
environment: environment:
GOPATH: C:\gopath GOPATH: C:\gopath
GOVERSION: 1.6 GOVERSION: 1.8.x
PYTHON: C:\Python27-x64 PYTHON: C:\Python36-x64
PYTHON_VERSION: 2.7.x PYTHON_VERSION: 3.6.x
PYTHON_ARCH: 64 PYTHON_ARCH: 64
install: install:

@ -10,7 +10,7 @@ type CommandCategory struct {
} }
func (c CommandCategories) Less(i, j int) bool { 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 { func (c CommandCategories) Len() int {

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"sort" "sort"
@ -55,6 +56,10 @@ type Command struct {
HideHelp bool HideHelp bool
// Boolean to hide this command from help or completion // Boolean to hide this command from help or completion
Hidden bool 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. // Full name of command for help, defaults to full command name, including parent commands.
HelpName string HelpName string
@ -73,7 +78,7 @@ func (c CommandsByName) Len() int {
} }
func (c CommandsByName) Less(i, j int) bool { 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) { 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) set, err := c.parseFlags(ctx.Args().Tail())
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
}
context := NewContext(ctx.App, set, ctx) context := NewContext(ctx.App, set, ctx)
context.Command = c context.Command = c
@ -167,7 +122,7 @@ func (c Command) Run(ctx *Context) (err error) {
if err != nil { if err != nil {
if c.OnUsageError != nil { if c.OnUsageError != nil {
err := c.OnUsageError(context, err, false) err := c.OnUsageError(context, err, false)
HandleExitCoder(err) context.App.handleExitCoder(context, err)
return err return err
} }
fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error())
@ -184,7 +139,7 @@ func (c Command) Run(ctx *Context) (err error) {
defer func() { defer func() {
afterErr := c.After(context) afterErr := c.After(context)
if afterErr != nil { if afterErr != nil {
HandleExitCoder(err) context.App.handleExitCoder(context, err)
if err != nil { if err != nil {
err = NewMultiError(err, afterErr) err = NewMultiError(err, afterErr)
} else { } else {
@ -198,7 +153,7 @@ func (c Command) Run(ctx *Context) (err error) {
err = c.Before(context) err = c.Before(context)
if err != nil { if err != nil {
ShowCommandHelp(context, c.Name) ShowCommandHelp(context, c.Name)
HandleExitCoder(err) context.App.handleExitCoder(context, err)
return err return err
} }
} }
@ -210,11 +165,88 @@ func (c Command) Run(ctx *Context) (err error) {
err = HandleAction(c.Action, context) err = HandleAction(c.Action, context)
if err != nil { if err != nil {
HandleExitCoder(err) context.App.handleExitCoder(context, err)
} }
return 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. // Names returns the names including short names and aliases.
func (c Command) Names() []string { func (c Command) Names() []string {
names := []string{c.Name} names := []string{c.Name}

@ -11,20 +11,24 @@ import (
func TestCommandFlagParsing(t *testing.T) { func TestCommandFlagParsing(t *testing.T) {
cases := []struct { cases := []struct {
testArgs []string testArgs []string
skipFlagParsing bool skipFlagParsing bool
skipArgReorder bool skipArgReorder bool
expectedErr error expectedErr error
UseShortOptionHandling bool
}{ }{
// Test normal "not ignoring flags" flow // 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 // 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 { for _, c := range cases {
@ -36,13 +40,14 @@ func TestCommandFlagParsing(t *testing.T) {
context := NewContext(app, set, nil) context := NewContext(app, set, nil)
command := Command{ command := Command{
Name: "test-cmd", Name: "test-cmd",
Aliases: []string{"tc"}, Aliases: []string{"tc"},
Usage: "this is for testing", Usage: "this is for testing",
Description: "testing", Description: "testing",
Action: func(_ *Context) error { return nil }, Action: func(_ *Context) error { return nil },
SkipFlagParsing: c.skipFlagParsing, SkipFlagParsing: c.skipFlagParsing,
SkipArgReorder: c.skipArgReorder, SkipArgReorder: c.skipArgReorder,
UseShortOptionHandling: c.UseShortOptionHandling,
} }
err := command.Run(context) err := command.Run(context)
@ -238,3 +243,77 @@ func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) {
t.Fatal(err) 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)
}
}

@ -3,6 +3,7 @@ package cli
import ( import (
"errors" "errors"
"flag" "flag"
"os"
"reflect" "reflect"
"strings" "strings"
"syscall" "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 // change in version 2 to add `IsSet` to the Flag interface to push the
// responsibility closer to where the information required to determine // responsibility closer to where the information required to determine
// whether a flag is set by non-standard means such as environment // 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 // See https://github.com/urfave/cli/issues/294 for additional discussion
flags := c.Command.Flags flags := c.Command.Flags
@ -93,18 +94,26 @@ func (c *Context) IsSet(name string) bool {
val = val.Elem() val = val.Elem()
} }
envVarValue := val.FieldByName("EnvVar") filePathValue := val.FieldByName("FilePath")
if !envVarValue.IsValid() { if filePathValue.IsValid() {
return eachName(filePathValue.String(), func(filePath string) {
if _, err := os.Stat(filePath); err == nil {
c.setFlags[name] = true
return
}
})
} }
eachName(envVarValue.String(), func(envVar string) { envVarValue := val.FieldByName("EnvVar")
envVar = strings.TrimSpace(envVar) if envVarValue.IsValid() {
if _, ok := syscall.Getenv(envVar); ok { eachName(envVarValue.String(), func(envVar string) {
c.setFlags[name] = true envVar = strings.TrimSpace(envVar)
return if _, ok := syscall.Getenv(envVar); ok {
} c.setFlags[name] = true
}) return
}
})
}
}) })
} }
} }

@ -3,6 +3,7 @@ package cli
import ( import (
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"reflect" "reflect"
"runtime" "runtime"
"strconv" "strconv"
@ -37,6 +38,18 @@ var HelpFlag Flag = BoolFlag{
// to display a flag. // to display a flag.
var FlagStringer FlagStringFunc = stringifyFlag 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. // FlagsByName is a slice of Flag.
type FlagsByName []Flag type FlagsByName []Flag
@ -45,7 +58,7 @@ func (f FlagsByName) Len() int {
} }
func (f FlagsByName) Less(i, j int) bool { 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) { 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 // provided by the user for parsing by the flag
func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error { func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error {
val := f.Value val := f.Value
if f.EnvVar != "" { if fileEnvVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { if err := val.Set(fileEnvVal); err != nil {
envVar = strings.TrimSpace(envVar) return fmt.Errorf("could not parse %s as value for flag %s: %s", fileEnvVal, f.Name, err)
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
}
} }
} }
@ -163,21 +170,19 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) {
// ApplyWithError populates the flag given the flag set and environment // ApplyWithError populates the flag given the flag set and environment
func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { newVal := &StringSlice{}
envVar = strings.TrimSpace(envVar) for _, s := range strings.Split(envVal, ",") {
if envVal, ok := syscall.Getenv(envVar); ok { s = strings.TrimSpace(s)
newVal := &StringSlice{} if err := newVal.Set(s); err != nil {
for _, s := range strings.Split(envVal, ",") { return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err)
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 f.Value == nil {
f.Value = newVal
} else {
*f.Value = *newVal
}
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { newVal := &IntSlice{}
envVar = strings.TrimSpace(envVar) for _, s := range strings.Split(envVal, ",") {
if envVal, ok := syscall.Getenv(envVar); ok { s = strings.TrimSpace(s)
newVal := &IntSlice{} if err := newVal.Set(s); err != nil {
for _, s := range strings.Split(envVal, ",") { return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err)
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 f.Value == nil {
f.Value = newVal
} else {
*f.Value = *newVal
}
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { newVal := &Int64Slice{}
envVar = strings.TrimSpace(envVar) for _, s := range strings.Split(envVal, ",") {
if envVal, ok := syscall.Getenv(envVar); ok { s = strings.TrimSpace(s)
newVal := &Int64Slice{} if err := newVal.Set(s); err != nil {
for _, s := range strings.Split(envVal, ",") { return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err)
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 f.Value == nil {
f.Value = newVal
} else {
*f.Value = *newVal
}
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error { func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error {
val := false val := false
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { if envVal == "" {
envVar = strings.TrimSpace(envVar) val = false
if envVal, ok := syscall.Getenv(envVar); ok { } else {
if envVal == "" { envValBool, err := strconv.ParseBool(envVal)
val = false if err != nil {
break return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err)
}
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
} }
val = envValBool
} }
} }
@ -364,23 +357,16 @@ func (f BoolTFlag) Apply(set *flag.FlagSet) {
// ApplyWithError populates the flag given the flag set and environment // ApplyWithError populates the flag given the flag set and environment
func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error { func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error {
val := true 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 envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
if err != nil { if envVal == "" {
return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) val = false
} } else {
envValBool, err := strconv.ParseBool(envVal)
val = envValBool if err != nil {
break 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 // ApplyWithError populates the flag given the flag set and environment
func (f StringFlag) ApplyWithError(set *flag.FlagSet) error { func (f StringFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { f.Value = envVal
envVar = strings.TrimSpace(envVar)
if envVal, ok := syscall.Getenv(envVar); ok {
f.Value = envVal
break
}
}
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f IntFlag) ApplyWithError(set *flag.FlagSet) error { func (f IntFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { envValInt, err := strconv.ParseInt(envVal, 0, 64)
envVar = strings.TrimSpace(envVar) if err != nil {
if envVal, ok := syscall.Getenv(envVar); ok { return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
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
}
} }
f.Value = int(envValInt)
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error { func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { envValInt, err := strconv.ParseInt(envVal, 0, 64)
envVar = strings.TrimSpace(envVar) if err != nil {
if envVal, ok := syscall.Getenv(envVar); ok { return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
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
}
} }
f.Value = envValInt
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f UintFlag) ApplyWithError(set *flag.FlagSet) error { func (f UintFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { envValInt, err := strconv.ParseUint(envVal, 0, 64)
envVar = strings.TrimSpace(envVar) if err != nil {
if envVal, ok := syscall.Getenv(envVar); ok { return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err)
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
}
} }
f.Value = uint(envValInt)
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error { func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { envValInt, err := strconv.ParseUint(envVal, 0, 64)
envVar = strings.TrimSpace(envVar) if err != nil {
if envVal, ok := syscall.Getenv(envVar); ok { return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err)
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
}
} }
f.Value = uint64(envValInt)
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error { func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { envValDuration, err := time.ParseDuration(envVal)
envVar = strings.TrimSpace(envVar) if err != nil {
if envVal, ok := syscall.Getenv(envVar); ok { return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err)
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
}
} }
f.Value = envValDuration
} }
eachName(f.Name, func(name string) { 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 // ApplyWithError populates the flag given the flag set and environment
func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error { func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
for _, envVar := range strings.Split(f.EnvVar, ",") { envValFloat, err := strconv.ParseFloat(envVal, 10)
envVar = strings.TrimSpace(envVar) if err != nil {
if envVal, ok := syscall.Getenv(envVar); ok { return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err)
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
}
} }
f.Value = float64(envValFloat)
} }
eachName(f.Name, func(name string) { eachName(f.Name, func(name string) {
@ -692,11 +636,19 @@ func withEnvHint(envVar, str string) string {
suffix = "%" suffix = "%"
sep = "%, %" 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 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 { func flagValue(f Flag) reflect.Value {
fv := reflect.ValueOf(f) fv := reflect.ValueOf(f)
for fv.Kind() == reflect.Ptr { for fv.Kind() == reflect.Ptr {
@ -710,14 +662,29 @@ func stringifyFlag(f Flag) string {
switch f.(type) { switch f.(type) {
case IntSliceFlag: case IntSliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(), return FlagFileHinter(
stringifyIntSliceFlag(f.(IntSliceFlag))) fv.FieldByName("FilePath").String(),
FlagEnvHinter(
fv.FieldByName("EnvVar").String(),
stringifyIntSliceFlag(f.(IntSliceFlag)),
),
)
case Int64SliceFlag: case Int64SliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(), return FlagFileHinter(
stringifyInt64SliceFlag(f.(Int64SliceFlag))) fv.FieldByName("FilePath").String(),
FlagEnvHinter(
fv.FieldByName("EnvVar").String(),
stringifyInt64SliceFlag(f.(Int64SliceFlag)),
),
)
case StringSliceFlag: case StringSliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(), return FlagFileHinter(
stringifyStringSliceFlag(f.(StringSliceFlag))) fv.FieldByName("FilePath").String(),
FlagEnvHinter(
fv.FieldByName("EnvVar").String(),
stringifyStringSliceFlag(f.(StringSliceFlag)),
),
)
} }
placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String())
@ -742,17 +709,22 @@ func stringifyFlag(f Flag) string {
placeholder = defaultPlaceholder placeholder = defaultPlaceholder
} }
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultValueString)) usageWithDefault := strings.TrimSpace(usage + defaultValueString)
return withEnvHint(fv.FieldByName("EnvVar").String(), return FlagFileHinter(
fmt.Sprintf("%s\t%s", prefixedNames(fv.FieldByName("Name").String(), placeholder), usageWithDefault)) fv.FieldByName("FilePath").String(),
FlagEnvHinter(
fv.FieldByName("EnvVar").String(),
FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder)+"\t"+usageWithDefault,
),
)
} }
func stringifyIntSliceFlag(f IntSliceFlag) string { func stringifyIntSliceFlag(f IntSliceFlag) string {
defaultVals := []string{} defaultVals := []string{}
if f.Value != nil && len(f.Value.Value()) > 0 { if f.Value != nil && len(f.Value.Value()) > 0 {
for _, i := range f.Value.Value() { 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{} defaultVals := []string{}
if f.Value != nil && len(f.Value.Value()) > 0 { if f.Value != nil && len(f.Value.Value()) > 0 {
for _, i := range f.Value.Value() { 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 { if f.Value != nil && len(f.Value.Value()) > 0 {
for _, s := range f.Value.Value() { for _, s := range f.Value.Value() {
if len(s) > 0 { 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, ", ")) defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", "))
} }
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) usageWithDefault := strings.TrimSpace(usage + defaultVal)
return fmt.Sprintf("%s\t%s", prefixedNames(name, placeholder), usageWithDefault) 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
} }

@ -13,6 +13,7 @@ type BoolFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Destination *bool Destination *bool
} }
@ -60,6 +61,7 @@ type BoolTFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Destination *bool Destination *bool
} }
@ -107,6 +109,7 @@ type DurationFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Value time.Duration Value time.Duration
Destination *time.Duration Destination *time.Duration
@ -155,6 +158,7 @@ type Float64Flag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Value float64 Value float64
Destination *float64 Destination *float64
@ -200,11 +204,12 @@ func lookupFloat64(name string, set *flag.FlagSet) float64 {
// GenericFlag is a flag with type Generic // GenericFlag is a flag with type Generic
type GenericFlag struct { type GenericFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
Hidden bool FilePath string
Value Generic Hidden bool
Value Generic
} }
// String returns a readable representation of this value // String returns a readable representation of this value
@ -250,6 +255,7 @@ type Int64Flag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Value int64 Value int64
Destination *int64 Destination *int64
@ -298,6 +304,7 @@ type IntFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Value int Value int
Destination *int Destination *int
@ -343,11 +350,12 @@ func lookupInt(name string, set *flag.FlagSet) int {
// IntSliceFlag is a flag with type *IntSlice // IntSliceFlag is a flag with type *IntSlice
type IntSliceFlag struct { type IntSliceFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
Hidden bool FilePath string
Value *IntSlice Hidden bool
Value *IntSlice
} }
// String returns a readable representation of this value // 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 // Int64SliceFlag is a flag with type *Int64Slice
type Int64SliceFlag struct { type Int64SliceFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
Hidden bool FilePath string
Value *Int64Slice Hidden bool
Value *Int64Slice
} }
// String returns a readable representation of this value // String returns a readable representation of this value
@ -440,6 +449,7 @@ type StringFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Value string Value string
Destination *string Destination *string
@ -485,11 +495,12 @@ func lookupString(name string, set *flag.FlagSet) string {
// StringSliceFlag is a flag with type *StringSlice // StringSliceFlag is a flag with type *StringSlice
type StringSliceFlag struct { type StringSliceFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
Hidden bool FilePath string
Value *StringSlice Hidden bool
Value *StringSlice
} }
// String returns a readable representation of this value // String returns a readable representation of this value
@ -535,6 +546,7 @@ type Uint64Flag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Value uint64 Value uint64
Destination *uint64 Destination *uint64
@ -583,6 +595,7 @@ type UintFlag struct {
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
Value uint Value uint
Destination *uint Destination *uint

@ -2,6 +2,8 @@ package cli
import ( import (
"fmt" "fmt"
"io"
"io/ioutil"
"os" "os"
"reflect" "reflect"
"regexp" "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 { var stringSliceFlagTests = []struct {
name string name string
value *StringSlice value *StringSlice
@ -969,6 +1048,31 @@ func TestParseMultiBool(t *testing.T) {
a.Run([]string{"run", "--serve"}) 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) { func TestParseDestinationBool(t *testing.T) {
var dest bool var dest bool
a := App{ a := App{
@ -1213,3 +1317,38 @@ func TestParseGenericFromEnvCascade(t *testing.T) {
} }
a.Run([]string{"run"}) 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)
}
}
}

@ -23,6 +23,22 @@ type CommandNotFoundFunc func(*Context, string)
// is displayed and the execution is interrupted. // is displayed and the execution is interrupted.
type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error 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 // FlagStringFunc is used by the help generation to display a flag, which is
// expected to be a single line. // expected to be a single line.
type FlagStringFunc func(Flag) string 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

@ -142,6 +142,7 @@ def _write_cli_flag_types(outfile, types):
Name string Name string
Usage string Usage string
EnvVar string EnvVar string
FilePath string
Hidden bool Hidden bool
""".format(**typedef)) """.format(**typedef))

@ -29,6 +29,7 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
{{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}} COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{end}}{{range .VisibleCommands}} {{.Name}}:{{end}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}

@ -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
}

@ -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)
}
}
}
Loading…
Cancel
Save