Merge pull request #655 from urfave/merging-from-v1

Merging from v1
This commit is contained in:
Dan Buch 2017-08-14 09:43:17 -04:00 committed by GitHub
commit 50a0fdeea6
30 changed files with 1435 additions and 405 deletions

View File

@ -1,35 +1,23 @@
language: go
sudo: false
dist: trusty
osx_image: xcode8.3
go: 1.8.x
os:
- linux
- osx
cache:
directories:
- node_modules
go:
- 1.2.x
- 1.3.x
- 1.4.2
- 1.5.x
- 1.6.x
- 1.7.x
- master
env: pip_install="pip install --user"
matrix:
allow_failures:
- go: master
include:
- go: 1.6.x
os: osx
env: pip_install="sudo pip install"
- go: 1.7.x
os: osx
env: pip_install="sudo pip install"
before_script:
- $pip_install flake8
- if [[ $(uname) == Darwin ]]; then
sudo pip install flake8;
else
pip install --user flake8;
fi
- mkdir -p ${GOPATH%%:*}/src/gopkg.in/urfave
- rm -rvf ${GOPATH%%:*}/src/gopkg.in/urfave/cli.v2
- rm -rvf ${GOPATH%%:*}/pkg/*/gopkg.in/urfave/cli.v2.a

View File

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

View File

@ -461,13 +461,13 @@ error.
Flags for the application and commands are shown in the order they are defined.
However, it's possible to sort them from outside this library by using `FlagsByName`
with `sort`.
or `CommandsByName` with `sort`.
For example this:
<!-- {
"args": ["&#45;&#45;help"],
"output": "Load configuration from FILE\n.*Language for the greeting.*"
"output": "add a task to the list\n.*complete a task on the list\n.*\n\n.*\n.*Load configuration from FILE\n.*Language for the greeting.*"
} -->
``` go
package main
@ -492,9 +492,28 @@ func main() {
Usage: "Load configuration from `FILE`",
},
},
Commands: []*cli.Command{
{
Name: "complete",
Aliases: []string{"c"},
Usage: "complete a task on the list",
Action: func(c *cli.Context) error {
return nil
},
},
{
Name: "add",
Aliases: []string{"a"},
Usage: "add a task to the list",
Action: func(c *cli.Context) error {
return nil
},
},
},
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
app.Run(os.Args)
}
@ -1001,16 +1020,13 @@ SUPPORT: support@awesometown.example.com
cli.AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command
[command options]{{end}} {{if
.ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
{{if len .Authors}}
AUTHOR(S):
AUTHOR:
{{range .Authors}}{{ . }}{{end}}
{{end}}{{if .Commands}}
COMMANDS:
{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"
}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
GLOBAL OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}{{if .Copyright }}

View File

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

View File

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

View File

@ -63,7 +63,7 @@ func TestStringSliceApplyInputSourceValue(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []string{"hello", "world"},
MapValue: []interface{}{"hello", "world"},
})
expect(t, c.StringSlice("test"), []string{"hello", "world"})
}
@ -72,7 +72,7 @@ func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []string{"hello", "world"},
MapValue: []interface{}{"hello", "world"},
ContextValueString: "ohno",
})
expect(t, c.StringSlice("test"), []string{"ohno"})
@ -82,7 +82,7 @@ func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: []string{"hello", "world"},
MapValue: []interface{}{"hello", "world"},
EnvVarName: "TEST",
EnvVarValue: "oh,no",
})
@ -93,7 +93,7 @@ func TestIntSliceApplyInputSourceValue(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []int{1, 2},
MapValue: []interface{}{1, 2},
})
expect(t, c.IntSlice("test"), []int{1, 2})
}
@ -102,7 +102,7 @@ func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []int{1, 2},
MapValue: []interface{}{1, 2},
ContextValueString: "3",
})
expect(t, c.IntSlice("test"), []int{3})
@ -112,7 +112,7 @@ func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: []int{1, 2},
MapValue: []interface{}{1, 2},
EnvVarName: "TEST",
EnvVarValue: "3,4",
})

View File

@ -130,45 +130,59 @@ func (fsm *MapInputSource) String(name string) (string, error) {
// StringSlice returns an []string from the map if it exists otherwise returns nil
func (fsm *MapInputSource) StringSlice(name string) ([]string, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.([]string)
if !isType {
return nil, incorrectTypeForFlagError(name, "[]string", otherGenericValue)
if !exists {
otherGenericValue, exists = nestedVal(name, fsm.valueMap)
if !exists {
return nil, nil
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.([]string)
if !isType {
return nil, incorrectTypeForFlagError(name, "[]string", nestedGenericValue)
}
return otherValue, nil
}
return nil, nil
otherValue, isType := otherGenericValue.([]interface{})
if !isType {
return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue)
}
var stringSlice = make([]string, 0, len(otherValue))
for i, v := range otherValue {
stringValue, isType := v.(string)
if !isType {
return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "string", v)
}
stringSlice = append(stringSlice, stringValue)
}
return stringSlice, nil
}
// IntSlice returns an []int from the map if it exists otherwise returns nil
func (fsm *MapInputSource) IntSlice(name string) ([]int, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.([]int)
if !isType {
return nil, incorrectTypeForFlagError(name, "[]int", otherGenericValue)
if !exists {
otherGenericValue, exists = nestedVal(name, fsm.valueMap)
if !exists {
return nil, nil
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.([]int)
if !isType {
return nil, incorrectTypeForFlagError(name, "[]int", nestedGenericValue)
}
return otherValue, nil
}
return nil, nil
otherValue, isType := otherGenericValue.([]interface{})
if !isType {
return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue)
}
var intSlice = make([]int, 0, len(otherValue))
for i, v := range otherValue {
intValue, isType := v.(int)
if !isType {
return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v)
}
intSlice = append(intSlice, intValue)
}
return intSlice, nil
}
// Generic returns an cli.Generic from the map if it exists otherwise returns nil

View File

@ -57,8 +57,8 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) {
} else {
return nil, err
}
case reflect.Array:
fallthrough // [todo] - Support array type
case reflect.Array, reflect.Slice:
ret[key] = val.([]interface{})
default:
return nil, fmt.Errorf("Unsupported: type = %#v", v.Kind())
}

View File

@ -11,6 +11,8 @@ import (
"net/http"
"net/url"
"os"
"runtime"
"strings"
"gopkg.in/urfave/cli.v2"
@ -78,6 +80,12 @@ func loadDataFrom(filePath string) ([]byte, error) {
return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath)
}
return ioutil.ReadFile(filePath)
} else if runtime.GOOS == "windows" && strings.Contains(u.String(), "\\") {
// on Windows systems u.Path is always empty, so we need to check the string directly.
if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil {
return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath)
}
return ioutil.ReadFile(filePath)
} else {
return nil, fmt.Errorf("unable to determine how to load from path %s", filePath)
}

36
app.go
View File

@ -65,6 +65,12 @@ type App struct {
ErrWriter io.Writer
// Other custom info
Metadata map[string]interface{}
// Carries a function which returns app specific info.
ExtraInfo func() map[string]string
// CustomAppHelpTemplate the text template for app help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
CustomAppHelpTemplate string
didSetup bool
}
@ -156,6 +162,10 @@ func (a *App) Setup() {
if a.Metadata == nil {
a.Metadata = make(map[string]interface{})
}
if a.Writer == nil {
a.Writer = os.Stdout
}
}
// Run is the entry point to the cli app. Parses the arguments slice and routes
@ -163,8 +173,20 @@ func (a *App) Setup() {
func (a *App) Run(arguments []string) (err error) {
a.Setup()
// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
// flag name as the value of the flag before it which is undesirable
// note that we can only do this because the shell autocomplete function
// always appends the completion flag at the end of the command
shellComplete, arguments := checkShellCompleteFlag(a, arguments)
// parse flags
set := flagSet(a.Name, a.Flags)
set, err := flagSet(a.Name, a.Flags)
if err != nil {
return err
}
set.SetOutput(ioutil.Discard)
err = set.Parse(arguments[1:])
nerr := normalizeFlags(a.Flags, set)
@ -174,6 +196,7 @@ func (a *App) Run(arguments []string) (err error) {
ShowAppHelp(context)
return nerr
}
context.shellComplete = shellComplete
if checkCompletions(context) {
return nil
@ -223,7 +246,6 @@ func (a *App) Run(arguments []string) (err error) {
if a.Before != nil {
beforeErr := a.Before(context)
if beforeErr != nil {
fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
ShowAppHelp(context)
HandleExitCoder(beforeErr)
err = beforeErr
@ -240,6 +262,10 @@ func (a *App) Run(arguments []string) (err error) {
}
}
if a.Action == nil {
a.Action = helpCommand.Action
}
// Run default Action
err = a.Action(context)
@ -278,7 +304,11 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
}
// parse flags
set := flagSet(a.Name, a.Flags)
set, err := flagSet(a.Name, a.Flags)
if err != nil {
return err
}
set.SetOutput(ioutil.Discard)
err = set.Parse(ctx.Args().Tail())
nerr := normalizeFlags(a.Flags, set)

View File

@ -181,9 +181,59 @@ func ExampleApp_Run_commandHelp() {
// This is how we describe describeit the function
}
func ExampleApp_Run_noAction() {
app := App{}
app.Name = "greet"
app.Run([]string{"greet"})
// Output:
// NAME:
// greet - A new cli application
//
// USAGE:
// greet [global options] command [command options] [arguments...]
//
// VERSION:
// 0.0.0
//
// COMMANDS:
// help, h Shows a list of commands or help for one command
//
// GLOBAL OPTIONS:
// --help, -h show help (default: false)
// --version, -v print the version (default: false)
}
func ExampleApp_Run_subcommandNoAction() {
app := &App{
Name: "greet",
Commands: []*Command{
{
Name: "describeit",
Aliases: []string{"d"},
Usage: "use it to see a description",
Description: "This is how we describe describeit the function",
},
},
}
app.Run([]string{"greet", "describeit"})
// Output:
// NAME:
// greet describeit - use it to see a description
//
// USAGE:
// greet describeit [command options] [arguments...]
//
// DESCRIPTION:
// This is how we describe describeit the function
//
// OPTIONS:
// --help, -h show help (default: false)
}
func ExampleApp_Run_shellComplete() {
// set args for examples sake
os.Args = []string{"greet", "--generate-completion"}
os.Args = []string{"greet", fmt.Sprintf("--%s", genCompName())}
app := &App{
Name: "greet",
@ -249,12 +299,11 @@ var commandAppTests = []struct {
}
func TestApp_Command(t *testing.T) {
app := &App{}
fooCommand := &Command{Name: "foobar", Aliases: []string{"f"}}
batCommand := &Command{Name: "batbaz", Aliases: []string{"b"}}
app.Commands = []*Command{
fooCommand,
batCommand,
app := &App{
Commands: []*Command{
{Name: "foobar", Aliases: []string{"f"}},
{Name: "batbaz", Aliases: []string{"b"}},
},
}
for _, test := range commandAppTests {
@ -262,6 +311,12 @@ func TestApp_Command(t *testing.T) {
}
}
func TestApp_Setup_defaultsWriter(t *testing.T) {
app := &App{}
app.Setup()
expect(t, app.Writer, os.Stdout)
}
func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
var context *Context
@ -312,19 +367,21 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {
var parsedOption string
var args Args
app := &App{}
command := &Command{
Name: "cmd",
Flags: []Flag{
&StringFlag{Name: "option", Value: "", Usage: "some option"},
},
Action: func(c *Context) error {
parsedOption = c.String("option")
args = c.Args()
return nil
app := &App{
Commands: []*Command{
{
Name: "cmd",
Flags: []Flag{
&StringFlag{Name: "option", Value: "", Usage: "some option"},
},
Action: func(c *Context) error {
parsedOption = c.String("option")
args = c.Args()
return nil
},
},
},
}
app.Commands = []*Command{command}
app.Run([]string{"", "cmd", "--option", "my-option", "my-arg", "--", "--notARealFlag"})
@ -337,15 +394,17 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {
func TestApp_CommandWithDash(t *testing.T) {
var args Args
app := &App{}
command := &Command{
Name: "cmd",
Action: func(c *Context) error {
args = c.Args()
return nil
app := &App{
Commands: []*Command{
{
Name: "cmd",
Action: func(c *Context) error {
args = c.Args()
return nil
},
},
},
}
app.Commands = []*Command{command}
app.Run([]string{"", "cmd", "my-arg", "-"})
@ -356,15 +415,17 @@ func TestApp_CommandWithDash(t *testing.T) {
func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) {
var args Args
app := &App{}
command := &Command{
Name: "cmd",
Action: func(c *Context) error {
args = c.Args()
return nil
app := &App{
Commands: []*Command{
{
Name: "cmd",
Action: func(c *Context) error {
args = c.Args()
return nil
},
},
},
}
app.Commands = []*Command{command}
app.Run([]string{"", "cmd", "my-arg", "--", "notAFlagAtAll"})
@ -446,22 +507,24 @@ func TestApp_ParseSliceFlags(t *testing.T) {
var parsedIntSlice []int
var parsedStringSlice []string
app := &App{}
command := &Command{
Name: "cmd",
Flags: []Flag{
&IntSliceFlag{Name: "p", Value: NewIntSlice(), Usage: "set one or more ip addr"},
&StringSliceFlag{Name: "ip", Value: NewStringSlice(), Usage: "set one or more ports to open"},
},
Action: func(c *Context) error {
parsedIntSlice = c.IntSlice("p")
parsedStringSlice = c.StringSlice("ip")
parsedOption = c.String("option")
firstArg = c.Args().First()
return nil
app := &App{
Commands: []*Command{
{
Name: "cmd",
Flags: []Flag{
&IntSliceFlag{Name: "p", Value: NewIntSlice(), Usage: "set one or more ip addr"},
&StringSliceFlag{Name: "ip", Value: NewStringSlice(), Usage: "set one or more ports to open"},
},
Action: func(c *Context) error {
parsedIntSlice = c.IntSlice("p")
parsedStringSlice = c.StringSlice("ip")
parsedOption = c.String("option")
firstArg = c.Args().First()
return nil
},
},
},
}
app.Commands = []*Command{command}
app.Run([]string{"", "cmd", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4", "my-arg"})
@ -504,20 +567,22 @@ func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) {
var parsedIntSlice []int
var parsedStringSlice []string
app := &App{}
command := &Command{
Name: "cmd",
Flags: []Flag{
&IntSliceFlag{Name: "a", Usage: "set numbers"},
&StringSliceFlag{Name: "str", Usage: "set strings"},
},
Action: func(c *Context) error {
parsedIntSlice = c.IntSlice("a")
parsedStringSlice = c.StringSlice("str")
return nil
app := &App{
Commands: []*Command{
{
Name: "cmd",
Flags: []Flag{
&IntSliceFlag{Name: "a", Usage: "set numbers"},
&StringSliceFlag{Name: "str", Usage: "set strings"},
},
Action: func(c *Context) error {
parsedIntSlice = c.IntSlice("a")
parsedStringSlice = c.StringSlice("str")
return nil
},
},
},
}
app.Commands = []*Command{command}
app.Run([]string{"", "cmd", "-a", "2", "-str", "A", "my-arg"})
@ -824,6 +889,7 @@ func TestApp_OrderOfOperations(t *testing.T) {
app := &App{
EnableShellCompletion: true,
ShellComplete: func(c *Context) {
fmt.Fprintf(os.Stderr, "---> ShellComplete(%#v)\n", c)
counts.Total++
counts.ShellComplete = counts.Total
},
@ -888,7 +954,7 @@ func TestApp_OrderOfOperations(t *testing.T) {
resetCounts()
_ = app.Run([]string{"command", "--generate-completion"})
_ = app.Run([]string{"command", fmt.Sprintf("--%s", genCompName())})
expect(t, counts.ShellComplete, 1)
expect(t, counts.Total, 1)
@ -1485,3 +1551,107 @@ func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}
// A custom flag that conforms to the relevant interfaces, but has none of the
// fields that the other flag types do.
type customBoolFlag struct {
Nombre string
}
// Don't use the normal FlagStringer
func (c *customBoolFlag) String() string {
return "***" + c.Nombre + "***"
}
func (c *customBoolFlag) Names() []string {
return []string{c.Nombre}
}
func (c *customBoolFlag) Apply(set *flag.FlagSet) {
set.String(c.Nombre, c.Nombre, "")
}
func TestCustomFlagsUnused(t *testing.T) {
app := &App{
Flags: []Flag{&customBoolFlag{"custom"}},
}
err := app.Run([]string{"foo"})
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
}
func TestCustomFlagsUsed(t *testing.T) {
app := &App{
Flags: []Flag{&customBoolFlag{"custom"}},
}
err := app.Run([]string{"foo", "--custom=bar"})
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
}
func TestCustomHelpVersionFlags(t *testing.T) {
app := &App{}
// Be sure to reset the global flags
defer func(helpFlag Flag, versionFlag Flag) {
HelpFlag = helpFlag.(*BoolFlag)
VersionFlag = versionFlag.(*BoolFlag)
}(HelpFlag, VersionFlag)
HelpFlag = &customBoolFlag{"help-custom"}
VersionFlag = &customBoolFlag{"version-custom"}
err := app.Run([]string{"foo", "--help-custom=bar"})
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
}
func TestShellCompletionForIncompleteFlags(t *testing.T) {
app := &App{
Flags: []Flag{
&IntFlag{
Name: "test-completion",
},
},
EnableShellCompletion: true,
ShellComplete: func(ctx *Context) {
for _, command := range ctx.App.Commands {
if command.Hidden {
continue
}
for _, name := range command.Names() {
fmt.Fprintln(ctx.App.Writer, name)
}
}
for _, flag := range ctx.App.Flags {
for _, name := range flag.Names() {
if name == genCompName() {
continue
}
switch name = strings.TrimSpace(name); len(name) {
case 0:
case 1:
fmt.Fprintln(ctx.App.Writer, "-"+name)
default:
fmt.Fprintln(ctx.App.Writer, "--"+name)
}
}
}
},
Action: func(ctx *Context) error {
return fmt.Errorf("should not get here")
},
}
err := app.Run([]string{"", "--test-completion", "--" + genCompName()})
if err != nil {
t.Errorf("app should not return an error: %s", err)
}
}

View File

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

20
autocomplete/bash_autocomplete Normal file → Executable file
View File

@ -3,12 +3,14 @@
: ${PROG:=$(basename ${BASH_SOURCE})}
_cli_bash_autocomplete() {
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _cli_bash_autocomplete $PROG
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _cli_bash_autocomplete $PROG
unset PROG

View File

@ -236,7 +236,11 @@ def _flag_name_stringly_repl(match):
@_migrator
def _commands_opaque_type(source):
return re.sub('cli\\.Commands', '[]*cli.Command', source, flags=re.UNICODE)
return _subfmt(
'cli\\.Commands(?P<suffix>[^B])',
'[]*cli.Command{suffix}',
source
)
@_migrator

3
cli.go
View File

@ -11,7 +11,8 @@
// Name: "greet",
// Usage: "say a greeting",
// Action: func(c *cli.Context) error {
// println("Greetings")
// fmt.Println("Greetings")
// return nil
// },
// }
//

View File

@ -49,6 +49,25 @@ type Command struct {
// Full name of command for help, defaults to full command name, including parent commands.
HelpName string
commandNamePath []string
// CustomHelpTemplate the text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
CustomHelpTemplate string
}
type CommandsByName []*Command
func (c CommandsByName) Len() int {
return len(c)
}
func (c CommandsByName) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
func (c CommandsByName) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// FullName returns the full name of the command.
@ -75,7 +94,10 @@ func (c *Command) Run(ctx *Context) (err error) {
c.appendFlag(GenerateCompletionFlag)
}
set := flagSet(c.Name, c.Flags)
set, err := flagSet(c.Name, c.Flags)
if err != nil {
return err
}
set.SetOutput(ioutil.Discard)
if c.SkipFlagParsing {
@ -84,18 +106,6 @@ func (c *Command) Run(ctx *Context) (err error) {
err = set.Parse(ctx.Args().Tail())
}
if err != nil {
if c.OnUsageError != nil {
err := c.OnUsageError(ctx, err, false)
HandleExitCoder(err)
return err
}
fmt.Fprintln(ctx.App.Writer, "Incorrect Usage:", err.Error())
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
return err
}
nerr := normalizeFlags(c.Flags, set)
if nerr != nil {
fmt.Fprintln(ctx.App.Writer, nerr)
@ -105,11 +115,23 @@ func (c *Command) Run(ctx *Context) (err error) {
}
context := NewContext(ctx.App, set, ctx)
context.Command = c
if checkCommandCompletions(context, c.Name) {
return nil
}
if err != nil {
if c.OnUsageError != nil {
err := c.OnUsageError(context, err, false)
HandleExitCoder(err)
return err
}
fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error())
fmt.Fprintln(context.App.Writer)
ShowCommandHelp(context, c.Name)
return err
}
if checkCommandHelp(context, c.Name) {
return nil
}
@ -131,14 +153,16 @@ func (c *Command) Run(ctx *Context) (err error) {
if c.Before != nil {
err = c.Before(context)
if err != nil {
fmt.Fprintln(ctx.App.Writer, err)
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
ShowCommandHelp(context, c.Name)
HandleExitCoder(err)
return err
}
}
if c.Action == nil {
c.Action = helpSubcommand.Action
}
context.Command = c
err = c.Action(context)
@ -175,14 +199,13 @@ func (c *Command) startApp(ctx *Context) error {
app.HelpName = app.Name
}
if c.Description != "" {
app.Usage = c.Description
} else {
app.Usage = c.Usage
}
app.Usage = c.Usage
app.Description = c.Description
app.ArgsUsage = c.ArgsUsage
// set CommandNotFound
app.CommandNotFound = ctx.App.CommandNotFound
app.CustomAppHelpTemplate = c.CustomHelpTemplate
// set the flags and commands
app.Commands = c.Subcommands
@ -193,6 +216,7 @@ func (c *Command) startApp(ctx *Context) error {
app.HideVersion = ctx.App.HideVersion
app.Compiled = ctx.App.Compiled
app.Writer = ctx.App.Writer
app.ErrWriter = ctx.App.ErrWriter
app.Categories = newCommandCategories()
for _, command := range c.Subcommands {
@ -215,6 +239,7 @@ func (c *Command) startApp(ctx *Context) error {
} else {
app.Action = helpSubcommand.Action
}
app.OnUsageError = c.OnUsageError
for index, cc := range app.Commands {
app.Commands[index].commandNamePath = []string{c.Name, cc.Name}

View File

@ -123,6 +123,31 @@ func TestCommand_Run_BeforeSavesMetadata(t *testing.T) {
}
}
func TestCommand_OnUsageError_hasCommandContext(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "bar",
Flags: []Flag{
&IntFlag{Name: "flag"},
},
OnUsageError: func(c *Context, err error, _ bool) error {
return fmt.Errorf("intercepted in %s: %s", c.Command.Name, err.Error())
},
},
},
}
err := app.Run([]string{"foo", "bar", "--flag=wrong"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.HasPrefix(err.Error(), "intercepted in bar") {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}
func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) {
app := &App{
Commands: []*Command{
@ -150,3 +175,66 @@ func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}
func TestCommand_OnUsageError_WithSubcommand(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "bar",
Subcommands: []*Command{
{
Name: "baz",
},
},
Flags: []Flag{
&IntFlag{Name: "flag"},
},
OnUsageError: func(c *Context, err error, _ bool) error {
if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") {
t.Errorf("Expect an invalid value error, but got \"%v\"", err)
}
return errors.New("intercepted: " + err.Error())
},
},
},
}
err := app.Run([]string{"foo", "bar", "--flag=wrong"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.HasPrefix(err.Error(), "intercepted: invalid value") {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}
func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) {
app := &App{
ErrWriter: ioutil.Discard,
Commands: []*Command{
{
Name: "bar",
Usage: "this is for testing",
Subcommands: []*Command{
{
Name: "baz",
Usage: "this is for testing",
Action: func(c *Context) error {
if c.App.ErrWriter != ioutil.Discard {
return fmt.Errorf("ErrWriter not passed")
}
return nil
},
},
},
},
},
}
err := app.Run([]string{"foo", "bar", "baz"})
if err != nil {
t.Fatal(err)
}
}

View File

@ -13,8 +13,9 @@ import (
// can be used to retrieve context-specific args and
// parsed command-line options.
type Context struct {
App *App
Command *Command
App *App
Command *Command
shellComplete bool
flagSet *flag.FlagSet
parentContext *Context
@ -22,7 +23,13 @@ type Context struct {
// NewContext creates a new context. For use in when invoking an App or Command action.
func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
return &Context{App: app, flagSet: set, parentContext: parentCtx}
c := &Context{App: app, flagSet: set, parentContext: parentCtx}
if parentCtx != nil {
c.shellComplete = parentCtx.shellComplete
}
return c
}
// NumFlags returns the number of flags set
@ -114,6 +121,11 @@ func (c *Context) Lineage() []*Context {
return lineage
}
// value returns the value of the flag corresponding to `name`
func (c *Context) value(name string) interface{} {
return c.flagSet.Lookup(name).Value.(flag.Getter).Get()
}
// Args returns the command line arguments associated with the context.
func (c *Context) Args() Args {
ret := args(c.flagSet.Args())

View File

@ -2,7 +2,6 @@ package cli
import (
"flag"
"os"
"sort"
"testing"
"time"
@ -158,68 +157,6 @@ func TestContext_IsSet(t *testing.T) {
expect(t, ctx.IsSet("bogus"), false)
}
// XXX Corresponds to hack in context.IsSet for flags with EnvVar field
// Should be moved to `flag_test` in v2
func TestContext_IsSet_fromEnv(t *testing.T) {
var (
timeoutIsSet, tIsSet, noEnvVarIsSet, nIsSet bool
globalTimeoutIsSet, TIsSet, globalNoEnvVarIsSet, NIsSet bool
)
os.Clearenv()
os.Setenv("GLOBAL_APP_TIMEOUT_SECONDS", "15.5")
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
a := App{
Flags: []Flag{
&Float64Flag{
Name: "global-timeout",
Aliases: []string{"T"},
EnvVars: []string{"GLOBAL_APP_TIMEOUT_SECONDS"},
},
&Float64Flag{
Name: "global-no-env-var",
Aliases: []string{"N"},
},
},
Commands: []*Command{
{
Name: "hello",
Flags: []Flag{
&Float64Flag{
Name: "timeout",
Aliases: []string{"t"},
EnvVars: []string{"APP_TIMEOUT_SECONDS"},
},
&Float64Flag{
Name: "no-env-var",
Aliases: []string{"n"},
},
},
Action: func(ctx *Context) error {
globalTimeoutIsSet = ctx.IsSet("global-timeout")
TIsSet = ctx.IsSet("T")
globalNoEnvVarIsSet = ctx.IsSet("global-no-env-var")
NIsSet = ctx.IsSet("N")
timeoutIsSet = ctx.IsSet("timeout")
tIsSet = ctx.IsSet("t")
noEnvVarIsSet = ctx.IsSet("no-env-var")
nIsSet = ctx.IsSet("n")
return nil
},
},
},
}
a.Run([]string{"run", "hello"})
expect(t, globalTimeoutIsSet, true)
expect(t, TIsSet, true)
expect(t, globalNoEnvVarIsSet, false)
expect(t, NIsSet, false)
expect(t, timeoutIsSet, true)
expect(t, tIsSet, true)
expect(t, noEnvVarIsSet, false)
expect(t, nIsSet, false)
}
func TestContext_NumFlags(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
@ -238,8 +175,10 @@ func TestContext_Set(t *testing.T) {
set.Int("int", 5, "an int")
c := NewContext(nil, set, nil)
expect(t, c.IsSet("int"), false)
c.Set("int", "1")
expect(t, c.Int("int"), 1)
expect(t, c.IsSet("int"), true)
}
func TestContext_LocalFlagNames(t *testing.T) {

View File

@ -48,6 +48,10 @@ func (m *multiError) Errors() []error {
return errs
}
type ErrorFormatter interface {
Format(s fmt.State, verb rune)
}
// ExitCoder is the interface checked by `App` and `Command` for a custom exit
// code
type ExitCoder interface {
@ -57,12 +61,12 @@ type ExitCoder interface {
type exitError struct {
exitCode int
message string
message interface{}
}
// Exit wraps a message and exit code into an ExitCoder suitable for handling by
// HandleExitCoder
func Exit(message string, exitCode int) ExitCoder {
func Exit(message interface{}, exitCode int) ExitCoder {
return &exitError{
exitCode: exitCode,
message: message,
@ -70,7 +74,7 @@ func Exit(message string, exitCode int) ExitCoder {
}
func (ee *exitError) Error() string {
return ee.message
return fmt.Sprintf("%v", ee.message)
}
func (ee *exitError) ExitCode() int {
@ -80,7 +84,7 @@ func (ee *exitError) ExitCode() int {
// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if
// so prints the error to stderr (if it is non-empty) and calls OsExiter with the
// given exit code. If the given error is a MultiError, then this func is
// called on all members of the Errors slice.
// called on all members of the Errors slice and calls OsExiter with the last exit code.
func HandleExitCoder(err error) {
if err == nil {
return
@ -88,21 +92,34 @@ func HandleExitCoder(err error) {
if exitErr, ok := err.(ExitCoder); ok {
if err.Error() != "" {
fmt.Fprintln(ErrWriter, err)
if _, ok := exitErr.(ErrorFormatter); ok {
fmt.Fprintf(ErrWriter, "%+v\n", err)
} else {
fmt.Fprintln(ErrWriter, err)
}
}
OsExiter(exitErr.ExitCode())
return
}
if multiErr, ok := err.(MultiError); ok {
for _, merr := range multiErr.Errors() {
HandleExitCoder(merr)
}
code := handleMultiError(multiErr)
OsExiter(code)
return
}
if err.Error() != "" {
fmt.Fprintln(ErrWriter, err)
}
OsExiter(1)
}
func handleMultiError(multiErr MultiError) int {
code := 1
for _, merr := range multiErr.Errors() {
if multiErr2, ok := merr.(MultiError); ok {
code = handleMultiError(multiErr2)
} else if merr != nil {
fmt.Fprintln(ErrWriter, merr)
if exitErr, ok := merr.(ExitCoder); ok {
code = exitErr.ExitCode()
}
}
}
return code
}

View File

@ -3,6 +3,7 @@ package cli
import (
"bytes"
"errors"
"fmt"
"testing"
)
@ -11,8 +12,10 @@ func TestHandleExitCoder_nil(t *testing.T) {
called := false
OsExiter = func(rc int) {
exitCode = rc
called = true
if !called {
exitCode = rc
called = true
}
}
defer func() { OsExiter = fakeOsExiter }()
@ -28,8 +31,10 @@ func TestHandleExitCoder_ExitCoder(t *testing.T) {
called := false
OsExiter = func(rc int) {
exitCode = rc
called = true
if !called {
exitCode = rc
called = true
}
}
defer func() { OsExiter = fakeOsExiter }()
@ -45,27 +50,43 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) {
called := false
OsExiter = func(rc int) {
exitCode = rc
called = true
if !called {
exitCode = rc
called = true
}
}
defer func() { OsExiter = fakeOsExiter }()
exitErr := Exit("galactic perimeter breach", 9)
err := newMultiError(errors.New("wowsa"), errors.New("egad"), exitErr)
exitErr2 := Exit("last ExitCoder", 11)
err := newMultiError(errors.New("wowsa"), errors.New("egad"), exitErr, exitErr2)
HandleExitCoder(err)
expect(t, exitCode, 9)
expect(t, exitCode, 11)
expect(t, called, true)
}
func TestHandleExitCoder_ErrorWithMessage(t *testing.T) {
exitCode := 0
// make a stub to not import pkg/errors
type ErrorWithFormat struct {
error
}
func NewErrorWithFormat(m string) *ErrorWithFormat {
return &ErrorWithFormat{error: errors.New(m)}
}
func (f *ErrorWithFormat) Format(s fmt.State, verb rune) {
fmt.Fprintf(s, "This the format: %v", f.error)
}
func TestHandleExitCoder_ErrorWithFormat(t *testing.T) {
called := false
OsExiter = func(rc int) {
exitCode = rc
called = true
if !called {
called = true
}
}
ErrWriter = &bytes.Buffer{}
@ -74,33 +95,28 @@ func TestHandleExitCoder_ErrorWithMessage(t *testing.T) {
ErrWriter = fakeErrWriter
}()
err := errors.New("gourd havens")
err := Exit(NewErrorWithFormat("I am formatted"), 1)
HandleExitCoder(err)
expect(t, exitCode, 1)
expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "gourd havens\n")
expect(t, ErrWriter.(*bytes.Buffer).String(), "This the format: I am formatted\n")
}
func TestHandleExitCoder_ErrorWithoutMessage(t *testing.T) {
exitCode := 0
func TestHandleExitCoder_MultiErrorWithFormat(t *testing.T) {
called := false
OsExiter = func(rc int) {
exitCode = rc
called = true
if !called {
called = true
}
}
ErrWriter = &bytes.Buffer{}
defer func() {
OsExiter = fakeOsExiter
ErrWriter = fakeErrWriter
}()
defer func() { OsExiter = fakeOsExiter }()
err := errors.New("")
err := newMultiError(NewErrorWithFormat("err1"), NewErrorWithFormat("err2"))
HandleExitCoder(err)
expect(t, exitCode, 1)
expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "")
expect(t, ErrWriter.(*bytes.Buffer).String(), "This the format: err1\nThis the format: err2\n")
}

252
flag.go
View File

@ -4,12 +4,12 @@ import (
"encoding/json"
"flag"
"fmt"
"os"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"syscall"
"time"
)
@ -22,11 +22,19 @@ var (
)
// GenerateCompletionFlag enables completion for all commands and subcommands
var GenerateCompletionFlag = &BoolFlag{
var GenerateCompletionFlag Flag = &BoolFlag{
Name: "generate-completion",
Hidden: true,
}
func genCompName() string {
names := GenerateCompletionFlag.Names()
if len(names) == 0 {
return "generate-completion"
}
return names[0]
}
// InitCompletionFlag generates completion code
var InitCompletionFlag = &StringFlag{
Name: "init-completion",
@ -34,7 +42,7 @@ var InitCompletionFlag = &StringFlag{
}
// VersionFlag prints the version for the application
var VersionFlag = &BoolFlag{
var VersionFlag Flag = &BoolFlag{
Name: "version",
Aliases: []string{"v"},
Usage: "print the version",
@ -43,7 +51,7 @@ var VersionFlag = &BoolFlag{
// HelpFlag prints the help for all commands and subcommands.
// Set to nil to disable the flag. The subcommand
// will still be added unless HideHelp is set to true.
var HelpFlag = &BoolFlag{
var HelpFlag Flag = &BoolFlag{
Name: "help",
Aliases: []string{"h"},
Usage: "show help",
@ -88,13 +96,29 @@ type Flag interface {
Names() []string
}
func flagSet(name string, flags []Flag) *flag.FlagSet {
// errorableFlag is an interface that allows us to return errors during apply
// it allows flags defined in this library to return errors in a fashion backwards compatible
// TODO remove in v2 and modify the existing Flag interface to return errors
type errorableFlag interface {
Flag
ApplyWithError(*flag.FlagSet) error
}
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
set := flag.NewFlagSet(name, flag.ContinueOnError)
for _, f := range flags {
f.Apply(set)
//TODO remove in v2 when errorableFlag is removed
if ef, ok := f.(errorableFlag); ok {
if err := ef.ApplyWithError(set); err != nil {
return nil, err
}
} else {
f.Apply(set)
}
}
return set
return set, nil
}
// Generic is a generic parseable type identified by a specific flag
@ -105,11 +129,18 @@ type Generic interface {
// Apply takes the flagset and calls Set on the generic flag with the value
// provided by the user for parsing by the flag
// Ignores parsing errors
func (f *GenericFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError takes the flagset and calls Set on the generic flag with the
// value provided by the user for parsing by the flag
func (f *GenericFlag) ApplyWithError(set *flag.FlagSet) error {
val := f.Value
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
val.Set(envVal)
break
}
@ -119,6 +150,7 @@ func (f *GenericFlag) Apply(set *flag.FlagSet) {
for _, name := range f.Names() {
set.Var(val, name, f.Usage)
}
return nil
}
// StringSlice wraps a []string to satisfy flag.Value
@ -166,15 +198,28 @@ func (f *StringSlice) Value() []string {
return f.slice
}
// Get returns the slice of strings set by this flag
func (f *StringSlice) Get() interface{} {
return *f
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *StringSliceFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
newVal := NewStringSlice()
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
newVal.Set(s)
if err := newVal.Set(s); err != nil {
return fmt.Errorf("could not parse %q as string value for flag %s: %s", envVal, f.Name, err)
}
}
f.Value = newVal
break
@ -189,6 +234,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) {
for _, name := range f.Names() {
set.Var(f.Value, name, f.Usage)
}
return nil
}
// IntSlice wraps an []int to satisfy flag.Value
@ -256,17 +302,27 @@ func (i *IntSlice) Value() []int {
return i.slice
}
// Get returns the slice of ints set by this flag
func (f *IntSlice) Get() interface{} {
return *f
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *IntSliceFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
newVal := NewIntSlice()
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
err := newVal.Set(s)
if err != nil {
fmt.Fprintf(ErrWriter, err.Error())
if err := newVal.Set(s); err != nil {
return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", envVal, f.Name, err)
}
}
f.Value = newVal
@ -282,6 +338,7 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) {
for _, name := range f.Names() {
set.Var(f.Value, name, f.Usage)
}
return nil
}
// Int64Slice is an opaque type for []int to satisfy flag.Value
@ -329,17 +386,27 @@ func (f *Int64Slice) Value() []int64 {
return f.slice
}
// Get returns the slice of ints set by this flag
func (f *Int64Slice) Get() interface{} {
return *f
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
newVal := NewInt64Slice()
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
err := newVal.Set(s)
if err != nil {
fmt.Fprintf(ErrWriter, err.Error())
if err := newVal.Set(s); err != nil {
return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", envVal, f.Name, err)
}
}
f.Value = newVal
@ -355,17 +422,30 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) {
for _, name := range f.Names() {
set.Var(f.Value, name, f.Usage)
}
return nil
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *BoolFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
envValBool, err := strconv.ParseBool(envVal)
if err == nil {
f.Value = envValBool
if envVal, ok := syscall.Getenv(envVar); ok {
if envVal == "" {
f.Value = false
break
}
envValBool, err := strconv.ParseBool(envVal)
if err != nil {
return fmt.Errorf("could not parse %q as bool value for flag %s: %s", envVal, f.Name, err)
}
f.Value = envValBool
break
}
}
@ -378,13 +458,20 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) {
}
set.Bool(name, f.Value, f.Usage)
}
return nil
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *StringFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
f.Value = envVal
break
}
@ -398,18 +485,26 @@ func (f *StringFlag) Apply(set *flag.FlagSet) {
}
set.String(name, f.Value, f.Usage)
}
return nil
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *IntFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err == nil {
f.Value = int(envValInt)
break
if err != nil {
return fmt.Errorf("could not parse %q as int value for flag %s: %s", envVal, f.Name, err)
}
f.Value = int(envValInt)
break
}
}
}
@ -421,18 +516,27 @@ func (f *IntFlag) Apply(set *flag.FlagSet) {
}
set.Int(name, f.Value, f.Usage)
}
return nil
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *Int64Flag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err == nil {
f.Value = envValInt
break
if err != nil {
return fmt.Errorf("could not parse %q as int value for flag %s: %s", envVal, f.Name, err)
}
f.Value = envValInt
break
}
}
}
@ -440,22 +544,31 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) {
for _, name := range f.Names() {
if f.Destination != nil {
set.Int64Var(f.Destination, name, f.Value, f.Usage)
return
return nil
}
set.Int64(name, f.Value, f.Usage)
}
return nil
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *UintFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err == nil {
f.Value = uint(envValInt)
break
if err != nil {
return fmt.Errorf("could not parse %q as uint value for flag %s: %s", envVal, f.Name, err)
}
f.Value = uint(envValInt)
break
}
}
}
@ -463,22 +576,31 @@ func (f *UintFlag) Apply(set *flag.FlagSet) {
for _, name := range f.Names() {
if f.Destination != nil {
set.UintVar(f.Destination, name, f.Value, f.Usage)
return
return nil
}
set.Uint(name, f.Value, f.Usage)
}
return nil
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *Uint64Flag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err == nil {
f.Value = uint64(envValInt)
break
if err != nil {
return fmt.Errorf("could not parse %q as uint64 value for flag %s: %s", envVal, f.Name, err)
}
f.Value = uint64(envValInt)
break
}
}
}
@ -486,22 +608,31 @@ func (f *Uint64Flag) Apply(set *flag.FlagSet) {
for _, name := range f.Names() {
if f.Destination != nil {
set.Uint64Var(f.Destination, name, f.Value, f.Usage)
return
return nil
}
set.Uint64(name, f.Value, f.Usage)
}
return nil
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *DurationFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
envValDuration, err := time.ParseDuration(envVal)
if err == nil {
f.Value = envValDuration
break
if err != nil {
return fmt.Errorf("could not parse %q as duration for flag %s: %s", envVal, f.Name, err)
}
f.Value = envValDuration
break
}
}
}
@ -513,17 +644,27 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) {
}
set.Duration(name, f.Value, f.Usage)
}
return nil
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *Float64Flag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
envValFloat, err := strconv.ParseFloat(envVal, 10)
if err == nil {
f.Value = float64(envValFloat)
if err != nil {
return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", envVal, f.Name, err)
}
f.Value = float64(envValFloat)
break
}
}
}
@ -535,6 +676,7 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) {
}
set.Float64(name, f.Value, f.Usage)
}
return nil
}
// NewFloat64Slice makes a *Float64Slice with default values
@ -588,10 +730,16 @@ func (f *Float64Slice) Value() []float64 {
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *Float64SliceFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f *Float64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal := os.Getenv(envVar); envVal != "" {
if envVal, ok := syscall.Getenv(envVar); ok {
newVal := NewFloat64Slice()
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
@ -613,12 +761,14 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) {
for _, name := range f.Names() {
set.Var(f.Value, name, f.Usage)
}
return nil
}
func visibleFlags(fl []Flag) []Flag {
visible := []Flag{}
for _, flag := range fl {
if !flagValue(flag).FieldByName("Hidden").Bool() {
field := flagValue(flag).FieldByName("Hidden")
if !field.IsValid() || !field.Bool() {
visible = append(visible, flag)
}
}

View File

@ -17,6 +17,7 @@ type BoolFlag struct {
Hidden bool
Value bool
DefaultText string
Destination *bool
}
@ -61,6 +62,7 @@ type DurationFlag struct {
Hidden bool
Value time.Duration
DefaultText string
Destination *time.Duration
}
@ -105,6 +107,7 @@ type Float64Flag struct {
Hidden bool
Value float64
DefaultText string
Destination *float64
}
@ -192,6 +195,7 @@ type Int64Flag struct {
Hidden bool
Value int64
DefaultText string
Destination *int64
}
@ -236,6 +240,7 @@ type IntFlag struct {
Hidden bool
Value int
DefaultText string
Destination *int
}
@ -409,6 +414,7 @@ type StringFlag struct {
Hidden bool
Value string
DefaultText string
Destination *string
}
@ -496,6 +502,7 @@ type Uint64Flag struct {
Hidden bool
Value uint64
DefaultText string
Destination *uint64
}
@ -540,6 +547,7 @@ type UintFlag struct {
Hidden bool
Value uint
DefaultText string
Destination *uint
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"reflect"
"regexp"
"runtime"
"strings"
"testing"
@ -41,6 +42,106 @@ func TestBoolFlagApply_SetsAllNames(t *testing.T) {
expect(t, v, true)
}
func TestFlagsFromEnv(t *testing.T) {
newSetIntSlice := func(defaults ...int) IntSlice {
s := NewIntSlice(defaults...)
s.hasBeenSet = true
return *s
}
newSetInt64Slice := func(defaults ...int64) Int64Slice {
s := NewInt64Slice(defaults...)
s.hasBeenSet = true
return *s
}
newSetStringSlice := func(defaults ...string) StringSlice {
s := NewStringSlice(defaults...)
s.hasBeenSet = true
return *s
}
var flagTests = []struct {
input string
output interface{}
flag Flag
errRegexp string
}{
{"", false, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""},
{"1", true, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""},
{"false", false, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""},
{"foobar", true, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, `could not parse "foobar" as bool value for flag debug: .*`},
{"1s", 1 * time.Second, &DurationFlag{Name: "time", EnvVars: []string{"TIME"}}, ""},
{"foobar", false, &DurationFlag{Name: "time", EnvVars: []string{"TIME"}}, `could not parse "foobar" as duration for flag time: .*`},
{"1.2", 1.2, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1", 1.0, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"foobar", 0, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as float64 value for flag seconds: .*`},
{"1", int64(1), &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`},
{"foobar", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`},
{"1", 1, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`},
{"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`},
{"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value for flag seconds: .*`},
{"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value for flag seconds: .*`},
{"1,2", newSetInt64Slice(1, 2), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2,2", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int64 slice value for flag seconds: .*`},
{"foobar", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int64 slice value for flag seconds: .*`},
{"foo", "foo", &StringFlag{Name: "name", EnvVars: []string{"NAME"}}, ""},
{"foo,bar", newSetStringSlice("foo", "bar"), &StringSliceFlag{Name: "names", EnvVars: []string{"NAMES"}}, ""},
{"1", uint(1), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint value for flag seconds: .*`},
{"foobar", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint value for flag seconds: .*`},
{"1", uint64(1), &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint64 value for flag seconds: .*`},
{"foobar", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint64 value for flag seconds: .*`},
{"foo,bar", &Parser{"foo", "bar"}, &GenericFlag{Name: "names", Value: &Parser{}, EnvVars: []string{"NAMES"}}, ""},
}
for i, test := range flagTests {
clearenv()
envVarSlice := reflect.Indirect(reflect.ValueOf(test.flag)).FieldByName("EnvVars").Slice(0, 1)
os.Setenv(envVarSlice.Index(0).String(), test.input)
a := App{
Flags: []Flag{test.flag},
Action: func(ctx *Context) error {
if !reflect.DeepEqual(ctx.value(test.flag.Names()[0]), test.output) {
t.Errorf("ex:%01d expected %q to be parsed as %#v, instead was %#v", i, test.input, test.output, ctx.value(test.flag.Names()[0]))
}
return nil
},
}
err := a.Run([]string{"run"})
if test.errRegexp != "" {
if err == nil {
t.Errorf("expected error to match %q, got none", test.errRegexp)
} else {
if matched, _ := regexp.MatchString(test.errRegexp, err.Error()); !matched {
t.Errorf("expected error to match %q, got error %s", test.errRegexp, err)
}
}
} else {
if err != nil && test.errRegexp == "" {
t.Errorf("expected no error got %q", err)
}
}
}
}
var stringFlagTests = []struct {
name string
aliases []string
@ -78,7 +179,7 @@ func TestStringFlagDefaultText(t *testing.T) {
}
func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_FOO", "derp")
for _, test := range stringFlagTests {
flag := &StringFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_FOO"}}
@ -130,7 +231,7 @@ func TestStringSliceFlagHelpOutput(t *testing.T) {
}
func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_QWWX", "11,4")
for _, test := range stringSliceFlagTests {
flag := &StringSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_QWWX"}}
@ -175,7 +276,7 @@ func TestIntFlagHelpOutput(t *testing.T) {
}
func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_BAR", "2")
for _, test := range intFlagTests {
flag := &IntFlag{Name: test.name, EnvVars: []string{"APP_BAR"}}
@ -222,7 +323,7 @@ func TestInt64FlagHelpOutput(t *testing.T) {
}
func TestInt64FlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_BAR", "2")
for _, test := range int64FlagTests {
flag := IntFlag{Name: test.name, EnvVars: []string{"APP_BAR"}}
@ -258,7 +359,7 @@ func TestUintFlagHelpOutput(t *testing.T) {
}
func TestUintFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_BAR", "2")
for _, test := range uintFlagTests {
flag := UintFlag{Name: test.name, EnvVars: []string{"APP_BAR"}}
@ -294,7 +395,7 @@ func TestUint64FlagHelpOutput(t *testing.T) {
}
func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_BAR", "2")
for _, test := range uint64FlagTests {
flag := UintFlag{Name: test.name, EnvVars: []string{"APP_BAR"}}
@ -330,7 +431,7 @@ func TestDurationFlagHelpOutput(t *testing.T) {
}
func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_BAR", "2h3m6s")
for _, test := range durationFlagTests {
flag := &DurationFlag{Name: test.name, EnvVars: []string{"APP_BAR"}}
@ -380,7 +481,7 @@ func TestIntSliceFlagHelpOutput(t *testing.T) {
}
func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_SMURF", "42,3")
for _, test := range intSliceFlagTests {
flag := &IntSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_SMURF"}}
@ -429,7 +530,7 @@ func TestInt64SliceFlagHelpOutput(t *testing.T) {
}
func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_SMURF", "42,17179869184")
for _, test := range int64SliceFlagTests {
flag := Int64SliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}}
@ -465,7 +566,7 @@ func TestFloat64FlagHelpOutput(t *testing.T) {
}
func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_BAZ", "99.4")
for _, test := range float64FlagTests {
flag := &Float64Flag{Name: test.name, EnvVars: []string{"APP_BAZ"}}
@ -516,7 +617,7 @@ func TestFloat64SliceFlagHelpOutput(t *testing.T) {
}
func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_SMURF", "0.1234,-10.5")
for _, test := range float64SliceFlagTests {
flag := Float64SliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}}
@ -553,7 +654,7 @@ func TestGenericFlagHelpOutput(t *testing.T) {
}
func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_ZAP", "3")
for _, test := range genericFlagTests {
flag := &GenericFlag{Name: test.name, EnvVars: []string{"APP_ZAP"}}
@ -615,7 +716,7 @@ func TestParseDestinationString(t *testing.T) {
}
func TestParseMultiStringFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_COUNT", "20")
(&App{
Flags: []Flag{
@ -634,7 +735,7 @@ func TestParseMultiStringFromEnv(t *testing.T) {
}
func TestParseMultiStringFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_COUNT", "20")
(&App{
Flags: []Flag{
@ -706,7 +807,7 @@ func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) {
}
func TestParseMultiStringSliceFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,40")
(&App{
@ -726,7 +827,7 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) {
}
func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,40")
(&App{
@ -746,7 +847,7 @@ func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) {
}
func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,40")
(&App{
@ -766,7 +867,7 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
}
func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,40")
(&App{
@ -823,7 +924,7 @@ func TestParseDestinationInt(t *testing.T) {
}
func TestParseMultiIntFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "10")
a := App{
Flags: []Flag{
@ -843,7 +944,7 @@ func TestParseMultiIntFromEnv(t *testing.T) {
}
func TestParseMultiIntFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "10")
a := App{
Flags: []Flag{
@ -914,7 +1015,7 @@ func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) {
}
func TestParseMultiIntSliceFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,40")
(&App{
@ -934,7 +1035,7 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) {
}
func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,40")
(&App{
@ -954,7 +1055,7 @@ func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) {
}
func TestParseMultiIntSliceFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,40")
(&App{
@ -991,7 +1092,7 @@ func TestParseMultiInt64Slice(t *testing.T) {
}
func TestParseMultiInt64SliceFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,17179869184")
(&App{
@ -1011,7 +1112,7 @@ func TestParseMultiInt64SliceFromEnv(t *testing.T) {
}
func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "20,30,17179869184")
(&App{
@ -1068,7 +1169,7 @@ func TestParseDestinationFloat64(t *testing.T) {
}
func TestParseMultiFloat64FromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
a := App{
Flags: []Flag{
@ -1088,7 +1189,7 @@ func TestParseMultiFloat64FromEnv(t *testing.T) {
}
func TestParseMultiFloat64FromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
a := App{
Flags: []Flag{
@ -1108,7 +1209,7 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) {
}
func TestParseMultiFloat64SliceFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "0.1,-10.5")
(&App{
@ -1128,7 +1229,7 @@ func TestParseMultiFloat64SliceFromEnv(t *testing.T) {
}
func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_INTERVALS", "0.1234,-10.5")
(&App{
@ -1185,7 +1286,7 @@ func TestParseDestinationBool(t *testing.T) {
}
func TestParseMultiBoolFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_DEBUG", "1")
a := App{
Flags: []Flag{
@ -1205,7 +1306,7 @@ func TestParseMultiBoolFromEnv(t *testing.T) {
}
func TestParseMultiBoolFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_DEBUG", "1")
a := App{
Flags: []Flag{
@ -1264,7 +1365,7 @@ func TestParseDestinationBoolTrue(t *testing.T) {
}
func TestParseMultiBoolTrueFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_DEBUG", "0")
a := App{
Flags: []Flag{
@ -1289,7 +1390,7 @@ func TestParseMultiBoolTrueFromEnv(t *testing.T) {
}
func TestParseMultiBoolTrueFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_DEBUG", "0")
a := App{
Flags: []Flag{
@ -1331,6 +1432,10 @@ func (p *Parser) String() string {
return fmt.Sprintf("%s,%s", p[0], p[1])
}
func (p *Parser) Get() interface{} {
return p
}
func TestParseGeneric(t *testing.T) {
a := App{
Flags: []Flag{
@ -1350,7 +1455,7 @@ func TestParseGeneric(t *testing.T) {
}
func TestParseGenericFromEnv(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_SERVE", "20,30")
a := App{
Flags: []Flag{
@ -1375,7 +1480,7 @@ func TestParseGenericFromEnv(t *testing.T) {
}
func TestParseGenericFromEnvCascade(t *testing.T) {
os.Clearenv()
clearenv()
os.Setenv("APP_FOO", "99,2000")
a := App{
Flags: []Flag{

View File

@ -68,6 +68,9 @@ import tempfile
import textwrap
_PY3 = sys.version_info.major == 3
class _FancyFormatter(argparse.ArgumentDefaultsHelpFormatter,
argparse.RawDescriptionHelpFormatter):
pass
@ -104,7 +107,7 @@ def main(sysargs=sys.argv[:]):
def _generate_flag_types(writefunc, output_go, input_json):
types = json.load(input_json)
tmp = tempfile.NamedTemporaryFile(suffix='.go', delete=False)
tmp = _get_named_tmp_go()
writefunc(tmp, types)
tmp.close()
@ -117,6 +120,13 @@ def _generate_flag_types(writefunc, output_go, input_json):
os.remove(tmp.name)
def _get_named_tmp_go():
tmp_args = dict(suffix='.go', mode='w', delete=False)
if _PY3:
tmp_args['encoding'] = 'utf-8'
return tempfile.NamedTemporaryFile(**tmp_args)
def _set_typedef_defaults(typedef):
typedef.setdefault('doctail', '')
typedef.setdefault('context_type', typedef['type'])
@ -222,11 +232,18 @@ def _write_altsrc_flag_types(outfile, types):
f.set = set
f.{name}Flag.Apply(set)
}}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped {name}Flag.ApplyWithError
func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{
f.set = set
return f.{name}Flag.ApplyWithError(set)
}}
""".format(**typedef))
def _fwrite(outfile, text):
print(textwrap.dedent(text), end='', file=outfile)
print(textwrap.dedent(text), end=None, file=outfile)
_WRITEFUNCS = {

122
help.go
View File

@ -13,7 +13,7 @@ import (
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
@ -47,7 +47,7 @@ var CommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}}
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
@ -64,10 +64,10 @@ OPTIONS:
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var SubcommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
{{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}}
USAGE:
{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{end}}{{range .VisibleCommands}}
@ -112,17 +112,43 @@ var helpSubcommand = &Command{
// Prints help for the App or Command
type helpPrinter func(w io.Writer, templ string, data interface{})
// Prints help for the App or Command with custom template function.
type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{})
// HelpPrinter is a function that writes the help output. If not set a default
// is used. The function signature is:
// func(w io.Writer, templ string, data interface{})
var HelpPrinter helpPrinter = printHelp
// HelpPrinterCustom is same as HelpPrinter but
// takes a custom function for template function map.
var HelpPrinterCustom helpPrinterCustom = printHelpCustom
// VersionPrinter prints the version for the App
var VersionPrinter = printVersion
// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code.
func ShowAppHelpAndExit(c *Context, exitCode int) {
ShowAppHelp(c)
os.Exit(exitCode)
}
// ShowAppHelp is an action that displays the help.
func ShowAppHelp(c *Context) {
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
func ShowAppHelp(c *Context) (err error) {
if c.App.CustomAppHelpTemplate == "" {
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
return
}
customAppData := func() map[string]interface{} {
if c.App.ExtraInfo == nil {
return nil
}
return map[string]interface{}{
"ExtraInfo": c.App.ExtraInfo,
}
}
HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData())
return nil
}
// DefaultAppComplete prints the list of subcommands as the default app completion method
@ -137,6 +163,12 @@ func DefaultAppComplete(c *Context) {
}
}
// ShowCommandHelpAndExit - exits with code after showing help
func ShowCommandHelpAndExit(c *Context, command string, code int) {
ShowCommandHelp(c, command)
os.Exit(code)
}
// ShowCommandHelp prints help for the given command
func ShowCommandHelp(ctx *Context, command string) error {
// show the subcommand help for a command with subcommands
@ -147,7 +179,11 @@ func ShowCommandHelp(ctx *Context, command string) error {
for _, c := range ctx.App.Commands {
if c.HasName(command) {
HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c)
if c.CustomHelpTemplate != "" {
HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil)
} else {
HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c)
}
return nil
}
}
@ -198,10 +234,15 @@ func ShowCommandCompletions(ctx *Context, command string) {
}
}
func printHelp(out io.Writer, templ string, data interface{}) {
func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) {
funcMap := template.FuncMap{
"join": strings.Join,
}
if customFunc != nil {
for key, value := range customFunc {
funcMap[key] = value
}
}
w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
@ -219,13 +260,15 @@ func printHelp(out io.Writer, templ string, data interface{}) {
w.Flush()
}
func printHelp(out io.Writer, templ string, data interface{}) {
printHelpCustom(out, templ, data, nil)
}
func checkVersion(c *Context) bool {
found := false
if VersionFlag.Name != "" {
for _, name := range VersionFlag.Names() {
if c.Bool(name) {
found = true
}
for _, name := range VersionFlag.Names() {
if c.Bool(name) {
found = true
}
}
return found
@ -233,11 +276,9 @@ func checkVersion(c *Context) bool {
func checkHelp(c *Context) bool {
found := false
if HelpFlag.Name != "" {
for _, name := range HelpFlag.Names() {
if c.Bool(name) {
found = true
}
for _, name := range HelpFlag.Names() {
if c.Bool(name) {
found = true
}
}
return found
@ -261,22 +302,45 @@ func checkSubcommandHelp(c *Context) bool {
return false
}
func checkCompletions(c *Context) bool {
if c.Bool(GenerateCompletionFlag.Name) && c.App.EnableShellCompletion {
ShowCompletions(c)
return true
func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) {
if !a.EnableShellCompletion {
return false, arguments
}
return false
pos := len(arguments) - 1
lastArg := arguments[pos]
if lastArg != "--"+genCompName() {
return false, arguments
}
return true, arguments[:pos]
}
func checkCompletions(c *Context) bool {
if !c.shellComplete {
return false
}
if args := c.Args(); args.Present() {
name := args.First()
if cmd := c.App.Command(name); cmd != nil {
// let the command handle the completion
return false
}
}
ShowCompletions(c)
return true
}
func checkCommandCompletions(c *Context, name string) bool {
if c.Bool(GenerateCompletionFlag.Name) && c.App.EnableShellCompletion {
ShowCommandCompletions(c, name)
return true
if !c.shellComplete {
return false
}
return false
ShowCommandCompletions(c, name)
return true
}
func checkInitCompletion(c *Context) (bool, error) {
@ -302,12 +366,12 @@ func bashCompletionCode(progName string) string {
local cur opts base;
COMPREPLY=();
cur="${COMP_WORDS[COMP_CWORD]}";
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-completion );
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --%s );
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) );
return 0;
};
complete -F _cli_bash_autocomplete %s`
return fmt.Sprintf(template, progName)
return fmt.Sprintf(template, genCompName(), progName)
}
func zshCompletionCode(progName string) string {

View File

@ -3,6 +3,8 @@ package cli
import (
"bytes"
"flag"
"fmt"
"runtime"
"strings"
"testing"
)
@ -255,6 +257,92 @@ func TestShowSubcommandHelp_CommandAliases(t *testing.T) {
}
}
func TestShowCommandHelp_Customtemplate(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "frobbly",
Action: func(ctx *Context) error {
return nil
},
HelpName: "foo frobbly",
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET [TARGET ...]
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Frobbly runs with this param locally.
$ {{.HelpName}} wobbly
`,
},
},
}
output := &bytes.Buffer{}
app.Writer = output
app.Run([]string{"foo", "help", "frobbly"})
if strings.Contains(output.String(), "2. Frobbly runs without this param locally.") {
t.Errorf("expected output to exclude \"2. Frobbly runs without this param locally.\"; got: %q", output.String())
}
if !strings.Contains(output.String(), "1. Frobbly runs with this param locally.") {
t.Errorf("expected output to include \"1. Frobbly runs with this param locally.\"; got: %q", output.String())
}
if !strings.Contains(output.String(), "$ foo frobbly wobbly") {
t.Errorf("expected output to include \"$ foo frobbly wobbly\"; got: %q", output.String())
}
}
func TestShowSubcommandHelp_CommandUsageText(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "frobbly",
UsageText: "this is usage text",
},
},
}
output := &bytes.Buffer{}
app.Writer = output
app.Run([]string{"foo", "frobbly", "--help"})
if !strings.Contains(output.String(), "this is usage text") {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "frobbly",
Subcommands: []*Command{
{
Name: "bobbly",
UsageText: "this is usage text",
},
},
},
},
}
output := &bytes.Buffer{}
app.Writer = output
app.Run([]string{"foo", "frobbly", "bobbly", "--help"})
if !strings.Contains(output.String(), "this is usage text") {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestShowAppHelp_HiddenCommand(t *testing.T) {
app := &App{
Commands: []*Command{
@ -286,3 +374,78 @@ func TestShowAppHelp_HiddenCommand(t *testing.T) {
t.Errorf("expected output to include \"frobbly\"; got: %q", output.String())
}
}
func TestShowAppHelp_CustomAppTemplate(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "frobbly",
Action: func(ctx *Context) error {
return nil
},
},
{
Name: "secretfrob",
Hidden: true,
Action: func(ctx *Context) error {
return nil
},
},
},
ExtraInfo: func() map[string]string {
platform := fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH)
goruntime := fmt.Sprintf("Version: %s | CPUs: %d", runtime.Version(), runtime.NumCPU())
return map[string]string{
"PLATFORM": platform,
"RUNTIME": goruntime,
}
},
CustomAppHelpTemplate: `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.Name}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...]
COMMANDS:
{{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
{{end}}{{if .VisibleFlags}}
GLOBAL FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
VERSION:
2.0.0
{{"\n"}}{{range $key, $value := ExtraInfo}}
{{$key}}:
{{$value}}
{{end}}`,
}
output := &bytes.Buffer{}
app.Writer = output
app.Run([]string{"app", "--help"})
if strings.Contains(output.String(), "secretfrob") {
t.Errorf("expected output to exclude \"secretfrob\"; got: %q", output.String())
}
if !strings.Contains(output.String(), "frobbly") {
t.Errorf("expected output to include \"frobbly\"; got: %q", output.String())
}
if !strings.Contains(output.String(), "PLATFORM:") ||
!strings.Contains(output.String(), "OS:") ||
!strings.Contains(output.String(), "Arch:") {
t.Errorf("expected output to include \"PLATFORM:, OS: and Arch:\"; got: %q", output.String())
}
if !strings.Contains(output.String(), "RUNTIME:") ||
!strings.Contains(output.String(), "Version:") ||
!strings.Contains(output.String(), "CPUs:") {
t.Errorf("expected output to include \"RUNTIME:, Version: and CPUs:\"; got: %q", output.String())
}
if !strings.Contains(output.String(), "VERSION:") ||
!strings.Contains(output.String(), "2.0.0") {
t.Errorf("expected output to include \"VERSION:, 2.0.0\"; got: %q", output.String())
}
}

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

View File

@ -1,7 +1,8 @@
#!/usr/bin/env python
from __future__ import print_function
from __future__ import print_function, unicode_literals
import argparse
import codecs
import glob
import os
import platform
@ -12,6 +13,7 @@ import tempfile
from subprocess import check_call, check_output
_PY3 = sys.version_info.major == 3
_WINDOWS = platform.system().lower() == 'windows'
_PACKAGE_NAME = os.environ.get(
'CLI_PACKAGE_NAME', 'github.com/urfave/cli'
@ -37,7 +39,7 @@ def _target(func):
@_target
def _test():
if check_output('go version'.split()).split()[2] < 'go1.2':
if _go_version() < 'go1.2':
_run('go test -v .')
return
@ -61,7 +63,7 @@ def _test():
@_target
def _gfmrun():
go_version = check_output('go version'.split()).split()[2]
go_version = _go_version()
if go_version < 'go1.3':
print('runtests: skip on {}'.format(go_version), file=sys.stderr)
return
@ -75,7 +77,7 @@ def _vet():
@_target
def _migrations():
go_version = check_output('go version'.split()).split()[2]
go_version = _go_version()
if go_version < 'go1.3':
print('runtests: skip on {}'.format(go_version), file=sys.stderr)
return
@ -115,7 +117,7 @@ def _toc():
@_target
def _gen():
go_version = check_output('go version'.split()).split()[2]
go_version = _go_version()
if go_version < 'go1.5':
print('runtests: skip on {}'.format(go_version), file=sys.stderr)
return
@ -133,23 +135,29 @@ def _run(command):
def _gfmrun_count():
with open('README.md') as infile:
with codecs.open('README.md', 'r', 'utf-8') as infile:
lines = infile.read().splitlines()
return len(filter(_is_go_runnable, lines))
return len(list(filter(_is_go_runnable, lines)))
def _is_go_runnable(line):
return line.startswith('package main')
def _go_version():
return check_output('go version'.split()).decode('utf-8').split()[2]
def _combine_coverprofiles(coverprofiles):
combined = tempfile.NamedTemporaryFile(
suffix='.coverprofile', delete=False
)
tmp_args = dict(suffix='.coverprofile', mode='w', delete=False)
if _PY3:
tmp_args['encoding'] = 'utf-8'
combined = tempfile.NamedTemporaryFile(**tmp_args)
combined.write('mode: set\n')
for coverprofile in coverprofiles:
with open(coverprofile, 'r') as infile:
with codecs.open(coverprofile, 'r', 'utf-8') as infile:
for line in infile.readlines():
if not line.startswith('mode: '):
combined.write(line)