diff --git a/altsrc/altsrc.go b/altsrc/altsrc.go deleted file mode 100644 index ac34bf633b..0000000000 --- a/altsrc/altsrc.go +++ /dev/null @@ -1,3 +0,0 @@ -package altsrc - -//go:generate python ../generate-flag-types altsrc -i ../flag-types.json -o flag_generated.go diff --git a/altsrc/flag.go b/altsrc/flag.go deleted file mode 100644 index 84ef009a5b..0000000000 --- a/altsrc/flag.go +++ /dev/null @@ -1,261 +0,0 @@ -package altsrc - -import ( - "fmt" - "strconv" - "strings" - "syscall" - - "gopkg.in/urfave/cli.v1" -) - -// FlagInputSourceExtension is an extension interface of cli.Flag that -// allows a value to be set on the existing parsed flags. -type FlagInputSourceExtension interface { - cli.Flag - ApplyInputSourceValue(context *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 { - for _, f := range flags { - inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension) - if isType { - err := inputSourceExtendedFlag.ApplyInputSourceValue(context, inputSourceContext) - if err != nil { - return err - } - } - } - - return nil -} - -// InitInputSource 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. 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 { - 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) - } -} - -// 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) - if err != nil { - return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error()) - } - - return ApplyInputSourceValues(context, 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.EnvVar) { - value, err := isc.Generic(f.GenericFlag.Name) - if err != nil { - return err - } - if value != nil { - eachName(f.Name, func(name string) { - f.set.Set(f.Name, value.String()) - }) - } - } - } - - return nil -} - -// 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.EnvVar) { - value, err := isc.StringSlice(f.StringSliceFlag.Name) - if err != nil { - return err - } - if value != nil { - var sliceValue cli.StringSlice = value - eachName(f.Name, func(name string) { - underlyingFlag := f.set.Lookup(f.Name) - if underlyingFlag != nil { - underlyingFlag.Value = &sliceValue - } - }) - } - } - } - return nil -} - -// 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.EnvVar) { - value, err := isc.IntSlice(f.IntSliceFlag.Name) - if err != nil { - return err - } - if value != nil { - var sliceValue cli.IntSlice = value - eachName(f.Name, func(name string) { - underlyingFlag := f.set.Lookup(f.Name) - if underlyingFlag != nil { - underlyingFlag.Value = &sliceValue - } - }) - } - } - } - return nil -} - -// 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.EnvVar) { - value, err := isc.Bool(f.BoolFlag.Name) - if err != nil { - return err - } - if value { - eachName(f.Name, func(name string) { - f.set.Set(f.Name, strconv.FormatBool(value)) - }) - } - } - } - return nil -} - -// ApplyInputSourceValue applies a BoolT value to the flagSet if required -func (f *BoolTFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { - if f.set != nil { - if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { - value, err := isc.BoolT(f.BoolTFlag.Name) - if err != nil { - return err - } - if !value { - eachName(f.Name, func(name string) { - f.set.Set(f.Name, strconv.FormatBool(value)) - }) - } - } - } - return nil -} - -// 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.EnvVar)) { - value, err := isc.String(f.StringFlag.Name) - if err != nil { - return err - } - if value != "" { - eachName(f.Name, func(name string) { - f.set.Set(f.Name, value) - }) - } - } - } - return nil -} - -// 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.EnvVar)) { - value, err := isc.Int(f.IntFlag.Name) - if err != nil { - return err - } - if value > 0 { - eachName(f.Name, func(name string) { - f.set.Set(f.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.EnvVar)) { - value, err := isc.Duration(f.DurationFlag.Name) - if err != nil { - return err - } - if value > 0 { - eachName(f.Name, func(name string) { - f.set.Set(f.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.EnvVar)) { - value, err := isc.Float64(f.Float64Flag.Name) - if err != nil { - return err - } - if value > 0 { - floatStr := float64ToString(value) - eachName(f.Name, func(name string) { - f.set.Set(f.Name, floatStr) - }) - } - } - } - return nil -} - -func isEnvVarSet(envVars string) bool { - for _, envVar := range strings.Split(envVars, ",") { - envVar = strings.TrimSpace(envVar) - if _, ok := syscall.Getenv(envVar); ok { - // TODO: Can't use this for bools as - // set means that it was true or false based on - // Bool flag type, should work for other types - return true - } - } - - return false -} - -func float64ToString(f float64) string { - return fmt.Sprintf("%v", f) -} - -func eachName(longName string, fn func(string)) { - parts := strings.Split(longName, ",") - for _, name := range parts { - name = strings.Trim(name, " ") - fn(name) - } -} diff --git a/altsrc/flag_generated.go b/altsrc/flag_generated.go deleted file mode 100644 index 0aeb0b0414..0000000000 --- a/altsrc/flag_generated.go +++ /dev/null @@ -1,347 +0,0 @@ -package altsrc - -import ( - "flag" - - "gopkg.in/urfave/cli.v1" -) - -// WARNING: This file is generated! - -// BoolFlag is the flag type that wraps cli.BoolFlag to allow -// for other values to be specified -type BoolFlag struct { - cli.BoolFlag - set *flag.FlagSet -} - -// NewBoolFlag creates a new BoolFlag -func NewBoolFlag(fl cli.BoolFlag) *BoolFlag { - return &BoolFlag{BoolFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped BoolFlag.Apply -func (f *BoolFlag) Apply(set *flag.FlagSet) { - f.set = set - f.BoolFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped BoolFlag.ApplyWithError -func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.BoolFlag.ApplyWithError(set) -} - -// BoolTFlag is the flag type that wraps cli.BoolTFlag to allow -// for other values to be specified -type BoolTFlag struct { - cli.BoolTFlag - set *flag.FlagSet -} - -// NewBoolTFlag creates a new BoolTFlag -func NewBoolTFlag(fl cli.BoolTFlag) *BoolTFlag { - return &BoolTFlag{BoolTFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped BoolTFlag.Apply -func (f *BoolTFlag) Apply(set *flag.FlagSet) { - f.set = set - f.BoolTFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped BoolTFlag.ApplyWithError -func (f *BoolTFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.BoolTFlag.ApplyWithError(set) -} - -// DurationFlag is the flag type that wraps cli.DurationFlag to allow -// for other values to be specified -type DurationFlag struct { - cli.DurationFlag - set *flag.FlagSet -} - -// NewDurationFlag creates a new DurationFlag -func NewDurationFlag(fl cli.DurationFlag) *DurationFlag { - return &DurationFlag{DurationFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped DurationFlag.Apply -func (f *DurationFlag) Apply(set *flag.FlagSet) { - f.set = set - f.DurationFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped DurationFlag.ApplyWithError -func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.DurationFlag.ApplyWithError(set) -} - -// Float64Flag is the flag type that wraps cli.Float64Flag to allow -// for other values to be specified -type Float64Flag struct { - cli.Float64Flag - set *flag.FlagSet -} - -// NewFloat64Flag creates a new Float64Flag -func NewFloat64Flag(fl cli.Float64Flag) *Float64Flag { - return &Float64Flag{Float64Flag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped Float64Flag.Apply -func (f *Float64Flag) Apply(set *flag.FlagSet) { - f.set = set - f.Float64Flag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Float64Flag.ApplyWithError -func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.Float64Flag.ApplyWithError(set) -} - -// GenericFlag is the flag type that wraps cli.GenericFlag to allow -// for other values to be specified -type GenericFlag struct { - cli.GenericFlag - set *flag.FlagSet -} - -// NewGenericFlag creates a new GenericFlag -func NewGenericFlag(fl cli.GenericFlag) *GenericFlag { - return &GenericFlag{GenericFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped GenericFlag.Apply -func (f *GenericFlag) Apply(set *flag.FlagSet) { - f.set = set - f.GenericFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped GenericFlag.ApplyWithError -func (f *GenericFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.GenericFlag.ApplyWithError(set) -} - -// Int64Flag is the flag type that wraps cli.Int64Flag to allow -// for other values to be specified -type Int64Flag struct { - cli.Int64Flag - set *flag.FlagSet -} - -// NewInt64Flag creates a new Int64Flag -func NewInt64Flag(fl cli.Int64Flag) *Int64Flag { - return &Int64Flag{Int64Flag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped Int64Flag.Apply -func (f *Int64Flag) Apply(set *flag.FlagSet) { - f.set = set - f.Int64Flag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Int64Flag.ApplyWithError -func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.Int64Flag.ApplyWithError(set) -} - -// IntFlag is the flag type that wraps cli.IntFlag to allow -// for other values to be specified -type IntFlag struct { - cli.IntFlag - set *flag.FlagSet -} - -// NewIntFlag creates a new IntFlag -func NewIntFlag(fl cli.IntFlag) *IntFlag { - return &IntFlag{IntFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped IntFlag.Apply -func (f *IntFlag) Apply(set *flag.FlagSet) { - f.set = set - f.IntFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped IntFlag.ApplyWithError -func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.IntFlag.ApplyWithError(set) -} - -// IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow -// for other values to be specified -type IntSliceFlag struct { - cli.IntSliceFlag - set *flag.FlagSet -} - -// NewIntSliceFlag creates a new IntSliceFlag -func NewIntSliceFlag(fl cli.IntSliceFlag) *IntSliceFlag { - return &IntSliceFlag{IntSliceFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped IntSliceFlag.Apply -func (f *IntSliceFlag) Apply(set *flag.FlagSet) { - f.set = set - f.IntSliceFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped IntSliceFlag.ApplyWithError -func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.IntSliceFlag.ApplyWithError(set) -} - -// Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow -// for other values to be specified -type Int64SliceFlag struct { - cli.Int64SliceFlag - set *flag.FlagSet -} - -// NewInt64SliceFlag creates a new Int64SliceFlag -func NewInt64SliceFlag(fl cli.Int64SliceFlag) *Int64SliceFlag { - return &Int64SliceFlag{Int64SliceFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped Int64SliceFlag.Apply -func (f *Int64SliceFlag) Apply(set *flag.FlagSet) { - f.set = set - f.Int64SliceFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Int64SliceFlag.ApplyWithError -func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.Int64SliceFlag.ApplyWithError(set) -} - -// StringFlag is the flag type that wraps cli.StringFlag to allow -// for other values to be specified -type StringFlag struct { - cli.StringFlag - set *flag.FlagSet -} - -// NewStringFlag creates a new StringFlag -func NewStringFlag(fl cli.StringFlag) *StringFlag { - return &StringFlag{StringFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped StringFlag.Apply -func (f *StringFlag) Apply(set *flag.FlagSet) { - f.set = set - f.StringFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped StringFlag.ApplyWithError -func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.StringFlag.ApplyWithError(set) -} - -// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow -// for other values to be specified -type StringSliceFlag struct { - cli.StringSliceFlag - set *flag.FlagSet -} - -// NewStringSliceFlag creates a new StringSliceFlag -func NewStringSliceFlag(fl cli.StringSliceFlag) *StringSliceFlag { - return &StringSliceFlag{StringSliceFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped StringSliceFlag.Apply -func (f *StringSliceFlag) Apply(set *flag.FlagSet) { - f.set = set - f.StringSliceFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped StringSliceFlag.ApplyWithError -func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.StringSliceFlag.ApplyWithError(set) -} - -// Uint64Flag is the flag type that wraps cli.Uint64Flag to allow -// for other values to be specified -type Uint64Flag struct { - cli.Uint64Flag - set *flag.FlagSet -} - -// NewUint64Flag creates a new Uint64Flag -func NewUint64Flag(fl cli.Uint64Flag) *Uint64Flag { - return &Uint64Flag{Uint64Flag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped Uint64Flag.Apply -func (f *Uint64Flag) Apply(set *flag.FlagSet) { - f.set = set - f.Uint64Flag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Uint64Flag.ApplyWithError -func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.Uint64Flag.ApplyWithError(set) -} - -// UintFlag is the flag type that wraps cli.UintFlag to allow -// for other values to be specified -type UintFlag struct { - cli.UintFlag - set *flag.FlagSet -} - -// NewUintFlag creates a new UintFlag -func NewUintFlag(fl cli.UintFlag) *UintFlag { - return &UintFlag{UintFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls the -// wrapped UintFlag.Apply -func (f *UintFlag) Apply(set *flag.FlagSet) { - f.set = set - f.UintFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped UintFlag.ApplyWithError -func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.UintFlag.ApplyWithError(set) -} diff --git a/altsrc/flag_test.go b/altsrc/flag_test.go deleted file mode 100644 index cd182942d5..0000000000 --- a/altsrc/flag_test.go +++ /dev/null @@ -1,336 +0,0 @@ -package altsrc - -import ( - "flag" - "fmt" - "os" - "strings" - "testing" - "time" - - "gopkg.in/urfave/cli.v1" -) - -type testApplyInputSource struct { - Flag FlagInputSourceExtension - FlagName string - FlagSetName string - Expected string - ContextValueString string - ContextValue flag.Value - EnvVarValue string - EnvVarName string - MapValue interface{} -} - -func TestGenericApplyInputSourceValue(t *testing.T) { - v := &Parser{"abc", "def"} - c := runTest(t, testApplyInputSource{ - Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}}), - FlagName: "test", - MapValue: v, - }) - expect(t, v, c.Generic("test")) -} - -func TestGenericApplyInputSourceMethodContextSet(t *testing.T) { - p := &Parser{"abc", "def"} - c := runTest(t, testApplyInputSource{ - Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}}), - FlagName: "test", - MapValue: &Parser{"efg", "hig"}, - ContextValueString: p.String(), - }) - expect(t, p, c.Generic("test")) -} - -func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}, EnvVar: "TEST"}), - FlagName: "test", - MapValue: &Parser{"efg", "hij"}, - EnvVarName: "TEST", - EnvVarValue: "abc,def", - }) - expect(t, &Parser{"abc", "def"}, c.Generic("test")) -} - -func TestStringSliceApplyInputSourceValue(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}), - FlagName: "test", - MapValue: []interface{}{"hello", "world"}, - }) - expect(t, c.StringSlice("test"), []string{"hello", "world"}) -} - -func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}), - FlagName: "test", - MapValue: []interface{}{"hello", "world"}, - ContextValueString: "ohno", - }) - expect(t, c.StringSlice("test"), []string{"ohno"}) -} - -func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test", EnvVar: "TEST"}), - FlagName: "test", - MapValue: []interface{}{"hello", "world"}, - EnvVarName: "TEST", - EnvVarValue: "oh,no", - }) - expect(t, c.StringSlice("test"), []string{"oh", "no"}) -} - -func TestIntSliceApplyInputSourceValue(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - }) - expect(t, c.IntSlice("test"), []int{1, 2}) -} - -func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - ContextValueString: "3", - }) - expect(t, c.IntSlice("test"), []int{3}) -} - -func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test", EnvVar: "TEST"}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - EnvVarName: "TEST", - EnvVarValue: "3,4", - }) - expect(t, c.IntSlice("test"), []int{3, 4}) -} - -func TestBoolApplyInputSourceMethodSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewBoolFlag(cli.BoolFlag{Name: "test"}), - FlagName: "test", - MapValue: true, - }) - expect(t, true, c.Bool("test")) -} - -func TestBoolApplyInputSourceMethodContextSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewBoolFlag(cli.BoolFlag{Name: "test"}), - FlagName: "test", - MapValue: false, - ContextValueString: "true", - }) - expect(t, true, c.Bool("test")) -} - -func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewBoolFlag(cli.BoolFlag{Name: "test", EnvVar: "TEST"}), - FlagName: "test", - MapValue: false, - EnvVarName: "TEST", - EnvVarValue: "true", - }) - expect(t, true, c.Bool("test")) -} - -func TestBoolTApplyInputSourceMethodSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test"}), - FlagName: "test", - MapValue: false, - }) - expect(t, false, c.BoolT("test")) -} - -func TestBoolTApplyInputSourceMethodContextSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test"}), - FlagName: "test", - MapValue: true, - ContextValueString: "false", - }) - expect(t, false, c.BoolT("test")) -} - -func TestBoolTApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test", EnvVar: "TEST"}), - FlagName: "test", - MapValue: true, - EnvVarName: "TEST", - EnvVarValue: "false", - }) - expect(t, false, c.BoolT("test")) -} - -func TestStringApplyInputSourceMethodSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewStringFlag(cli.StringFlag{Name: "test"}), - FlagName: "test", - MapValue: "hello", - }) - expect(t, "hello", c.String("test")) -} - -func TestStringApplyInputSourceMethodContextSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewStringFlag(cli.StringFlag{Name: "test"}), - FlagName: "test", - MapValue: "hello", - ContextValueString: "goodbye", - }) - expect(t, "goodbye", c.String("test")) -} - -func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewStringFlag(cli.StringFlag{Name: "test", EnvVar: "TEST"}), - FlagName: "test", - MapValue: "hello", - EnvVarName: "TEST", - EnvVarValue: "goodbye", - }) - expect(t, "goodbye", c.String("test")) -} - -func TestIntApplyInputSourceMethodSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewIntFlag(cli.IntFlag{Name: "test"}), - FlagName: "test", - MapValue: 15, - }) - expect(t, 15, c.Int("test")) -} - -func TestIntApplyInputSourceMethodContextSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewIntFlag(cli.IntFlag{Name: "test"}), - FlagName: "test", - MapValue: 15, - ContextValueString: "7", - }) - expect(t, 7, c.Int("test")) -} - -func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "TEST"}), - FlagName: "test", - MapValue: 15, - EnvVarName: "TEST", - EnvVarValue: "12", - }) - expect(t, 12, c.Int("test")) -} - -func TestDurationApplyInputSourceMethodSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewDurationFlag(cli.DurationFlag{Name: "test"}), - FlagName: "test", - MapValue: time.Duration(30 * time.Second), - }) - expect(t, time.Duration(30*time.Second), c.Duration("test")) -} - -func TestDurationApplyInputSourceMethodContextSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewDurationFlag(cli.DurationFlag{Name: "test"}), - FlagName: "test", - MapValue: time.Duration(30 * time.Second), - ContextValueString: time.Duration(15 * time.Second).String(), - }) - expect(t, time.Duration(15*time.Second), c.Duration("test")) -} - -func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewDurationFlag(cli.DurationFlag{Name: "test", EnvVar: "TEST"}), - FlagName: "test", - MapValue: time.Duration(30 * time.Second), - EnvVarName: "TEST", - EnvVarValue: time.Duration(15 * time.Second).String(), - }) - expect(t, time.Duration(15*time.Second), c.Duration("test")) -} - -func TestFloat64ApplyInputSourceMethodSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewFloat64Flag(cli.Float64Flag{Name: "test"}), - FlagName: "test", - MapValue: 1.3, - }) - expect(t, 1.3, c.Float64("test")) -} - -func TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewFloat64Flag(cli.Float64Flag{Name: "test"}), - FlagName: "test", - MapValue: 1.3, - ContextValueString: fmt.Sprintf("%v", 1.4), - }) - expect(t, 1.4, c.Float64("test")) -} - -func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewFloat64Flag(cli.Float64Flag{Name: "test", EnvVar: "TEST"}), - FlagName: "test", - MapValue: 1.3, - EnvVarName: "TEST", - EnvVarValue: fmt.Sprintf("%v", 1.4), - }) - expect(t, 1.4, c.Float64("test")) -} - -func runTest(t *testing.T, test testApplyInputSource) *cli.Context { - inputSource := &MapInputSource{valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue}} - set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError) - c := cli.NewContext(nil, set, nil) - if test.EnvVarName != "" && test.EnvVarValue != "" { - os.Setenv(test.EnvVarName, test.EnvVarValue) - defer os.Setenv(test.EnvVarName, "") - } - - test.Flag.Apply(set) - if test.ContextValue != nil { - flag := set.Lookup(test.FlagName) - flag.Value = test.ContextValue - } - if test.ContextValueString != "" { - set.Set(test.FlagName, test.ContextValueString) - } - test.Flag.ApplyInputSourceValue(c, inputSource) - - return c -} - -type Parser [2]string - -func (p *Parser) Set(value string) error { - parts := strings.Split(value, ",") - if len(parts) != 2 { - return fmt.Errorf("invalid format") - } - - (*p)[0] = parts[0] - (*p)[1] = parts[1] - - return nil -} - -func (p *Parser) String() string { - return fmt.Sprintf("%s,%s", p[0], p[1]) -} diff --git a/altsrc/helpers_test.go b/altsrc/helpers_test.go deleted file mode 100644 index 3b7f7e94e0..0000000000 --- a/altsrc/helpers_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package altsrc - -import ( - "reflect" - "testing" -) - -func expect(t *testing.T, a interface{}, b interface{}) { - if !reflect.DeepEqual(b, a) { - t.Errorf("Expected %#v (type %v) - Got %#v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) - } -} - -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)) - } -} diff --git a/altsrc/input_source_context.go b/altsrc/input_source_context.go deleted file mode 100644 index 276dcda089..0000000000 --- a/altsrc/input_source_context.go +++ /dev/null @@ -1,21 +0,0 @@ -package altsrc - -import ( - "time" - - "gopkg.in/urfave/cli.v1" -) - -// InputSourceContext is an interface used to allow -// other input sources to be implemented as needed. -type InputSourceContext interface { - Int(name string) (int, error) - Duration(name string) (time.Duration, error) - Float64(name string) (float64, error) - String(name string) (string, error) - StringSlice(name string) ([]string, error) - IntSlice(name string) ([]int, error) - Generic(name string) (cli.Generic, error) - Bool(name string) (bool, error) - BoolT(name string) (bool, error) -} diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go deleted file mode 100644 index c5d211475b..0000000000 --- a/altsrc/map_input_source.go +++ /dev/null @@ -1,262 +0,0 @@ -package altsrc - -import ( - "fmt" - "reflect" - "strings" - "time" - - "gopkg.in/urfave/cli.v1" -) - -// MapInputSource implements InputSourceContext to return -// data from the map that is loaded. -type MapInputSource struct { - valueMap map[interface{}]interface{} -} - -// nestedVal checks if the name has '.' delimiters. -// If so, it tries to traverse the tree by the '.' delimited sections to find -// a nested value for the key. -func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool) { - if sections := strings.Split(name, "."); len(sections) > 1 { - node := tree - for _, section := range sections[:len(sections)-1] { - if child, ok := node[section]; !ok { - return nil, false - } else { - if ctype, ok := child.(map[interface{}]interface{}); !ok { - return nil, false - } else { - node = ctype - } - } - } - if val, ok := node[sections[len(sections)-1]]; ok { - return val, true - } - } - return nil, false -} - -// Int returns an int from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Int(name string) (int, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(int) - if !isType { - return 0, incorrectTypeForFlagError(name, "int", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(int) - if !isType { - return 0, incorrectTypeForFlagError(name, "int", nestedGenericValue) - } - return otherValue, nil - } - - return 0, nil -} - -// Duration returns a duration from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Duration(name string) (time.Duration, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(time.Duration) - if !isType { - return 0, incorrectTypeForFlagError(name, "duration", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(time.Duration) - if !isType { - return 0, incorrectTypeForFlagError(name, "duration", nestedGenericValue) - } - return otherValue, nil - } - - return 0, nil -} - -// Float64 returns an float64 from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Float64(name string) (float64, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(float64) - if !isType { - return 0, incorrectTypeForFlagError(name, "float64", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(float64) - if !isType { - return 0, incorrectTypeForFlagError(name, "float64", nestedGenericValue) - } - return otherValue, nil - } - - return 0, nil -} - -// String returns a string from the map if it exists otherwise returns an empty string -func (fsm *MapInputSource) String(name string) (string, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(string) - if !isType { - return "", incorrectTypeForFlagError(name, "string", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(string) - if !isType { - return "", incorrectTypeForFlagError(name, "string", nestedGenericValue) - } - return otherValue, nil - } - - return "", nil -} - -// StringSlice returns an []string from the map if it exists otherwise returns nil -func (fsm *MapInputSource) StringSlice(name string) ([]string, error) { - otherGenericValue, exists := fsm.valueMap[name] - if !exists { - otherGenericValue, exists = nestedVal(name, fsm.valueMap) - if !exists { - return nil, nil - } - } - - otherValue, isType := otherGenericValue.([]interface{}) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) - } - - stringSlice := make([]string, 0, len(otherValue)) - for i, v := range otherValue { - stringValue, isType := v.(string) - - if !isType { - return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "string", v) - } - - stringSlice = append(stringSlice, stringValue) - } - - return stringSlice, nil -} - -// IntSlice returns an []int from the map if it exists otherwise returns nil -func (fsm *MapInputSource) IntSlice(name string) ([]int, error) { - otherGenericValue, exists := fsm.valueMap[name] - if !exists { - otherGenericValue, exists = nestedVal(name, fsm.valueMap) - if !exists { - return nil, nil - } - } - - otherValue, isType := otherGenericValue.([]interface{}) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) - } - - intSlice := make([]int, 0, len(otherValue)) - for i, v := range otherValue { - intValue, isType := v.(int) - - if !isType { - return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v) - } - - intSlice = append(intSlice, intValue) - } - - return intSlice, nil -} - -// Generic returns an cli.Generic from the map if it exists otherwise returns nil -func (fsm *MapInputSource) Generic(name string) (cli.Generic, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(cli.Generic) - if !isType { - return nil, incorrectTypeForFlagError(name, "cli.Generic", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(cli.Generic) - if !isType { - return nil, incorrectTypeForFlagError(name, "cli.Generic", nestedGenericValue) - } - return otherValue, nil - } - - return nil, nil -} - -// Bool returns an bool from the map otherwise returns false -func (fsm *MapInputSource) Bool(name string) (bool, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(bool) - if !isType { - return false, incorrectTypeForFlagError(name, "bool", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(bool) - if !isType { - return false, incorrectTypeForFlagError(name, "bool", nestedGenericValue) - } - return otherValue, nil - } - - return false, nil -} - -// BoolT returns an bool from the map otherwise returns true -func (fsm *MapInputSource) BoolT(name string) (bool, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(bool) - if !isType { - return true, incorrectTypeForFlagError(name, "bool", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(bool) - if !isType { - return true, incorrectTypeForFlagError(name, "bool", nestedGenericValue) - } - return otherValue, nil - } - - return true, nil -} - -func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error { - valueType := reflect.TypeOf(value) - valueTypeName := "" - if valueType != nil { - valueTypeName = valueType.Name() - } - - return fmt.Errorf("Mismatched type for flag '%s'. Expected '%s' but actual is '%s'", name, expectedTypeName, valueTypeName) -} diff --git a/altsrc/toml_command_test.go b/altsrc/toml_command_test.go deleted file mode 100644 index 22ecba919d..0000000000 --- a/altsrc/toml_command_test.go +++ /dev/null @@ -1,320 +0,0 @@ -// Disabling building of toml support in cases where golang is 1.0 or 1.1 -// as the encoding library is not implemented or supported. - -//go:build go1.2 -// +build go1.2 - -package altsrc - -import ( - "flag" - "io/ioutil" - "os" - "testing" - - "gopkg.in/urfave/cli.v1" -) - -func TestCommandTomFileTest(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - - os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "THE_TEST"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) - defer os.Remove("current.toml") - - os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "top.test", EnvVar: "THE_TEST"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - - test := []string{"test-cmd", "--load", "current.toml", "--test", "7"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte(`[top] - test = 15`), 0666) - defer os.Remove("current.toml") - - test := []string{"test-cmd", "--load", "current.toml", "--top.test", "7"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "top.test"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - - test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test", Value: 7}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) - defer os.Remove("current.toml") - - test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - - os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test", Value: 7, EnvVar: "THE_TEST"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWinsNested(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) - defer os.Remove("current.toml") - - os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7, EnvVar: "THE_TEST"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) - - expect(t, err, nil) -} diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go deleted file mode 100644 index ad865ebdbe..0000000000 --- a/altsrc/toml_file_loader.go +++ /dev/null @@ -1,114 +0,0 @@ -// Disabling building of toml support in cases where golang is 1.0 or 1.1 -// as the encoding library is not implemented or supported. - -//go:build go1.2 -// +build go1.2 - -package altsrc - -import ( - "fmt" - "reflect" - - "github.com/BurntSushi/toml" - "gopkg.in/urfave/cli.v1" -) - -type tomlMap struct { - Map map[interface{}]interface{} -} - -func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { - ret = make(map[interface{}]interface{}) - m := i.(map[string]interface{}) - for key, val := range m { - v := reflect.ValueOf(val) - switch v.Kind() { - case reflect.Bool: - ret[key] = val.(bool) - case reflect.String: - ret[key] = val.(string) - case reflect.Int: - ret[key] = int(val.(int)) - case reflect.Int8: - ret[key] = int(val.(int8)) - case reflect.Int16: - ret[key] = int(val.(int16)) - case reflect.Int32: - ret[key] = int(val.(int32)) - case reflect.Int64: - ret[key] = int(val.(int64)) - case reflect.Uint: - ret[key] = int(val.(uint)) - case reflect.Uint8: - ret[key] = int(val.(uint8)) - case reflect.Uint16: - ret[key] = int(val.(uint16)) - case reflect.Uint32: - ret[key] = int(val.(uint32)) - case reflect.Uint64: - ret[key] = int(val.(uint64)) - case reflect.Float32: - ret[key] = float64(val.(float32)) - case reflect.Float64: - ret[key] = float64(val.(float64)) - case reflect.Map: - if tmp, err := unmarshalMap(val); err == nil { - ret[key] = tmp - } else { - return nil, err - } - case reflect.Array, reflect.Slice: - ret[key] = val.([]interface{}) - default: - return nil, fmt.Errorf("Unsupported: type = %#v", v.Kind()) - } - } - return ret, nil -} - -func (self *tomlMap) UnmarshalTOML(i interface{}) error { - if tmp, err := unmarshalMap(i); err == nil { - self.Map = tmp - } else { - return err - } - return nil -} - -type tomlSourceContext struct { - FilePath string -} - -// NewTomlSourceFromFile creates a new TOML InputSourceContext from a filepath. -func NewTomlSourceFromFile(file string) (InputSourceContext, error) { - tsc := &tomlSourceContext{FilePath: file} - var results tomlMap = tomlMap{} - if err := readCommandToml(tsc.FilePath, &results); err != nil { - return nil, fmt.Errorf("Unable to load TOML file '%s': inner error: \n'%v'", tsc.FilePath, err.Error()) - } - return &MapInputSource{valueMap: results.Map}, nil -} - -// 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) { - filePath := context.String(flagFileName) - return NewTomlSourceFromFile(filePath) - } -} - -func readCommandToml(filePath string, container interface{}) (err error) { - b, err := loadDataFrom(filePath) - if err != nil { - return err - } - - err = toml.Unmarshal(b, container) - if err != nil { - return err - } - - err = nil - return -} diff --git a/altsrc/yaml_command_test.go b/altsrc/yaml_command_test.go deleted file mode 100644 index 44645a1813..0000000000 --- a/altsrc/yaml_command_test.go +++ /dev/null @@ -1,323 +0,0 @@ -// Disabling building of yaml support in cases where golang is 1.0 or 1.1 -// as the encoding library is not implemented or supported. - -//go:build go1.2 -// +build go1.2 - -package altsrc - -import ( - "flag" - "io/ioutil" - "os" - "testing" - - "gopkg.in/urfave/cli.v1" -) - -func TestCommandYamlFileTest(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - - os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "THE_TEST"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestGlobalEnvVarWinsNested(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte(`top: - test: 15`), 0666) - defer os.Remove("current.yaml") - - os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "top.test", EnvVar: "THE_TEST"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - - test := []string{"test-cmd", "--load", "current.yaml", "--test", "7"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestSpecifiedFlagWinsNested(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte(`top: - test: 15`), 0666) - defer os.Remove("current.yaml") - - test := []string{"test-cmd", "--load", "current.yaml", "--top.test", "7"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "top.test"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - - test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test", Value: 7}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestDefaultValueFileWinsNested(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte(`top: - test: 15`), 0666) - defer os.Remove("current.yaml") - - test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - - os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test", Value: 7, EnvVar: "THE_TEST"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) - - expect(t, err, nil) -} - -func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWinsNested(t *testing.T) { - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte(`top: - test: 15`), 0666) - defer os.Remove("current.yaml") - - os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7, EnvVar: "THE_TEST"}), - cli.StringFlag{Name: "load"}, - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) - - expect(t, err, nil) -} diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go deleted file mode 100644 index 4d32bbb5db..0000000000 --- a/altsrc/yaml_file_loader.go +++ /dev/null @@ -1,93 +0,0 @@ -// Disabling building of yaml support in cases where golang is 1.0 or 1.1 -// as the encoding library is not implemented or supported. - -//go:build go1.2 -// +build go1.2 - -package altsrc - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "runtime" - "strings" - - "gopkg.in/urfave/cli.v1" - - "gopkg.in/yaml.v2" -) - -type yamlSourceContext struct { - FilePath string -} - -// NewYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath. -func NewYamlSourceFromFile(file string) (InputSourceContext, error) { - ysc := &yamlSourceContext{FilePath: file} - var results map[interface{}]interface{} - err := readCommandYaml(ysc.FilePath, &results) - if err != nil { - return nil, fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", ysc.FilePath, err.Error()) - } - - return &MapInputSource{valueMap: results}, nil -} - -// 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) { - filePath := context.String(flagFileName) - return NewYamlSourceFromFile(filePath) - } -} - -func readCommandYaml(filePath string, container interface{}) (err error) { - b, err := loadDataFrom(filePath) - if err != nil { - return err - } - - err = yaml.Unmarshal(b, container) - if err != nil { - return err - } - - err = nil - return -} - -func loadDataFrom(filePath string) ([]byte, error) { - u, err := url.Parse(filePath) - if err != nil { - return nil, err - } - - if u.Host != "" { // i have a host, now do i support the scheme? - switch u.Scheme { - case "http", "https": - res, err := http.Get(filePath) - if err != nil { - return nil, err - } - return ioutil.ReadAll(res.Body) - default: - return nil, fmt.Errorf("scheme of %s is unsupported", filePath) - } - } else if u.Path != "" { // i dont have a host, but I have a path. I am a local file. - if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil { - return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) - } - return ioutil.ReadFile(filePath) - } else if runtime.GOOS == "windows" && strings.Contains(u.String(), "\\") { - // on Windows systems u.Path is always empty, so we need to check the string directly. - if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil { - return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) - } - return ioutil.ReadFile(filePath) - } else { - return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) - } -} diff --git a/app.go b/app.go index 6ebfa19adb..a96f77bc84 100644 --- a/app.go +++ b/app.go @@ -46,8 +46,6 @@ type App struct { HideVersion bool // Populate on app startup, only gettable through method Categories() categories CommandCategories - // An action to execute when the bash-completion flag is set - BashComplete BashCompleteFunc // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run Before BeforeFunc @@ -124,16 +122,15 @@ func compileTime() time.Time { // Usage, Version and Action. func NewApp() *App { return &App{ - Name: filepath.Base(os.Args[0]), - HelpName: filepath.Base(os.Args[0]), - Usage: "A new cli application", - UsageText: "", - Version: "0.0.0", - BashComplete: DefaultAppComplete, - Action: helpCommand.Action, - Compiled: compileTime(), - Writer: os.Stdout, - HelpWriter: os.Stdout, + Name: filepath.Base(os.Args[0]), + HelpName: filepath.Base(os.Args[0]), + Usage: "A new cli application", + UsageText: "", + Version: "0.0.0", + Action: helpCommand.Action, + Compiled: compileTime(), + Writer: os.Stdout, + HelpWriter: os.Stdout, } } @@ -198,13 +195,13 @@ func (a *App) Setup() { func (a *App) Run(arguments []string) (err error) { a.Setup() - // handle the completion flag separately from the flagset since - // completion could be attempted after a flag, but before its value was put - // on the command line. this causes the flagset to interpret the completion - // flag name as the value of the flag before it which is undesirable - // note that we can only do this because the shell autocomplete function - // always appends the completion flag at the end of the command - shellComplete, arguments := checkShellCompleteFlag(a, arguments) + // Answer shell-completion requests: when completion is enabled and the + // process was spawned by the shell for completion (COMP_LINE is set), + // emit predictions and return without running the command. + if a.EnableBashCompletion && os.Getenv("COMP_LINE") != "" { + a.runShellCompletion() + return nil + } // parse flags set, err := flagSet(a.Name, a.Flags) @@ -220,11 +217,6 @@ func (a *App) Run(arguments []string) (err error) { fmt.Fprintln(a.Writer, nerr) return nerr } - context.shellComplete = shellComplete - - if checkCompletions(context) { - return nil - } if err != nil { if onUsageError := a.resolveOnUsageError(); onUsageError != nil { @@ -337,10 +329,6 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { return nerr } - if checkCompletions(context) { - return nil - } - if err != nil { if onUsageError := a.resolveOnUsageError(); onUsageError != nil { err = onUsageError(context, err, true) diff --git a/app_test.go b/app_test.go index b38927d87a..6fb4b380fd 100644 --- a/app_test.go +++ b/app_test.go @@ -11,6 +11,8 @@ import ( "reflect" "strings" "testing" + + "github.com/posener/complete" ) var ( @@ -27,7 +29,7 @@ func init() { } type opCounts struct { - Total, BashComplete, OnUsageError, Before, CommandNotFound, Action, After, SubCommand int + Total, OnUsageError, Before, CommandNotFound, Action, After, SubCommand int } func ExampleApp_Run() { @@ -228,43 +230,6 @@ func ExampleApp_Run_subcommandNoAction() { // --help, -h show help } -func ExampleApp_Run_bashComplete() { - // set args for examples sake - os.Args = []string{"greet", "--generate-bash-completion"} - - app := NewApp() - app.Name = "greet" - app.EnableBashCompletion = true - app.Commands = []Command{ - { - Name: "describeit", - Aliases: []string{"d"}, - Usage: "use it to see a description", - Description: "This is how we describe describeit the function", - Action: func(c *Context) error { - fmt.Printf("i like to describe things") - return nil - }, - }, { - Name: "next", - Usage: "next example", - Description: "more stuff to see when generating bash completion", - Action: func(c *Context) error { - fmt.Printf("the next example") - return nil - }, - }, - } - - app.Run(os.Args) - // Output: - // describeit - // d - // next - // help - // h -} - func TestApp_Run(t *testing.T) { s := "" @@ -884,12 +849,6 @@ func TestApp_OrderOfOperations(t *testing.T) { resetCounts := func() { counts = &opCounts{} } app := NewApp() - app.EnableBashCompletion = true - app.BashComplete = func(c *Context) { - counts.Total++ - counts.BashComplete = counts.Total - } - app.OnUsageError = func(c *Context, err error, isSubcommand bool) error { counts.Total++ counts.OnUsageError = counts.Total @@ -950,12 +909,6 @@ func TestApp_OrderOfOperations(t *testing.T) { resetCounts() - _ = app.Run([]string{"command", "--generate-bash-completion"}) - expect(t, counts.BashComplete, 1) - expect(t, counts.Total, 1) - - resetCounts() - oldOnUsageError := app.OnUsageError app.OnUsageError = nil _ = app.Run([]string{"command", "--nope"}) @@ -1537,8 +1490,13 @@ func (c *customBoolFlag) GetName() string { return c.Nombre } -func (c *customBoolFlag) Apply(set *flag.FlagSet) { +func (c *customBoolFlag) Apply(set *flag.FlagSet) error { set.String(c.Nombre, c.Nombre, "") + return nil +} + +func (c *customBoolFlag) GetPredictor() complete.Predictor { + return complete.PredictNothing } func TestCustomFlagsUnused(t *testing.T) { @@ -1595,47 +1553,3 @@ func TestHandleAction_WithUnknownPanic(t *testing.T) { } app.Action(NewContext(app, fs, nil)) } - -func TestShellCompletionForIncompleteFlags(t *testing.T) { - app := NewApp() - app.Flags = []Flag{ - IntFlag{ - Name: "test-completion", - }, - } - app.EnableBashCompletion = true - app.BashComplete = func(ctx *Context) { - for _, command := range ctx.App.Commands { - if command.Hidden { - continue - } - - for _, name := range command.Names() { - fmt.Fprintln(ctx.App.Writer, name) - } - } - - for _, flag := range ctx.App.Flags { - for _, name := range strings.Split(flag.GetName(), ",") { - if name == BashCompletionFlag.GetName() { - continue - } - - switch name = strings.TrimSpace(name); len(name) { - case 0: - case 1: - fmt.Fprintln(ctx.App.Writer, "-"+name) - default: - fmt.Fprintln(ctx.App.Writer, "--"+name) - } - } - } - } - app.Action = func(ctx *Context) error { - return fmt.Errorf("should not get here") - } - err := app.Run([]string{"", "--test-completion", "--" + BashCompletionFlag.GetName()}) - if err != nil { - t.Errorf("app should not return an error: %s", err) - } -} diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete deleted file mode 100755 index 37d9c14513..0000000000 --- a/autocomplete/bash_autocomplete +++ /dev/null @@ -1,16 +0,0 @@ -#! /bin/bash - -: ${PROG:=$(basename ${BASH_SOURCE})} - -_cli_bash_autocomplete() { - local cur opts base - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 -} - -complete -F _cli_bash_autocomplete $PROG - -unset PROG diff --git a/autocomplete/zsh_autocomplete b/autocomplete/zsh_autocomplete deleted file mode 100644 index 5430a18f95..0000000000 --- a/autocomplete/zsh_autocomplete +++ /dev/null @@ -1,5 +0,0 @@ -autoload -U compinit && compinit -autoload -U bashcompinit && bashcompinit - -script_dir=$(dirname $0) -source ${script_dir}/bash_autocomplete diff --git a/cli.go b/cli.go index 74fd101f45..c5c6f4cf01 100644 --- a/cli.go +++ b/cli.go @@ -1,21 +1,23 @@ // Package cli provides a minimal framework for creating and organizing command line // Go applications. cli is designed to be easy to understand and write, the most simple // cli application can be written as follows: -// func main() { -// cli.NewApp().Run(os.Args) -// } +// +// func main() { +// cli.NewApp().Run(os.Args) +// } // // Of course this application does not do much, so let's make this an actual application: -// func main() { -// app := cli.NewApp() -// app.Name = "greet" -// app.Usage = "say a greeting" -// app.Action = func(c *cli.Context) error { -// println("Greetings") -// } // -// app.Run(os.Args) -// } +// func main() { +// app := cli.NewApp() +// app.Name = "greet" +// app.Usage = "say a greeting" +// app.Action = func(c *cli.Context) error { +// println("Greetings") +// } +// +// app.Run(os.Args) +// } package cli -//go:generate python ./generate-flag-types cli -i flag-types.json -o flag_generated.go +//go:generate python3 ./generate-flag-types.py cli -i flag-types.json -o flag_generated.go diff --git a/command.go b/command.go index 3bf7a0452d..198caf1742 100644 --- a/command.go +++ b/command.go @@ -6,6 +6,8 @@ import ( "slices" "sort" "strings" + + "github.com/posener/complete" ) // Command is a subcommand for a cli.App. @@ -26,8 +28,9 @@ type Command struct { ArgsUsage string // The category the command is part of Category string - // The function to call when checking for bash command completions - BashComplete BashCompleteFunc + // CustomCompletePredictor predicts positional-argument completions for + // this command during shell completion. + CustomCompletePredictor complete.Predictor // An action to execute before any sub-subcommands are run, but after the context is ready // If a non-nil error is returned, no sub-subcommands are run Before BeforeFunc @@ -43,8 +46,6 @@ type Command struct { Subcommands Commands // List of flags to parse Flags []Flag - // Do not append the app-wide App.GlobalFlags to this command's flags - NoGlobalFlags bool // Treat all flags as normal arguments if true SkipFlagParsing bool // Skip argument reordering which attempts to move flags before arguments, @@ -199,9 +200,6 @@ func (c Command) Run(ctx *Context) (err error) { context := NewContext(ctx.App, set, ctx) context.Command = c - if checkCommandCompletions(context, c.Name) { - return nil - } if err != nil { if onUsageError := c.resolveOnUsageError(context); onUsageError != nil { @@ -337,12 +335,6 @@ func (c Command) startApp(ctx *Context) error { sort.Sort(app.categories) - // bash completion - app.EnableBashCompletion = ctx.App.EnableBashCompletion - if c.BashComplete != nil { - app.BashComplete = c.BashComplete - } - // set the actions app.Before = c.resolveBefore(ctx) app.After = c.resolveAfter(ctx) @@ -396,9 +388,6 @@ func (c Command) resolveAction(ctx *Context) ActionFunc { } func (c Command) resolveFlags(ctx *Context) []Flag { - if c.NoGlobalFlags { - return c.Flags - } return slices.Concat(c.Flags, ctx.App.GlobalFlags) } diff --git a/complete.go b/complete.go new file mode 100644 index 0000000000..33a9b89d3c --- /dev/null +++ b/complete.go @@ -0,0 +1,80 @@ +package cli + +import ( + "os" + "path/filepath" + "strings" + + "github.com/posener/complete" +) + +// flagsToCompleteFlags transforms a cli.Flag to complete.Flags +// understood by posener/complete library. +func flagsToCompleteFlags(flags []Flag) complete.Flags { + complFlags := make(complete.Flags) + flags = visibleFlags(flags) + for _, f := range flags { + for _, s := range strings.Split(f.GetName(), ",") { + var flagName string + s = strings.TrimSpace(s) + if len(s) == 1 { + flagName = "-" + s + } else { + flagName = "--" + s + } + complFlags[flagName] = f.GetPredictor() + } + } + return complFlags +} + +// cmdToCompleteCmd recursively transforms a Command (and its Subcommands) into +// a complete.Command understood by the posener/complete library. Hidden +// commands are skipped; aliases are registered alongside the primary name. The +// argument and flag-value predictors come from the command's own +// CustomCompletePredictor / CustomFlagPredictor fields. +func cmdToCompleteCmd(cmd Command, parentSubcommandMap complete.Commands) { + if cmd.Hidden { + return + } + + sub := make(complete.Commands) + for _, subCmd := range cmd.Subcommands { + cmdToCompleteCmd(subCmd, sub) + } + + compCmd := complete.Command{ + Sub: sub, + Args: cmd.CustomCompletePredictor, + Flags: flagsToCompleteFlags(cmd.Flags), + } + parentSubcommandMap[cmd.Name] = compCmd + if cmd.HiddenAliases { + return + } + for _, alias := range cmd.Aliases { + parentSubcommandMap[alias] = compCmd + } +} + +// shellCompleteCommand builds the root complete.Command for the application by +// walking its visible commands and global flags. +func (a *App) shellCompleteCommand() complete.Command { + sub := make(complete.Commands) + for _, cmd := range a.Commands { + cmdToCompleteCmd(cmd, sub) + } + return complete.Command{ + Sub: sub, + GlobalFlags: flagsToCompleteFlags(a.GlobalFlags), + } +} + +// runShellCompletion answers a single shell-completion request described by the +// COMP_LINE / COMP_POINT environment variables and prints the predicted +// options. It is invoked from App.Run when EnableBashCompletion is set and the +// process was spawned by the shell for completion. The name passed to posener +// must match how the shell invoked the binary, hence filepath.Base(os.Args[0]). +func (a *App) runShellCompletion() { + complete.New(filepath.Base(os.Args[0]), a.shellCompleteCommand()).Complete() +} diff --git a/complete_test.go b/complete_test.go new file mode 100644 index 0000000000..1d947cba79 --- /dev/null +++ b/complete_test.go @@ -0,0 +1,194 @@ +package cli + +import ( + "bufio" + "os" + "sort" + "strconv" + "strings" + "testing" + + "github.com/posener/complete" +) + +// newCompletionTestApp builds an app exercising every completion path: +// commands with aliases (including a hidden top-level alias and a hidden +// nested alias), a hidden command, a subcommand tree (with a hidden child), +// a leaf command carrying an arg predictor, a command with both a visible +// and a hidden flag-value predictor, and app-wide/global flags (again with a +// visible and a hidden one). Help/version are hidden to keep predictions +// clean. +func newCompletionTestApp() *App { + app := NewApp() + app.Name = "prog" + app.HideHelp = true + app.HideHelpCommand = true + app.HideVersion = true + app.EnableBashCompletion = true + app.GlobalFlags = []Flag{ + StringFlag{Name: "profile", CustomFlagPredictor: complete.PredictSet("dev", "prod")}, + StringFlag{Name: "secret-global-flag", Hidden: true}, + } + app.Commands = []Command{ + { + Name: "widget", + Aliases: []string{"w"}, + Subcommands: Commands{ + // mk is a hidden alias of make: exercises the nested + // HiddenAliases path alongside the top-level one on pick. + {Name: "make", Aliases: []string{"mk"}, HiddenAliases: true}, + {Name: "list"}, + {Name: "internal", Hidden: true}, + }, + }, + { + Name: "pick", + Aliases: []string{"pk"}, + HiddenAliases: true, + CustomCompletePredictor: complete.PredictSet("alpha", "beta"), + }, + { + Name: "paint", + Flags: []Flag{ + StringFlag{Name: "color", CustomFlagPredictor: complete.PredictSet("green", "red")}, + StringFlag{Name: "secret-color-flag", Hidden: true}, + }, + }, + { + Name: "secret", + Hidden: true, + }, + } + return app +} + +func TestShellCompletion(t *testing.T) { + app := newCompletionTestApp() + + cases := []struct { + name string + line string + want []string + }{ + { + name: "top-level command names, hidden excluded, aliases included, hidden alias excluded", + line: "prog ", + want: []string{"paint", "pick", "w", "widget"}, + }, + { + name: "subcommand recursion, hidden child excluded, hidden nested alias excluded", + line: "prog widget ", + want: []string{"list", "make"}, + }, + { + name: "alias resolves to same subcommands", + line: "prog w ", + want: []string{"list", "make"}, + }, + { + name: "arg predictor on leaf command", + line: "prog pick ", + want: []string{"alpha", "beta"}, + }, + { + name: "arg predictor honors prefix", + line: "prog pick a", + want: []string{"alpha"}, + }, + { + name: "flag name completion, hidden local flag excluded, global flag inherited", + line: "prog paint -", + want: []string{"--color", "--profile"}, + }, + { + name: "flag value prediction", + line: "prog paint --color ", + want: []string{"green", "red"}, + }, + { + name: "global flag value predicted inside a child command", + line: "prog widget --profile ", + want: []string{"dev", "prod"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := completeLine(t, app, tc.line) + if !eqStrings(got, tc.want) { + t.Errorf("line %q: got %v, want %v", tc.line, got, tc.want) + } + }) + } +} + +// TestShellCompletionDisabled verifies that without EnableBashCompletion the +// COMP_LINE path is not taken. +func TestShellCompletionDisabled(t *testing.T) { + app := newCompletionTestApp() + app.EnableBashCompletion = false + // No-op action so the normal (non-completion) path produces no output. + app.Action = func(*Context) error { return nil } + + got := completeLine(t, app, "prog ") + if len(got) != 0 { + t.Errorf("expected no completion output when disabled, got %v", got) + } +} + +// completeLine drives a single COMP_LINE completion request through app.Run and +// returns the predicted options, sorted, capturing what posener writes to +// stdout. +func completeLine(t *testing.T, app *App, line string) []string { + t.Helper() + + if err := os.Setenv("COMP_LINE", line); err != nil { + t.Fatal(err) + } + if err := os.Setenv("COMP_POINT", strconv.Itoa(len(line))); err != nil { + t.Fatal(err) + } + defer os.Unsetenv("COMP_LINE") + defer os.Unsetenv("COMP_POINT") + + old := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = w + + runErr := app.Run([]string{"prog"}) + + w.Close() + os.Stdout = old + + if runErr != nil { + t.Fatalf("Run returned error during completion: %v", runErr) + } + + var out []string + sc := bufio.NewScanner(r) + for sc.Scan() { + if s := strings.TrimSpace(sc.Text()); s != "" { + out = append(out, s) + } + } + if err := sc.Err(); err != nil { + t.Fatal(err) + } + sort.Strings(out) + return out +} + +func eqStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/context.go b/context.go index cb89e92a08..19de0d8482 100644 --- a/context.go +++ b/context.go @@ -15,7 +15,6 @@ import ( type Context struct { App *App Command Command - shellComplete bool flagSet *flag.FlagSet setFlags map[string]bool parentContext *Context @@ -23,13 +22,7 @@ type Context struct { // NewContext creates a new context. For use in when invoking an App or Command action. func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { - c := &Context{App: app, flagSet: set, parentContext: parentCtx} - - if parentCtx != nil { - c.shellComplete = parentCtx.shellComplete - } - - return c + return &Context{App: app, flagSet: set, parentContext: parentCtx} } // NumFlags returns the number of flags set diff --git a/flag.go b/flag.go index 8547f5385d..ae88b87923 100644 --- a/flag.go +++ b/flag.go @@ -9,16 +9,12 @@ import ( "strings" "syscall" "time" + + "github.com/posener/complete" ) const defaultPlaceholder = "value" -// BashCompletionFlag enables bash-completion for all commands and subcommands -var BashCompletionFlag Flag = BoolFlag{ - Name: "generate-bash-completion", - Hidden: true, -} - // VersionFlag prints the version for the application var VersionFlag Flag = BoolFlag{ Name: "version, v", @@ -58,30 +54,17 @@ func (f FlagsByName) Swap(i, j int) { type Flag interface { fmt.Stringer // Apply Flag settings to the given flag set - Apply(*flag.FlagSet) + Apply(*flag.FlagSet) error GetName() string -} - -// errorableFlag is an interface that allows us to return errors during apply -// it allows flags defined in this library to return errors in a fashion backwards compatible -// TODO remove in v2 and modify the existing Flag interface to return errors -type errorableFlag interface { - Flag - - ApplyWithError(*flag.FlagSet) error + GetPredictor() complete.Predictor } func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) for _, f := range flags { - // TODO remove in v2 when errorableFlag is removed - if ef, ok := f.(errorableFlag); ok { - if err := ef.ApplyWithError(set); err != nil { - return nil, err - } - } else { - f.Apply(set) + if err := f.Apply(set); err != nil { + return nil, err } } return set, nil @@ -103,14 +86,7 @@ type Generic interface { // Apply takes the flagset and calls Set on the generic flag with the value // provided by the user for parsing by the flag -// Ignores parsing errors -func (f GenericFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError takes the flagset and calls Set on the generic flag with the value -// provided by the user for parsing by the flag -func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error { +func (f GenericFlag) Apply(set *flag.FlagSet) error { val := f.Value if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { @@ -156,13 +132,7 @@ func (f *StringSlice) Get() interface{} { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f StringSliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { +func (f StringSliceFlag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -219,13 +189,7 @@ func (f *IntSlice) Get() interface{} { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f IntSliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { +func (f IntSliceFlag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -282,13 +246,7 @@ func (f *Int64Slice) Get() interface{} { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f Int64SliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { +func (f Int64SliceFlag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -316,13 +274,7 @@ func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f BoolFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error { +func (f BoolFlag) Apply(set *flag.FlagSet) error { val := false if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { @@ -356,13 +308,7 @@ func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f BoolTFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error { +func (f BoolTFlag) Apply(set *flag.FlagSet) error { val := true if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { @@ -396,13 +342,7 @@ func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f StringFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f StringFlag) ApplyWithError(set *flag.FlagSet) error { +func (f StringFlag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -425,13 +365,7 @@ func (f StringFlag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f IntFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f IntFlag) ApplyWithError(set *flag.FlagSet) error { +func (f IntFlag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -458,13 +392,7 @@ func (f IntFlag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f Int64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error { +func (f Int64Flag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -492,13 +420,7 @@ func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f UintFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f UintFlag) ApplyWithError(set *flag.FlagSet) error { +func (f UintFlag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -526,13 +448,7 @@ func (f UintFlag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f Uint64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error { +func (f Uint64Flag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -560,13 +476,7 @@ func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f DurationFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error { +func (f DurationFlag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) @@ -594,13 +504,7 @@ func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error { } // Apply populates the flag given the flag set and environment -// Ignores errors -func (f Float64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error { +func (f Float64Flag) Apply(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) diff --git a/flag_generated.go b/flag_generated.go index 491b61956c..8890556259 100644 --- a/flag_generated.go +++ b/flag_generated.go @@ -4,6 +4,8 @@ import ( "flag" "strconv" "time" + + "github.com/posener/complete" ) // WARNING: This file is generated! @@ -28,6 +30,12 @@ func (f BoolFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f BoolFlag) GetPredictor() complete.Predictor { + return complete.PredictNothing +} + // Bool looks up the value of a local BoolFlag, returns // false if not found func (c *Context) Bool(name string) bool { @@ -75,6 +83,12 @@ func (f BoolTFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f BoolTFlag) GetPredictor() complete.Predictor { + return complete.PredictNothing +} + // BoolT looks up the value of a local BoolTFlag, returns // false if not found func (c *Context) BoolT(name string) bool { @@ -104,12 +118,13 @@ func lookupBoolT(name string, set *flag.FlagSet) bool { // DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) type DurationFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value time.Duration - Destination *time.Duration + Name string + Usage string + EnvVar string + Hidden bool + Value time.Duration + Destination *time.Duration + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -123,6 +138,12 @@ func (f DurationFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f DurationFlag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // Duration looks up the value of a local DurationFlag, returns // 0 if not found func (c *Context) Duration(name string) time.Duration { @@ -152,12 +173,13 @@ func lookupDuration(name string, set *flag.FlagSet) time.Duration { // Float64Flag is a flag with type float64 type Float64Flag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value float64 - Destination *float64 + Name string + Usage string + EnvVar string + Hidden bool + Value float64 + Destination *float64 + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -171,6 +193,12 @@ func (f Float64Flag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f Float64Flag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // Float64 looks up the value of a local Float64Flag, returns // 0 if not found func (c *Context) Float64(name string) float64 { @@ -200,11 +228,12 @@ func lookupFloat64(name string, set *flag.FlagSet) float64 { // GenericFlag is a flag with type Generic type GenericFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value Generic + Name string + Usage string + EnvVar string + Hidden bool + Value Generic + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -218,6 +247,12 @@ func (f GenericFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f GenericFlag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // Generic looks up the value of a local GenericFlag, returns // nil if not found func (c *Context) Generic(name string) interface{} { @@ -247,12 +282,13 @@ func lookupGeneric(name string, set *flag.FlagSet) interface{} { // Int64Flag is a flag with type int64 type Int64Flag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value int64 - Destination *int64 + Name string + Usage string + EnvVar string + Hidden bool + Value int64 + Destination *int64 + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -266,6 +302,12 @@ func (f Int64Flag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f Int64Flag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // Int64 looks up the value of a local Int64Flag, returns // 0 if not found func (c *Context) Int64(name string) int64 { @@ -295,12 +337,13 @@ func lookupInt64(name string, set *flag.FlagSet) int64 { // IntFlag is a flag with type int type IntFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value int - Destination *int + Name string + Usage string + EnvVar string + Hidden bool + Value int + Destination *int + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -314,6 +357,12 @@ func (f IntFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f IntFlag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // Int looks up the value of a local IntFlag, returns // 0 if not found func (c *Context) Int(name string) int { @@ -343,11 +392,12 @@ func lookupInt(name string, set *flag.FlagSet) int { // IntSliceFlag is a flag with type *IntSlice type IntSliceFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value *IntSlice + Name string + Usage string + EnvVar string + Hidden bool + Value *IntSlice + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -361,6 +411,12 @@ func (f IntSliceFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f IntSliceFlag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // IntSlice looks up the value of a local IntSliceFlag, returns // nil if not found func (c *Context) IntSlice(name string) []int { @@ -390,11 +446,12 @@ func lookupIntSlice(name string, set *flag.FlagSet) []int { // Int64SliceFlag is a flag with type *Int64Slice type Int64SliceFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value *Int64Slice + Name string + Usage string + EnvVar string + Hidden bool + Value *Int64Slice + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -408,6 +465,12 @@ func (f Int64SliceFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f Int64SliceFlag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // Int64Slice looks up the value of a local Int64SliceFlag, returns // nil if not found func (c *Context) Int64Slice(name string) []int64 { @@ -437,12 +500,13 @@ func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { // StringFlag is a flag with type string type StringFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value string - Destination *string + Name string + Usage string + EnvVar string + Hidden bool + Value string + Destination *string + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -456,6 +520,12 @@ func (f StringFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f StringFlag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // String looks up the value of a local StringFlag, returns // "" if not found func (c *Context) String(name string) string { @@ -485,11 +555,12 @@ func lookupString(name string, set *flag.FlagSet) string { // StringSliceFlag is a flag with type *StringSlice type StringSliceFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value *StringSlice + Name string + Usage string + EnvVar string + Hidden bool + Value *StringSlice + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -503,6 +574,12 @@ func (f StringSliceFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f StringSliceFlag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // StringSlice looks up the value of a local StringSliceFlag, returns // nil if not found func (c *Context) StringSlice(name string) []string { @@ -532,12 +609,13 @@ func lookupStringSlice(name string, set *flag.FlagSet) []string { // Uint64Flag is a flag with type uint64 type Uint64Flag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value uint64 - Destination *uint64 + Name string + Usage string + EnvVar string + Hidden bool + Value uint64 + Destination *uint64 + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -551,6 +629,12 @@ func (f Uint64Flag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f Uint64Flag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // Uint64 looks up the value of a local Uint64Flag, returns // 0 if not found func (c *Context) Uint64(name string) uint64 { @@ -580,12 +664,13 @@ func lookupUint64(name string, set *flag.FlagSet) uint64 { // UintFlag is a flag with type uint type UintFlag struct { - Name string - Usage string - EnvVar string - Hidden bool - Value uint - Destination *uint + Name string + Usage string + EnvVar string + Hidden bool + Value uint + Destination *uint + CustomFlagPredictor complete.Predictor } // String returns a readable representation of this value @@ -599,6 +684,12 @@ func (f UintFlag) GetName() string { return f.Name } +// GetPredictor returns the predictor to use for shell completion +// of this flag's value +func (f UintFlag) GetPredictor() complete.Predictor { + return f.CustomFlagPredictor +} + // Uint looks up the value of a local UintFlag, returns // 0 if not found func (c *Context) Uint(name string) uint { diff --git a/funcs.go b/funcs.go index cba5e6cb0c..2451eedada 100644 --- a/funcs.go +++ b/funcs.go @@ -1,8 +1,5 @@ package cli -// BashCompleteFunc is an action to execute when the bash-completion flag is set -type BashCompleteFunc func(*Context) - // BeforeFunc is an action to execute before any subcommands are run, but after // the context is ready if a non-nil error is returned, no subcommands are run type BeforeFunc func(*Context) error diff --git a/generate-flag-types b/generate-flag-types.py similarity index 83% rename from generate-flag-types rename to generate-flag-types.py index 7147381ce3..bb3d891705 100755 --- a/generate-flag-types +++ b/generate-flag-types.py @@ -103,7 +103,7 @@ def main(sysargs=sys.argv[:]): def _generate_flag_types(writefunc, output_go, input_json): types = json.load(input_json) - tmp = tempfile.NamedTemporaryFile(suffix='.go', delete=False) + tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.go', delete=False) writefunc(tmp, types) tmp.close() @@ -129,6 +129,10 @@ def _write_cli_flag_types(outfile, types): _fwrite(outfile, """\ package cli + import ( + "github.com/posener/complete" + ) + // WARNING: This file is generated! """) @@ -155,6 +159,11 @@ def _write_cli_flag_types(outfile, types): Destination *{type} """.format(**typedef)) + if typedef['value']: + _fwrite(outfile, """\ + CustomFlagPredictor complete.Predictor + """.format(**typedef)) + _fwrite(outfile, "\n}\n\n") _fwrite(outfile, """\ @@ -169,6 +178,22 @@ def _write_cli_flag_types(outfile, types): return f.Name }} + """.format(**typedef)) + + predictor_body = ( + "return f.CustomFlagPredictor" if typedef['value'] + else "return complete.PredictNothing" + ) + _fwrite(outfile, """\ + // GetPredictor returns the predictor to use for shell completion + // of this flag's value + func (f {name}Flag) GetPredictor() complete.Predictor {{ + {predictor_body} + }} + + """.format(name=typedef['name'], predictor_body=predictor_body)) + + _fwrite(outfile, """\ // {name} looks up the value of a local {name}Flag, returns // {context_default} if not found func (c *Context) {name}(name string) {context_type} {{ @@ -197,58 +222,12 @@ def _write_cli_flag_types(outfile, types): }} """.format(**typedef)) - -def _write_altsrc_flag_types(outfile, types): - _fwrite(outfile, """\ - package altsrc - - import ( - "gopkg.in/urfave/cli.v1" - ) - - // WARNING: This file is generated! - - """) - - for typedef in types: - _set_typedef_defaults(typedef) - - _fwrite(outfile, """\ - // {name}Flag is the flag type that wraps cli.{name}Flag to allow - // for other values to be specified - type {name}Flag struct {{ - cli.{name}Flag - set *flag.FlagSet - }} - - // New{name}Flag creates a new {name}Flag - func New{name}Flag(fl cli.{name}Flag) *{name}Flag {{ - return &{name}Flag{{{name}Flag: fl, set: nil}} - }} - - // Apply saves the flagSet for later usage calls, then calls the - // wrapped {name}Flag.Apply - func (f *{name}Flag) Apply(set *flag.FlagSet) {{ - f.set = set - f.{name}Flag.Apply(set) - }} - - // ApplyWithError saves the flagSet for later usage calls, then calls the - // wrapped {name}Flag.ApplyWithError - func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{ - f.set = set - return f.{name}Flag.ApplyWithError(set) - }} - """.format(**typedef)) - - def _fwrite(outfile, text): print(textwrap.dedent(text), end='', file=outfile) _WRITEFUNCS = { 'cli': _write_cli_flag_types, - 'altsrc': _write_altsrc_flag_types } if __name__ == '__main__': diff --git a/go.mod b/go.mod index 3e3aae9630..8717495523 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ -module github.com/minio/cli +module github.com/minio/cli/v2 go 1.22 +require github.com/posener/complete v1.2.3 + require ( - github.com/BurntSushi/toml v0.3.1 - gopkg.in/urfave/cli.v1 v1.20.0 - gopkg.in/yaml.v2 v2.2.2 + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 7e08f2a6cc..e6ca257962 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,16 @@ -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= -gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/help.go b/help.go index 2b1c041e80..7d20b77060 100644 --- a/help.go +++ b/help.go @@ -150,18 +150,6 @@ func ShowAppHelp(c *Context) (err error) { return nil } -// DefaultAppComplete prints the list of subcommands as the default app completion method -func DefaultAppComplete(c *Context) { - for _, command := range c.App.Commands { - if command.Hidden { - continue - } - for _, name := range command.NamesWithHiddenAliases() { - fmt.Fprintln(c.App.Writer, name) - } - } -} - // ShowCommandHelpAndExit - exits with code after showing help func ShowCommandHelpAndExit(c *Context, command string, code int) { ShowCommandHelp(c, command) @@ -198,10 +186,7 @@ func ShowCommandHelp(ctx *Context, command string) error { c.EnableHistory = defaultEnableHistory } - // expose global flags to the help template, honoring NoGlobalFlags - if !c.NoGlobalFlags { - c.globalFlags = ctx.App.GlobalFlags - } + c.globalFlags = ctx.App.GlobalFlags if c.HasName(command) { if c.CustomHelpTemplate != "" { @@ -235,22 +220,6 @@ func printVersion(c *Context) { fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) } -// ShowCompletions prints the lists of commands within a given context -func ShowCompletions(c *Context) { - a := c.App - if a != nil && a.BashComplete != nil { - a.BashComplete(c) - } -} - -// ShowCommandCompletions prints the custom completions for a given command -func ShowCommandCompletions(ctx *Context, command string) { - c := ctx.App.Command(command) - if c != nil && c.BashComplete != nil { - c.BashComplete(ctx) - } -} - func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { funcMap := template.FuncMap{ "join": strings.Join, @@ -320,44 +289,3 @@ func checkSubcommandHelp(c *Context) bool { return false } - -func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { - if !a.EnableBashCompletion { - return false, arguments - } - - pos := len(arguments) - 1 - lastArg := arguments[pos] - - if lastArg != "--"+BashCompletionFlag.GetName() { - return false, arguments - } - - return true, arguments[:pos] -} - -func checkCompletions(c *Context) bool { - if !c.shellComplete { - return false - } - - if args := c.Args(); args.Present() { - name := args.First() - if cmd := c.App.Command(name); cmd != nil { - // let the command handle the completion - return false - } - } - - ShowCompletions(c) - return true -} - -func checkCommandCompletions(c *Context, name string) bool { - if !c.shellComplete { - return false - } - - ShowCommandCompletions(c, name) - return true -} diff --git a/help_test.go b/help_test.go index 84d2709cd2..2104f53103 100644 --- a/help_test.go +++ b/help_test.go @@ -342,29 +342,6 @@ FLAGS: t.Errorf("expected output to exclude hidden global flag; got: %q", output.String()) } }) - - t.Run("excluded when NoGlobalFlags", func(t *testing.T) { - app := &App{ - GlobalFlags: globalFlags, - Commands: []Command{ - { - Name: "frobbly", - HelpName: "foo frobbly", - Action: func(*Context) error { return nil }, - NoGlobalFlags: true, - CustomHelpTemplate: customTemplate, - }, - }, - } - - output := &bytes.Buffer{} - app.HelpWriter = output - app.Run([]string{"foo", "help", "frobbly"}) - - if strings.Contains(output.String(), "--config") { - t.Errorf("expected output to exclude global flag --config; got: %q", output.String()) - } - }) } func TestShowAppHelp_HiddenCommand(t *testing.T) { diff --git a/install.go b/install.go new file mode 100644 index 0000000000..f132a198cf --- /dev/null +++ b/install.go @@ -0,0 +1,121 @@ +package cli + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + completeinstall "github.com/posener/complete/cmd/install" +) + +// Errors returned by SetupShellCompletion so callers can switch on them to +// format their own user-facing messages. The framework itself prints nothing. +var ( + // ErrCompletionUnsupportedOS is returned when shell completion cannot be + // installed on the current operating system (e.g. Windows). + ErrCompletionUnsupportedOS = errors.New("shell completion is not supported on this operating system") + // ErrCompletionUnsupportedShell is returned when the detected shell is not + // one of the supported shells (bash, zsh, fish). + ErrCompletionUnsupportedShell = errors.New("unsupported shell") +) + +// supportedCompletionShells is the set of shells for which completion can be +// installed. posener/complete writes to whichever of these it finds present. +var supportedCompletionShells = map[string]bool{ + "bash": true, + "zsh": true, + "fish": true, +} + +// DetectShell reports the user's shell name (lowercased base name, e.g. "bash") +// and whether it was determined from the $SHELL environment variable. When +// $SHELL is unset it falls back to inspecting the parent process; fromEnv is +// false in that case. On Windows it returns early with fromEnv true and an +// empty name. +func DetectShell() (name string, fromEnv bool, err error) { + shellName := os.Getenv("SHELL") + if shellName != "" || runtime.GOOS == "windows" { + return strings.ToLower(filepath.Base(shellName)), true, nil + } + + ppid := os.Getppid() + out, err := exec.Command("ps", "-p", strconv.Itoa(ppid), "-o", "comm=").Output() + if err != nil { + return "", false, err + } + shellName = strings.TrimSpace(string(out)) + return strings.ToLower(filepath.Base(shellName)), false, nil +} + +// installer and isInstalled are seams over posener/complete's real Install +// and IsInstalled, for tests. +var ( + installer = completeinstall.Install + isInstalled = completeinstall.IsInstalled +) + +// InstallShellCompletion registers cmd for shell completion in the user's shell +// rc files (bash/zsh/fish — whichever are present). It is a thin wrapper over +// posener/complete's installer. +func InstallShellCompletion(cmd string) error { return installer(cmd) } + +// UninstallShellCompletion removes cmd's shell-completion registration. +func UninstallShellCompletion(cmd string) error { return completeinstall.Uninstall(cmd) } + +// IsShellCompletionInstalled reports whether cmd's completion is already +// registered in any of the user's shell rc files. +func IsShellCompletionInstalled(cmd string) bool { return isInstalled(cmd) } + +// ShellCompletionResult describes the outcome of SetupShellCompletion so the +// caller can render an appropriate message. +type ShellCompletionResult struct { + // Shell is the detected shell name (e.g. "bash"). + Shell string + // DetectedFromEnv reports whether Shell came from $SHELL (vs. a fallback). + DetectedFromEnv bool + // AlreadyInstalled reports whether completion was already registered, in + // which case no changes were made. Best-effort: if InstallShellCompletion + // partially fails on an unrelated shell config, this may report true even + // though Shell's own config was just freshly written. + AlreadyInstalled bool +} + +// SetupShellCompletion is the one-call convenience that installs shell +// completion for cmd: it checks OS support, detects and validates the shell, +// and registers completion. It performs no output; callers inspect the +// returned result and error to present their own messages. Returns +// ErrCompletionUnsupportedOS or ErrCompletionUnsupportedShell for the +// respective unsupported cases. +func SetupShellCompletion(cmd string) (ShellCompletionResult, error) { + var res ShellCompletionResult + + if runtime.GOOS == "windows" { + return res, ErrCompletionUnsupportedOS + } + + shell, fromEnv, err := DetectShell() + if err != nil { + return res, err + } + res.Shell = shell + res.DetectedFromEnv = fromEnv + + if !supportedCompletionShells[shell] { + return res, ErrCompletionUnsupportedShell + } + + if installErr := InstallShellCompletion(cmd); installErr != nil { + // Install() may have partially failed (e.g. some shell config + // already had it) while still succeeding for shell; re-check actual + // disk state rather than trust the error alone. + if !isInstalled(cmd) { + return res, installErr + } + res.AlreadyInstalled = true + } + return res, nil +} diff --git a/install_test.go b/install_test.go new file mode 100644 index 0000000000..4ace849e2c --- /dev/null +++ b/install_test.go @@ -0,0 +1,90 @@ +package cli + +import ( + "errors" + "testing" +) + +// withInstaller/withIsInstalled swap in fn for the real posener/complete +// call for the test's duration. Never let the real ones run here: they +// resolve the home dir via os/user.Current(), which ignores $HOME, so they'd +// touch your actual shell rc files. +func withInstaller(t *testing.T, fn func(cmd string) error) { + t.Helper() + old := installer + installer = fn + t.Cleanup(func() { installer = old }) +} + +func withIsInstalled(t *testing.T, fn func(cmd string) bool) { + t.Helper() + old := isInstalled + isInstalled = fn + t.Cleanup(func() { isInstalled = old }) +} + +func TestSetupShellCompletionFreshInstall(t *testing.T) { + t.Setenv("SHELL", "/bin/bash") + withInstaller(t, func(cmd string) error { return nil }) + + res, err := SetupShellCompletion("prog") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.Shell != "bash" { + t.Errorf("res.Shell = %q, want %q", res.Shell, "bash") + } + if res.AlreadyInstalled { + t.Error("res.AlreadyInstalled = true, want false on a fresh install") + } +} + +// Regression test: SetupShellCompletion must not skip installing just +// because some *other* shell's config already had it — Install() always +// runs, and only a post-install disk check decides AlreadyInstalled. +func TestSetupShellCompletionAlreadyInstalled(t *testing.T) { + t.Setenv("SHELL", "/bin/bash") + withInstaller(t, func(cmd string) error { + return errors.New("1 error occurred: * already installed in /home/user/.bashrc") + }) + withIsInstalled(t, func(cmd string) bool { return true }) + + res, err := SetupShellCompletion("prog") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !res.AlreadyInstalled { + t.Error("res.AlreadyInstalled = false, want true") + } +} + +// A genuine install failure (not just some shell already having it) must +// propagate: the post-install disk check finds nothing installed either. +func TestSetupShellCompletionPropagatesGenuineFailure(t *testing.T) { + t.Setenv("SHELL", "/bin/bash") + wantErr := errors.New("open /home/user/.bashrc: permission denied") + withInstaller(t, func(cmd string) error { return wantErr }) + withIsInstalled(t, func(cmd string) bool { return false }) + + res, err := SetupShellCompletion("prog") + if !errors.Is(err, wantErr) { + t.Errorf("err = %v, want %v", err, wantErr) + } + if res.AlreadyInstalled { + t.Error("res.AlreadyInstalled = true, want false when install genuinely failed") + } +} + +func TestSetupShellCompletionUnsupportedShell(t *testing.T) { + t.Setenv("SHELL", "/usr/bin/tcsh") + // installer must not even be consulted for an unsupported shell. + withInstaller(t, func(cmd string) error { + t.Fatal("installer should not be called for an unsupported shell") + return nil + }) + + _, err := SetupShellCompletion("prog") + if !errors.Is(err, ErrCompletionUnsupportedShell) { + t.Errorf("err = %v, want %v", err, ErrCompletionUnsupportedShell) + } +}