diff --git a/README.md b/README.md index 5facb70..364c964 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ export PATH=$PATH:$GOPATH/bin ## Getting Started -One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`. +One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`. ``` go package main @@ -60,7 +60,7 @@ func main() { app.Action = func(c *cli.Context) { println("boom! I say!") } - + app.Run(os.Args) } ``` @@ -238,6 +238,48 @@ app.Flags = []cli.Flag { } ``` +#### Values from alternate input sources (YAML and others) + +There is a separate package altsrc that adds support for getting flag values from other input sources like YAML. + +In order to get values for a flag from an alternate input source the following code would be added to wrap an existing cli.Flag like below: + +``` go + altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) +``` + +Initialization must also occur for these flags. Below is an example initializing getting data from a yaml file below. + +``` go + command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) +``` + +The code above will use the "load" string as a flag name to get the file name of a yaml file from the cli.Context. +It will then use that file name to initialize the yaml input source for any flags that are defined on that command. +As a note the "load" flag used would also have to be defined on the command flags in order for this code snipped to work. + +Currently only YAML files are supported but developers can add support for other input sources by implementing the +altsrc.InputSourceContext for their given sources. + +Here is a more complete sample of a command using YAML support: + +``` go + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + // Action to run + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) + err := command.Run(c) +``` + ### Subcommands Subcommands can be defined for a more git-like command line app. diff --git a/altsrc/flag.go b/altsrc/flag.go new file mode 100644 index 0000000..f13ffb4 --- /dev/null +++ b/altsrc/flag.go @@ -0,0 +1,439 @@ +package altsrc + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/codegangsta/cli" +) + +// 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)) func(context *cli.Context) error { + 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)) func(context *cli.Context) error { + 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) + } +} + +// 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(flag cli.GenericFlag) *GenericFlag { + return &GenericFlag{GenericFlag: flag, set: nil} +} + +// 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped GenericFlag.Apply +func (f *GenericFlag) Apply(set *flag.FlagSet) { + f.set = set + f.GenericFlag.Apply(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(flag cli.StringSliceFlag) *StringSliceFlag { + return &StringSliceFlag{StringSliceFlag: flag, set: 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped StringSliceFlag.Apply +func (f *StringSliceFlag) Apply(set *flag.FlagSet) { + f.set = set + f.StringSliceFlag.Apply(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(flag cli.IntSliceFlag) *IntSliceFlag { + return &IntSliceFlag{IntSliceFlag: flag, set: 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped IntSliceFlag.Apply +func (f *IntSliceFlag) Apply(set *flag.FlagSet) { + f.set = set + f.IntSliceFlag.Apply(set) +} + +// 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(flag cli.BoolFlag) *BoolFlag { + return &BoolFlag{BoolFlag: flag, set: 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped BoolFlag.Apply +func (f *BoolFlag) Apply(set *flag.FlagSet) { + f.set = set + f.BoolFlag.Apply(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(flag cli.BoolTFlag) *BoolTFlag { + return &BoolTFlag{BoolTFlag: flag, set: 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped BoolTFlag.Apply +func (f *BoolTFlag) Apply(set *flag.FlagSet) { + f.set = set + + f.BoolTFlag.Apply(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(flag cli.StringFlag) *StringFlag { + return &StringFlag{StringFlag: flag, set: 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped StringFlag.Apply +func (f *StringFlag) Apply(set *flag.FlagSet) { + f.set = set + + f.StringFlag.Apply(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(flag cli.IntFlag) *IntFlag { + return &IntFlag{IntFlag: flag, set: 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped IntFlag.Apply +func (f *IntFlag) Apply(set *flag.FlagSet) { + f.set = set + f.IntFlag.Apply(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(flag cli.DurationFlag) *DurationFlag { + return &DurationFlag{DurationFlag: flag, set: 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped DurationFlag.Apply +func (f *DurationFlag) Apply(set *flag.FlagSet) { + f.set = set + + f.DurationFlag.Apply(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(flag cli.Float64Flag) *Float64Flag { + return &Float64Flag{Float64Flag: flag, set: 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 +} + +// Apply saves the flagSet for later usage then calls +// the wrapped Float64Flag.Apply +func (f *Float64Flag) Apply(set *flag.FlagSet) { + f.set = set + + f.Float64Flag.Apply(set) +} + +func isEnvVarSet(envVars string) bool { + for _, envVar := range strings.Split(envVars, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + // TODO: Can't use this for bools as + // set means that it was true or false based on + // Bool flag type, should work for other types + if len(envVal) > 0 { + return true + } + } + } + + return 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_test.go b/altsrc/flag_test.go new file mode 100644 index 0000000..ac4d1f5 --- /dev/null +++ b/altsrc/flag_test.go @@ -0,0 +1,336 @@ +package altsrc + +import ( + "flag" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/codegangsta/cli" +) + +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: []string{"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: []string{"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: []string{"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: []int{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: []int{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: []int{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[string]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 new file mode 100644 index 0000000..3b7f7e9 --- /dev/null +++ b/altsrc/helpers_test.go @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..6d695ff --- /dev/null +++ b/altsrc/input_source_context.go @@ -0,0 +1,21 @@ +package altsrc + +import ( + "time" + + "github.com/codegangsta/cli" +) + +// 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 new file mode 100644 index 0000000..f1670fb --- /dev/null +++ b/altsrc/map_input_source.go @@ -0,0 +1,152 @@ +package altsrc + +import ( + "fmt" + "reflect" + "time" + + "github.com/codegangsta/cli" +) + +// MapInputSource implements InputSourceContext to return +// data from the map that is loaded. +type MapInputSource struct { + valueMap map[string]interface{} +} + +// 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 + } + + 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 + } + + 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 + } + + 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 + } + + 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 { + otherValue, isType := otherGenericValue.([]string) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]string", otherGenericValue) + } + return otherValue, nil + } + + return nil, nil +} + +// IntSlice returns an []int from the map if it exists otherwise returns nil +func (fsm *MapInputSource) IntSlice(name string) ([]int, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.([]int) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]int", otherGenericValue) + } + return otherValue, nil + } + + return nil, 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 + } + + 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 + } + + 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 + } + + 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/yaml_command_test.go b/altsrc/yaml_command_test.go new file mode 100644 index 0000000..c7ccbf7 --- /dev/null +++ b/altsrc/yaml_command_test.go @@ -0,0 +1,172 @@ +// Disabling building of yaml support in cases where golang is 1.0 or 1.1 +// as the encoding library is not implemented or supported. + +// +build !go1,!go1.1 + +package altsrc + +import ( + "flag" + "io/ioutil" + "os" + "testing" + + "github.com/codegangsta/cli" +) + +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) { + val := c.Int("test") + expect(t, val, 15) + }, + 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) { + val := c.Int("test") + expect(t, val, 10) + }, + 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 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) { + val := c.Int("test") + expect(t, val, 7) + }, + 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 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) { + val := c.Int("test") + expect(t, val, 15) + }, + 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 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) { + val := c.Int("test") + expect(t, val, 11) + }, + 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) +} diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go new file mode 100644 index 0000000..1251aeb --- /dev/null +++ b/altsrc/yaml_file_loader.go @@ -0,0 +1,84 @@ +// Disabling building of yaml support in cases where golang is 1.0 or 1.1 +// as the encoding library is not implemented or supported. + +// +build !go1,!go1.1 + +package altsrc + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + + "github.com/codegangsta/cli" + + "gopkg.in/yaml.v2" +) + +type yamlSourceContext struct { + FilePath string +} + +// NewYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath. +func NewYamlSourceFromFile(file string) (InputSourceContext, error) { + ymlLoader := &yamlSourceLoader{FilePath: file} + var results map[string]interface{} + err := readCommandYaml(ysl.FilePath, &results) + if err != nil { + return fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", 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(InputSourceContext, error) { + return func(context cli.Context) { + 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 { + return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) + } +}