Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PathFlag #670

Merged
merged 1 commit into from
Sep 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions altsrc/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package altsrc

import (
"fmt"
"path/filepath"
"strconv"
"syscall"

Expand Down Expand Up @@ -160,6 +161,34 @@ func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSource
return nil
}

// ApplyInputSourceValue applies a Path value to the flagSet if required
func (f *PathFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.String(f.PathFlag.Name)
if err != nil {
return err
}
if value != "" {
for _, name := range f.Names() {

if !filepath.IsAbs(value) && isc.Source() != "" {
basePathAbs, err := filepath.Abs(isc.Source())
if err != nil {
return err
}

value = filepath.Join(filepath.Dir(basePathAbs), value)
}

f.set.Set(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 {
Expand Down
26 changes: 26 additions & 0 deletions altsrc/flag_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,32 @@ func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error {
return f.StringFlag.ApplyWithError(set)
}

// PathFlag is the flag type that wraps cli.PathFlag to allow
// for other values to be specified
type PathFlag struct {
*cli.PathFlag
set *flag.FlagSet
}

// NewPathFlag creates a new PathFlag
func NewPathFlag(fl *cli.PathFlag) *PathFlag {
return &PathFlag{PathFlag: fl, set: nil}
}

// Apply saves the flagSet for later usage calls, then calls the
// wrapped PathFlag.Apply
func (f *PathFlag) Apply(set *flag.FlagSet) {
f.set = set
f.PathFlag.Apply(set)
}

// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped PathFlag.ApplyWithError
func (f *PathFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.PathFlag.ApplyWithError(set)
}

// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow
// for other values to be specified
type StringSliceFlag struct {
Expand Down
44 changes: 43 additions & 1 deletion altsrc/flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"os"
"runtime"
"strings"
"testing"
"time"
Expand All @@ -20,6 +21,7 @@ type testApplyInputSource struct {
ContextValue flag.Value
EnvVarValue string
EnvVarName string
SourcePath string
MapValue interface{}
}

Expand Down Expand Up @@ -178,6 +180,43 @@ func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) {
})
expect(t, "goodbye", c.String("test"))
}
func TestPathApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test"}),
FlagName: "test",
MapValue: "hello",
SourcePath: "/path/to/source/file",
})

expected := "/path/to/source/hello"
if runtime.GOOS == "windows" {
expected = `C:\path\to\source\hello`
}
expect(t, expected, c.String("test"))
}

func TestPathApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test"}),
FlagName: "test",
MapValue: "hello",
ContextValueString: "goodbye",
SourcePath: "/path/to/source/file",
})
expect(t, "goodbye", c.String("test"))
}

func TestPathApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test",
MapValue: "hello",
EnvVarName: "TEST",
EnvVarValue: "goodbye",
SourcePath: "/path/to/source/file",
})
expect(t, "goodbye", c.String("test"))
}

func TestIntApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Expand Down Expand Up @@ -270,7 +309,10 @@ func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) {
}

