Merge branch 'v1' into v1

This commit is contained in:
Jesse Szwedko 2016-11-27 13:32:19 -08:00 committed by GitHub
commit 6752c11c1c
19 changed files with 1097 additions and 215 deletions

View File

@ -12,6 +12,7 @@ go:
- 1.4.2 - 1.4.2
- 1.5.x - 1.5.x
- 1.6.x - 1.6.x
- 1.7.x
- master - master
matrix: matrix:
@ -20,6 +21,8 @@ matrix:
include: include:
- go: 1.6.x - go: 1.6.x
os: osx os: osx
- go: 1.7.x
os: osx
before_script: before_script:
- go get github.com/urfave/gfmrun/... || true - go get github.com/urfave/gfmrun/... || true

View File

@ -3,14 +3,62 @@
**ATTN**: This project uses [semantic versioning](http://semver.org/). **ATTN**: This project uses [semantic versioning](http://semver.org/).
## [Unreleased] ## [Unreleased]
## [1.19.1] - 2016-11-21
### Fixed
- Fixes regression introduced in 1.19.0 where using an `ActionFunc` as
the `Action` for a command would cause it to error rather than calling the
function. Should not have a affected declarative cases using `func(c
*cli.Context) err)`.
- Shell completion now handles the case where the user specifies
`--generate-bash-completion` immediately after a flag that takes an argument.
Previously it call the application with `--generate-bash-completion` as the
flag value.
## [1.19.0] - 2016-11-19
### Added ### Added
- `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`)
- A `Description` field was added to `App` for a more detailed description of
the application (similar to the existing `Description` field on `Command`)
- Flag type code generation via `go generate` - Flag type code generation via `go generate`
- Write to stderr and exit 1 if action returns non-nil error - Write to stderr and exit 1 if action returns non-nil error
- Added support for TOML to the `altsrc` loader - Added support for TOML to the `altsrc` loader
- `SkipArgReorder` was added to allow users to skip the argument reordering.
This is useful if you want to consider all "flags" after an argument as
arguments rather than flags (the default behavior of the stdlib `flag`
library). This is backported functionality from the [removal of the flag
reordering](https://github.com/urfave/cli/pull/398) in the unreleased version
2
- For formatted errors (those implementing `ErrorFormatter`), the errors will
be formatted during output. Compatible with `pkg/errors`.
### Changed ### Changed
- Raise minimum tested/supported Go version to 1.2+ - Raise minimum tested/supported Go version to 1.2+
### Fixed
- Consider empty environment variables as set (previously environment variables
with the equivalent of `""` would be skipped rather than their value used).
- Return an error if the value in a given environment variable cannot be parsed
as the flag type. Previously these errors were silently swallowed.
- Print full error when an invalid flag is specified (which includes the invalid flag)
- `App.Writer` defaults to `stdout` when `nil`
- If no action is specified on a command or app, the help is now printed instead of `panic`ing
- `App.Metadata` is initialized automatically now (previously was `nil` unless initialized)
- Correctly show help message if `-h` is provided to a subcommand
- `context.(Global)IsSet` now respects environment variables. Previously it
would return `false` if a flag was specified in the environment rather than
as an argument
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
- `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This
fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well
as `altsrc` where Go would complain that the types didn't match
## [1.18.1] - 2016-08-28
### Fixed
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported)
## [1.18.0] - 2016-06-27 ## [1.18.0] - 2016-06-27
### Added ### Added
- `./runtests` test runner with coverage tracking by default - `./runtests` test runner with coverage tracking by default
@ -29,6 +77,10 @@
- No longer swallows `panic`s that occur within the `Action`s themselves when - No longer swallows `panic`s that occur within the `Action`s themselves when
detecting the signature of the `Action` field detecting the signature of the `Action` field
## [1.17.1] - 2016-08-28
### Fixed
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
## [1.17.0] - 2016-05-09 ## [1.17.0] - 2016-05-09
### Added ### Added
- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` - Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc`
@ -50,6 +102,10 @@
- cleanups based on [Go Report Card - cleanups based on [Go Report Card
feedback](https://goreportcard.com/report/github.com/urfave/cli) feedback](https://goreportcard.com/report/github.com/urfave/cli)
## [1.16.1] - 2016-08-28
### Fixed
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
## [1.16.0] - 2016-05-02 ## [1.16.0] - 2016-05-02
### Added ### Added
- `Hidden` field on all flag struct types to omit from generated help text - `Hidden` field on all flag struct types to omit from generated help text

View File

@ -23,13 +23,14 @@ applications in an expressive way.
- [Installation](#installation) - [Installation](#installation)
* [Supported platforms](#supported-platforms) * [Supported platforms](#supported-platforms)
* [Using the `v2` branch](#using-the-v2-branch) * [Using the `v2` branch](#using-the-v2-branch)
* [Pinning to the `v1` branch](#pinning-to-the-v1-branch) * [Pinning to the `v1` releases](#pinning-to-the-v1-releases)
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Examples](#examples) - [Examples](#examples)
* [Arguments](#arguments) * [Arguments](#arguments)
* [Flags](#flags) * [Flags](#flags)
+ [Placeholder Values](#placeholder-values) + [Placeholder Values](#placeholder-values)
+ [Alternate Names](#alternate-names) + [Alternate Names](#alternate-names)
+ [Ordering](#ordering)
+ [Values from the Environment](#values-from-the-environment) + [Values from the Environment](#values-from-the-environment)
+ [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)
* [Subcommands](#subcommands) * [Subcommands](#subcommands)
@ -104,11 +105,11 @@ import (
... ...
``` ```
### Pinning to the `v1` branch ### Pinning to the `v1` releases
Similarly to the section above describing use of the `v2` branch, if one wants Similarly to the section above describing use of the `v2` branch, if one wants
to avoid any unexpected compatibility pains once `v2` becomes `master`, then to avoid any unexpected compatibility pains once `v2` becomes `master`, then
pinning to the `v1` branch is an acceptable option, e.g.: pinning to `v1` is an acceptable option, e.g.:
``` ```
$ go get gopkg.in/urfave/cli.v1 $ go get gopkg.in/urfave/cli.v1
@ -122,6 +123,8 @@ import (
... ...
``` ```
This will pull the latest tagged `v1` release (e.g. `v1.18.1` at the time of writing).
## Getting Started ## Getting Started
One of the philosophies behind cli is that an API should be playful and full of One of the philosophies behind cli is that an API should be playful and full of
@ -448,6 +451,56 @@ That flag can then be set with `--lang spanish` or `-l spanish`. Note that
giving two different forms of the same flag in the same command invocation is an giving two different forms of the same flag in the same command invocation is an
error. error.
#### Ordering
Flags for the application and commands are shown in the order they are defined.
However, it's possible to sort them from outside this library by using `FlagsByName`
with `sort`.
For example this:
<!-- {
"args": ["&#45;&#45;help"],
"output": "Load configuration from FILE\n.*Language for the greeting.*"
} -->
``` go
package main
import (
"os"
"sort"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Flags = []cli.Flag {
cli.StringFlag{
Name: "lang, l",
Value: "english",
Usage: "Language for the greeting",
},
cli.StringFlag{
Name: "config, c",
Usage: "Load configuration from `FILE`",
},
}
sort.Sort(cli.FlagsByName(app.Flags))
app.Run(os.Args)
}
```
Will result in help output like:
```
--config FILE, -c FILE Load configuration from FILE
--lang value, -l value Language for the greeting (default: "english")
```
#### Values from the Environment #### Values from the Environment
You can also have the default value set from the environment via `EnvVar`. e.g. You can also have the default value set from the environment via `EnvVar`. e.g.
@ -952,7 +1005,7 @@ is checked by the cli internals in order to print the `App.Version` via
#### Customization #### Customization
The default flag may be cusomized to something other than `-v/--version` by The default flag may be customized to something other than `-v/--version` by
setting `cli.VersionFlag`, e.g.: setting `cli.VersionFlag`, e.g.:
<!-- { <!-- {

View File

@ -2,9 +2,9 @@ package altsrc
import ( import (
"fmt" "fmt"
"os"
"strconv" "strconv"
"strings" "strings"
"syscall"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
) )
@ -237,13 +237,11 @@ func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourc
func isEnvVarSet(envVars string) bool { func isEnvVarSet(envVars string) bool {
for _, envVar := range strings.Split(envVars, ",") { for _, envVar := range strings.Split(envVars, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if _, ok := syscall.Getenv(envVar); ok {
// TODO: Can't use this for bools as // TODO: Can't use this for bools as
// set means that it was true or false based on // set means that it was true or false based on
// Bool flag type, should work for other types // Bool flag type, should work for other types
if len(envVal) > 0 { return true
return true
}
} }
} }

View File

@ -27,6 +27,13 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) {
f.BoolFlag.Apply(set) f.BoolFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped BoolFlag.ApplyWithError
func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.BoolFlag.ApplyWithError(set)
}
// BoolTFlag is the flag type that wraps cli.BoolTFlag to allow // BoolTFlag is the flag type that wraps cli.BoolTFlag to allow
// for other values to be specified // for other values to be specified
type BoolTFlag struct { type BoolTFlag struct {
@ -46,6 +53,13 @@ func (f *BoolTFlag) Apply(set *flag.FlagSet) {
f.BoolTFlag.Apply(set) f.BoolTFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped BoolTFlag.ApplyWithError
func (f *BoolTFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.BoolTFlag.ApplyWithError(set)
}
// DurationFlag is the flag type that wraps cli.DurationFlag to allow // DurationFlag is the flag type that wraps cli.DurationFlag to allow
// for other values to be specified // for other values to be specified
type DurationFlag struct { type DurationFlag struct {
@ -65,6 +79,13 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) {
f.DurationFlag.Apply(set) f.DurationFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped DurationFlag.ApplyWithError
func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.DurationFlag.ApplyWithError(set)
}
// Float64Flag is the flag type that wraps cli.Float64Flag to allow // Float64Flag is the flag type that wraps cli.Float64Flag to allow
// for other values to be specified // for other values to be specified
type Float64Flag struct { type Float64Flag struct {
@ -84,6 +105,13 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) {
f.Float64Flag.Apply(set) f.Float64Flag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped Float64Flag.ApplyWithError
func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.Float64Flag.ApplyWithError(set)
}
// GenericFlag is the flag type that wraps cli.GenericFlag to allow // GenericFlag is the flag type that wraps cli.GenericFlag to allow
// for other values to be specified // for other values to be specified
type GenericFlag struct { type GenericFlag struct {
@ -103,6 +131,13 @@ func (f *GenericFlag) Apply(set *flag.FlagSet) {
f.GenericFlag.Apply(set) f.GenericFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped GenericFlag.ApplyWithError
func (f *GenericFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.GenericFlag.ApplyWithError(set)
}
// Int64Flag is the flag type that wraps cli.Int64Flag to allow // Int64Flag is the flag type that wraps cli.Int64Flag to allow
// for other values to be specified // for other values to be specified
type Int64Flag struct { type Int64Flag struct {
@ -122,6 +157,13 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) {
f.Int64Flag.Apply(set) f.Int64Flag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped Int64Flag.ApplyWithError
func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.Int64Flag.ApplyWithError(set)
}
// IntFlag is the flag type that wraps cli.IntFlag to allow // IntFlag is the flag type that wraps cli.IntFlag to allow
// for other values to be specified // for other values to be specified
type IntFlag struct { type IntFlag struct {
@ -141,6 +183,13 @@ func (f *IntFlag) Apply(set *flag.FlagSet) {
f.IntFlag.Apply(set) f.IntFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped IntFlag.ApplyWithError
func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.IntFlag.ApplyWithError(set)
}
// IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow // IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow
// for other values to be specified // for other values to be specified
type IntSliceFlag struct { type IntSliceFlag struct {
@ -160,6 +209,13 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) {
f.IntSliceFlag.Apply(set) f.IntSliceFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped IntSliceFlag.ApplyWithError
func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.IntSliceFlag.ApplyWithError(set)
}
// Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow // Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow
// for other values to be specified // for other values to be specified
type Int64SliceFlag struct { type Int64SliceFlag struct {
@ -179,6 +235,13 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) {
f.Int64SliceFlag.Apply(set) f.Int64SliceFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped Int64SliceFlag.ApplyWithError
func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.Int64SliceFlag.ApplyWithError(set)
}
// StringFlag is the flag type that wraps cli.StringFlag to allow // StringFlag is the flag type that wraps cli.StringFlag to allow
// for other values to be specified // for other values to be specified
type StringFlag struct { type StringFlag struct {
@ -198,6 +261,13 @@ func (f *StringFlag) Apply(set *flag.FlagSet) {
f.StringFlag.Apply(set) f.StringFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped StringFlag.ApplyWithError
func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.StringFlag.ApplyWithError(set)
}
// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow // StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow
// for other values to be specified // for other values to be specified
type StringSliceFlag struct { type StringSliceFlag struct {
@ -217,6 +287,13 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) {
f.StringSliceFlag.Apply(set) f.StringSliceFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped StringSliceFlag.ApplyWithError
func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.StringSliceFlag.ApplyWithError(set)
}
// Uint64Flag is the flag type that wraps cli.Uint64Flag to allow // Uint64Flag is the flag type that wraps cli.Uint64Flag to allow
// for other values to be specified // for other values to be specified
type Uint64Flag struct { type Uint64Flag struct {
@ -236,6 +313,13 @@ func (f *Uint64Flag) Apply(set *flag.FlagSet) {
f.Uint64Flag.Apply(set) f.Uint64Flag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped Uint64Flag.ApplyWithError
func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.Uint64Flag.ApplyWithError(set)
}
// UintFlag is the flag type that wraps cli.UintFlag to allow // UintFlag is the flag type that wraps cli.UintFlag to allow
// for other values to be specified // for other values to be specified
type UintFlag struct { type UintFlag struct {
@ -254,3 +338,10 @@ func (f *UintFlag) Apply(set *flag.FlagSet) {
f.set = set f.set = set
f.UintFlag.Apply(set) f.UintFlag.Apply(set)
} }
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped UintFlag.ApplyWithError
func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.UintFlag.ApplyWithError(set)
}

100
app.go
View File

@ -6,9 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"sort" "sort"
"strings"
"time" "time"
) )
@ -19,11 +17,8 @@ var (
contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you."
errNonFuncAction = NewExitError("ERROR invalid Action type. "+ errInvalidActionType = NewExitError("ERROR invalid Action type. "+
fmt.Sprintf("Must be a func of type `cli.ActionFunc`. %s", contactSysadmin)+ fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+
fmt.Sprintf("See %s", appActionDeprecationURL), 2)
errInvalidActionSignature = NewExitError("ERROR invalid Action signature. "+
fmt.Sprintf("Must be `cli.ActionFunc`. %s", contactSysadmin)+
fmt.Sprintf("See %s", appActionDeprecationURL), 2) fmt.Sprintf("See %s", appActionDeprecationURL), 2)
) )
@ -42,6 +37,8 @@ type App struct {
ArgsUsage string ArgsUsage string
// Version of the program // Version of the program
Version string Version string
// Description of the program
Description string
// List of commands to execute // List of commands to execute
Commands []Command Commands []Command
// List of flags to parse // List of flags to parse
@ -148,10 +145,6 @@ func (a *App) Setup() {
} }
} }
if a.EnableBashCompletion {
a.appendFlag(BashCompletionFlag)
}
if !a.HideVersion { if !a.HideVersion {
a.appendFlag(VersionFlag) a.appendFlag(VersionFlag)
} }
@ -165,6 +158,10 @@ func (a *App) Setup() {
if a.Metadata == nil { if a.Metadata == nil {
a.Metadata = make(map[string]interface{}) a.Metadata = make(map[string]interface{})
} }
if a.Writer == nil {
a.Writer = os.Stdout
}
} }
// Run is the entry point to the cli app. Parses the arguments slice and routes // Run is the entry point to the cli app. Parses the arguments slice and routes
@ -172,8 +169,20 @@ func (a *App) Setup() {
func (a *App) Run(arguments []string) (err error) { func (a *App) Run(arguments []string) (err error) {
a.Setup() a.Setup()
// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
// flag name as the value of the flag before it which is undesirable
// note that we can only do this because the shell autocomplete function
// always appends the completion flag at the end of the command
shellComplete, arguments := checkShellCompleteFlag(a, arguments)
// parse flags // parse flags
set := flagSet(a.Name, a.Flags) set, err := flagSet(a.Name, a.Flags)
if err != nil {
return err
}
set.SetOutput(ioutil.Discard) set.SetOutput(ioutil.Discard)
err = set.Parse(arguments[1:]) err = set.Parse(arguments[1:])
nerr := normalizeFlags(a.Flags, set) nerr := normalizeFlags(a.Flags, set)
@ -183,6 +192,7 @@ func (a *App) Run(arguments []string) (err error) {
ShowAppHelp(context) ShowAppHelp(context)
return nerr return nerr
} }
context.shellComplete = shellComplete
if checkCompletions(context) { if checkCompletions(context) {
return nil return nil
@ -194,7 +204,7 @@ func (a *App) Run(arguments []string) (err error) {
HandleExitCoder(err) HandleExitCoder(err)
return err return err
} }
fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
ShowAppHelp(context) ShowAppHelp(context)
return err return err
} }
@ -241,6 +251,10 @@ func (a *App) Run(arguments []string) (err error) {
} }
} }
if a.Action == nil {
a.Action = helpCommand.Action
}
// Run default Action // Run default Action
err = HandleAction(a.Action, context) err = HandleAction(a.Action, context)
@ -282,13 +296,12 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
} }
a.Commands = newCmds a.Commands = newCmds
// append flags // parse flags
if a.EnableBashCompletion { set, err := flagSet(a.Name, a.Flags)
a.appendFlag(BashCompletionFlag) if err != nil {
return err
} }
// parse flags
set := flagSet(a.Name, a.Flags)
set.SetOutput(ioutil.Discard) set.SetOutput(ioutil.Discard)
err = set.Parse(ctx.Args().Tail()) err = set.Parse(ctx.Args().Tail())
nerr := normalizeFlags(a.Flags, set) nerr := normalizeFlags(a.Flags, set)
@ -315,7 +328,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
HandleExitCoder(err) HandleExitCoder(err)
return err return err
} }
fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
ShowSubcommandHelp(context) ShowSubcommandHelp(context)
return err return err
} }
@ -456,47 +469,24 @@ type Author struct {
func (a Author) String() string { func (a Author) String() string {
e := "" e := ""
if a.Email != "" { if a.Email != "" {
e = "<" + a.Email + "> " e = " <" + a.Email + ">"
} }
return fmt.Sprintf("%v %v", a.Name, e) return fmt.Sprintf("%v%v", a.Name, e)
} }
// HandleAction uses ✧✧✧reflection✧✧✧ to figure out if the given Action is an // HandleAction attempts to figure out which Action signature was used. If
// ActionFunc, a func with the legacy signature for Action, or some other // it's an ActionFunc or a func with the legacy signature for Action, the func
// invalid thing. If it's an ActionFunc or a func with the legacy signature for // is run!
// Action, the func is run!
func HandleAction(action interface{}, context *Context) (err error) { func HandleAction(action interface{}, context *Context) (err error) {
defer func() { if a, ok := action.(ActionFunc); ok {
if r := recover(); r != nil { return a(context)
// Try to detect a known reflection error from *this scope*, rather than } else if a, ok := action.(func(*Context) error); ok {
// swallowing all panics that may happen when calling an Action func. return a(context)
s := fmt.Sprintf("%v", r) } else if a, ok := action.(func(*Context)); ok { // deprecated function signature
if strings.HasPrefix(s, "reflect: ") && strings.Contains(s, "too many input arguments") { a(context)
err = NewExitError(fmt.Sprintf("ERROR unknown Action error: %v.", r), 2)
} else {
panic(r)
}
}
}()
if reflect.TypeOf(action).Kind() != reflect.Func {
return errNonFuncAction
}
vals := reflect.ValueOf(action).Call([]reflect.Value{reflect.ValueOf(context)})
if len(vals) == 0 {
return nil return nil
} else {
return errInvalidActionType
} }
if len(vals) > 1 {
return errInvalidActionSignature
}
if retErr, ok := vals[0].Interface().(error); vals[0].IsValid() && ok {
return retErr
}
return err
} }

View File

@ -90,7 +90,62 @@ func ExampleApp_Run_subcommand() {
// Hello, Jeremy // Hello, Jeremy
} }
func ExampleApp_Run_help() { func ExampleApp_Run_appHelp() {
// set args for examples sake
os.Args = []string{"greet", "help"}
app := NewApp()
app.Name = "greet"
app.Version = "0.1.0"
app.Description = "This is how we describe greet the app"
app.Authors = []Author{
{Name: "Harrison", Email: "harrison@lolwut.com"},
{Name: "Oliver Allen", Email: "oliver@toyshop.com"},
}
app.Flags = []Flag{
StringFlag{Name: "name", Value: "bob", Usage: "a name to say"},
}
app.Commands = []Command{
{
Name: "describeit",
Aliases: []string{"d"},
Usage: "use it to see a description",
Description: "This is how we describe describeit the function",
Action: func(c *Context) error {
fmt.Printf("i like to describe things")
return nil
},
},
}
app.Run(os.Args)
// Output:
// NAME:
// greet - A new cli application
//
// USAGE:
// greet [global options] command [command options] [arguments...]
//
// VERSION:
// 0.1.0
//
// DESCRIPTION:
// This is how we describe greet the app
//
// AUTHORS:
// Harrison <harrison@lolwut.com>
// Oliver Allen <oliver@toyshop.com>
//
// COMMANDS:
// describeit, d use it to see a description
// help, h Shows a list of commands or help for one command
//
// GLOBAL OPTIONS:
// --name value a name to say (default: "bob")
// --help, -h show help
// --version, -v print the version
}
func ExampleApp_Run_commandHelp() {
// set args for examples sake // set args for examples sake
os.Args = []string{"greet", "h", "describeit"} os.Args = []string{"greet", "h", "describeit"}
@ -123,6 +178,49 @@ func ExampleApp_Run_help() {
// This is how we describe describeit the function // This is how we describe describeit the function
} }
func ExampleApp_Run_noAction() {
app := App{}
app.Name = "greet"
app.Run([]string{"greet"})
// Output:
// NAME:
// greet
//
// USAGE:
// [global options] command [command options] [arguments...]
//
// COMMANDS:
// help, h Shows a list of commands or help for one command
//
// GLOBAL OPTIONS:
// --help, -h show help
// --version, -v print the version
}
func ExampleApp_Run_subcommandNoAction() {
app := App{}
app.Name = "greet"
app.Commands = []Command{
{
Name: "describeit",
Aliases: []string{"d"},
Usage: "use it to see a description",
Description: "This is how we describe describeit the function",
},
}
app.Run([]string{"greet", "describeit"})
// Output:
// NAME:
// describeit - use it to see a description
//
// USAGE:
// describeit [arguments...]
//
// DESCRIPTION:
// This is how we describe describeit the function
}
func ExampleApp_Run_bashComplete() { func ExampleApp_Run_bashComplete() {
// set args for examples sake // set args for examples sake
os.Args = []string{"greet", "--generate-bash-completion"} os.Args = []string{"greet", "--generate-bash-completion"}
@ -202,6 +300,12 @@ func TestApp_Command(t *testing.T) {
} }
} }
func TestApp_Setup_defaultsWriter(t *testing.T) {
app := &App{}
app.Setup()
expect(t, app.Writer, os.Stdout)
}
func TestApp_CommandWithArgBeforeFlags(t *testing.T) { func TestApp_CommandWithArgBeforeFlags(t *testing.T) {
var parsedOption, firstArg string var parsedOption, firstArg string
@ -252,6 +356,23 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
expect(t, context.String("lang"), "spanish") expect(t, context.String("lang"), "spanish")
} }
func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) {
a := App{
Flags: []Flag{
StringFlag{Name: "--foo"},
},
Writer: bytes.NewBufferString(""),
}
set := flag.NewFlagSet("", flag.ContinueOnError)
set.Parse([]string{"", "---foo"})
c := &Context{flagSet: set}
err := a.RunAsSubcommand(c)
expect(t, err, errors.New("bad flag syntax: ---foo"))
}
func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {
var parsedOption string var parsedOption string
var args []string var args []string
@ -1402,7 +1523,39 @@ func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) {
func TestHandleAction_WithNonFuncAction(t *testing.T) { func TestHandleAction_WithNonFuncAction(t *testing.T) {
app := NewApp() app := NewApp()
app.Action = 42 app.Action = 42
err := HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil)) fs, err := flagSet(app.Name, app.Flags)
if err != nil {
t.Errorf("error creating FlagSet: %s", err)
}
err = HandleAction(app.Action, NewContext(app, fs, nil))
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
exitErr, ok := err.(*ExitError)
if !ok {
t.Fatalf("expected to receive a *ExitError")
}
if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type.") {
t.Fatalf("expected an unknown Action error, but got: %v", exitErr.Error())
}
if exitErr.ExitCode() != 2 {
t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode())
}
}
func TestHandleAction_WithInvalidFuncSignature(t *testing.T) {
app := NewApp()
app.Action = func() string { return "" }
fs, err := flagSet(app.Name, app.Flags)
if err != nil {
t.Errorf("error creating FlagSet: %s", err)
}
err = HandleAction(app.Action, NewContext(app, fs, nil))
if err == nil { if err == nil {
t.Fatalf("expected to receive error from Run, got none") t.Fatalf("expected to receive error from Run, got none")
@ -1423,34 +1576,14 @@ func TestHandleAction_WithNonFuncAction(t *testing.T) {
} }
} }
func TestHandleAction_WithInvalidFuncSignature(t *testing.T) {
app := NewApp()
app.Action = func() string { return "" }
err := HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil))
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
exitErr, ok := err.(*ExitError)
if !ok {
t.Fatalf("expected to receive a *ExitError")
}
if !strings.HasPrefix(exitErr.Error(), "ERROR unknown Action error") {
t.Fatalf("expected an unknown Action error, but got: %v", exitErr.Error())
}
if exitErr.ExitCode() != 2 {
t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode())
}
}
func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) { func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) {
app := NewApp() app := NewApp()
app.Action = func(_ *Context) (int, error) { return 0, nil } app.Action = func(_ *Context) (int, error) { return 0, nil }
err := HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil)) fs, err := flagSet(app.Name, app.Flags)
if err != nil {
t.Errorf("error creating FlagSet: %s", err)
}
err = HandleAction(app.Action, NewContext(app, fs, nil))
if err == nil { if err == nil {
t.Fatalf("expected to receive error from Run, got none") t.Fatalf("expected to receive error from Run, got none")
@ -1462,7 +1595,7 @@ func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) {
t.Fatalf("expected to receive a *ExitError") t.Fatalf("expected to receive a *ExitError")
} }
if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action signature") { if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type") {
t.Fatalf("expected an invalid Action signature error, but got: %v", exitErr.Error()) t.Fatalf("expected an invalid Action signature error, but got: %v", exitErr.Error())
} }
@ -1481,5 +1614,72 @@ func TestHandleAction_WithUnknownPanic(t *testing.T) {
fn(ctx) fn(ctx)
return nil return nil
} }
HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil)) fs, err := flagSet(app.Name, app.Flags)
if err != nil {
t.Errorf("error creating FlagSet: %s", err)
}
HandleAction(app.Action, NewContext(app, fs, nil))
}
func TestShellCompletionForIncompleteFlags(t *testing.T) {
app := NewApp()
app.Flags = []Flag{
IntFlag{
Name: "test-completion",
},
}
app.EnableBashCompletion = true
app.BashComplete = func(ctx *Context) {
for _, command := range ctx.App.Commands {
if command.Hidden {
continue
}
for _, name := range command.Names() {
fmt.Fprintln(ctx.App.Writer, name)
}
}
for _, flag := range ctx.App.Flags {
for _, name := range strings.Split(flag.GetName(), ",") {
if name == BashCompletionFlag.Name {
continue
}
switch name = strings.TrimSpace(name); len(name) {
case 0:
case 1:
fmt.Fprintln(ctx.App.Writer, "-"+name)
default:
fmt.Fprintln(ctx.App.Writer, "--"+name)
}
}
}
}
app.Action = func(ctx *Context) error {
return fmt.Errorf("should not get here")
}
err := app.Run([]string{"", "--test-completion", "--" + BashCompletionFlag.Name})
if err != nil {
t.Errorf("app should not return an error: %s", err)
}
}
func TestHandleActionActuallyWorksWithActions(t *testing.T) {
var f ActionFunc
called := false
f = func(c *Context) error {
called = true
return nil
}
err := HandleAction(f, nil)
if err != nil {
t.Errorf("Should not have errored: %v", err)
}
if !called {
t.Errorf("Function was not called")
}
} }

View File

@ -46,6 +46,11 @@ type Command struct {
Flags []Flag Flags []Flag
// Treat all flags as normal arguments if true // Treat all flags as normal arguments if true
SkipFlagParsing bool SkipFlagParsing bool
// Skip argument reordering which attempts to move flags before arguments,
// but only works if all flags appear after all arguments. This behavior was
// removed n version 2 since it only works under specific conditions so we
// backport here by exposing it as an option for compatibility.
SkipArgReorder bool
// Boolean to hide built-in help command // Boolean to hide built-in help command
HideHelp bool HideHelp bool
// Boolean to hide this command from help or completion // Boolean to hide this command from help or completion
@ -82,14 +87,15 @@ func (c Command) Run(ctx *Context) (err error) {
) )
} }
if ctx.App.EnableBashCompletion { set, err := flagSet(c.Name, c.Flags)
c.Flags = append(c.Flags, BashCompletionFlag) if err != nil {
return err
} }
set := flagSet(c.Name, c.Flags)
set.SetOutput(ioutil.Discard) set.SetOutput(ioutil.Discard)
if !c.SkipFlagParsing { if c.SkipFlagParsing {
err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...))
} else if !c.SkipArgReorder {
firstFlagIndex := -1 firstFlagIndex := -1
terminatorIndex := -1 terminatorIndex := -1
for index, arg := range ctx.Args() { for index, arg := range ctx.Args() {
@ -122,21 +128,7 @@ func (c Command) Run(ctx *Context) (err error) {
err = set.Parse(ctx.Args().Tail()) err = set.Parse(ctx.Args().Tail())
} }
} else { } else {
if c.SkipFlagParsing { err = set.Parse(ctx.Args().Tail())
err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...))
}
}
if err != nil {
if c.OnUsageError != nil {
err := c.OnUsageError(ctx, err, false)
HandleExitCoder(err)
return err
}
fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.")
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
return err
} }
nerr := normalizeFlags(c.Flags, set) nerr := normalizeFlags(c.Flags, set)
@ -148,11 +140,22 @@ func (c Command) Run(ctx *Context) (err error) {
} }
context := NewContext(ctx.App, set, ctx) context := NewContext(ctx.App, set, ctx)
if checkCommandCompletions(context, c.Name) { if checkCommandCompletions(context, c.Name) {
return nil return nil
} }
if err != nil {
if c.OnUsageError != nil {
err := c.OnUsageError(ctx, err, false)
HandleExitCoder(err)
return err
}
fmt.Fprintln(ctx.App.Writer, "Incorrect Usage:", err.Error())
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
return err
}
if checkCommandHelp(context, c.Name) { if checkCommandHelp(context, c.Name) {
return nil return nil
} }
@ -182,6 +185,10 @@ func (c Command) Run(ctx *Context) (err error) {
} }
} }
if c.Action == nil {
c.Action = helpSubcommand.Action
}
context.Command = c context.Command = c
err = HandleAction(c.Action, context) err = HandleAction(c.Action, context)

View File

@ -13,12 +13,18 @@ func TestCommandFlagParsing(t *testing.T) {
cases := []struct { cases := []struct {
testArgs []string testArgs []string
skipFlagParsing bool skipFlagParsing bool
skipArgReorder bool
expectedErr error expectedErr error
}{ }{
{[]string{"blah", "blah", "-break"}, false, errors.New("flag provided but not defined: -break")}, // Test normal "not ignoring flags" flow // Test normal "not ignoring flags" flow
{[]string{"blah", "blah"}, true, nil}, // Test SkipFlagParsing without any args that look like flags {[]string{"test-cmd", "blah", "blah", "-break"}, false, false, errors.New("flag provided but not defined: -break")},
{[]string{"blah", "-break"}, true, nil}, // Test SkipFlagParsing with random flag arg
{[]string{"blah", "-help"}, true, nil}, // Test SkipFlagParsing with "special" help flag arg // Test no arg reorder
{[]string{"test-cmd", "blah", "blah", "-break"}, false, true, nil},
{[]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 {
@ -30,15 +36,15 @@ 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,
SkipArgReorder: c.skipArgReorder,
} }
command.SkipFlagParsing = c.skipFlagParsing
err := command.Run(context) err := command.Run(context)
expect(t, err, c.expectedErr) expect(t, err, c.expectedErr)

View File

@ -3,9 +3,9 @@ package cli
import ( import (
"errors" "errors"
"flag" "flag"
"os"
"reflect" "reflect"
"strings" "strings"
"syscall"
) )
// Context is a type that is passed through to // Context is a type that is passed through to
@ -15,6 +15,7 @@ import (
type Context struct { type Context struct {
App *App App *App
Command Command Command Command
shellComplete bool
flagSet *flag.FlagSet flagSet *flag.FlagSet
setFlags map[string]bool setFlags map[string]bool
parentContext *Context parentContext *Context
@ -22,7 +23,13 @@ type Context struct {
// NewContext creates a new context. For use in when invoking an App or Command action. // NewContext creates a new context. For use in when invoking an App or Command action.
func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
return &Context{App: app, flagSet: set, parentContext: parentCtx} c := &Context{App: app, flagSet: set, parentContext: parentCtx}
if parentCtx != nil {
c.shellComplete = parentCtx.shellComplete
}
return c
} }
// NumFlags returns the number of flags set // NumFlags returns the number of flags set
@ -79,14 +86,19 @@ func (c *Context) IsSet(name string) bool {
return return
} }
envVarValue := reflect.ValueOf(f).FieldByName("EnvVar") val := reflect.ValueOf(f)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
envVarValue := val.FieldByName("EnvVar")
if !envVarValue.IsValid() { if !envVarValue.IsValid() {
return return
} }
eachName(envVarValue.String(), func(envVar string) { eachName(envVarValue.String(), func(envVar string) {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if _, ok := syscall.Getenv(envVar); ok {
c.setFlags[name] = true c.setFlags[name] = true
return return
} }
@ -142,6 +154,11 @@ func (c *Context) Parent() *Context {
return c.parentContext return c.parentContext
} }
// value returns the value of the flag coressponding to `name`
func (c *Context) value(name string) interface{} {
return c.flagSet.Lookup(name).Value.(flag.Getter).Get()
}
// Args contains apps console arguments // Args contains apps console arguments
type Args []string type Args []string

View File

@ -184,18 +184,30 @@ func TestContext_IsSet(t *testing.T) {
// XXX Corresponds to hack in context.IsSet for flags with EnvVar field // XXX Corresponds to hack in context.IsSet for flags with EnvVar field
// Should be moved to `flag_test` in v2 // Should be moved to `flag_test` in v2
func TestContext_IsSet_fromEnv(t *testing.T) { func TestContext_IsSet_fromEnv(t *testing.T) {
var timeoutIsSet, tIsSet, noEnvVarIsSet, nIsSet bool var (
timeoutIsSet, tIsSet bool
noEnvVarIsSet, nIsSet bool
passwordIsSet, pIsSet bool
unparsableIsSet, uIsSet bool
)
os.Clearenv() clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "15.5") os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
os.Setenv("APP_PASSWORD", "")
a := App{ a := App{
Flags: []Flag{ Flags: []Flag{
Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
StringFlag{Name: "password, p", EnvVar: "APP_PASSWORD"},
Float64Flag{Name: "unparsable, u", EnvVar: "APP_UNPARSABLE"},
Float64Flag{Name: "no-env-var, n"}, Float64Flag{Name: "no-env-var, n"},
}, },
Action: func(ctx *Context) error { Action: func(ctx *Context) error {
timeoutIsSet = ctx.IsSet("timeout") timeoutIsSet = ctx.IsSet("timeout")
tIsSet = ctx.IsSet("t") tIsSet = ctx.IsSet("t")
passwordIsSet = ctx.IsSet("password")
pIsSet = ctx.IsSet("p")
unparsableIsSet = ctx.IsSet("unparsable")
uIsSet = ctx.IsSet("u")
noEnvVarIsSet = ctx.IsSet("no-env-var") noEnvVarIsSet = ctx.IsSet("no-env-var")
nIsSet = ctx.IsSet("n") nIsSet = ctx.IsSet("n")
return nil return nil
@ -204,8 +216,15 @@ func TestContext_IsSet_fromEnv(t *testing.T) {
a.Run([]string{"run"}) a.Run([]string{"run"})
expect(t, timeoutIsSet, true) expect(t, timeoutIsSet, true)
expect(t, tIsSet, true) expect(t, tIsSet, true)
expect(t, passwordIsSet, true)
expect(t, pIsSet, true)
expect(t, noEnvVarIsSet, false) expect(t, noEnvVarIsSet, false)
expect(t, nIsSet, false) expect(t, nIsSet, false)
os.Setenv("APP_UNPARSABLE", "foobar")
a.Run([]string{"run"})
expect(t, unparsableIsSet, false)
expect(t, uIsSet, false)
} }
func TestContext_GlobalIsSet(t *testing.T) { func TestContext_GlobalIsSet(t *testing.T) {
@ -230,14 +249,22 @@ func TestContext_GlobalIsSet(t *testing.T) {
// XXX Corresponds to hack in context.IsSet for flags with EnvVar field // XXX Corresponds to hack in context.IsSet for flags with EnvVar field
// Should be moved to `flag_test` in v2 // Should be moved to `flag_test` in v2
func TestContext_GlobalIsSet_fromEnv(t *testing.T) { func TestContext_GlobalIsSet_fromEnv(t *testing.T) {
var timeoutIsSet, tIsSet, noEnvVarIsSet, nIsSet bool var (
timeoutIsSet, tIsSet bool
noEnvVarIsSet, nIsSet bool
passwordIsSet, pIsSet bool
unparsableIsSet, uIsSet bool
)
os.Clearenv() clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "15.5") os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
os.Setenv("APP_PASSWORD", "")
a := App{ a := App{
Flags: []Flag{ Flags: []Flag{
Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
StringFlag{Name: "password, p", EnvVar: "APP_PASSWORD"},
Float64Flag{Name: "no-env-var, n"}, Float64Flag{Name: "no-env-var, n"},
Float64Flag{Name: "unparsable, u", EnvVar: "APP_UNPARSABLE"},
}, },
Commands: []Command{ Commands: []Command{
{ {
@ -245,6 +272,10 @@ func TestContext_GlobalIsSet_fromEnv(t *testing.T) {
Action: func(ctx *Context) error { Action: func(ctx *Context) error {
timeoutIsSet = ctx.GlobalIsSet("timeout") timeoutIsSet = ctx.GlobalIsSet("timeout")
tIsSet = ctx.GlobalIsSet("t") tIsSet = ctx.GlobalIsSet("t")
passwordIsSet = ctx.GlobalIsSet("password")
pIsSet = ctx.GlobalIsSet("p")
unparsableIsSet = ctx.GlobalIsSet("unparsable")
uIsSet = ctx.GlobalIsSet("u")
noEnvVarIsSet = ctx.GlobalIsSet("no-env-var") noEnvVarIsSet = ctx.GlobalIsSet("no-env-var")
nIsSet = ctx.GlobalIsSet("n") nIsSet = ctx.GlobalIsSet("n")
return nil return nil
@ -252,11 +283,22 @@ func TestContext_GlobalIsSet_fromEnv(t *testing.T) {
}, },
}, },
} }
a.Run([]string{"run", "hello"}) if err := a.Run([]string{"run", "hello"}); err != nil {
t.Logf("error running Run(): %+v", err)
}
expect(t, timeoutIsSet, true) expect(t, timeoutIsSet, true)
expect(t, tIsSet, true) expect(t, tIsSet, true)
expect(t, passwordIsSet, true)
expect(t, pIsSet, true)
expect(t, noEnvVarIsSet, false) expect(t, noEnvVarIsSet, false)
expect(t, nIsSet, false) expect(t, nIsSet, false)
os.Setenv("APP_UNPARSABLE", "foobar")
if err := a.Run([]string{"run"}); err != nil {
t.Logf("error running Run(): %+v", err)
}
expect(t, unparsableIsSet, false)
expect(t, uIsSet, false)
} }
func TestContext_NumFlags(t *testing.T) { func TestContext_NumFlags(t *testing.T) {

View File

@ -24,7 +24,7 @@ func NewMultiError(err ...error) MultiError {
return MultiError{Errors: err} return MultiError{Errors: err}
} }
// Error implents the error interface. // Error implements the error interface.
func (m MultiError) Error() string { func (m MultiError) Error() string {
errs := make([]string, len(m.Errors)) errs := make([]string, len(m.Errors))
for i, err := range m.Errors { for i, err := range m.Errors {
@ -34,6 +34,10 @@ func (m MultiError) Error() string {
return strings.Join(errs, "\n") return strings.Join(errs, "\n")
} }
type ErrorFormatter interface {
Format(s fmt.State, verb rune)
}
// ExitCoder is the interface checked by `App` and `Command` for a custom exit // ExitCoder is the interface checked by `App` and `Command` for a custom exit
// code // code
type ExitCoder interface { type ExitCoder interface {
@ -44,11 +48,11 @@ type ExitCoder interface {
// ExitError fulfills both the builtin `error` interface and `ExitCoder` // ExitError fulfills both the builtin `error` interface and `ExitCoder`
type ExitError struct { type ExitError struct {
exitCode int exitCode int
message string message interface{}
} }
// NewExitError makes a new *ExitError // NewExitError makes a new *ExitError
func NewExitError(message string, exitCode int) *ExitError { func NewExitError(message interface{}, exitCode int) *ExitError {
return &ExitError{ return &ExitError{
exitCode: exitCode, exitCode: exitCode,
message: message, message: message,
@ -58,7 +62,7 @@ func NewExitError(message string, exitCode int) *ExitError {
// Error returns the string message, fulfilling the interface required by // Error returns the string message, fulfilling the interface required by
// `error` // `error`
func (ee *ExitError) Error() string { func (ee *ExitError) Error() string {
return ee.message return fmt.Sprintf("%v", ee.message)
} }
// ExitCode returns the exit code, fulfilling the interface required by // ExitCode returns the exit code, fulfilling the interface required by
@ -78,7 +82,11 @@ func HandleExitCoder(err error) {
if exitErr, ok := err.(ExitCoder); ok { if exitErr, ok := err.(ExitCoder); ok {
if err.Error() != "" { if err.Error() != "" {
fmt.Fprintln(ErrWriter, err) if _, ok := exitErr.(ErrorFormatter); ok {
fmt.Fprintf(ErrWriter, "%+v\n", err)
} else {
fmt.Fprintln(ErrWriter, err)
}
} }
OsExiter(exitErr.ExitCode()) OsExiter(exitErr.ExitCode())
return return
@ -92,7 +100,11 @@ func HandleExitCoder(err error) {
} }
if err.Error() != "" { if err.Error() != "" {
fmt.Fprintln(ErrWriter, err) if _, ok := err.(ErrorFormatter); ok {
fmt.Fprintf(ErrWriter, "%+v\n", err)
} else {
fmt.Fprintln(ErrWriter, err)
}
} }
OsExiter(1) OsExiter(1)
} }

View File

@ -3,6 +3,7 @@ package cli
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"testing" "testing"
) )
@ -104,3 +105,53 @@ func TestHandleExitCoder_ErrorWithoutMessage(t *testing.T) {
expect(t, called, true) expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "") expect(t, ErrWriter.(*bytes.Buffer).String(), "")
} }
// make a stub to not import pkg/errors
type ErrorWithFormat struct {
error
}
func NewErrorWithFormat(m string) *ErrorWithFormat {
return &ErrorWithFormat{error: errors.New(m)}
}
func (f *ErrorWithFormat) Format(s fmt.State, verb rune) {
fmt.Fprintf(s, "This the format: %v", f.error)
}
func TestHandleExitCoder_ErrorWithFormat(t *testing.T) {
called := false
OsExiter = func(rc int) {
called = true
}
ErrWriter = &bytes.Buffer{}
defer func() {
OsExiter = fakeOsExiter
ErrWriter = fakeErrWriter
}()
err := NewErrorWithFormat("I am formatted")
HandleExitCoder(err)
expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "This the format: I am formatted\n")
}
func TestHandleExitCoder_MultiErrorWithFormat(t *testing.T) {
called := false
OsExiter = func(rc int) {
called = true
}
ErrWriter = &bytes.Buffer{}
defer func() { OsExiter = fakeOsExiter }()
err := NewMultiError(NewErrorWithFormat("err1"), NewErrorWithFormat("err2"))
HandleExitCoder(err)
expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "This the format: err1\nThis the format: err2\n")
}

280
flag.go
View File

@ -3,11 +3,11 @@ package cli
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
"reflect" "reflect"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
) )
@ -37,6 +37,21 @@ var HelpFlag = BoolFlag{
// to display a flag. // to display a flag.
var FlagStringer FlagStringFunc = stringifyFlag var FlagStringer FlagStringFunc = stringifyFlag
// FlagsByName is a slice of Flag.
type FlagsByName []Flag
func (f FlagsByName) Len() int {
return len(f)
}
func (f FlagsByName) Less(i, j int) bool {
return f[i].GetName() < f[j].GetName()
}
func (f FlagsByName) Swap(i, j int) {
f[i], f[j] = f[j], f[i]
}
// Flag is a common interface related to parsing flags in cli. // Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recommended that // For more advanced flag parsing techniques, it is recommended that
// this interface be implemented. // this interface be implemented.
@ -47,13 +62,29 @@ type Flag interface {
GetName() string GetName() string
} }
func flagSet(name string, flags []Flag) *flag.FlagSet { // errorableFlag is an interface that allows us to return errors during apply
// it allows flags defined in this library to return errors in a fashion backwards compatible
// TODO remove in v2 and modify the existing Flag interface to return errors
type errorableFlag interface {
Flag
ApplyWithError(*flag.FlagSet) error
}
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
set := flag.NewFlagSet(name, flag.ContinueOnError) set := flag.NewFlagSet(name, flag.ContinueOnError)
for _, f := range flags { for _, f := range flags {
f.Apply(set) //TODO remove in v2 when errorableFlag is removed
if ef, ok := f.(errorableFlag); ok {
if err := ef.ApplyWithError(set); err != nil {
return nil, err
}
} else {
f.Apply(set)
}
} }
return set return set, nil
} }
func eachName(longName string, fn func(string)) { func eachName(longName string, fn func(string)) {
@ -72,13 +103,22 @@ type Generic interface {
// Apply takes the flagset and calls Set on the generic flag with the value // Apply takes the flagset and calls Set on the generic flag with the value
// provided by the user for parsing by the flag // provided by the user for parsing by the flag
// Ignores parsing errors
func (f GenericFlag) Apply(set *flag.FlagSet) { func (f GenericFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError takes the flagset and calls Set on the generic flag with the value
// provided by the user for parsing by the flag
func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error {
val := f.Value val := f.Value
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
val.Set(envVal) 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 break
} }
} }
@ -87,9 +127,11 @@ func (f GenericFlag) Apply(set *flag.FlagSet) {
eachName(f.Name, func(name string) { eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage) set.Var(f.Value, name, f.Usage)
}) })
return nil
} }
// StringSlice is an opaque type for []string to satisfy flag.Value // StringSlice is an opaque type for []string to satisfy flag.Value and flag.Getter
type StringSlice []string type StringSlice []string
// Set appends the string value to the list of values // Set appends the string value to the list of values
@ -108,16 +150,29 @@ func (f *StringSlice) Value() []string {
return *f return *f
} }
// Get returns the slice of strings set by this flag
func (f *StringSlice) Get() interface{} {
return *f
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f StringSliceFlag) Apply(set *flag.FlagSet) { func (f StringSliceFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
newVal := &StringSlice{} newVal := &StringSlice{}
for _, s := range strings.Split(envVal, ",") { for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
newVal.Set(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 f.Value = newVal
break break
@ -131,9 +186,11 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) {
} }
set.Var(f.Value, name, f.Usage) set.Var(f.Value, name, f.Usage)
}) })
return nil
} }
// IntSlice is an opaque type for []int to satisfy flag.Value // IntSlice is an opaque type for []int to satisfy flag.Value and flag.Getter
type IntSlice []int type IntSlice []int
// Set parses the value into an integer and appends it to the list of values // Set parses the value into an integer and appends it to the list of values
@ -156,18 +213,28 @@ func (f *IntSlice) Value() []int {
return *f return *f
} }
// Get returns the slice of ints set by this flag
func (f *IntSlice) Get() interface{} {
return *f
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f IntSliceFlag) Apply(set *flag.FlagSet) { func (f IntSliceFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
newVal := &IntSlice{} newVal := &IntSlice{}
for _, s := range strings.Split(envVal, ",") { for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
err := newVal.Set(s) if err := newVal.Set(s); err != nil {
if err != nil { return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err)
fmt.Fprintf(ErrWriter, err.Error())
} }
} }
f.Value = newVal f.Value = newVal
@ -182,9 +249,11 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) {
} }
set.Var(f.Value, name, f.Usage) set.Var(f.Value, name, f.Usage)
}) })
return nil
} }
// Int64Slice is an opaque type for []int to satisfy flag.Value // Int64Slice is an opaque type for []int to satisfy flag.Value and flag.Getter
type Int64Slice []int64 type Int64Slice []int64
// Set parses the value into an integer and appends it to the list of values // Set parses the value into an integer and appends it to the list of values
@ -207,18 +276,28 @@ func (f *Int64Slice) Value() []int64 {
return *f return *f
} }
// Get returns the slice of ints set by this flag
func (f *Int64Slice) Get() interface{} {
return *f
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f Int64SliceFlag) Apply(set *flag.FlagSet) { func (f Int64SliceFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
newVal := &Int64Slice{} newVal := &Int64Slice{}
for _, s := range strings.Split(envVal, ",") { for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
err := newVal.Set(s) if err := newVal.Set(s); err != nil {
if err != nil { return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err)
fmt.Fprintf(ErrWriter, err.Error())
} }
} }
f.Value = newVal f.Value = newVal
@ -233,19 +312,33 @@ func (f Int64SliceFlag) Apply(set *flag.FlagSet) {
} }
set.Var(f.Value, name, f.Usage) set.Var(f.Value, name, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f BoolFlag) Apply(set *flag.FlagSet) { func (f BoolFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error {
val := false val := false
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
envValBool, err := strconv.ParseBool(envVal) if envVal == "" {
if err == nil { val = false
val = envValBool 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 break
} }
} }
@ -258,20 +351,35 @@ func (f BoolFlag) Apply(set *flag.FlagSet) {
} }
set.Bool(name, val, f.Usage) set.Bool(name, val, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f BoolTFlag) Apply(set *flag.FlagSet) { func (f BoolTFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error {
val := true val := true
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
envValBool, err := strconv.ParseBool(envVal) if envVal == "" {
if err == nil { val = false
val = envValBool
break 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
} }
} }
} }
@ -283,14 +391,22 @@ func (f BoolTFlag) Apply(set *flag.FlagSet) {
} }
set.Bool(name, val, f.Usage) set.Bool(name, val, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f StringFlag) Apply(set *flag.FlagSet) { func (f StringFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f StringFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
f.Value = envVal f.Value = envVal
break break
} }
@ -304,19 +420,28 @@ func (f StringFlag) Apply(set *flag.FlagSet) {
} }
set.String(name, f.Value, f.Usage) set.String(name, f.Value, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f IntFlag) Apply(set *flag.FlagSet) { func (f IntFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f IntFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
envValInt, err := strconv.ParseInt(envVal, 0, 64) envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err == nil { if err != nil {
f.Value = int(envValInt) return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
break
} }
f.Value = int(envValInt)
break
} }
} }
} }
@ -328,19 +453,29 @@ func (f IntFlag) Apply(set *flag.FlagSet) {
} }
set.Int(name, f.Value, f.Usage) set.Int(name, f.Value, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f Int64Flag) Apply(set *flag.FlagSet) { func (f Int64Flag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
envValInt, err := strconv.ParseInt(envVal, 0, 64) envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err == nil { if err != nil {
f.Value = envValInt return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
break
} }
f.Value = envValInt
break
} }
} }
} }
@ -352,19 +487,29 @@ func (f Int64Flag) Apply(set *flag.FlagSet) {
} }
set.Int64(name, f.Value, f.Usage) set.Int64(name, f.Value, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f UintFlag) Apply(set *flag.FlagSet) { func (f UintFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f UintFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
envValInt, err := strconv.ParseUint(envVal, 0, 64) envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err == nil { if err != nil {
f.Value = uint(envValInt) return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err)
break
} }
f.Value = uint(envValInt)
break
} }
} }
} }
@ -376,19 +521,29 @@ func (f UintFlag) Apply(set *flag.FlagSet) {
} }
set.Uint(name, f.Value, f.Usage) set.Uint(name, f.Value, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f Uint64Flag) Apply(set *flag.FlagSet) { func (f Uint64Flag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
envValInt, err := strconv.ParseUint(envVal, 0, 64) envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err == nil { if err != nil {
f.Value = uint64(envValInt) return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err)
break
} }
f.Value = uint64(envValInt)
break
} }
} }
} }
@ -400,19 +555,29 @@ func (f Uint64Flag) Apply(set *flag.FlagSet) {
} }
set.Uint64(name, f.Value, f.Usage) set.Uint64(name, f.Value, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f DurationFlag) Apply(set *flag.FlagSet) { func (f DurationFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
envValDuration, err := time.ParseDuration(envVal) envValDuration, err := time.ParseDuration(envVal)
if err == nil { if err != nil {
f.Value = envValDuration return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err)
break
} }
f.Value = envValDuration
break
} }
} }
} }
@ -424,18 +589,29 @@ func (f DurationFlag) Apply(set *flag.FlagSet) {
} }
set.Duration(name, f.Value, f.Usage) set.Duration(name, f.Value, f.Usage)
}) })
return nil
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
// Ignores errors
func (f Float64Flag) Apply(set *flag.FlagSet) { func (f Float64Flag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVar != "" { if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") { for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" { if envVal, ok := syscall.Getenv(envVar); ok {
envValFloat, err := strconv.ParseFloat(envVal, 10) envValFloat, err := strconv.ParseFloat(envVal, 10)
if err == nil { if err != nil {
f.Value = float64(envValFloat) return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err)
} }
f.Value = float64(envValFloat)
break
} }
} }
} }
@ -447,6 +623,8 @@ func (f Float64Flag) Apply(set *flag.FlagSet) {
} }
set.Float64(name, f.Value, f.Usage) set.Float64(name, f.Value, f.Usage)
}) })
return nil
} }
func visibleFlags(fl []Flag) []Flag { func visibleFlags(fl []Flag) []Flag {

View File

@ -29,6 +29,81 @@ func TestBoolFlagHelpOutput(t *testing.T) {
} }
} }
func TestFlagsFromEnv(t *testing.T) {
var flagTests = []struct {
input string
output interface{}
flag Flag
err error
}{
{"", false, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, nil},
{"1", true, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, nil},
{"false", false, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, nil},
{"foobar", true, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, fmt.Errorf(`could not parse foobar as bool value for flag debug: strconv.ParseBool: parsing "foobar": invalid syntax`)},
{"", false, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, nil},
{"1", true, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, nil},
{"false", false, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, nil},
{"foobar", true, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, fmt.Errorf(`could not parse foobar as bool value for flag debug: strconv.ParseBool: parsing "foobar": invalid syntax`)},
{"1s", 1 * time.Second, DurationFlag{Name: "time", EnvVar: "TIME"}, nil},
{"foobar", false, DurationFlag{Name: "time", EnvVar: "TIME"}, fmt.Errorf(`could not parse foobar as duration for flag time: time: invalid duration foobar`)},
{"1.2", 1.2, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, nil},
{"1", 1.0, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, nil},
{"foobar", 0, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as float64 value for flag seconds: strconv.ParseFloat: parsing "foobar": invalid syntax`)},
{"1", int64(1), Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, nil},
{"1.2", 0, Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2 as int value for flag seconds: strconv.ParseInt: parsing "1.2": invalid syntax`)},
{"foobar", 0, Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as int value for flag seconds: strconv.ParseInt: parsing "foobar": invalid syntax`)},
{"1", 1, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, nil},
{"1.2", 0, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2 as int value for flag seconds: strconv.ParseInt: parsing "1.2": invalid syntax`)},
{"foobar", 0, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as int value for flag seconds: strconv.ParseInt: parsing "foobar": invalid syntax`)},
{"1,2", IntSlice{1, 2}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, nil},
{"1.2,2", IntSlice{}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2,2 as int slice value for flag seconds: strconv.ParseInt: parsing "1.2": invalid syntax`)},
{"foobar", IntSlice{}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as int slice value for flag seconds: strconv.ParseInt: parsing "foobar": invalid syntax`)},
{"1,2", Int64Slice{1, 2}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, nil},
{"1.2,2", Int64Slice{}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2,2 as int64 slice value for flag seconds: strconv.ParseInt: parsing "1.2": invalid syntax`)},
{"foobar", Int64Slice{}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as int64 slice value for flag seconds: strconv.ParseInt: parsing "foobar": invalid syntax`)},
{"foo", "foo", StringFlag{Name: "name", EnvVar: "NAME"}, nil},
{"foo,bar", StringSlice{"foo", "bar"}, StringSliceFlag{Name: "names", EnvVar: "NAMES"}, nil},
{"1", uint(1), UintFlag{Name: "seconds", EnvVar: "SECONDS"}, nil},
{"1.2", 0, UintFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2 as uint value for flag seconds: strconv.ParseUint: parsing "1.2": invalid syntax`)},
{"foobar", 0, UintFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as uint value for flag seconds: strconv.ParseUint: parsing "foobar": invalid syntax`)},
{"1", uint64(1), Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, nil},
{"1.2", 0, Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2 as uint64 value for flag seconds: strconv.ParseUint: parsing "1.2": invalid syntax`)},
{"foobar", 0, Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as uint64 value for flag seconds: strconv.ParseUint: parsing "foobar": invalid syntax`)},
{"foo,bar", &Parser{"foo", "bar"}, GenericFlag{Name: "names", Value: &Parser{}, EnvVar: "NAMES"}, nil},
}
for _, test := range flagTests {
os.Clearenv()
os.Setenv(reflect.ValueOf(test.flag).FieldByName("EnvVar").String(), test.input)
a := App{
Flags: []Flag{test.flag},
Action: func(ctx *Context) error {
if !reflect.DeepEqual(ctx.value(test.flag.GetName()), test.output) {
t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, ctx.value(test.flag.GetName()))
}
return nil
},
}
err := a.Run([]string{"run"})
if !reflect.DeepEqual(test.err, err) {
t.Errorf("expected error %s, got error %s", test.err, err)
}
}
}
var stringFlagTests = []struct { var stringFlagTests = []struct {
name string name string
usage string usage string
@ -941,6 +1016,38 @@ func TestParseMultiBoolFromEnvCascade(t *testing.T) {
a.Run([]string{"run"}) a.Run([]string{"run"})
} }
func TestParseBoolTFromEnv(t *testing.T) {
var boolTFlagTests = []struct {
input string
output bool
}{
{"", false},
{"1", true},
{"false", false},
{"true", true},
}
for _, test := range boolTFlagTests {
os.Clearenv()
os.Setenv("DEBUG", test.input)
a := App{
Flags: []Flag{
BoolTFlag{Name: "debug, d", EnvVar: "DEBUG"},
},
Action: func(ctx *Context) error {
if ctx.Bool("debug") != test.output {
t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, ctx.Bool("debug"))
}
if ctx.Bool("d") != test.output {
t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, ctx.Bool("d"))
}
return nil
},
}
a.Run([]string{"run"})
}
}
func TestParseMultiBoolT(t *testing.T) { func TestParseMultiBoolT(t *testing.T) {
a := App{ a := App{
Flags: []Flag{ Flags: []Flag{
@ -1036,6 +1143,10 @@ func (p *Parser) String() string {
return fmt.Sprintf("%s,%s", p[0], p[1]) return fmt.Sprintf("%s,%s", p[0], p[1])
} }
func (p *Parser) Get() interface{} {
return p
}
func TestParseGeneric(t *testing.T) { func TestParseGeneric(t *testing.T) {
a := App{ a := App{
Flags: []Flag{ Flags: []Flag{

View File

@ -202,6 +202,10 @@ def _write_altsrc_flag_types(outfile, types):
_fwrite(outfile, """\ _fwrite(outfile, """\
package altsrc package altsrc
import (
"gopkg.in/urfave/cli.v1"
)
// WARNING: This file is generated! // WARNING: This file is generated!
""") """)
@ -228,6 +232,13 @@ def _write_altsrc_flag_types(outfile, types):
f.set = set f.set = set
f.{name}Flag.Apply(set) f.{name}Flag.Apply(set)
}} }}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped {name}Flag.ApplyWithError
func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{
f.set = set
return f.{name}Flag.ApplyWithError(set)
}}
""".format(**typedef)) """.format(**typedef))

73
help.go
View File

@ -13,27 +13,31 @@ import (
// cli.go uses text/template to render templates. You can // cli.go uses text/template to render templates. You can
// render custom help text by setting this variable. // render custom help text by setting this variable.
var AppHelpTemplate = `NAME: var AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}} {{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
USAGE: USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
{{if .Version}}{{if not .HideVersion}}
VERSION: VERSION:
{{.Version}} {{.Version}}{{end}}{{end}}{{if .Description}}
{{end}}{{end}}{{if len .Authors}}
AUTHOR(S): DESCRIPTION:
{{range .Authors}}{{.}}{{end}} {{.Description}}{{end}}{{if len .Authors}}
{{end}}{{if .VisibleCommands}}
AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
{{range $index, $author := .Authors}}{{if $index}}
{{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}} {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
{{end}}{{end}}{{if .VisibleFlags}}
GLOBAL OPTIONS: GLOBAL OPTIONS:
{{range .VisibleFlags}}{{.}} {{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{end}}{{if .Copyright}} {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}}
COPYRIGHT: COPYRIGHT:
{{.Copyright}} {{.Copyright}}{{end}}
{{end}}
` `
// CommandHelpTemplate is the text template for the command help topic. // CommandHelpTemplate is the text template for the command help topic.
@ -248,20 +252,43 @@ func checkSubcommandHelp(c *Context) bool {
return false return false
} }
func checkCompletions(c *Context) bool { func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) {
if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion { if !a.EnableBashCompletion {
ShowCompletions(c) return false, arguments
return true
} }
return false pos := len(arguments) - 1
lastArg := arguments[pos]
if lastArg != "--"+BashCompletionFlag.Name {
return false, arguments
}
return true, arguments[:pos]
}
func checkCompletions(c *Context) bool {
if !c.shellComplete {
return false
}
if args := c.Args(); args.Present() {
name := args.First()
if cmd := c.App.Command(name); cmd != nil {
// let the command handle the completion
return false
}
}
ShowCompletions(c)
return true
} }
func checkCommandCompletions(c *Context, name string) bool { func checkCommandCompletions(c *Context, name string) bool {
if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { if !c.shellComplete {
ShowCommandCompletions(c, name) return false
return true
} }
return false ShowCommandCompletions(c, name)
return true
} }

9
helpers_unix_test.go Normal file
View File

@ -0,0 +1,9 @@
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
package cli
import "os"
func clearenv() {
os.Clearenv()
}

20
helpers_windows_test.go Normal file
View File

@ -0,0 +1,20 @@
package cli
import (
"os"
"syscall"
)
// os.Clearenv() doesn't actually unset variables on Windows
// See: https://github.com/golang/go/issues/17902
func clearenv() {
for _, s := range os.Environ() {
for j := 1; j < len(s); j++ {
if s[j] == '=' {
keyp, _ := syscall.UTF16PtrFromString(s[0:j])
syscall.SetEnvironmentVariable(keyp, nil)
break
}
}
}
}