Merge remote-tracking branch 'origin/v1' into merging-from-v1
This commit is contained in:
commit
a61867e5e6
36
.travis.yml
36
.travis.yml
@ -1,35 +1,23 @@
|
||||
language: go
|
||||
|
||||
sudo: false
|
||||
dist: trusty
|
||||
osx_image: xcode8.3
|
||||
go: 1.8.x
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
go:
|
||||
- 1.2.x
|
||||
- 1.3.x
|
||||
- 1.4.2
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- master
|
||||
|
||||
env: pip_install="pip install --user"
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
include:
|
||||
- go: 1.6.x
|
||||
os: osx
|
||||
env: pip_install="sudo pip install"
|
||||
- go: 1.7.x
|
||||
os: osx
|
||||
env: pip_install="sudo pip install"
|
||||
|
||||
before_script:
|
||||
- $pip_install flake8
|
||||
- if [[ $(uname) == Darwin ]]; then
|
||||
sudo pip install flake8;
|
||||
else
|
||||
pip install --user flake8;
|
||||
fi
|
||||
- mkdir -p ${GOPATH%%:*}/src/gopkg.in/urfave
|
||||
- rm -rvf ${GOPATH%%:*}/src/gopkg.in/urfave/cli.v2
|
||||
- rm -rvf ${GOPATH%%:*}/pkg/*/gopkg.in/urfave/cli.v2.a
|
||||
|
65
CHANGELOG.md
65
CHANGELOG.md
@ -34,13 +34,70 @@
|
||||
|
||||
## [Unreleased] - (1.x series)
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Security
|
||||
|
||||
## [1.19.1] - 2016-11-21
|
||||
### Fixed
|
||||
- Fixes regression introduced in 1.19.0 where using an `ActionFunc` as
|
||||
the `Action` for a command would cause it to error rather than calling the
|
||||
function. Should not have a affected declarative cases using `func(c
|
||||
*cli.Context) err)`.
|
||||
- Shell completion now handles the case where the user specifies
|
||||
`--generate-bash-completion` immediately after a flag that takes an argument.
|
||||
Previously it call the application with `--generate-bash-completion` as the
|
||||
flag value.
|
||||
|
||||
## [1.19.0] - 2016-11-19
|
||||
### Added
|
||||
- `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`)
|
||||
- A `Description` field was added to `App` for a more detailed description of
|
||||
the application (similar to the existing `Description` field on `Command`)
|
||||
- Flag type code generation via `go generate`
|
||||
- Write to stderr and exit 1 if action returns non-nil error
|
||||
- Added support for TOML to the `altsrc` loader
|
||||
- `SkipArgReorder` was added to allow users to skip the argument reordering.
|
||||
This is useful if you want to consider all "flags" after an argument as
|
||||
arguments rather than flags (the default behavior of the stdlib `flag`
|
||||
library). This is backported functionality from the [removal of the flag
|
||||
reordering](https://github.com/urfave/cli/pull/398) in the unreleased version
|
||||
2
|
||||
- For formatted errors (those implementing `ErrorFormatter`), the errors will
|
||||
be formatted during output. Compatible with `pkg/errors`.
|
||||
|
||||
### Changed
|
||||
- Raise minimum tested/supported Go version to 1.2+
|
||||
|
||||
### Fixed
|
||||
- Consider empty environment variables as set (previously environment variables
|
||||
with the equivalent of `""` would be skipped rather than their value used).
|
||||
- Return an error if the value in a given environment variable cannot be parsed
|
||||
as the flag type. Previously these errors were silently swallowed.
|
||||
- Print full error when an invalid flag is specified (which includes the invalid flag)
|
||||
- `App.Writer` defaults to `stdout` when `nil`
|
||||
- If no action is specified on a command or app, the help is now printed instead of `panic`ing
|
||||
- `App.Metadata` is initialized automatically now (previously was `nil` unless initialized)
|
||||
- Correctly show help message if `-h` is provided to a subcommand
|
||||
- `context.(Global)IsSet` now respects environment variables. Previously it
|
||||
would return `false` if a flag was specified in the environment rather than
|
||||
as an argument
|
||||
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||
- `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This
|
||||
fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well
|
||||
as `altsrc` where Go would complain that the types didn't match
|
||||
|
||||
## [1.18.1] - 2016-08-28
|
||||
### Fixed
|
||||
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported)
|
||||
|
||||
## [1.18.0] - 2016-06-27
|
||||
### Added
|
||||
- `./runtests` test runner with coverage tracking by default
|
||||
@ -59,6 +116,10 @@
|
||||
- No longer swallows `panic`s that occur within the `Action`s themselves when
|
||||
detecting the signature of the `Action` field
|
||||
|
||||
## [1.17.1] - 2016-08-28
|
||||
### Fixed
|
||||
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||
|
||||
## [1.17.0] - 2016-05-09
|
||||
### Added
|
||||
- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc`
|
||||
@ -80,6 +141,10 @@
|
||||
- cleanups based on [Go Report Card
|
||||
feedback](https://goreportcard.com/report/github.com/urfave/cli)
|
||||
|
||||
## [1.16.1] - 2016-08-28
|
||||
### Fixed
|
||||
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||
|
||||
## [1.16.0] - 2016-05-02
|
||||
### Added
|
||||
- `Hidden` field on all flag struct types to omit from generated help text
|
||||
|
33
README.md
33
README.md
@ -461,13 +461,13 @@ error.
|
||||
|
||||
Flags for the application and commands are shown in the order they are defined.
|
||||
However, it's possible to sort them from outside this library by using `FlagsByName`
|
||||
with `sort`.
|
||||
or `CommandsByName` with `sort`.
|
||||
|
||||
For example this:
|
||||
|
||||
<!-- {
|
||||
"args": ["--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
|
||||
@ -494,7 +494,27 @@ func main() {
|
||||
},
|
||||
}
|
||||
|
||||
app.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 +1021,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 }}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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",
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
36
app.go
@ -65,6 +65,12 @@ type App struct {
|
||||
ErrWriter io.Writer
|
||||
// Other custom info
|
||||
Metadata map[string]interface{}
|
||||
// Carries a function which returns app specific info.
|
||||
ExtraInfo func() map[string]string
|
||||
// CustomAppHelpTemplate the text template for app help topic.
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
CustomAppHelpTemplate string
|
||||
|
||||
didSetup bool
|
||||
}
|
||||
@ -156,6 +162,10 @@ func (a *App) Setup() {
|
||||
if a.Metadata == nil {
|
||||
a.Metadata = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if a.Writer == nil {
|
||||
a.Writer = os.Stdout
|
||||
}
|
||||
}
|
||||
|
||||
// Run is the entry point to the cli app. Parses the arguments slice and routes
|
||||
@ -163,8 +173,20 @@ func (a *App) Setup() {
|
||||
func (a *App) Run(arguments []string) (err error) {
|
||||
a.Setup()
|
||||
|
||||
// handle the completion flag separately from the flagset since
|
||||
// completion could be attempted after a flag, but before its value was put
|
||||
// on the command line. this causes the flagset to interpret the completion
|
||||
// flag name as the value of the flag before it which is undesirable
|
||||
// note that we can only do this because the shell autocomplete function
|
||||
// always appends the completion flag at the end of the command
|
||||
shellComplete, arguments := checkShellCompleteFlag(a, arguments)
|
||||
|
||||
// parse flags
|
||||
set := flagSet(a.Name, a.Flags)
|
||||
set, err := flagSet(a.Name, a.Flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set.SetOutput(ioutil.Discard)
|
||||
err = set.Parse(arguments[1:])
|
||||
nerr := normalizeFlags(a.Flags, set)
|
||||
@ -174,6 +196,7 @@ func (a *App) Run(arguments []string) (err error) {
|
||||
ShowAppHelp(context)
|
||||
return nerr
|
||||
}
|
||||
context.shellComplete = shellComplete
|
||||
|
||||
if checkCompletions(context) {
|
||||
return nil
|
||||
@ -223,7 +246,6 @@ func (a *App) Run(arguments []string) (err error) {
|
||||
if a.Before != nil {
|
||||
beforeErr := a.Before(context)
|
||||
if beforeErr != nil {
|
||||
fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
|
||||
ShowAppHelp(context)
|
||||
HandleExitCoder(beforeErr)
|
||||
err = beforeErr
|
||||
@ -240,6 +262,10 @@ func (a *App) Run(arguments []string) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if a.Action == nil {
|
||||
a.Action = helpCommand.Action
|
||||
}
|
||||
|
||||
// Run default Action
|
||||
err = a.Action(context)
|
||||
|
||||
@ -278,7 +304,11 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
|
||||
}
|
||||
|
||||
// parse flags
|
||||
set := flagSet(a.Name, a.Flags)
|
||||
set, err := flagSet(a.Name, a.Flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set.SetOutput(ioutil.Discard)
|
||||
err = set.Parse(ctx.Args().Tail())
|
||||
nerr := normalizeFlags(a.Flags, set)
|
||||
|
293
app_test.go
293
app_test.go
@ -181,6 +181,49 @@ 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
|
||||
//
|
||||
// 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_shellComplete() {
|
||||
// set args for examples sake
|
||||
os.Args = []string{"greet", "--generate-completion"}
|
||||
@ -262,6 +305,35 @@ 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) {
|
||||
var parsedOption, firstArg string
|
||||
|
||||
app := NewApp()
|
||||
command := Command{
|
||||
Name: "cmd",
|
||||
Flags: []Flag{
|
||||
StringFlag{Name: "option", Value: "", Usage: "some option"},
|
||||
},
|
||||
Action: func(c *Context) error {
|
||||
parsedOption = c.String("option")
|
||||
firstArg = c.Args().First()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
app.Commands = []Command{command}
|
||||
|
||||
app.Run([]string{"", "cmd", "my-arg", "--option", "my-option"})
|
||||
|
||||
expect(t, parsedOption, "my-option")
|
||||
expect(t, firstArg, "my-arg")
|
||||
}
|
||||
|
||||
func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
|
||||
var context *Context
|
||||
|
||||
@ -1485,3 +1557,224 @@ 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) GetName() string {
|
||||
return c.Nombre
|
||||
}
|
||||
|
||||
func (c *customBoolFlag) Apply(set *flag.FlagSet) {
|
||||
set.String(c.Nombre, c.Nombre, "")
|
||||
}
|
||||
|
||||
func TestCustomFlagsUnused(t *testing.T) {
|
||||
app := NewApp()
|
||||
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 := NewApp()
|
||||
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 := NewApp()
|
||||
|
||||
// Be sure to reset the global flags
|
||||
defer func(helpFlag Flag, versionFlag Flag) {
|
||||
HelpFlag = helpFlag
|
||||
VersionFlag = versionFlag
|
||||
}(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 TestHandleAction_WithNonFuncAction(t *testing.T) {
|
||||
app := NewApp()
|
||||
app.Action = 42
|
||||
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 {
|
||||
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_WithInvalidFuncReturnSignature(t *testing.T) {
|
||||
app := NewApp()
|
||||
app.Action = func(_ *Context) (int, error) { return 0, 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 invalid Action signature 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_WithUnknownPanic(t *testing.T) {
|
||||
defer func() { refute(t, recover(), nil) }()
|
||||
|
||||
var fn ActionFunc
|
||||
|
||||
app := NewApp()
|
||||
app.Action = func(ctx *Context) error {
|
||||
fn(ctx)
|
||||
return 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.GetName() {
|
||||
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.GetName()})
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
10
appveyor.yml
10
appveyor.yml
@ -1,6 +1,8 @@
|
||||
version: "{build}"
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
os: Windows Server 2016
|
||||
|
||||
image: Visual Studio 2017
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\urfave\cli
|
||||
|
||||
@ -9,9 +11,9 @@ cache:
|
||||
|
||||
environment:
|
||||
GOPATH: C:\gopath
|
||||
GOVERSION: 1.6
|
||||
PYTHON: C:\Python27-x64
|
||||
PYTHON_VERSION: 2.7.x
|
||||
GOVERSION: 1.8.x
|
||||
PYTHON: C:\Python36-x64
|
||||
PYTHON_VERSION: 3.6.x
|
||||
PYTHON_ARCH: 64
|
||||
|
||||
install:
|
||||
|
20
autocomplete/bash_autocomplete
Normal file → Executable file
20
autocomplete/bash_autocomplete
Normal file → Executable 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
|
||||
|
1
cli.go
1
cli.go
@ -12,6 +12,7 @@
|
||||
// Usage: "say a greeting",
|
||||
// Action: func(c *cli.Context) error {
|
||||
// println("Greetings")
|
||||
// return nil
|
||||
// },
|
||||
// }
|
||||
//
|
||||
|
65
command.go
65
command.go
@ -49,6 +49,25 @@ type Command struct {
|
||||
// Full name of command for help, defaults to full command name, including parent commands.
|
||||
HelpName string
|
||||
commandNamePath []string
|
||||
|
||||
// CustomHelpTemplate the text template for the command help topic.
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
CustomHelpTemplate string
|
||||
}
|
||||
|
||||
type CommandsByName []Command
|
||||
|
||||
func (c CommandsByName) Len() int {
|
||||
return len(c)
|
||||
}
|
||||
|
||||
func (c CommandsByName) Less(i, j int) bool {
|
||||
return c[i].Name < c[j].Name
|
||||
}
|
||||
|
||||
func (c CommandsByName) Swap(i, j int) {
|
||||
c[i], c[j] = c[j], c[i]
|
||||
}
|
||||
|
||||
// FullName returns the full name of the command.
|
||||
@ -75,7 +94,10 @@ func (c *Command) Run(ctx *Context) (err error) {
|
||||
c.appendFlag(GenerateCompletionFlag)
|
||||
}
|
||||
|
||||
set := flagSet(c.Name, c.Flags)
|
||||
set, err := flagSet(c.Name, c.Flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
set.SetOutput(ioutil.Discard)
|
||||
|
||||
if c.SkipFlagParsing {
|
||||
@ -84,18 +106,6 @@ func (c *Command) Run(ctx *Context) (err error) {
|
||||
err = set.Parse(ctx.Args().Tail())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if c.OnUsageError != nil {
|
||||
err := c.OnUsageError(ctx, err, false)
|
||||
HandleExitCoder(err)
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(ctx.App.Writer, "Incorrect Usage:", err.Error())
|
||||
fmt.Fprintln(ctx.App.Writer)
|
||||
ShowCommandHelp(ctx, c.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
nerr := normalizeFlags(c.Flags, set)
|
||||
if nerr != nil {
|
||||
fmt.Fprintln(ctx.App.Writer, nerr)
|
||||
@ -105,11 +115,23 @@ func (c *Command) Run(ctx *Context) (err error) {
|
||||
}
|
||||
|
||||
context := NewContext(ctx.App, set, ctx)
|
||||
|
||||
context.Command = c
|
||||
if checkCommandCompletions(context, c.Name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if c.OnUsageError != nil {
|
||||
err := c.OnUsageError(context, err, false)
|
||||
HandleExitCoder(err)
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error())
|
||||
fmt.Fprintln(context.App.Writer)
|
||||
ShowCommandHelp(context, c.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
if checkCommandHelp(context, c.Name) {
|
||||
return nil
|
||||
}
|
||||
@ -131,9 +153,7 @@ 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
|
||||
}
|
||||
@ -175,14 +195,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 +212,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 +235,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}
|
||||
|
@ -123,6 +123,30 @@ func TestCommand_Run_BeforeSavesMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommand_OnUsageError_hasCommandContext(t *testing.T) {
|
||||
app := NewApp()
|
||||
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 +174,64 @@ 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 := NewApp()
|
||||
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 := NewApp()
|
||||
app.ErrWriter = ioutil.Discard
|
||||
app.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)
|
||||
}
|
||||
}
|
||||
|
13
context.go
13
context.go
@ -13,8 +13,9 @@ import (
|
||||
// can be used to retrieve context-specific args and
|
||||
// parsed command-line options.
|
||||
type Context struct {
|
||||
App *App
|
||||
Command *Command
|
||||
App *App
|
||||
Command *Command
|
||||
shellComplete bool
|
||||
|
||||
flagSet *flag.FlagSet
|
||||
parentContext *Context
|
||||
@ -22,7 +23,13 @@ type Context struct {
|
||||
|
||||
// NewContext creates a new context. For use in when invoking an App or Command action.
|
||||
func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
|
||||
return &Context{App: app, flagSet: set, parentContext: parentCtx}
|
||||
c := &Context{App: app, flagSet: set, parentContext: parentCtx}
|
||||
|
||||
if parentCtx != nil {
|
||||
c.shellComplete = parentCtx.shellComplete
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// NumFlags returns the number of flags set
|
||||
|
@ -166,9 +166,10 @@ func TestContext_IsSet_fromEnv(t *testing.T) {
|
||||
globalTimeoutIsSet, TIsSet, globalNoEnvVarIsSet, NIsSet bool
|
||||
)
|
||||
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("GLOBAL_APP_TIMEOUT_SECONDS", "15.5")
|
||||
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
|
||||
os.Setenv("APP_PASSWORD", "")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
&Float64Flag{
|
||||
@ -216,8 +217,17 @@ func TestContext_IsSet_fromEnv(t *testing.T) {
|
||||
expect(t, NIsSet, false)
|
||||
expect(t, timeoutIsSet, true)
|
||||
expect(t, tIsSet, true)
|
||||
expect(t, passwordIsSet, true)
|
||||
expect(t, pIsSet, true)
|
||||
expect(t, noEnvVarIsSet, 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) {
|
||||
@ -238,8 +248,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) {
|
||||
|
34
errors.go
34
errors.go
@ -48,6 +48,10 @@ func (m *multiError) Errors() []error {
|
||||
return errs
|
||||
}
|
||||
|
||||
type ErrorFormatter interface {
|
||||
Format(s fmt.State, verb rune)
|
||||
}
|
||||
|
||||
// ExitCoder is the interface checked by `App` and `Command` for a custom exit
|
||||
// code
|
||||
type ExitCoder interface {
|
||||
@ -57,7 +61,7 @@ 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
|
||||
@ -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,7 +92,11 @@ 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
|
||||
@ -100,9 +108,19 @@ func HandleExitCoder(err error) {
|
||||
}
|
||||
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 {
|
||||
fmt.Fprintln(ErrWriter, merr)
|
||||
if exitErr, ok := merr.(ExitCoder); ok {
|
||||
code = exitErr.ExitCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
@ -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 := NewExitError(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")
|
||||
}
|
||||
|
130
flag.go
130
flag.go
@ -4,12 +4,12 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -88,13 +88,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
|
||||
@ -109,7 +125,7 @@ func (f *GenericFlag) Apply(set *flag.FlagSet) {
|
||||
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
|
||||
}
|
||||
@ -166,15 +182,22 @@ 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
|
||||
func (f *StringSliceFlag) Apply(set *flag.FlagSet) {
|
||||
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 %s as string value for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
}
|
||||
f.Value = newVal
|
||||
break
|
||||
@ -256,17 +279,21 @@ 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
|
||||
func (f *IntSliceFlag) Apply(set *flag.FlagSet) {
|
||||
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 %s as int slice value for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
}
|
||||
f.Value = newVal
|
||||
@ -329,17 +356,21 @@ 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
|
||||
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) {
|
||||
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 %s as int64 slice value for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
}
|
||||
f.Value = newVal
|
||||
@ -361,11 +392,13 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) {
|
||||
func (f *BoolFlag) Apply(set *flag.FlagSet) {
|
||||
if f.EnvVars != nil {
|
||||
for _, envVar := range f.EnvVars {
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||
envValBool, err := strconv.ParseBool(envVal)
|
||||
if err == nil {
|
||||
f.Value = envValBool
|
||||
}
|
||||
|
||||
val = envValBool
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -384,7 +417,7 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) {
|
||||
func (f *StringFlag) Apply(set *flag.FlagSet) {
|
||||
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
|
||||
}
|
||||
@ -404,12 +437,13 @@ func (f *StringFlag) Apply(set *flag.FlagSet) {
|
||||
func (f *IntFlag) Apply(set *flag.FlagSet) {
|
||||
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 %s as int value for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
f.Value = int(envValInt)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -427,12 +461,14 @@ func (f *IntFlag) Apply(set *flag.FlagSet) {
|
||||
func (f *Int64Flag) Apply(set *flag.FlagSet) {
|
||||
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 %s as int value for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
|
||||
f.Value = envValInt
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -450,12 +486,14 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) {
|
||||
func (f *UintFlag) Apply(set *flag.FlagSet) {
|
||||
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 %s as uint value for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
|
||||
f.Value = uint(envValInt)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -473,12 +511,14 @@ func (f *UintFlag) Apply(set *flag.FlagSet) {
|
||||
func (f *Uint64Flag) Apply(set *flag.FlagSet) {
|
||||
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 %s as uint64 value for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
|
||||
f.Value = uint64(envValInt)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -496,12 +536,14 @@ func (f *Uint64Flag) Apply(set *flag.FlagSet) {
|
||||
func (f *DurationFlag) Apply(set *flag.FlagSet) {
|
||||
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 %s as duration for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
|
||||
f.Value = envValDuration
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -519,11 +561,14 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) {
|
||||
func (f *Float64Flag) Apply(set *flag.FlagSet) {
|
||||
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 %s as float64 value for flag %s: %s", envVal, f.Name, err)
|
||||
}
|
||||
|
||||
f.Value = float64(envValFloat)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -591,7 +636,7 @@ func (f *Float64Slice) Value() []float64 {
|
||||
func (f *Float64SliceFlag) Apply(set *flag.FlagSet) {
|
||||
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)
|
||||
@ -618,7 +663,8 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
156
flag_test.go
156
flag_test.go
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -41,6 +42,87 @@ func TestBoolFlagApply_SetsAllNames(t *testing.T) {
|
||||
expect(t, v, true)
|
||||
}
|
||||
|
||||
func TestFlagsFromEnv(t *testing.T) {
|
||||
var flagTests = []struct {
|
||||
input string
|
||||
output interface{}
|
||||
flag Flag
|
||||
errRegexp string
|
||||
}{
|
||||
{"", false, &BoolFlag{Name: "debug", EnvVar: "DEBUG"}, ""},
|
||||
{"1", true, &BoolFlag{Name: "debug", EnvVar: "DEBUG"}, ""},
|
||||
{"false", false, &BoolFlag{Name: "debug", EnvVar: "DEBUG"}, ""},
|
||||
{"foobar", true, &BoolFlag{Name: "debug", EnvVar: "DEBUG"}, fmt.Sprintf(`could not parse foobar as bool value for flag debug: .*`)},
|
||||
|
||||
{"1s", 1 * time.Second, &DurationFlag{Name: "time", EnvVar: "TIME"}, ""},
|
||||
{"foobar", false, &DurationFlag{Name: "time", EnvVar: "TIME"}, fmt.Sprintf(`could not parse foobar as duration for flag time: .*`)},
|
||||
|
||||
{"1.2", 1.2, &Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, ""},
|
||||
{"1", 1.0, &Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, ""},
|
||||
{"foobar", 0, &Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as float64 value for flag seconds: .*`)},
|
||||
|
||||
{"1", int64(1), &Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, ""},
|
||||
{"1.2", 0, &Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2 as int value for flag seconds: .*`)},
|
||||
{"foobar", 0, &Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as int value for flag seconds: .*`)},
|
||||
|
||||
{"1", 1, &IntFlag{Name: "seconds", EnvVar: "SECONDS"}, ""},
|
||||
{"1.2", 0, &IntFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2 as int value for flag seconds: .*`)},
|
||||
{"foobar", 0, &IntFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as int value for flag seconds: .*`)},
|
||||
|
||||
{"1,2", NewIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, ""},
|
||||
{"1.2,2", NewIntSlice(), &IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2,2 as int slice value for flag seconds: .*`)},
|
||||
{"foobar", NewIntSlice(), &IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as int slice value for flag seconds: .*`)},
|
||||
|
||||
{"1,2", NewInt64Slice(1, 2), &Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, ""},
|
||||
{"1.2,2", NewInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2,2 as int64 slice value for flag seconds: .*`)},
|
||||
{"foobar", NewInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as int64 slice value for flag seconds: .*`)},
|
||||
|
||||
{"foo", "foo", &StringFlag{Name: "name", EnvVar: "NAME"}, ""},
|
||||
|
||||
{"foo,bar", NewStringSlice("foo", "bar"), &StringSliceFlag{Name: "names", EnvVar: "NAMES"}, ""},
|
||||
|
||||
{"1", uint(1), &UintFlag{Name: "seconds", EnvVar: "SECONDS"}, ""},
|
||||
{"1.2", 0, &UintFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2 as uint value for flag seconds: .*`)},
|
||||
{"foobar", 0, &UintFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as uint value for flag seconds: .*`)},
|
||||
|
||||
{"1", uint64(1), &Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, ""},
|
||||
{"1.2", 0, &Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2 as uint64 value for flag seconds: .*`)},
|
||||
{"foobar", 0, &Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as uint64 value for flag seconds: .*`)},
|
||||
|
||||
{"foo,bar", &Parser{"foo", "bar"}, &GenericFlag{Name: "names", Value: &Parser{}, EnvVar: "NAMES"}, ""},
|
||||
}
|
||||
|
||||
for _, test := range flagTests {
|
||||
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 test.errRegexp != "" {
|
||||
if err == nil {
|
||||
t.Errorf("expected error to match %s, got none", test.errRegexp)
|
||||
} else {
|
||||
if matched, _ := regexp.MatchString(test.errRegexp, err.Error()); !matched {
|
||||
t.Errorf("expected error to match %s, got error %s", test.errRegexp, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil && test.errRegexp == "" {
|
||||
t.Errorf("expected no error got %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stringFlagTests = []struct {
|
||||
name string
|
||||
aliases []string
|
||||
@ -78,7 +160,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 +212,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 +257,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 +304,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 +340,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 +376,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 +412,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 +462,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 +511,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 +547,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 +598,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 +635,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 +697,7 @@ func TestParseDestinationString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_COUNT", "20")
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
@ -634,7 +716,7 @@ func TestParseMultiStringFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_COUNT", "20")
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
@ -706,7 +788,7 @@ func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
@ -726,7 +808,7 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
@ -746,7 +828,7 @@ func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
@ -766,7 +848,7 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
@ -823,7 +905,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 +925,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 +996,7 @@ func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiIntSliceFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
@ -934,7 +1016,7 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
@ -954,7 +1036,7 @@ func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiIntSliceFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
@ -991,7 +1073,7 @@ func TestParseMultiInt64Slice(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiInt64SliceFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,17179869184")
|
||||
|
||||
(&App{
|
||||
@ -1011,7 +1093,7 @@ func TestParseMultiInt64SliceFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,17179869184")
|
||||
|
||||
(&App{
|
||||
@ -1068,7 +1150,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 +1170,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 +1190,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 +1210,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 +1267,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 +1287,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 +1346,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 +1371,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 +1413,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 +1436,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 +1461,7 @@ func TestParseGenericFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseGenericFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
clearenv()
|
||||
os.Setenv("APP_FOO", "99,2000")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
|
@ -104,7 +104,9 @@ 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 = tempfile.NamedTemporaryFile(
|
||||
suffix='.go', mode='w', delete=False, encoding='utf-8'
|
||||
)
|
||||
writefunc(tmp, types)
|
||||
tmp.close()
|
||||
|
||||
@ -222,11 +224,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 = {
|
||||
|
102
help.go
102
help.go
@ -13,7 +13,7 @@ import (
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
var AppHelpTemplate = `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
|
||||
|
||||
USAGE:
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
|
||||
@ -47,7 +47,7 @@ var CommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}}
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
||||
|
||||
CATEGORY:
|
||||
{{.Category}}{{end}}{{if .Description}}
|
||||
@ -64,10 +64,10 @@ OPTIONS:
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
var SubcommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
{{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}}
|
||||
|
||||
USAGE:
|
||||
{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}
|
||||
|
||||
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
||||
{{.Name}}:{{end}}{{range .VisibleCommands}}
|
||||
@ -112,17 +112,43 @@ var helpSubcommand = &Command{
|
||||
// Prints help for the App or Command
|
||||
type helpPrinter func(w io.Writer, templ string, data interface{})
|
||||
|
||||
// Prints help for the App or Command with custom template function.
|
||||
type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{})
|
||||
|
||||
// HelpPrinter is a function that writes the help output. If not set a default
|
||||
// is used. The function signature is:
|
||||
// func(w io.Writer, templ string, data interface{})
|
||||
var HelpPrinter helpPrinter = printHelp
|
||||
|
||||
// HelpPrinterCustom is same as HelpPrinter but
|
||||
// takes a custom function for template function map.
|
||||
var HelpPrinterCustom helpPrinterCustom = printHelpCustom
|
||||
|
||||
// VersionPrinter prints the version for the App
|
||||
var VersionPrinter = printVersion
|
||||
|
||||
// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code.
|
||||
func ShowAppHelpAndExit(c *Context, exitCode int) {
|
||||
ShowAppHelp(c)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
// ShowAppHelp is an action that displays the help.
|
||||
func ShowAppHelp(c *Context) {
|
||||
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
|
||||
func ShowAppHelp(c *Context) (err error) {
|
||||
if c.App.CustomAppHelpTemplate == "" {
|
||||
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
|
||||
return
|
||||
}
|
||||
customAppData := func() map[string]interface{} {
|
||||
if c.App.ExtraInfo == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"ExtraInfo": c.App.ExtraInfo,
|
||||
}
|
||||
}
|
||||
HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData())
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultAppComplete prints the list of subcommands as the default app completion method
|
||||
@ -137,6 +163,12 @@ func DefaultAppComplete(c *Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ShowCommandHelpAndExit - exits with code after showing help
|
||||
func ShowCommandHelpAndExit(c *Context, command string, code int) {
|
||||
ShowCommandHelp(c, command)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// ShowCommandHelp prints help for the given command
|
||||
func ShowCommandHelp(ctx *Context, command string) error {
|
||||
// show the subcommand help for a command with subcommands
|
||||
@ -147,7 +179,11 @@ func ShowCommandHelp(ctx *Context, command string) error {
|
||||
|
||||
for _, c := range ctx.App.Commands {
|
||||
if c.HasName(command) {
|
||||
HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c)
|
||||
if c.CustomHelpTemplate != "" {
|
||||
HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil)
|
||||
} else {
|
||||
HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -198,10 +234,15 @@ func ShowCommandCompletions(ctx *Context, command string) {
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp(out io.Writer, templ string, data interface{}) {
|
||||
func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) {
|
||||
funcMap := template.FuncMap{
|
||||
"join": strings.Join,
|
||||
}
|
||||
if customFunc != nil {
|
||||
for key, value := range customFunc {
|
||||
funcMap[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
|
||||
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
|
||||
@ -219,6 +260,10 @@ 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 != "" {
|
||||
@ -261,22 +306,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.EnableBashCompletion {
|
||||
return false, arguments
|
||||
}
|
||||
|
||||
return false
|
||||
pos := len(arguments) - 1
|
||||
lastArg := arguments[pos]
|
||||
|
||||
if lastArg != "--"+BashCompletionFlag.GetName() {
|
||||
return false, arguments
|
||||
}
|
||||
|
||||
return true, arguments[:pos]
|
||||
}
|
||||
|
||||
func checkCompletions(c *Context) bool {
|
||||
if !c.Bool(GenerateCompletionFlag.Name) && !c.App.EnableShellCompletion {
|
||||
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.Bool(GenerateCompletionFlag.Name) && !c.App.EnableShellCompletion {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
ShowCommandCompletions(c, name)
|
||||
return true
|
||||
}
|
||||
|
||||
func checkInitCompletion(c *Context) (bool, error) {
|
||||
|
163
help_test.go
163
help_test.go
@ -3,6 +3,8 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@ -255,6 +257,92 @@ func TestShowSubcommandHelp_CommandAliases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowCommandHelp_Customtemplate(t *testing.T) {
|
||||
app := &App{
|
||||
Commands: []Command{
|
||||
{
|
||||
Name: "frobbly",
|
||||
Action: func(ctx *Context) error {
|
||||
return nil
|
||||
},
|
||||
HelpName: "foo frobbly",
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.HelpName}} [FLAGS] TARGET [TARGET ...]
|
||||
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Frobbly runs with this param locally.
|
||||
$ {{.HelpName}} wobbly
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
output := &bytes.Buffer{}
|
||||
app.Writer = output
|
||||
app.Run([]string{"foo", "help", "frobbly"})
|
||||
|
||||
if strings.Contains(output.String(), "2. Frobbly runs without this param locally.") {
|
||||
t.Errorf("expected output to exclude \"2. Frobbly runs without this param locally.\"; got: %q", output.String())
|
||||
}
|
||||
|
||||
if !strings.Contains(output.String(), "1. Frobbly runs with this param locally.") {
|
||||
t.Errorf("expected output to include \"1. Frobbly runs with this param locally.\"; got: %q", output.String())
|
||||
}
|
||||
|
||||
if !strings.Contains(output.String(), "$ foo frobbly wobbly") {
|
||||
t.Errorf("expected output to include \"$ foo frobbly wobbly\"; got: %q", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowSubcommandHelp_CommandUsageText(t *testing.T) {
|
||||
app := &App{
|
||||
Commands: []Command{
|
||||
{
|
||||
Name: "frobbly",
|
||||
UsageText: "this is usage text",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output := &bytes.Buffer{}
|
||||
app.Writer = output
|
||||
|
||||
app.Run([]string{"foo", "frobbly", "--help"})
|
||||
|
||||
if !strings.Contains(output.String(), "this is usage text") {
|
||||
t.Errorf("expected output to include usage text; got: %q", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) {
|
||||
app := &App{
|
||||
Commands: []Command{
|
||||
{
|
||||
Name: "frobbly",
|
||||
Subcommands: []Command{
|
||||
{
|
||||
Name: "bobbly",
|
||||
UsageText: "this is usage text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output := &bytes.Buffer{}
|
||||
app.Writer = output
|
||||
app.Run([]string{"foo", "frobbly", "bobbly", "--help"})
|
||||
|
||||
if !strings.Contains(output.String(), "this is usage text") {
|
||||
t.Errorf("expected output to include usage text; got: %q", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowAppHelp_HiddenCommand(t *testing.T) {
|
||||
app := &App{
|
||||
Commands: []*Command{
|
||||
@ -286,3 +374,78 @@ func TestShowAppHelp_HiddenCommand(t *testing.T) {
|
||||
t.Errorf("expected output to include \"frobbly\"; got: %q", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowAppHelp_CustomAppTemplate(t *testing.T) {
|
||||
app := &App{
|
||||
Commands: []Command{
|
||||
{
|
||||
Name: "frobbly",
|
||||
Action: func(ctx *Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "secretfrob",
|
||||
Hidden: true,
|
||||
Action: func(ctx *Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
ExtraInfo: func() map[string]string {
|
||||
platform := fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH)
|
||||
goruntime := fmt.Sprintf("Version: %s | CPUs: %d", runtime.Version(), runtime.NumCPU())
|
||||
return map[string]string{
|
||||
"PLATFORM": platform,
|
||||
"RUNTIME": goruntime,
|
||||
}
|
||||
},
|
||||
CustomAppHelpTemplate: `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...]
|
||||
|
||||
COMMANDS:
|
||||
{{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}{{if .VisibleFlags}}
|
||||
GLOBAL FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
VERSION:
|
||||
2.0.0
|
||||
{{"\n"}}{{range $key, $value := ExtraInfo}}
|
||||
{{$key}}:
|
||||
{{$value}}
|
||||
{{end}}`,
|
||||
}
|
||||
|
||||
output := &bytes.Buffer{}
|
||||
app.Writer = output
|
||||
app.Run([]string{"app", "--help"})
|
||||
|
||||
if strings.Contains(output.String(), "secretfrob") {
|
||||
t.Errorf("expected output to exclude \"secretfrob\"; got: %q", output.String())
|
||||
}
|
||||
|
||||
if !strings.Contains(output.String(), "frobbly") {
|
||||
t.Errorf("expected output to include \"frobbly\"; got: %q", output.String())
|
||||
}
|
||||
|
||||
if !strings.Contains(output.String(), "PLATFORM:") ||
|
||||
!strings.Contains(output.String(), "OS:") ||
|
||||
!strings.Contains(output.String(), "Arch:") {
|
||||
t.Errorf("expected output to include \"PLATFORM:, OS: and Arch:\"; got: %q", output.String())
|
||||
}
|
||||
|
||||
if !strings.Contains(output.String(), "RUNTIME:") ||
|
||||
!strings.Contains(output.String(), "Version:") ||
|
||||
!strings.Contains(output.String(), "CPUs:") {
|
||||
t.Errorf("expected output to include \"RUNTIME:, Version: and CPUs:\"; got: %q", output.String())
|
||||
}
|
||||
|
||||
if !strings.Contains(output.String(), "VERSION:") ||
|
||||
!strings.Contains(output.String(), "2.0.0") {
|
||||
t.Errorf("expected output to include \"VERSION:, 2.0.0\"; got: %q", output.String())
|
||||
}
|
||||
}
|
||||
|
9
helpers_unix_test.go
Normal file
9
helpers_unix_test.go
Normal 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
20
helpers_windows_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user