Skip to content

Commit

Permalink
Merge pull request #241 from elwinar/env-namespace
Browse files Browse the repository at this point in the history
Add env namespace handling
  • Loading branch information
jessevdk committed Mar 31, 2018
2 parents c6ca198 + 97c7878 commit 1c38ed7
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 30 deletions.
4 changes: 4 additions & 0 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ The following is a list of tags for struct fields supported by go-flags:
gets prepended to every option's long name and
subgroup's namespace of this group, separated by
the parser's namespace delimiter (optional)
env-namespace: when specified on a group struct field, the env-namespace
gets prepended to every option's env key and
subgroup's env-namespace of this group, separated by
the parser's env-namespace delimiter (optional)
command: when specified on a struct field, makes the struct
field a (sub)command with the given name (optional)
subcommands-optional: when specified on a command struct field, makes
Expand Down
4 changes: 4 additions & 0 deletions group.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type Group struct {
// The namespace of the group
Namespace string

// The environment namespace of the group
EnvNamespace string

// If true, the group is not displayed in the help or man page
Hidden bool

Expand Down Expand Up @@ -358,6 +361,7 @@ func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.Struc
}

group.Namespace = mtag.Get("namespace")
group.EnvNamespace = mtag.Get("env-namespace")
group.Hidden = mtag.Get("hidden") != ""

return true, nil
Expand Down
6 changes: 3 additions & 3 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,12 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig
}

