Merge branch 'main' into feature/1316-simplified_flag_value_access

This commit is contained in:
Dan Buch 2022-04-26 08:12:17 -04:00 committed by GitHub
commit 8cc43782ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1025 additions and 564 deletions

17
.github/stale.yml vendored
View File

@ -1,17 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 90
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -37,12 +37,18 @@ jobs:
- name: vet
run: go run internal/build/build.go vet
- name: test with tags
run: go run internal/build/build.go -tags urfave_cli_no_docs test
- name: test
run: go run internal/build/build.go test
- name: check-binary-size
run: go run internal/build/build.go check-binary-size
- name: check-binary-size with tags (informational only)
run: go run internal/build/build.go -tags urfave_cli_no_docs check-binary-size || true
- name: Upload coverage to Codecov
if: success() && matrix.go == '1.18.x' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v2

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2016 Jeremy Saenz & Contributors
Copyright (c) 2022 urfave/cli maintainers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -55,6 +55,16 @@ import (
...
```
### Build tags
You can use the following build tags:
#### `urfave_cli_no_docs`
When set, this removes `ToMarkdown` and `ToMan` methods, so your application
won't be able to call those. This reduces the resulting binary size by about
300-400 KB (measured using Go 1.18.1 on Linux/amd64), due to less dependencies.
### GOPATH
Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can
@ -68,3 +78,7 @@ export PATH=$PATH:$GOPATH/bin
cli is tested against multiple versions of Go on Linux, and against the latest
released version of Go on OS X and Windows. This project uses Github Actions for
builds. To see our currently supported go versions and platforms, look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/main/.github/workflows/cli.yml).
## License
See [`LICENSE`](./LICENSE)

View File

@ -13,18 +13,18 @@ import (
// allows a value to be set on the existing parsed flags.
type FlagInputSourceExtension interface {
cli.Flag
ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error
ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error
}
// ApplyInputSourceValues iterates over all provided flags and
// executes ApplyInputSourceValue on flags implementing the
// FlagInputSourceExtension interface to initialize these flags
// to an alternate input source.
func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error {
func ApplyInputSourceValues(cCtx *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error {
for _, f := range flags {
inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension)
if isType {
err := inputSourceExtendedFlag.ApplyInputSourceValue(context, inputSourceContext)
err := inputSourceExtendedFlag.ApplyInputSourceValue(cCtx, inputSourceContext)
if err != nil {
return err
}
@ -38,42 +38,40 @@ func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSource
// input source based on the func provided. If there is no error it will then apply the new input source to any flags
// that are supported by the input source
func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc {
return func(context *cli.Context) error {
return func(cCtx *cli.Context) error {
inputSource, err := createInputSource()
if err != nil {
return fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error())
}
return ApplyInputSourceValues(context, inputSource, flags)
return ApplyInputSourceValues(cCtx, inputSource, flags)
}
}
// InitInputSourceWithContext is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new
// input source based on the func provided with potentially using existing cli.Context values to initialize itself. If there is
// no error it will then apply the new input source to any flags that are supported by the input source
func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(context *cli.Context) (InputSourceContext, error)) cli.BeforeFunc {
return func(context *cli.Context) error {
inputSource, err := createInputSource(context)
func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(cCtx *cli.Context) (InputSourceContext, error)) cli.BeforeFunc {
return func(cCtx *cli.Context) error {
inputSource, err := createInputSource(cCtx)
if err != nil {
return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error())
}
return ApplyInputSourceValues(context, inputSource, flags)
return ApplyInputSourceValues(cCtx, inputSource, flags)
}
}
// ApplyInputSourceValue applies a generic value to the flagSet if required
func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) {
value, err := isc.Generic(f.GenericFlag.Name)
if err != nil {
return err
}
if value != nil {
for _, name := range f.Names() {
_ = f.set.Set(name, value.String())
}
func (f *GenericFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.GenericFlag.Name) {
value, err := isc.Generic(f.GenericFlag.Name)
if err != nil {
return err
}
if value != nil {
for _, name := range f.Names() {
_ = f.set.Set(name, value.String())
}
}
}
@ -82,20 +80,18 @@ func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourc
}
// ApplyInputSourceValue applies a StringSlice value to the flagSet if required
func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) {
value, err := isc.StringSlice(f.StringSliceFlag.Name)
if err != nil {
return err
}
if value != nil {
var sliceValue cli.StringSlice = *(cli.NewStringSlice(value...))
for _, name := range f.Names() {
underlyingFlag := f.set.Lookup(name)
if underlyingFlag != nil {
underlyingFlag.Value = &sliceValue
}
func (f *StringSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.StringSliceFlag.Name) {
value, err := isc.StringSlice(f.StringSliceFlag.Name)
if err != nil {
return err
}
if value != nil {
var sliceValue cli.StringSlice = *(cli.NewStringSlice(value...))
for _, name := range f.Names() {
underlyingFlag := f.set.Lookup(name)
if underlyingFlag != nil {
underlyingFlag.Value = &sliceValue
}
}
}
@ -104,20 +100,18 @@ func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputS
}
// ApplyInputSourceValue applies a IntSlice value if required
func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) {
value, err := isc.IntSlice(f.IntSliceFlag.Name)
if err != nil {
return err
}
if value != nil {
var sliceValue cli.IntSlice = *(cli.NewIntSlice(value...))
for _, name := range f.Names() {
underlyingFlag := f.set.Lookup(name)
if underlyingFlag != nil {
underlyingFlag.Value = &sliceValue
}
func (f *IntSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.IntSliceFlag.Name) {
value, err := isc.IntSlice(f.IntSliceFlag.Name)
if err != nil {
return err
}
if value != nil {
var sliceValue cli.IntSlice = *(cli.NewIntSlice(value...))
for _, name := range f.Names() {
underlyingFlag := f.set.Lookup(name)
if underlyingFlag != nil {
underlyingFlag.Value = &sliceValue
}
}
}
@ -126,17 +120,15 @@ func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSour
}
// ApplyInputSourceValue applies a Bool value to the flagSet if required
func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) {
value, err := isc.Bool(f.BoolFlag.Name)
if err != nil {
return err
}
if value {
for _, name := range f.Names() {
_ = f.set.Set(name, strconv.FormatBool(value))
}
func (f *BoolFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.BoolFlag.Name) {
value, err := isc.Bool(f.BoolFlag.Name)
if err != nil {
return err
}
if value {
for _, name := range f.Names() {
_ = f.set.Set(name, strconv.FormatBool(value))
}
}
}
@ -144,17 +136,15 @@ func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo
}
// ApplyInputSourceValue applies a String value to the flagSet if required
func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.String(f.StringFlag.Name)
if err != nil {
return err
}
if value != "" {
for _, name := range f.Names() {
_ = f.set.Set(name, value)
}
func (f *StringFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.StringFlag.Name) {
value, err := isc.String(f.StringFlag.Name)
if err != nil {
return err
}
if value != "" {
for _, name := range f.Names() {
_ = f.set.Set(name, value)
}
}
}
@ -162,27 +152,25 @@ func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSource
}
// ApplyInputSourceValue applies a Path value to the flagSet if required
func (f *PathFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.String(f.PathFlag.Name)
if err != nil {
return err
}
if value != "" {
for _, name := range f.Names() {
func (f *PathFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.PathFlag.Name) {
value, err := isc.String(f.PathFlag.Name)
if err != nil {
return err
}
if value != "" {
for _, name := range f.Names() {
if !filepath.IsAbs(value) && isc.Source() != "" {
basePathAbs, err := filepath.Abs(isc.Source())
if err != nil {
return err
}
value = filepath.Join(filepath.Dir(basePathAbs), value)
if !filepath.IsAbs(value) && isc.Source() != "" {
basePathAbs, err := filepath.Abs(isc.Source())
if err != nil {
return err
}
_ = f.set.Set(name, value)
value = filepath.Join(filepath.Dir(basePathAbs), value)
}
_ = f.set.Set(name, value)
}
}
}
@ -190,55 +178,43 @@ func (f *PathFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo
}
// ApplyInputSourceValue applies a int value to the flagSet if required
func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.Int(f.IntFlag.Name)
if err != nil {
return err
}
if value > 0 {
for _, name := range f.Names() {
_ = f.set.Set(name, strconv.FormatInt(int64(value), 10))
}
}
func (f *IntFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.IntFlag.Name) {
value, err := isc.Int(f.IntFlag.Name)
if err != nil {
return err
}
for _, name := range f.Names() {
_ = f.set.Set(name, strconv.FormatInt(int64(value), 10))
}
}
return nil
}
// ApplyInputSourceValue applies a Duration value to the flagSet if required
func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.Duration(f.DurationFlag.Name)
if err != nil {
return err
}
if value > 0 {
for _, name := range f.Names() {
_ = f.set.Set(name, value.String())
}
}
func (f *DurationFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.DurationFlag.Name) {
value, err := isc.Duration(f.DurationFlag.Name)
if err != nil {
return err
}
for _, name := range f.Names() {
_ = f.set.Set(name, value.String())
}
}
return nil
}
// ApplyInputSourceValue applies a Float64 value to the flagSet if required
func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.Float64(f.Float64Flag.Name)
if err != nil {
return err
}
if value > 0 {
floatStr := float64ToString(value)
for _, name := range f.Names() {
_ = f.set.Set(name, floatStr)
}
}
func (f *Float64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.Float64Flag.Name) {
value, err := isc.Float64(f.Float64Flag.Name)
if err != nil {
return err
}
floatStr := float64ToString(value)
for _, name := range f.Names() {
_ = f.set.Set(name, floatStr)
}
}
return nil

View File

@ -26,29 +26,48 @@ type testApplyInputSource struct {
MapValue interface{}
}
type racyInputSource struct {
*MapInputSource
}
func (ris *racyInputSource) isSet(name string) bool {
if _, ok := ris.MapInputSource.valueMap[name]; ok {
ris.MapInputSource.valueMap[name] = bogus{0}
}
return true
}
func TestGenericApplyInputSourceValue(t *testing.T) {
v := &Parser{"abc", "def"}
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &Parser{}}),
FlagName: "test",
MapValue: v,
})
}
c := runTest(t, tis)
expect(t, v, c.Generic("test"))
c = runRacyTest(t, tis)
refute(t, v, c.Generic("test"))
}
func TestGenericApplyInputSourceMethodContextSet(t *testing.T) {
p := &Parser{"abc", "def"}
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &Parser{}}),
FlagName: "test",
MapValue: &Parser{"efg", "hig"},
ContextValueString: p.String(),
})
}
c := runTest(t, tis)
expect(t, p, c.Generic("test"))
c = runRacyTest(t, tis)
refute(t, p, c.Generic("test"))
}
func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{
Name: "test",
Value: &Parser{},
@ -58,17 +77,25 @@ func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) {
MapValue: &Parser{"efg", "hij"},
EnvVarName: "TEST",
EnvVarValue: "abc,def",
})
}
c := runTest(t, tis)
expect(t, &Parser{"abc", "def"}, c.Generic("test"))
c = runRacyTest(t, tis)
refute(t, &Parser{"abc", "def"}, c.Generic("test"))
}
func TestStringSliceApplyInputSourceValue(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []interface{}{"hello", "world"},
})
}
c := runTest(t, tis)
expect(t, c.StringSlice("test"), []string{"hello", "world"})
c = runRacyTest(t, tis)
refute(t, c.StringSlice("test"), []string{"hello", "world"})
}
func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) {
@ -82,112 +109,154 @@ func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) {
}
func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: []interface{}{"hello", "world"},
EnvVarName: "TEST",
EnvVarValue: "oh,no",
})
}
c := runTest(t, tis)
expect(t, c.StringSlice("test"), []string{"oh", "no"})
c = runRacyTest(t, tis)
refute(t, c.StringSlice("test"), []string{"oh", "no"})
}
func TestIntSliceApplyInputSourceValue(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []interface{}{1, 2},
})
}
c := runTest(t, tis)
expect(t, c.IntSlice("test"), []int{1, 2})
c = runRacyTest(t, tis)
refute(t, c.IntSlice("test"), []int{1, 2})
}
func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []interface{}{1, 2},
ContextValueString: "3",
})
}
c := runTest(t, tis)
expect(t, c.IntSlice("test"), []int{3})
c = runRacyTest(t, tis)
refute(t, c.IntSlice("test"), []int{3})
}
func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: []interface{}{1, 2},
EnvVarName: "TEST",
EnvVarValue: "3,4",
})
}
c := runTest(t, tis)
expect(t, c.IntSlice("test"), []int{3, 4})
c = runRacyTest(t, tis)
refute(t, c.IntSlice("test"), []int{3, 4})
}
func TestBoolApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}),
FlagName: "test",
MapValue: true,
})
}
c := runTest(t, tis)
expect(t, true, c.Bool("test"))
c = runRacyTest(t, tis)
refute(t, true, c.Bool("test"))
}
func TestBoolApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}),
FlagName: "test",
MapValue: false,
ContextValueString: "true",
})
}
c := runTest(t, tis)
expect(t, true, c.Bool("test"))
c = runRacyTest(t, tis)
refute(t, true, c.Bool("test"))
}
func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewBoolFlag(&cli.BoolFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: false,
EnvVarName: "TEST",
EnvVarValue: "true",
})
}
c := runTest(t, tis)
expect(t, true, c.Bool("test"))
c = runRacyTest(t, tis)
refute(t, true, c.Bool("test"))
}
func TestStringApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewStringFlag(&cli.StringFlag{Name: "test"}),
FlagName: "test",
MapValue: "hello",
})
}
c := runTest(t, tis)
expect(t, "hello", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "hello", c.String("test"))
}
func TestStringApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewStringFlag(&cli.StringFlag{Name: "test"}),
FlagName: "test",
MapValue: "hello",
ContextValueString: "goodbye",
})
}
c := runTest(t, tis)
expect(t, "goodbye", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "goodbye", c.String("test"))
}
func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewStringFlag(&cli.StringFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: "hello",
EnvVarName: "TEST",
EnvVarValue: "goodbye",
})
}
c := runTest(t, tis)
expect(t, "goodbye", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "goodbye", c.String("test"))
}
func TestPathApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test"}),
FlagName: "test",
MapValue: "hello",
SourcePath: "/path/to/source/file",
})
}
c := runTest(t, tis)
expected := "/path/to/source/hello"
if runtime.GOOS == "windows" {
@ -200,119 +269,214 @@ func TestPathApplyInputSourceMethodSet(t *testing.T) {
}
}
expect(t, expected, c.String("test"))
c = runRacyTest(t, tis)
refute(t, expected, c.String("test"))
}
func TestPathApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test"}),
FlagName: "test",
MapValue: "hello",
ContextValueString: "goodbye",
SourcePath: "/path/to/source/file",
})
}
c := runTest(t, tis)
expect(t, "goodbye", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "goodbye", c.String("test"))
}
func TestPathApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: "hello",
EnvVarName: "TEST",
EnvVarValue: "goodbye",
SourcePath: "/path/to/source/file",
})
}
c := runTest(t, tis)
expect(t, "goodbye", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "goodbye", c.String("test"))
}
func TestIntApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewIntFlag(&cli.IntFlag{Name: "test"}),
FlagName: "test",
MapValue: 15,
})
}
c := runTest(t, tis)
expect(t, 15, c.Int("test"))
c = runRacyTest(t, tis)
refute(t, 15, c.Int("test"))
}
func TestIntApplyInputSourceMethodSetNegativeValue(t *testing.T) {
tis := testApplyInputSource{
Flag: NewIntFlag(&cli.IntFlag{Name: "test"}),
FlagName: "test",
MapValue: -1,
}
c := runTest(t, tis)
expect(t, -1, c.Int("test"))
c = runRacyTest(t, tis)
refute(t, -1, c.Int("test"))
}
func TestIntApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewIntFlag(&cli.IntFlag{Name: "test"}),
FlagName: "test",
MapValue: 15,
ContextValueString: "7",
})
}
c := runTest(t, tis)
expect(t, 7, c.Int("test"))
c = runRacyTest(t, tis)
refute(t, 7, c.Int("test"))
}
func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: 15,
EnvVarName: "TEST",
EnvVarValue: "12",
})
}
c := runTest(t, tis)
expect(t, 12, c.Int("test"))
c = runRacyTest(t, tis)
refute(t, 12, c.Int("test"))
}
func TestDurationApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}),
FlagName: "test",
MapValue: 30 * time.Second,
})
}
c := runTest(t, tis)
expect(t, 30*time.Second, c.Duration("test"))
c = runRacyTest(t, tis)
refute(t, 30*time.Second, c.Duration("test"))
}
func TestDurationApplyInputSourceMethodSetNegativeValue(t *testing.T) {
tis := testApplyInputSource{
Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}),
FlagName: "test",
MapValue: -30 * time.Second,
}
c := runTest(t, tis)
expect(t, -30*time.Second, c.Duration("test"))
c = runRacyTest(t, tis)
refute(t, -30*time.Second, c.Duration("test"))
}
func TestDurationApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}),
FlagName: "test",
MapValue: 30 * time.Second,
ContextValueString: (15 * time.Second).String(),
})
}
c := runTest(t, tis)
expect(t, 15*time.Second, c.Duration("test"))
c = runRacyTest(t, tis)
refute(t, 15*time.Second, c.Duration("test"))
}
func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewDurationFlag(&cli.DurationFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: 30 * time.Second,
EnvVarName: "TEST",
EnvVarValue: (15 * time.Second).String(),
})
}
c := runTest(t, tis)
expect(t, 15*time.Second, c.Duration("test"))
c = runRacyTest(t, tis)
refute(t, 15*time.Second, c.Duration("test"))
}
func TestFloat64ApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}),
FlagName: "test",
MapValue: 1.3,
})
}
c := runTest(t, tis)
expect(t, 1.3, c.Float64("test"))
c = runRacyTest(t, tis)
refute(t, 1.3, c.Float64("test"))
}
func TestFloat64ApplyInputSourceMethodSetNegativeValue(t *testing.T) {
tis := testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}),
FlagName: "test",
MapValue: -1.3,
}
c := runTest(t, tis)
expect(t, -1.3, c.Float64("test"))
c = runRacyTest(t, tis)
refute(t, -1.3, c.Float64("test"))
}
func TestFloat64ApplyInputSourceMethodSetNegativeValueNotSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test1"}),
FlagName: "test1",
// dont set map value
})
expect(t, 0.0, c.Float64("test1"))
}
func TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}),
FlagName: "test",
MapValue: 1.3,
ContextValueString: fmt.Sprintf("%v", 1.4),
})
}
c := runTest(t, tis)
expect(t, 1.4, c.Float64("test"))
c = runRacyTest(t, tis)
refute(t, 1.4, c.Float64("test"))
}
func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
tis := testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: 1.3,
EnvVarName: "TEST",
EnvVarValue: fmt.Sprintf("%v", 1.4),
})
}
c := runTest(t, tis)
expect(t, 1.4, c.Float64("test"))
c = runRacyTest(t, tis)
refute(t, 1.4, c.Float64("test"))
}
func runTest(t *testing.T, test testApplyInputSource) *cli.Context {
@ -340,6 +504,19 @@ func runTest(t *testing.T, test testApplyInputSource) *cli.Context {
return c
}
func runRacyTest(t *testing.T, test testApplyInputSource) *cli.Context {
set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError)
c := cli.NewContext(nil, set, nil)
_ = test.Flag.ApplyInputSourceValue(c, &racyInputSource{
MapInputSource: &MapInputSource{
file: test.SourcePath,
valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue},
},
})
return c
}
type Parser [2]string
func (p *Parser) Set(value string) error {
@ -357,3 +534,5 @@ func (p *Parser) Set(value string) error {
func (p *Parser) String() string {
return fmt.Sprintf("%s,%s", p[0], p[1])
}
type bogus [1]uint

View File

@ -22,7 +22,10 @@ func expect(t *testing.T, a interface{}, b interface{}) {
}
func refute(t *testing.T, a interface{}, b interface{}) {
if a == b {
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
_, fn, line, _ := runtime.Caller(1)
fn = strings.Replace(fn, wd+"/", "", -1)
if reflect.DeepEqual(a, b) {
t.Errorf("(%s:%d) Did not expect %v (type %v) - Got %v (type %v)", fn, line, b, reflect.TypeOf(b), a, reflect.TypeOf(a))
}
}

View File

@ -22,4 +22,6 @@ type InputSourceContext interface {
IntSlice(name string) ([]int, error)
Generic(name string) (cli.Generic, error)
Bool(name string) (bool, error)
isSet(name string) bool
}

View File

@ -16,9 +16,9 @@ import (
// variables from a file containing JSON data with the file name defined
// by the given flag.
func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) {
if context.IsSet(flag) {
return NewJSONSourceFromFile(context.String(flag))
return func(cCtx *cli.Context) (InputSourceContext, error) {
if cCtx.IsSet(flag) {
return NewJSONSourceFromFile(cCtx.String(flag))
}
return defaultInputSource()
@ -184,6 +184,11 @@ func (x *jsonSource) Bool(name string) (bool, error) {
return v, nil
}
func (x *jsonSource) isSet(name string) bool {
_, err := x.getValue(name)
return err == nil
}
func (x *jsonSource) getValue(key string) (interface{}, error) {
return jsonGetValue(key, x.deserialized)
}

View File

@ -244,6 +244,15 @@ func (fsm *MapInputSource) Bool(name string) (bool, error) {
return false, nil
}
func (fsm *MapInputSource) isSet(name string) bool {
if _, exists := fsm.valueMap[name]; exists {
return exists
}
_, exists := nestedVal(name, fsm.valueMap)
return exists
}
func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error {
valueType := reflect.TypeOf(value)
valueTypeName := ""

View File

@ -85,10 +85,10 @@ func NewTomlSourceFromFile(file string) (InputSourceContext, error) {
}
// NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context.
func NewTomlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) {
if context.IsSet(flagFileName) {
filePath := context.String(flagFileName)
func NewTomlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (InputSourceContext, error) {
return func(cCtx *cli.Context) (InputSourceContext, error) {
if cCtx.IsSet(flagFileName) {
filePath := cCtx.String(flagFileName)
return NewTomlSourceFromFile(filePath)
}

View File

@ -31,10 +31,10 @@ func NewYamlSourceFromFile(file string) (InputSourceContext, error) {
}
// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context.
func NewYamlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) {
if context.IsSet(flagFileName) {
filePath := context.String(flagFileName)
func NewYamlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (InputSourceContext, error) {
return func(cCtx *cli.Context) (InputSourceContext, error) {
if cCtx.IsSet(flagFileName) {
filePath := cCtx.String(flagFileName)
return NewYamlSourceFromFile(filePath)
}

View File

@ -0,0 +1,87 @@
package altsrc_test
import (
"fmt"
"log"
"os"
"time"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
func ExampleApp_Run_yamlFileLoaderDuration() {
execServe := func(c *cli.Context) error {
keepaliveInterval := c.Duration("keepalive-interval")
fmt.Printf("keepalive %s\n", keepaliveInterval)
return nil
}
fileExists := func(filename string) bool {
stat, _ := os.Stat(filename)
return stat != nil
}
// initConfigFileInputSource is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks
// if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails.
initConfigFileInputSource := func(configFlag string, flags []cli.Flag) cli.BeforeFunc {
return func(context *cli.Context) error {
configFile := context.String(configFlag)
if context.IsSet(configFlag) && !fileExists(configFile) {
return fmt.Errorf("config file %s does not exist", configFile)
} else if !context.IsSet(configFlag) && !fileExists(configFile) {
return nil
}
inputSource, err := altsrc.NewYamlSourceFromFile(configFile)
if err != nil {
return err
}
return altsrc.ApplyInputSourceValues(context, inputSource, flags)
}
}
flagsServe := []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
EnvVars: []string{"CONFIG_FILE"},
Value: "../testdata/empty.yml",
DefaultText: "../testdata/empty.yml",
Usage: "config file",
},
altsrc.NewDurationFlag(
&cli.DurationFlag{
Name: "keepalive-interval",
Aliases: []string{"k"},
EnvVars: []string{"KEEPALIVE_INTERVAL"},
Value: 45 * time.Second,
Usage: "interval of keepalive messages",
},
),
}
cmdServe := &cli.Command{
Name: "serve",
Usage: "Run the server",
UsageText: "serve [OPTIONS..]",
Action: execServe,
Flags: flagsServe,
Before: initConfigFileInputSource("config", flagsServe),
}
c := &cli.App{
Name: "cmd",
HideVersion: true,
UseShortOptionHandling: true,
Commands: []*cli.Command{
cmdServe,
},
}
if err := c.Run([]string{"cmd", "serve", "--config", "../testdata/empty.yml"}); err != nil {
log.Fatal(err)
}
// Output:
// keepalive 45s
}

90
app.go
View File

@ -245,48 +245,48 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
err = parseIter(set, a, arguments[1:], shellComplete)
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, &Context{Context: ctx})
cCtx := NewContext(a, set, &Context{Context: ctx})
if nerr != nil {
_, _ = fmt.Fprintln(a.Writer, nerr)
_ = ShowAppHelp(context)
_ = ShowAppHelp(cCtx)
return nerr
}
context.shellComplete = shellComplete
cCtx.shellComplete = shellComplete
if checkCompletions(context) {
if checkCompletions(cCtx) {
return nil
}
if err != nil {
if a.OnUsageError != nil {
err := a.OnUsageError(context, err, false)
a.handleExitCoder(context, err)
err := a.OnUsageError(cCtx, err, false)
a.handleExitCoder(cCtx, err)
return err
}
_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
_ = ShowAppHelp(context)
_ = ShowAppHelp(cCtx)
return err
}
if !a.HideHelp && checkHelp(context) {
_ = ShowAppHelp(context)
if !a.HideHelp && checkHelp(cCtx) {
_ = ShowAppHelp(cCtx)
return nil
}
if !a.HideVersion && checkVersion(context) {
ShowVersion(context)
if !a.HideVersion && checkVersion(cCtx) {
ShowVersion(cCtx)
return nil
}
cerr := context.checkRequiredFlags(a.Flags)
cerr := cCtx.checkRequiredFlags(a.Flags)
if cerr != nil {
_ = ShowAppHelp(context)
_ = ShowAppHelp(cCtx)
return cerr
}
if a.After != nil {
defer func() {
if afterErr := a.After(context); afterErr != nil {
if afterErr := a.After(cCtx); afterErr != nil {
if err != nil {
err = newMultiError(err, afterErr)
} else {
@ -297,20 +297,20 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
}
if a.Before != nil {
beforeErr := a.Before(context)
beforeErr := a.Before(cCtx)
if beforeErr != nil {
a.handleExitCoder(context, beforeErr)
a.handleExitCoder(cCtx, beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
args := cCtx.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
return c.Run(cCtx)
}
}
@ -319,9 +319,9 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
}
// Run default Action
err = a.Action(context)
err = a.Action(cCtx)
a.handleExitCoder(context, err)
a.handleExitCoder(cCtx, err)
return err
}
@ -359,55 +359,55 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete)
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, ctx)
cCtx := NewContext(a, set, ctx)
if nerr != nil {
_, _ = fmt.Fprintln(a.Writer, nerr)
_, _ = fmt.Fprintln(a.Writer)
if len(a.Commands) > 0 {
_ = ShowSubcommandHelp(context)
_ = ShowSubcommandHelp(cCtx)
} else {
_ = ShowCommandHelp(ctx, context.Args().First())
_ = ShowCommandHelp(ctx, cCtx.Args().First())
}
return nerr
}
if checkCompletions(context) {
if checkCompletions(cCtx) {
return nil
}
if err != nil {
if a.OnUsageError != nil {
err = a.OnUsageError(context, err, true)
a.handleExitCoder(context, err)
err = a.OnUsageError(cCtx, err, true)
a.handleExitCoder(cCtx, err)
return err
}
_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
_ = ShowSubcommandHelp(context)
_ = ShowSubcommandHelp(cCtx)
return err
}
if len(a.Commands) > 0 {
if checkSubcommandHelp(context) {
if checkSubcommandHelp(cCtx) {
return nil
}
} else {
if checkCommandHelp(ctx, context.Args().First()) {
if checkCommandHelp(ctx, cCtx.Args().First()) {
return nil
}
}
cerr := context.checkRequiredFlags(a.Flags)
cerr := cCtx.checkRequiredFlags(a.Flags)
if cerr != nil {
_ = ShowSubcommandHelp(context)
_ = ShowSubcommandHelp(cCtx)
return cerr
}
if a.After != nil {
defer func() {
afterErr := a.After(context)
afterErr := a.After(cCtx)
if afterErr != nil {
a.handleExitCoder(context, err)
a.handleExitCoder(cCtx, err)
if err != nil {
err = newMultiError(err, afterErr)
} else {
@ -418,27 +418,27 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
}
if a.Before != nil {
beforeErr := a.Before(context)
beforeErr := a.Before(cCtx)
if beforeErr != nil {
a.handleExitCoder(context, beforeErr)
a.handleExitCoder(cCtx, beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
args := cCtx.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
return c.Run(cCtx)
}
}
// Run default Action
err = a.Action(context)
err = a.Action(cCtx)
a.handleExitCoder(context, err)
a.handleExitCoder(cCtx, err)
return err
}
@ -498,9 +498,9 @@ func (a *App) appendCommand(c *Command) {
}
}
func (a *App) handleExitCoder(context *Context, err error) {
func (a *App) handleExitCoder(cCtx *Context, err error) {
if a.ExitErrHandler != nil {
a.ExitErrHandler(context, err)
a.ExitErrHandler(cCtx, err)
} else {
HandleExitCoder(err)
}
@ -525,14 +525,14 @@ func (a *Author) String() string {
// HandleAction attempts to figure out which Action signature was used. If
// it's an ActionFunc or a func with the legacy signature for Action, the func
// is run!
func HandleAction(action interface{}, context *Context) (err error) {
func HandleAction(action interface{}, cCtx *Context) (err error) {
switch a := action.(type) {
case ActionFunc:
return a(context)
return a(cCtx)
case func(*Context) error:
return a(context)
return a(cCtx)
case func(*Context): // deprecated function signature
a(context)
a(cCtx)
return nil
}

View File

@ -390,6 +390,40 @@ func ExampleApp_Run_zshComplete() {
// h:Shows a list of commands or help for one command
}
func ExampleApp_Run_sliceValues() {
// set args for examples sake
os.Args = []string{"multi_values",
"--stringSclice", "parsed1,parsed2", "--stringSclice", "parsed3,parsed4",
"--float64Sclice", "13.3,14.4", "--float64Sclice", "15.5,16.6",
"--int64Sclice", "13,14", "--int64Sclice", "15,16",
"--intSclice", "13,14", "--intSclice", "15,16",
}
app := NewApp()
app.Name = "multi_values"
app.Flags = []Flag{
&StringSliceFlag{Name: "stringSclice"},
&Float64SliceFlag{Name: "float64Sclice"},
&Int64SliceFlag{Name: "int64Sclice"},
&IntSliceFlag{Name: "intSclice"},
}
app.Action = func(ctx *Context) error {
for i, v := range ctx.FlagNames() {
fmt.Printf("%d-%s %#v\n", i, v, ctx.Value(v))
}
err := ctx.Err()
fmt.Println("error:", err)
return err
}
_ = app.Run(os.Args)
// Output:
// 0-float64Sclice cli.Float64Slice{slice:[]float64{13.3, 14.4, 15.5, 16.6}, hasBeenSet:true}
// 1-int64Sclice cli.Int64Slice{slice:[]int64{13, 14, 15, 16}, hasBeenSet:true}
// 2-intSclice cli.IntSlice{slice:[]int{13, 14, 15, 16}, hasBeenSet:true}
// 3-stringSclice cli.StringSlice{slice:[]string{"parsed1", "parsed2", "parsed3", "parsed4"}, hasBeenSet:true}
// error: <nil>
}
func TestApp_Run(t *testing.T) {
s := ""
@ -445,14 +479,14 @@ func TestApp_Setup_defaultsWriter(t *testing.T) {
}
func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
var context *Context
var cCtx *Context
a := &App{
Commands: []*Command{
{
Name: "foo",
Action: func(c *Context) error {
context = c
cCtx = c
return nil
},
Flags: []Flag{
@ -468,8 +502,8 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
}
_ = a.Run([]string{"", "foo", "--lang", "spanish", "abcd"})
expect(t, context.Args().Get(0), "abcd")
expect(t, context.String("lang"), "spanish")
expect(t, cCtx.Args().Get(0), "abcd")
expect(t, cCtx.String("lang"), "spanish")
}
func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) {

View File

@ -105,39 +105,39 @@ func (c *Command) Run(ctx *Context) (err error) {
set, err := c.parseFlags(ctx.Args(), ctx.shellComplete)
context := NewContext(ctx.App, set, ctx)
context.Command = c
if checkCommandCompletions(context, c.Name) {
cCtx := NewContext(ctx.App, set, ctx)
cCtx.Command = c
if checkCommandCompletions(cCtx, c.Name) {
return nil
}
if err != nil {
if c.OnUsageError != nil {
err = c.OnUsageError(context, err, false)
context.App.handleExitCoder(context, err)
err = c.OnUsageError(cCtx, err, false)
cCtx.App.handleExitCoder(cCtx, err)
return err
}
_, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error())
_, _ = fmt.Fprintln(context.App.Writer)
_ = ShowCommandHelp(context, c.Name)
_, _ = fmt.Fprintln(cCtx.App.Writer, "Incorrect Usage:", err.Error())
_, _ = fmt.Fprintln(cCtx.App.Writer)
_ = ShowCommandHelp(cCtx, c.Name)
return err
}
if checkCommandHelp(context, c.Name) {
if checkCommandHelp(cCtx, c.Name) {
return nil
}
cerr := context.checkRequiredFlags(c.Flags)
cerr := cCtx.checkRequiredFlags(c.Flags)
if cerr != nil {
_ = ShowCommandHelp(context, c.Name)
_ = ShowCommandHelp(cCtx, c.Name)
return cerr
}
if c.After != nil {
defer func() {
afterErr := c.After(context)
afterErr := c.After(cCtx)
if afterErr != nil {
context.App.handleExitCoder(context, err)
cCtx.App.handleExitCoder(cCtx, err)
if err != nil {
err = newMultiError(err, afterErr)
} else {
@ -148,9 +148,9 @@ func (c *Command) Run(ctx *Context) (err error) {
}
if c.Before != nil {
err = c.Before(context)
err = c.Before(cCtx)
if err != nil {
context.App.handleExitCoder(context, err)
cCtx.App.handleExitCoder(cCtx, err)
return err
}
}
@ -159,11 +159,11 @@ func (c *Command) Run(ctx *Context) (err error) {
c.Action = helpSubcommand.Action
}
context.Command = c
err = c.Action(context)
cCtx.Command = c
err = c.Action(cCtx)
if err != nil {
context.App.handleExitCoder(context, err)
cCtx.App.handleExitCoder(cCtx, err)
}
return err
}

View File

@ -30,7 +30,7 @@ func TestCommandFlagParsing(t *testing.T) {
set := flag.NewFlagSet("test", 0)
_ = set.Parse(c.testArgs)
context := NewContext(app, set, nil)
cCtx := NewContext(app, set, nil)
command := Command{
Name: "test-cmd",
@ -41,10 +41,10 @@ func TestCommandFlagParsing(t *testing.T) {
SkipFlagParsing: c.skipFlagParsing,
}
err := command.Run(context)
err := command.Run(cCtx)
expect(t, err, c.expectedErr)
expect(t, context.Args().Slice(), c.testArgs)
expect(t, cCtx.Args().Slice(), c.testArgs)
}
}

View File

@ -40,18 +40,18 @@ func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
}
// NumFlags returns the number of flags set
func (c *Context) NumFlags() int {
return c.flagSet.NFlag()
func (cCtx *Context) NumFlags() int {
return cCtx.flagSet.NFlag()
}
// Set sets a context flag to a value.
func (c *Context) Set(name, value string) error {
return c.flagSet.Set(name, value)
func (cCtx *Context) Set(name, value string) error {
return cCtx.flagSet.Set(name, value)
}
// IsSet determines if the flag was actually set
func (c *Context) IsSet(name string) bool {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) IsSet(name string) bool {
if fs := cCtx.lookupFlagSet(name); fs != nil {
isSet := false
fs.Visit(func(f *flag.Flag) {
if f.Name == name {
@ -62,7 +62,7 @@ func (c *Context) IsSet(name string) bool {
return true
}
f := c.lookupFlag(name)
f := cCtx.lookupFlag(name)
if f == nil {
return false
}
@ -74,28 +74,28 @@ func (c *Context) IsSet(name string) bool {
}
// LocalFlagNames returns a slice of flag names used in this context.
func (c *Context) LocalFlagNames() []string {
func (cCtx *Context) LocalFlagNames() []string {
var names []string
c.flagSet.Visit(makeFlagNameVisitor(&names))
cCtx.flagSet.Visit(makeFlagNameVisitor(&names))
return names
}
// FlagNames returns a slice of flag names used by the this context and all of
// its parent contexts.
func (c *Context) FlagNames() []string {
func (cCtx *Context) FlagNames() []string {
var names []string
for _, ctx := range c.Lineage() {
ctx.flagSet.Visit(makeFlagNameVisitor(&names))
for _, pCtx := range cCtx.Lineage() {
pCtx.flagSet.Visit(makeFlagNameVisitor(&names))
}
return names
}
// Lineage returns *this* context and all of its ancestor contexts in order from
// child to parent
func (c *Context) Lineage() []*Context {
func (cCtx *Context) Lineage() []*Context {
var lineage []*Context
for cur := c; cur != nil; cur = cur.parentContext {
for cur := cCtx; cur != nil; cur = cur.parentContext {
lineage = append(lineage, cur)
}
@ -103,26 +103,26 @@ func (c *Context) Lineage() []*Context {
}
// Value returns the value of the flag corresponding to `name`
func (c *Context) Value(name string) interface{} {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Value(name string) interface{} {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return fs.Lookup(name).Value.(flag.Getter).Get()
}
return nil
}
// Args returns the command line arguments associated with the context.
func (c *Context) Args() Args {
ret := args(c.flagSet.Args())
func (cCtx *Context) Args() Args {
ret := args(cCtx.flagSet.Args())
return &ret
}
// NArg returns the number of the command line arguments.
func (c *Context) NArg() int {
return c.Args().Len()
func (cCtx *Context) NArg() int {
return cCtx.Args().Len()
}
func (ctx *Context) lookupFlag(name string) Flag {
for _, c := range ctx.Lineage() {
func (cCtx *Context) lookupFlag(name string) Flag {
for _, c := range cCtx.Lineage() {
if c.Command == nil {
continue
}
@ -136,8 +136,8 @@ func (ctx *Context) lookupFlag(name string) Flag {
}
}
if ctx.App != nil {
for _, f := range ctx.App.Flags {
if cCtx.App != nil {
for _, f := range cCtx.App.Flags {
for _, n := range f.Names() {
if n == name {
return f
@ -149,8 +149,8 @@ func (ctx *Context) lookupFlag(name string) Flag {
return nil
}
func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet {
for _, c := range ctx.Lineage() {
func (cCtx *Context) lookupFlagSet(name string) *flag.FlagSet {
for _, c := range cCtx.Lineage() {
if c.flagSet == nil {
continue
}
@ -162,7 +162,7 @@ func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet {
return nil
}
func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr {
func (cCtx *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr {
var missingFlags []string
for _, f := range flags {
if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() {
@ -174,7 +174,7 @@ func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr {
flagName = key
}
if context.IsSet(strings.TrimSpace(key)) {
if cCtx.IsSet(strings.TrimSpace(key)) {
flagPresent = true
}
}

View File

@ -1,3 +1,6 @@
//go:build !urfave_cli_no_docs
// +build !urfave_cli_no_docs
package cli
import (
@ -80,14 +83,14 @@ func prepareCommands(commands []*Command, level int) []string {
usageText,
)
flags := prepareArgsWithValues(command.Flags)
flags := prepareArgsWithValues(command.VisibleFlags())
if len(flags) > 0 {
prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n"))
}
coms = append(coms, prepared)
// recursevly iterate subcommands
// recursively iterate subcommands
if len(command.Subcommands) > 0 {
coms = append(
coms,

View File

@ -1,3 +1,9 @@
> :warning: This document is no longer being actively maintained. Please see the
> [releases page](https://github.com/urfave/cli/releases) for all release notes
> and related hypermedia for releases `>= 1.22.5`, `>= 2.3.0`.
---
# Change Log
**ATTN**: This project uses [semantic versioning](http://semver.org/).

61
docs/RELEASING.md Normal file
View File

@ -0,0 +1,61 @@
# Releasing urfave/cli
Releasing small batches often is [backed by
research](https://itrevolution.com/accelerate-book/) as part of the
virtuous cycles that keep teams and products healthy.
To that end, the overall goal of the release process is to send
changes out into the world as close to the time the commits were
merged to the `main` branch as possible. In this way, the community
of humans depending on this library are able to make use of the
changes they need **quickly**, which means they shouldn't have to
maintain long-lived forks of the project, which means they can get
back to focusing on the work on which they want to focus. This also
means that the @urfave/cli team should be able to focus on
delivering a steadily improving product with significantly eased
ability to associate bugs and regressions with specific releases.
## Process
- Release versions follow [semantic versioning](https://semver.org/)
- Releases are associated with **signed, annotated git tags**[^1].
- Release notes are **automatically generated**[^2].
In the `main` or `v1` branch, the current version is always
available via:
```sh
git describe --always --dirty --tags
```
**NOTE**: if the version reported contains `-dirty`, this is
indicative of a "dirty" work tree, which is not a great state for
creating a new release tag. Seek help from @urfave/cli teammates.
For example, given a described version of `v2.4.7-3-g68da1cd` and a
diff of `v2.4.7...` that contains only bug fixes, the next version
should be `v2.4.8`:
```sh
git tag -a -s -m 'Release 2.4.8' v2.4.8
git push origin v2.4.8
```
The tag push will trigger a GitHub Actions workflow. The remaining
steps require human intervention through the GitHub web view
although [automated solutions
exist](https://github.com/softprops/action-gh-release) that may be
adopted in the future.
- Open the [the new release page](https://github.com/urfave/cli/releases/new)
- At the top of the form, click on the `Choose a tag` select control and select `v2.4.8`
- In the `Write` tab below, click the `Auto-generate release notes` button
- At the bottom of the form, click the `Publish release` button
- :white_check_mark: you're done!
[^1]: This was not always true. There are many **lightweight git
tags** present in the repository history.
[^2]: This was not always true. The
[`docs/CHANGELOG.md`](./CHANGELOG.md) document used to be
manually maintained.

View File

@ -1,133 +1,13 @@
//go:build !urfave_cli_no_docs
// +build !urfave_cli_no_docs
package cli
import (
"bytes"
"errors"
"io/ioutil"
"testing"
)
func testApp() *App {
app := newTestApp()
app.Name = "greet"
app.Flags = []Flag{
&StringFlag{
Name: "socket",
Aliases: []string{"s"},
Usage: "some 'usage' text",
Value: "value",
TakesFile: true,
},
&StringFlag{Name: "flag", Aliases: []string{"fl", "f"}},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
&BoolFlag{
Name: "hidden-flag",
Hidden: true,
},
}
app.Commands = []*Command{{
Aliases: []string{"c"},
Flags: []Flag{
&StringFlag{
Name: "flag",
Aliases: []string{"fl", "f"},
TakesFile: true,
},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
},
Name: "config",
Usage: "another usage test",
Subcommands: []*Command{{
Aliases: []string{"s", "ss"},
Flags: []Flag{
&StringFlag{Name: "sub-flag", Aliases: []string{"sub-fl", "s"}},
&BoolFlag{
Name: "sub-command-flag",
Aliases: []string{"s"},
Usage: "some usage text",
},
},
Name: "sub-config",
Usage: "another usage test",
}},
}, {
Aliases: []string{"i", "in"},
Name: "info",
Usage: "retrieve generic information",
}, {
Name: "some-command",
}, {
Name: "hidden-command",
Hidden: true,
}, {
Aliases: []string{"u"},
Flags: []Flag{
&StringFlag{
Name: "flag",
Aliases: []string{"fl", "f"},
TakesFile: true,
},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
},
Name: "usage",
Usage: "standard usage text",
UsageText: `
Usage for the usage text
- formatted: Based on the specified ConfigMap and summon secrets.yml
- list: Inspect the environment for a specific process running on a Pod
- for_effect: Compare 'namespace' environment with 'local'
` + "```" + `
func() { ... }
` + "```" + `
Should be a part of the same code block
`,
Subcommands: []*Command{{
Aliases: []string{"su"},
Flags: []Flag{
&BoolFlag{
Name: "sub-command-flag",
Aliases: []string{"s"},
Usage: "some usage text",
},
},
Name: "sub-usage",
Usage: "standard usage text",
UsageText: "Single line of UsageText",
}},
}}
app.UsageText = "app [first_arg] [second_arg]"
app.Description = `Description of the application.`
app.Usage = "Some app"
app.Authors = []*Author{
{Name: "Harrison", Email: "harrison@lolwut.com"},
{Name: "Oliver Allen", Email: "oliver@toyshop.com"},
}
return app
}
func expectFileContent(t *testing.T, file, got string) {
data, err := ioutil.ReadFile(file)
// Ignore windows line endings
// TODO: Replace with bytes.ReplaceAll when support for Go 1.11 is dropped
data = bytes.Replace(data, []byte("\r\n"), []byte("\n"), -1)
expect(t, err, nil)
expect(t, got, string(data))
}
func TestToMarkdownFull(t *testing.T) {
// Given
app := testApp()

View File

@ -95,7 +95,7 @@ func (a *App) prepareFishCommands(commands []*Command, allCommands *[]string, pr
completions = append(completions, completion.String())
completions = append(
completions,
a.prepareFishFlags(command.Flags, command.Names())...,
a.prepareFishFlags(command.VisibleFlags(), command.Names())...,
)
// recursevly iterate subcommands

View File

@ -1,6 +1,8 @@
package cli
import (
"bytes"
"io/ioutil"
"testing"
)
@ -19,3 +21,124 @@ func TestFishCompletion(t *testing.T) {
expect(t, err, nil)
expectFileContent(t, "testdata/expected-fish-full.fish", res)
}
func testApp() *App {
app := newTestApp()
app.Name = "greet"
app.Flags = []Flag{
&StringFlag{
Name: "socket",
Aliases: []string{"s"},
Usage: "some 'usage' text",
Value: "value",
TakesFile: true,
},
&StringFlag{Name: "flag", Aliases: []string{"fl", "f"}},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
&BoolFlag{
Name: "hidden-flag",
Hidden: true,
},
}
app.Commands = []*Command{{
Aliases: []string{"c"},
Flags: []Flag{
&StringFlag{
Name: "flag",
Aliases: []string{"fl", "f"},
TakesFile: true,
},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
},
Name: "config",
Usage: "another usage test",
Subcommands: []*Command{{
Aliases: []string{"s", "ss"},
Flags: []Flag{
&StringFlag{Name: "sub-flag", Aliases: []string{"sub-fl", "s"}},
&BoolFlag{
Name: "sub-command-flag",
Aliases: []string{"s"},
Usage: "some usage text",
},
},
Name: "sub-config",
Usage: "another usage test",
}},
}, {
Aliases: []string{"i", "in"},
Name: "info",
Usage: "retrieve generic information",
}, {
Name: "some-command",
}, {
Name: "hidden-command",
Hidden: true,
}, {
Aliases: []string{"u"},
Flags: []Flag{
&StringFlag{
Name: "flag",
Aliases: []string{"fl", "f"},
TakesFile: true,
},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
},
Name: "usage",
Usage: "standard usage text",
UsageText: `
Usage for the usage text
- formatted: Based on the specified ConfigMap and summon secrets.yml
- list: Inspect the environment for a specific process running on a Pod
- for_effect: Compare 'namespace' environment with 'local'
` + "```" + `
func() { ... }
` + "```" + `
Should be a part of the same code block
`,
Subcommands: []*Command{{
Aliases: []string{"su"},
Flags: []Flag{
&BoolFlag{
Name: "sub-command-flag",
Aliases: []string{"s"},
Usage: "some usage text",
},
},
Name: "sub-usage",
Usage: "standard usage text",
UsageText: "Single line of UsageText",
}},
}}
app.UsageText = "app [first_arg] [second_arg]"
app.Description = `Description of the application.`
app.Usage = "Some app"
app.Authors = []*Author{
{Name: "Harrison", Email: "harrison@lolwut.com"},
{Name: "Oliver Allen", Email: "oliver@toyshop.com"},
}
return app
}
func expectFileContent(t *testing.T, file, got string) {
data, err := ioutil.ReadFile(file)
// Ignore windows line endings
// TODO: Replace with bytes.ReplaceAll when support for Go 1.11 is dropped
data = bytes.Replace(data, []byte("\r\n"), []byte("\n"), -1)
expect(t, err, nil)
expect(t, got, string(data))
}

26
flag.go
View File

@ -5,7 +5,6 @@ import (
"flag"
"fmt"
"io/ioutil"
"reflect"
"regexp"
"runtime"
"strconv"
@ -244,7 +243,7 @@ func prefixedNames(names []string, placeholder string) string {
func withEnvHint(envVars []string, str string) string {
envText := ""
if envVars != nil && len(envVars) > 0 {
if len(envVars) > 0 {
prefix := "$"
suffix := ""
sep := ", $"
@ -273,17 +272,6 @@ func flagNames(name string, aliases []string) []string {
return ret
}
func flagStringSliceField(f Flag, name string) []string {
fv := flagValue(f)
field := fv.FieldByName(name)
if field.IsValid() {
return field.Interface().([]string)
}
return []string{}
}
func withFileHint(filePath, str string) string {
fileText := ""
if filePath != "" {
@ -292,14 +280,6 @@ func withFileHint(filePath, str string) string {
return str + fileText
}
func flagValue(f Flag) reflect.Value {
fv := reflect.ValueOf(f)
for fv.Kind() == reflect.Ptr {
fv = reflect.Indirect(fv)
}
return fv
}
func formatDefault(format string) string {
return " (default: " + format + ")"
}
@ -422,3 +402,7 @@ func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool)
}
return "", false
}
func flagSplitMultiValues(val string) []string {
return strings.Split(val, ",")
}

View File

@ -109,8 +109,8 @@ func (f *BoolFlag) Get(ctx *Context) bool {
// Bool looks up the value of a local BoolFlag, returns
// false if not found
func (c *Context) Bool(name string) bool {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Bool(name string) bool {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupBool(name, fs)
}
return false

View File

@ -108,8 +108,8 @@ func (f *DurationFlag) Get(ctx *Context) time.Duration {
// Duration looks up the value of a local DurationFlag, returns
// 0 if not found
func (c *Context) Duration(name string) time.Duration {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Duration(name string) time.Duration {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupDuration(name, fs)
}
return 0

View File

@ -108,8 +108,8 @@ func (f *Float64Flag) Get(ctx *Context) float64 {
// Float64 looks up the value of a local Float64Flag, returns
// 0 if not found
func (c *Context) Float64(name string) float64 {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Float64(name string) float64 {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupFloat64(name, fs)
}
return 0

View File

@ -43,12 +43,14 @@ func (f *Float64Slice) Set(value string) error {
return nil
}
tmp, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
for _, s := range flagSplitMultiValues(value) {
tmp, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
if err != nil {
return err
}
f.slice = append(f.slice, tmp)
f.slice = append(f.slice, tmp)
}
return nil
}
@ -151,7 +153,7 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
if val != "" {
f.Value = &Float64Slice{}
for _, s := range strings.Split(val, ",") {
for _, s := range flagSplitMultiValues(val) {
if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as float64 slice value for flag %s: %s", f.Value, f.Name, err)
}
@ -182,8 +184,8 @@ func (f *Float64SliceFlag) Get(ctx *Context) []float64 {
// Float64Slice looks up the value of a local Float64SliceFlag, returns
// nil if not found
func (c *Context) Float64Slice(name string) []float64 {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Float64Slice(name string) []float64 {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupFloat64Slice(name, fs)
}
return nil

View File

@ -111,8 +111,8 @@ func (f *GenericFlag) Get(ctx *Context) interface{} {
// Generic looks up the value of a local GenericFlag, returns
// nil if not found
func (c *Context) Generic(name string) interface{} {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Generic(name string) interface{} {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupGeneric(name, fs)
}
return nil

View File

@ -109,8 +109,8 @@ func (f *IntFlag) Get(ctx *Context) int {
// Int looks up the value of a local IntFlag, returns
// 0 if not found
func (c *Context) Int(name string) int {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Int(name string) int {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupInt(name, fs)
}
return 0

View File

@ -108,8 +108,8 @@ func (f *Int64Flag) Get(ctx *Context) int64 {
// Int64 looks up the value of a local Int64Flag, returns
// 0 if not found
func (c *Context) Int64(name string) int64 {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Int64(name string) int64 {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupInt64(name, fs)
}
return 0

View File

@ -43,12 +43,14 @@ func (i *Int64Slice) Set(value string) error {
return nil
}
tmp, err := strconv.ParseInt(value, 0, 64)
if err != nil {
return err
}
for _, s := range flagSplitMultiValues(value) {
tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
if err != nil {
return err
}
i.slice = append(i.slice, tmp)
i.slice = append(i.slice, tmp)
}
return nil
}
@ -151,7 +153,7 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
f.Value = &Int64Slice{}
for _, s := range strings.Split(val, ",") {
for _, s := range flagSplitMultiValues(val) {
if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", val, f.Name, err)
}
@ -181,8 +183,8 @@ func (f *Int64SliceFlag) Get(ctx *Context) []int64 {
// Int64Slice looks up the value of a local Int64SliceFlag, returns
// nil if not found
func (c *Context) Int64Slice(name string) []int64 {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Int64Slice(name string) []int64 {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupInt64Slice(name, fs)
}
return nil

View File

@ -54,12 +54,14 @@ func (i *IntSlice) Set(value string) error {
return nil
}
tmp, err := strconv.ParseInt(value, 0, 64)
if err != nil {
return err
}
for _, s := range flagSplitMultiValues(value) {
tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
if err != nil {
return err
}
i.slice = append(i.slice, int(tmp))
i.slice = append(i.slice, int(tmp))
}
return nil
}
@ -162,7 +164,7 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
f.Value = &IntSlice{}
for _, s := range strings.Split(val, ",") {
for _, s := range flagSplitMultiValues(val) {
if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", val, f.Name, err)
}
@ -192,8 +194,8 @@ func (f *IntSliceFlag) Get(ctx *Context) []int {
// IntSlice looks up the value of a local IntSliceFlag, returns
// nil if not found
func (c *Context) IntSlice(name string) []int {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) IntSlice(name string) []int {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupIntSlice(name, fs)
}
return nil

View File

@ -103,8 +103,8 @@ func (f *PathFlag) Get(ctx *Context) string {
// Path looks up the value of a local PathFlag, returns
// "" if not found
func (c *Context) Path(name string) string {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Path(name string) string {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupPath(name, fs)
}

View File

@ -104,8 +104,8 @@ func (f *StringFlag) Get(ctx *Context) string {
// String looks up the value of a local StringFlag, returns
// "" if not found
func (c *Context) String(name string) string {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) String(name string) string {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupString(name, fs)
}
return ""

View File

@ -42,7 +42,9 @@ func (s *StringSlice) Set(value string) error {
return nil
}
s.slice = append(s.slice, value)
for _, t := range flagSplitMultiValues(value) {
s.slice = append(s.slice, strings.TrimSpace(t))
}
return nil
}
@ -160,7 +162,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
destination = f.Destination
}
for _, s := range strings.Split(val, ",") {
for _, s := range flagSplitMultiValues(val) {
if err := destination.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as string value for flag %s: %s", val, f.Name, err)
}
@ -193,8 +195,8 @@ func (f *StringSliceFlag) Get(ctx *Context) []string {
// StringSlice looks up the value of a local StringSliceFlag, returns
// nil if not found
func (c *Context) StringSlice(name string) []string {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) StringSlice(name string) []string {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupStringSlice(name, fs)
}
return nil

View File

@ -411,7 +411,7 @@ func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
}
}
var prefixStringFlagTests = []struct {
var _ = []struct {
name string
aliases []string
usage string
@ -517,7 +517,7 @@ func TestPathFlagValueFromContext(t *testing.T) {
expect(t, f.Get(ctx), "/my/path")
}
var envHintFlagTests = []struct {
var _ = []struct {
name string
env string
hinter FlagEnvHintFunc
@ -2298,43 +2298,43 @@ type flagDefaultTestCase struct {
func TestFlagDefaultValue(t *testing.T) {
cases := []*flagDefaultTestCase{
&flagDefaultTestCase{
{
name: "stringSclice",
flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")},
toParse: []string{"--flag", "parsed"},
expect: `--flag value (default: "default1", "default2") (accepts multiple inputs)`,
},
&flagDefaultTestCase{
{
name: "float64Sclice",
flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)},
toParse: []string{"--flag", "13.3"},
expect: `--flag value (default: 1.1, 2.2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
{
name: "int64Sclice",
flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)},
toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1, 2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
{
name: "intSclice",
flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)},
toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1, 2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
{
name: "string",
flag: &StringFlag{Name: "flag", Value: "default"},
toParse: []string{"--flag", "parsed"},
expect: `--flag value (default: "default")`,
},
&flagDefaultTestCase{
{
name: "bool",
flag: &BoolFlag{Name: "flag", Value: true},
toParse: []string{"--flag", "false"},
expect: `--flag (default: true)`,
},
&flagDefaultTestCase{
{
name: "uint64",
flag: &Uint64Flag{Name: "flag", Value: 1},
toParse: []string{"--flag", "13"},
@ -2354,6 +2354,54 @@ func TestFlagDefaultValue(t *testing.T) {
}
}
type flagValueTestCase struct {
name string
flag Flag
toParse []string
expect string
}
func TestFlagValue(t *testing.T) {
cases := []*flagValueTestCase{
&flagValueTestCase{
name: "stringSclice",
flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")},
toParse: []string{"--flag", "parsed,parsed2", "--flag", "parsed3,parsed4"},
expect: `[parsed parsed2 parsed3 parsed4]`,
},
&flagValueTestCase{
name: "float64Sclice",
flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)},
toParse: []string{"--flag", "13.3,14.4", "--flag", "15.5,16.6"},
expect: `[]float64{13.3, 14.4, 15.5, 16.6}`,
},
&flagValueTestCase{
name: "int64Sclice",
flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)},
toParse: []string{"--flag", "13,14", "--flag", "15,16"},
expect: `[]int64{13, 14, 15, 16}`,
},
&flagValueTestCase{
name: "intSclice",
flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)},
toParse: []string{"--flag", "13,14", "--flag", "15,16"},
expect: `[]int{13, 14, 15, 16}`,
},
}
for i, v := range cases {
set := flag.NewFlagSet("test", 0)
set.SetOutput(ioutil.Discard)
_ = v.flag.Apply(set)
if err := set.Parse(v.toParse); err != nil {
t.Error(err)
}
f := set.Lookup("flag")
if got := f.Value.String(); got != v.expect {
t.Errorf("TestFlagValue %d-%s\nexpect:%s\ngot:%s", i, v.name, v.expect, got)
}
}
}
func TestTimestampFlagApply_WithDestination(t *testing.T) {
var destination Timestamp
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
@ -2365,3 +2413,42 @@ func TestTimestampFlagApply_WithDestination(t *testing.T) {
expect(t, err, nil)
expect(t, *fl.Destination.timestamp, expectedResult)
}
// Test issue #1254
// StringSlice() with UseShortOptionHandling causes duplicated entries, depending on the ordering of the flags
func TestSliceShortOptionHandle(t *testing.T) {
wasCalled := false
err := (&App{
Commands: []*Command{
{
Name: "foobar",
UseShortOptionHandling: true,
Action: func(ctx *Context) error {
wasCalled = true
if ctx.Bool("i") != true {
t.Error("bool i not set")
}
if ctx.Bool("t") != true {
t.Error("bool i not set")
}
ss := ctx.StringSlice("net")
if !reflect.DeepEqual(ss, []string{"foo"}) {
t.Errorf("Got different slice(%v) than expected", ss)
}
return nil
},
Flags: []Flag{
&StringSliceFlag{Name: "net"},
&BoolFlag{Name: "i"},
&BoolFlag{Name: "t"},
},
},
},
}).Run([]string{"run", "foobar", "--net=foo", "-it"})
if err != nil {
t.Fatal(err)
}
if !wasCalled {
t.Fatal("Action callback was never called")
}
}

View File

@ -170,8 +170,8 @@ func (f *TimestampFlag) Get(ctx *Context) *time.Time {
}
// Timestamp gets the timestamp from a flag name
func (c *Context) Timestamp(name string) *time.Time {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Timestamp(name string) *time.Time {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupTimestamp(name, fs)
}
return nil

View File

@ -108,8 +108,8 @@ func (f *UintFlag) Get(ctx *Context) uint {
// Uint looks up the value of a local UintFlag, returns
// 0 if not found
func (c *Context) Uint(name string) uint {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Uint(name string) uint {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupUint(name, fs)
}
return 0

View File

@ -108,8 +108,8 @@ func (f *Uint64Flag) Get(ctx *Context) uint64 {
// Uint64 looks up the value of a local Uint64Flag, returns
// 0 if not found
func (c *Context) Uint64(name string) uint64 {
if fs := c.lookupFlagSet(name); fs != nil {
func (cCtx *Context) Uint64(name string) uint64 {
if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupUint64(name, fs)
}
return 0

View File

@ -21,11 +21,11 @@ type CommandNotFoundFunc func(*Context, string)
// customized usage error messages. This function is able to replace the
// original error messages. If this function is not set, the "Incorrect usage"
// is displayed and the execution is interrupted.
type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error
type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error
// ExitErrHandlerFunc is executed if provided in order to handle exitError values
// returned by Actions and Before/After functions.
type ExitErrHandlerFunc func(context *Context, err error)
type ExitErrHandlerFunc func(cCtx *Context, err error)
// FlagStringFunc is used by the help generation to display a flag, which is
// expected to be a single line.

92
help.go
View File

@ -15,13 +15,13 @@ var helpCommand = &Command{
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(c *Context) error {
args := c.Args()
Action: func(cCtx *Context) error {
args := cCtx.Args()
if args.Present() {
return ShowCommandHelp(c, args.First())
return ShowCommandHelp(cCtx, args.First())
}
_ = ShowAppHelp(c)
_ = ShowAppHelp(cCtx)
return nil
},
}
@ -31,13 +31,13 @@ var helpSubcommand = &Command{
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(c *Context) error {
args := c.Args()
Action: func(cCtx *Context) error {
args := cCtx.Args()
if args.Present() {
return ShowCommandHelp(c, args.First())
return ShowCommandHelp(cCtx, args.First())
}
return ShowSubcommandHelp(c)
return ShowSubcommandHelp(cCtx)
},
}
@ -71,30 +71,30 @@ func ShowAppHelpAndExit(c *Context, exitCode int) {
}
// ShowAppHelp is an action that displays the help.
func ShowAppHelp(c *Context) error {
tpl := c.App.CustomAppHelpTemplate
func ShowAppHelp(cCtx *Context) error {
tpl := cCtx.App.CustomAppHelpTemplate
if tpl == "" {
tpl = AppHelpTemplate
}
if c.App.ExtraInfo == nil {
HelpPrinter(c.App.Writer, tpl, c.App)
if cCtx.App.ExtraInfo == nil {
HelpPrinter(cCtx.App.Writer, tpl, cCtx.App)
return nil
}
customAppData := func() map[string]interface{} {
return map[string]interface{}{
"ExtraInfo": c.App.ExtraInfo,
"ExtraInfo": cCtx.App.ExtraInfo,
}
}
HelpPrinterCustom(c.App.Writer, tpl, c.App, customAppData())
HelpPrinterCustom(cCtx.App.Writer, tpl, cCtx.App, customAppData())
return nil
}
// DefaultAppComplete prints the list of subcommands as the default app completion method
func DefaultAppComplete(c *Context) {
DefaultCompleteWithFlags(nil)(c)
func DefaultAppComplete(cCtx *Context) {
DefaultCompleteWithFlags(nil)(cCtx)
}
func printCommandSuggestions(commands []*Command, writer io.Writer) {
@ -159,30 +159,30 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
}
}
func DefaultCompleteWithFlags(cmd *Command) func(c *Context) {
return func(c *Context) {
func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) {
return func(cCtx *Context) {
if len(os.Args) > 2 {
lastArg := os.Args[len(os.Args)-2]
if strings.HasPrefix(lastArg, "-") {
if cmd != nil {
printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer)
printFlagSuggestions(lastArg, cmd.Flags, cCtx.App.Writer)
return
}
printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer)
printFlagSuggestions(lastArg, cCtx.App.Flags, cCtx.App.Writer)
return
}
}
if cmd != nil {
printCommandSuggestions(cmd.Subcommands, c.App.Writer)
printCommandSuggestions(cmd.Subcommands, cCtx.App.Writer)
return
}
printCommandSuggestions(c.App.Commands, c.App.Writer)
printCommandSuggestions(cCtx.App.Commands, cCtx.App.Writer)
}
}
@ -228,32 +228,32 @@ func ShowSubcommandHelpAndExit(c *Context, exitCode int) {
}
// ShowSubcommandHelp prints help for the given subcommand
func ShowSubcommandHelp(c *Context) error {
if c == nil {
func ShowSubcommandHelp(cCtx *Context) error {
if cCtx == nil {
return nil
}
if c.Command != nil {
return ShowCommandHelp(c, c.Command.Name)
if cCtx.Command != nil {
return ShowCommandHelp(cCtx, cCtx.Command.Name)
}
return ShowCommandHelp(c, "")
return ShowCommandHelp(cCtx, "")
}
// ShowVersion prints the version number of the App
func ShowVersion(c *Context) {
VersionPrinter(c)
func ShowVersion(cCtx *Context) {
VersionPrinter(cCtx)
}
func printVersion(c *Context) {
_, _ = fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version)
func printVersion(cCtx *Context) {
_, _ = fmt.Fprintf(cCtx.App.Writer, "%v version %v\n", cCtx.App.Name, cCtx.App.Version)
}
// ShowCompletions prints the lists of commands within a given context
func ShowCompletions(c *Context) {
a := c.App
func ShowCompletions(cCtx *Context) {
a := cCtx.App
if a != nil && a.BashComplete != nil {
a.BashComplete(c)
a.BashComplete(cCtx)
}
}
@ -304,20 +304,20 @@ func printHelp(out io.Writer, templ string, data interface{}) {
HelpPrinterCustom(out, templ, data, nil)
}
func checkVersion(c *Context) bool {
func checkVersion(cCtx *Context) bool {
found := false
for _, name := range VersionFlag.Names() {
if c.Bool(name) {
if cCtx.Bool(name) {
found = true
}
}
return found
}
func checkHelp(c *Context) bool {
func checkHelp(cCtx *Context) bool {
found := false
for _, name := range HelpFlag.Names() {
if c.Bool(name) {
if cCtx.Bool(name) {
found = true
}
}
@ -333,9 +333,9 @@ func checkCommandHelp(c *Context, name string) bool {
return false
}
func checkSubcommandHelp(c *Context) bool {
if c.Bool("h") || c.Bool("help") {
_ = ShowSubcommandHelp(c)
func checkSubcommandHelp(cCtx *Context) bool {
if cCtx.Bool("h") || cCtx.Bool("help") {
_ = ShowSubcommandHelp(cCtx)
return true
}
@ -357,20 +357,20 @@ func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) {
return true, arguments[:pos]
}
func checkCompletions(c *Context) bool {
if !c.shellComplete {
func checkCompletions(cCtx *Context) bool {
if !cCtx.shellComplete {
return false
}
if args := c.Args(); args.Present() {
if args := cCtx.Args(); args.Present() {
name := args.First()
if cmd := c.App.Command(name); cmd != nil {
if cmd := cCtx.App.Command(name); cmd != nil {
// let the command handle the completion
return false
}
}
ShowCompletions(c)
ShowCompletions(cCtx)
return true
}

View File

@ -46,6 +46,12 @@ func main() {
Action: checkBinarySizeActionFunc,
},
}
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "tags",
Usage: "set build tags",
},
}
err := app.Run(os.Args)
if err != nil {
@ -68,6 +74,8 @@ func VetActionFunc(_ *cli.Context) error {
}
func TestActionFunc(c *cli.Context) error {
tags := c.String("tags")
for _, pkg := range packages {
var packageName string
@ -79,7 +87,7 @@ func TestActionFunc(c *cli.Context) error {
coverProfile := fmt.Sprintf("--coverprofile=%s.coverprofile", pkg)
err := runCmd("go", "test", "-v", coverProfile, packageName)
err := runCmd("go", "test", "-tags", tags, "-v", coverProfile, packageName)
if err != nil {
return err
}
@ -201,14 +209,16 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
mbStringFormatter = "%.1fMB"
)
tags := c.String("tags")
// get cli example size
cliSize, err := getSize(cliSourceFilePath, cliBuiltFilePath)
cliSize, err := getSize(cliSourceFilePath, cliBuiltFilePath, tags)
if err != nil {
return err
}
// get hello world size
helloSize, err := getSize(helloSourceFilePath, helloBuiltFilePath)
helloSize, err := getSize(helloSourceFilePath, helloBuiltFilePath, tags)
if err != nil {
return err
}
@ -270,9 +280,9 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
return nil
}
func getSize(sourcePath string, builtPath string) (size int64, err error) {
func getSize(sourcePath string, builtPath string, tags string) (size int64, err error) {
// build example binary
err = runCmd("go", "build", "-o", builtPath, "-ldflags", "-s -w", sourcePath)
err = runCmd("go", "build", "-tags", tags, "-o", builtPath, "-ldflags", "-s -w", sourcePath)
if err != nil {
fmt.Println("issue getting size for example binary")
return 0, err

1
testdata/empty.yml vendored Normal file
View File

@ -0,0 +1 @@
# empty file