diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 0ab3baf360..c60ed4c2fa 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -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 diff --git a/LICENSE b/LICENSE index 42a597e29b..2c84c78a11 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 5b8d2b6711..6e4d698c24 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/altsrc/flag.go b/altsrc/flag.go index 31b8a04e87..db959493bc 100644 --- a/altsrc/flag.go +++ b/altsrc/flag.go @@ -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() { - - if !filepath.IsAbs(value) && isc.Source() != "" { - basePathAbs, err := filepath.Abs(isc.Source()) - if err != nil { - return err - } +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() { - 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 diff --git a/altsrc/flag_test.go b/altsrc/flag_test.go index 204833118f..e3725f7d1c 100644 --- a/altsrc/flag_test.go +++ b/altsrc/flag_test.go @@ -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 TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) { +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) { + 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 diff --git a/altsrc/helpers_test.go b/altsrc/helpers_test.go index 33e8a4b976..1f8d5c25fc 100644 --- a/altsrc/helpers_test.go +++ b/altsrc/helpers_test.go @@ -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)) } } diff --git a/altsrc/input_source_context.go b/altsrc/input_source_context.go index a639d8b38e..d743253050 100644 --- a/altsrc/input_source_context.go +++ b/altsrc/input_source_context.go @@ -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 } diff --git a/altsrc/json_source_context.go b/altsrc/json_source_context.go index 6e7bf11f91..168b6dade0 100644 --- a/altsrc/json_source_context.go +++ b/altsrc/json_source_context.go @@ -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) } diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go index 117461f5a0..e065c7cc43 100644 --- a/altsrc/map_input_source.go +++ b/altsrc/map_input_source.go @@ -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 := "" diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go index 9b86ee1a63..dfc9b7b1c8 100644 --- a/altsrc/toml_file_loader.go +++ b/altsrc/toml_file_loader.go @@ -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) } diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index a49df56f35..4ace1f23ec 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -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) } diff --git a/altsrc/yaml_file_loader_test.go b/altsrc/yaml_file_loader_test.go new file mode 100644 index 0000000000..814586b917 --- /dev/null +++ b/altsrc/yaml_file_loader_test.go @@ -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 +} diff --git a/app.go b/app.go index fdc4ea6ad0..83ebe30c0d 100644 --- a/app.go +++ b/app.go @@ -265,48 +265,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 { @@ -317,20 +317,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) } } @@ -339,9 +339,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 } @@ -379,55 +379,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 { @@ -438,27 +438,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 } @@ -523,9 +523,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) } @@ -550,14 +550,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 } diff --git a/app_test.go b/app_test.go index 3ba194574e..38c7ce8b88 100644 --- a/app_test.go +++ b/app_test.go @@ -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: +} + 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) { diff --git a/command.go b/command.go index 93fd483c39..1f24dc7b73 100644 --- a/command.go +++ b/command.go @@ -107,39 +107,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 { @@ -150,9 +150,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 } } @@ -161,11 +161,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 } diff --git a/command_test.go b/command_test.go index 6add442865..9dfd46f476 100644 --- a/command_test.go +++ b/command_test.go @@ -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) } } diff --git a/context.go b/context.go index da090e8256..6b497ed20d 100644 --- a/context.go +++ b/context.go @@ -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 } } diff --git a/docs.go b/docs.go index 9f82fc6b0b..8b1c9c8a2c 100644 --- a/docs.go +++ b/docs.go @@ -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, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e957d945d1..6aec50b429 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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/). diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000000..b10e4e0dbf --- /dev/null +++ b/docs/RELEASING.md @@ -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. diff --git a/docs_test.go b/docs_test.go index adccbbb9ad..12d5d3c8b1 100644 --- a/docs_test.go +++ b/docs_test.go @@ -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() diff --git a/fish.go b/fish.go index 588e070eab..eec3253cda 100644 --- a/fish.go +++ b/fish.go @@ -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 diff --git a/fish_test.go b/fish_test.go index 4ca8c47903..af1a14c441 100644 --- a/fish_test.go +++ b/fish_test.go @@ -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)) +} diff --git a/flag.go b/flag.go index 16778d77e0..87d2d54be5 100644 --- a/flag.go +++ b/flag.go @@ -5,7 +5,6 @@ import ( "flag" "fmt" "io/ioutil" - "reflect" "regexp" "runtime" "strconv" @@ -252,7 +251,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 := ", $" @@ -281,17 +280,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 != "" { @@ -300,14 +288,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 + ")" } @@ -430,3 +410,7 @@ func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) } return "", false } + +func flagSplitMultiValues(val string) []string { + return strings.Split(val, ",") +} diff --git a/flag_bool.go b/flag_bool.go index ef5f6346f2..405c9a9c58 100644 --- a/flag_bool.go +++ b/flag_bool.go @@ -110,8 +110,8 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_duration.go b/flag_duration.go index 7592180fa2..08f48bd3b8 100644 --- a/flag_duration.go +++ b/flag_duration.go @@ -109,8 +109,8 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_float64.go b/flag_float64.go index 1cda961438..5fca711631 100644 --- a/flag_float64.go +++ b/flag_float64.go @@ -109,8 +109,8 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_float64_slice.go b/flag_float64_slice.go index d2328b6765..6045ed8e64 100644 --- a/flag_float64_slice.go +++ b/flag_float64_slice.go @@ -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 } @@ -157,7 +159,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) } @@ -183,8 +185,8 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_generic.go b/flag_generic.go index 6c973f99de..c7e8a5b2ff 100644 --- a/flag_generic.go +++ b/flag_generic.go @@ -112,8 +112,8 @@ func (f GenericFlag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_int.go b/flag_int.go index 7979c6409a..460a13d8b2 100644 --- a/flag_int.go +++ b/flag_int.go @@ -110,8 +110,8 @@ func (f *IntFlag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_int64.go b/flag_int64.go index 7083c60c30..4d7ec6389d 100644 --- a/flag_int64.go +++ b/flag_int64.go @@ -109,8 +109,8 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_int64_slice.go b/flag_int64_slice.go index 5f883bf3ec..1fe557a0df 100644 --- a/flag_int64_slice.go +++ b/flag_int64_slice.go @@ -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 } @@ -157,7 +159,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) } @@ -182,8 +184,8 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_int_slice.go b/flag_int_slice.go index 1a2c679872..9aa09655ca 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -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 } @@ -168,7 +170,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) } @@ -193,8 +195,8 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_path.go b/flag_path.go index 8a90685878..c5c6ffbeff 100644 --- a/flag_path.go +++ b/flag_path.go @@ -104,8 +104,8 @@ func (f *PathFlag) Apply(set *flag.FlagSet) error { // 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) } diff --git a/flag_string.go b/flag_string.go index 7d904a0296..b25e50dc98 100644 --- a/flag_string.go +++ b/flag_string.go @@ -105,8 +105,8 @@ func (f *StringFlag) Apply(set *flag.FlagSet) error { // 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 "" diff --git a/flag_string_slice.go b/flag_string_slice.go index a280505e52..33300f9b2a 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -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 } @@ -166,7 +168,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) } @@ -194,8 +196,8 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { // 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 diff --git a/flag_test.go b/flag_test.go index 44c3500a63..e385276cd7 100644 --- a/flag_test.go +++ b/flag_test.go @@ -400,7 +400,7 @@ func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { } } -var prefixStringFlagTests = []struct { +var _ = []struct { name string aliases []string usage string @@ -490,7 +490,7 @@ func TestPathFlagApply_SetsAllNames(t *testing.T) { expect(t, v, "/path/to/file/PATH") } -var envHintFlagTests = []struct { +var _ = []struct { name string env string hinter FlagEnvHintFunc @@ -2174,43 +2174,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"}, @@ -2230,6 +2230,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") @@ -2241,3 +2289,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") + } +} diff --git a/flag_timestamp.go b/flag_timestamp.go index 14be5f8c82..a38a5a8da8 100644 --- a/flag_timestamp.go +++ b/flag_timestamp.go @@ -171,8 +171,8 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error { } // 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 diff --git a/flag_uint.go b/flag_uint.go index f7efed11a0..809d836aac 100644 --- a/flag_uint.go +++ b/flag_uint.go @@ -109,8 +109,8 @@ func (f *UintFlag) GetEnvVars() []string { // 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 diff --git a/flag_uint64.go b/flag_uint64.go index 0b3edfe9b8..a1ba446ed8 100644 --- a/flag_uint64.go +++ b/flag_uint64.go @@ -109,8 +109,8 @@ func (f *Uint64Flag) GetEnvVars() []string { // 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 diff --git a/funcs.go b/funcs.go index 842b4aa99c..0a9b22c94a 100644 --- a/funcs.go +++ b/funcs.go @@ -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. diff --git a/help.go b/help.go index fad990a6e3..2f8156fca4 100644 --- a/help.go +++ b/help.go @@ -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 } diff --git a/internal/build/build.go b/internal/build/build.go index 4cbaa68979..dec0dfb64f 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -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 diff --git a/testdata/empty.yml b/testdata/empty.yml new file mode 100644 index 0000000000..ab2fc5dd55 --- /dev/null +++ b/testdata/empty.yml @@ -0,0 +1 @@ +# empty file \ No newline at end of file