var envDef string
if option.EnvDefaultKey != "" {
if option.EnvKeyWithNamespace() != "" {
var envPrintable string
if runtime.GOOS == "windows" {
envPrintable = "%" + option.EnvDefaultKey + "%"
envPrintable = "%" + option.EnvKeyWithNamespace() + "%"
} else {
envPrintable = "$" + option.EnvDefaultKey
envPrintable = "$" + option.EnvKeyWithNamespace()
}
envDef = fmt.Sprintf(" [%s]", envPrintable)
}
Expand Down
6 changes: 3 additions & 3 deletions man.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ func writeManPageOptions(wr io.Writer, grp *Group) {

if len(opt.Default) != 0 {
fmt.Fprintf(wr, " <default: \\fI%s\\fR>", manQuote(strings.Join(quoteV(opt.Default), ", ")))
} else if len(opt.EnvDefaultKey) != 0 {
} else if len(opt.EnvKeyWithNamespace()) != 0 {
if runtime.GOOS == "windows" {
fmt.Fprintf(wr, " <default: \\fI%%%s%%\\fR>", manQuote(opt.EnvDefaultKey))
fmt.Fprintf(wr, " <default: \\fI%%%s%%\\fR>", manQuote(opt.EnvKeyWithNamespace()))
} else {
fmt.Fprintf(wr, " <default: \\fI$%s\\fR>", manQuote(opt.EnvDefaultKey))
fmt.Fprintf(wr, " <default: \\fI$%s\\fR>", manQuote(opt.EnvKeyWithNamespace()))
}
}

Expand Down
56 changes: 53 additions & 3 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,57 @@ func (option *Option) LongNameWithNamespace() string {
return longName
}

// EnvKeyWithNamespace returns the option's env key with the group namespaces
// prepended by walking up the option's group tree. Namespaces and the env key
// itself are separated by the parser's namespace delimiter. If the env key is
// empty an empty string is returned.
func (option *Option) EnvKeyWithNamespace() string {
if len(option.EnvDefaultKey) == 0 {
return ""
}

// fetch the namespace delimiter from the parser which is always at the
// end of the group hierarchy
namespaceDelimiter := ""
g := option.group

for {
if p, ok := g.parent.(*Parser); ok {
namespaceDelimiter = p.EnvNamespaceDelimiter

break
}

switch i := g.parent.(type) {
case *Command:
g = i.Group
case *Group:
g = i
}
}

// concatenate long name with namespace
key := option.EnvDefaultKey
g = option.group

for g != nil {
if g.EnvNamespace != "" {
key = g.EnvNamespace + namespaceDelimiter + key
}

switch i := g.parent.(type) {
case *Command:
g = i.Group
case *Group:
g = i
case *Parser:
g = nil
}
}

return key
}

// String converts an option to a human friendly readable string describing the
// option.
func (option *Option) String() string {
Expand Down Expand Up @@ -260,11 +311,10 @@ func (option *Option) empty() {
func (option *Option) clearDefault() {
usedDefault := option.Default

if envKey := option.EnvDefaultKey; envKey != "" {
if envKey := option.EnvKeyWithNamespace(); envKey != "" {
if value, ok := os.LookupEnv(envKey); ok {
if option.EnvDefaultDelim != "" {
usedDefault = strings.Split(value,
option.EnvDefaultDelim)
usedDefault = strings.Split(value, option.EnvDefaultDelim)
} else {
usedDefault = []string{value}
}
Expand Down
10 changes: 7 additions & 3 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ type Parser struct {
// NamespaceDelimiter separates group namespaces and option long names
NamespaceDelimiter string

// EnvNamespaceDelimiter separates group env namespaces and env keys
EnvNamespaceDelimiter string

// UnknownOptionsHandler is a function which gets called when the parser
// encounters an unknown option. The function receives the unknown option
// name, a SplitArgument which specifies its value if set with an argument
Expand Down Expand Up @@ -170,9 +173,10 @@ func NewParser(data interface{}, options Options) *Parser {
// be added to this parser by using AddGroup and AddCommand.
func NewNamedParser(appname string, options Options) *Parser {
p := &Parser{
Command: newCommand(appname, "", "", nil),
Options: options,
NamespaceDelimiter: ".",
Command: newCommand(appname, "", "", nil),
Options: options,
NamespaceDelimiter: ".",
EnvNamespaceDelimiter: "_",
}

p.Command.parent = p
Expand Down
56 changes: 38 additions & 18 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,16 @@ func EnvSnapshot() *EnvRestorer {
return &r
}

type envNestedOptions struct {
Foo string `long:"foo" default:"z" env:"FOO"`
}

type envDefaultOptions struct {
Int int `long:"i" default:"1" env:"TEST_I"`
Time time.Duration `long:"t" default:"1m" env:"TEST_T"`
Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"`
Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","`
Int int `long:"i" default:"1" env:"TEST_I"`
Time time.Duration `long:"t" default:"1m" env:"TEST_T"`
Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"`
Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","`
Nested envNestedOptions `group:"nested" namespace:"nested" env-namespace:"NESTED"`
}

func TestEnvDefaults(t *testing.T) {
Expand All @@ -267,6 +272,9 @@ func TestEnvDefaults(t *testing.T) {
Time: time.Minute,
Map: map[string]int{"a": 1},
Slice: []int{1, 2},
Nested: envNestedOptions{
Foo: "z",
},
},
},
{
Expand All @@ -277,44 +285,56 @@ func TestEnvDefaults(t *testing.T) {
Time: 2 * time.Minute,
Map: map[string]int{"a": 2, "b": 3},
Slice: []int{4, 5, 6},
Nested: envNestedOptions{
Foo: "a",
},
},
env: map[string]string{
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
"NESTED_FOO": "a",
},
},
{
msg: "non-zero value arguments, expecting overwritten arguments",
args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3"},
args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3", "--nested.foo=\"p\""},
expected: envDefaultOptions{
Int: 3,
Time: 3 * time.Millisecond,
Map: map[string]int{"c": 3},
Slice: []int{3},
Nested: envNestedOptions{
Foo: "p",
},
},
env: map[string]string{
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
"NESTED_FOO": "a",
},
},
{
msg: "zero value arguments, expecting overwritten arguments",
args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0"},
args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0", "--nested.foo=\"\""},
expected: envDefaultOptions{
Int: 0,
Time: 0,
Map: map[string]int{"": 0},
Slice: []int{0},
Nested: envNestedOptions{
Foo: "",
},
},
env: map[string]string{
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
"TEST_I": "2",
"TEST_T": "2m",
"TEST_M": "a:2;b:3",
"TEST_S": "4,5,6",
"NESTED_FOO": "a",
},
},
}
Expand Down

0 comments on commit 1c38ed7

Please sign in to comment.