func runTest(t *testing.T, test testApplyInputSource) *cli.Context {
inputSource := &MapInputSource{valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue}}
inputSource := &MapInputSource{
file: test.SourcePath,
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 != "" {
Expand Down
5 changes: 5 additions & 0 deletions altsrc/input_source_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import (

// InputSourceContext is an interface used to allow
// other input sources to be implemented as needed.
//
// Source returns an identifier for the input source. In case of file source
// it should return path to the file.
type InputSourceContext interface {
Source() string

Int(name string) (int, error)
Duration(name string) (time.Duration, error)
Float64(name string) (float64, error)
Expand Down
17 changes: 16 additions & 1 deletion altsrc/json_source_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ func NewJSONSourceFromFile(f string) (InputSourceContext, error) {
if err != nil {
return nil, err
}
return NewJSONSource(data)
s, err := newJSONSource(data)
if err != nil {
return nil, err
}

s.file = f
return s, nil
}

// NewJSONSourceFromReader returns an InputSourceContext suitable for
Expand All @@ -45,13 +51,21 @@ func NewJSONSourceFromReader(r io.Reader) (InputSourceContext, error) {
// NewJSONSource returns an InputSourceContext suitable for retrieving
// config variables from raw JSON data.
func NewJSONSource(data []byte) (InputSourceContext, error) {
return newJSONSource(data)
}

func newJSONSource(data []byte) (*jsonSource, error) {
var deserialized map[string]interface{}
if err := json.Unmarshal(data, &deserialized); err != nil {
return nil, err
}
return &jsonSource{deserialized: deserialized}, nil
}

func (x *jsonSource) Source() string {
return x.file
}

func (x *jsonSource) Int(name string) (int, error) {
i, err := x.getValue(name)
if err != nil {
Expand Down Expand Up @@ -198,5 +212,6 @@ func jsonGetValue(key string, m map[string]interface{}) (interface{}, error) {
}

type jsonSource struct {
file string
deserialized map[string]interface{}
}
6 changes: 6 additions & 0 deletions altsrc/map_input_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
// MapInputSource implements InputSourceContext to return
// data from the map that is loaded.
type MapInputSource struct {
file string
valueMap map[interface{}]interface{}
}

Expand Down Expand Up @@ -39,6 +40,11 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool
return nil, false
}

// Source returns the path of the source file
func (fsm *MapInputSource) Source() string {
return fsm.file
}

// 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]
Expand Down
2 changes: 1 addition & 1 deletion altsrc/toml_file_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func NewTomlSourceFromFile(file string) (InputSourceContext, error) {
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
return &MapInputSource{file: file, valueMap: results.Map}, nil
}

// NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context.
Expand Down
2 changes: 1 addition & 1 deletion altsrc/yaml_file_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func NewYamlSourceFromFile(file string) (InputSourceContext, error) {
return nil, fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", ysc.FilePath, err.Error())
}

return &MapInputSource{valueMap: results}, nil
return &MapInputSource{file: file, valueMap: results}, nil
}

// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context.
Expand Down
11 changes: 11 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ func TestContext_String(t *testing.T) {
expect(t, c.String("top-flag"), "hai veld")
}

func TestContext_Path(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.String("path", "path/to/file", "path to file")
parentSet := flag.NewFlagSet("test", 0)
parentSet.String("top-path", "path/to/top/file", "doc")
parentCtx := NewContext(nil, parentSet, nil)
c := NewContext(nil, set, parentCtx)
expect(t, c.Path("path"), "path/to/file")
expect(t, c.Path("top-path"), "path/to/top/file")
}

func TestContext_Bool(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
Expand Down
6 changes: 6 additions & 0 deletions flag-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@
"context_default": "\"\"",
"parser": "f.Value.String(), error(nil)"
},
{
"name": "Path",
"type": "string",
"context_default": "\"\"",
"parser": "f.Value.String(), error(nil)"
},
{
"name": "StringSlice",
"type": "*StringSlice",
Expand Down
27 changes: 27 additions & 0 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,33 @@ func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error {
return nil
}

// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *PathFlag) Apply(set *flag.FlagSet) {
f.ApplyWithError(set)
}

// ApplyWithError populates the flag given the flag set and environment
func (f *PathFlag) ApplyWithError(set *flag.FlagSet) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it we don't need to resolve the path in here since it'll be relative to the working directory.

if f.EnvVars != nil {
for _, envVar := range f.EnvVars {
if envVal, ok := syscall.Getenv(envVar); ok {
f.Value = envVal
break
}
}
}

for _, name := range f.Names() {
if f.Destination != nil {
set.StringVar(f.Destination, name, f.Value, f.Usage)
continue
}
set.String(name, f.Value, f.Usage)
}
return nil
}

// Apply populates the flag given the flag set and environment
// Ignores errors
func (f *IntFlag) Apply(set *flag.FlagSet) {
Expand Down
45 changes: 45 additions & 0 deletions flag_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,51 @@ func lookupString(name string, set *flag.FlagSet) string {
return ""
}

// PathFlag is a flag with type string
type PathFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
Hidden bool
Value string
DefaultText string

Destination *string
}

// String returns a readable representation of this value
// (for usage defaults)
func (f *PathFlag) String() string {
return FlagStringer(f)
}

// Names returns the names of the flag
func (f *PathFlag) Names() []string {
return flagNames(f)
}

// Path looks up the value of a local PathFlag, returns
// "" if not found
func (c *Context) Path(name string) string {
if fs := lookupFlagSet(name, c); fs != nil {
return lookupPath(name, fs)
}
return ""
}

func lookupPath(name string, set *flag.FlagSet) string {
f := set.Lookup(name)
if f != nil {
parsed, err := f.Value.String(), error(nil)
if err != nil {
return ""
}
return parsed
}
return ""
}

// StringSliceFlag is a flag with type *StringSlice
type StringSliceFlag struct {
Name string
Expand Down
Loading