Merge pull request #87 from melor/env_default
Optional default values from environment variables
diff --git a/flags.go b/flags.go
index 68173e7..eb2012f 100644
--- a/flags.go
+++ b/flags.go
@@ -17,6 +17,7 @@
Options with long names (--verbose)
Options with and without arguments (bool v.s. other type)
Options with optional arguments and default values
+ Option default values from ENVIRONMENT_VARIABLES, including slice and map values
Multiple option groups each containing a set of options
Generate and print well-formatted help message
Passing remaining command line arguments after -- (optional)
@@ -95,6 +96,12 @@
showing up in the help. If default-mask takes the special
value "-", then no default value will be shown at all
(optional)
+ env: the default value of the option is overridden from the
+ specified environment variable, if one has been defined.
+ (optional)
+ env-delim: the 'env' default value from environment is split into
+ multiple values with the given delimiter string, use with
+ slices and maps (optional)
value-name: the name of the argument value (to be shown in the help,
(optional)
diff --git a/group_private.go b/group_private.go
index 26ee92a..15251ce 100644
--- a/group_private.go
+++ b/group_private.go
@@ -124,6 +124,7 @@
description := mtag.Get("description")
def := mtag.GetMany("default")
+
optionalValue := mtag.GetMany("optional-value")
valueName := mtag.Get("value-name")
defaultMask := mtag.Get("default-mask")
@@ -136,6 +137,8 @@
ShortName: short,
LongName: longname,
Default: def,
+ EnvDefaultKey: mtag.Get("env"),
+ EnvDefaultDelim: mtag.Get("env-delim"),
OptionalArgument: optional,
OptionalValue: optionalValue,
Required: required,
diff --git a/help.go b/help.go
index 0139215..d16f3b1 100644
--- a/help.go
+++ b/help.go
@@ -10,6 +10,7 @@
"fmt"
"io"
"reflect"
+ "runtime"
"strings"
"unicode/utf8"
)
@@ -173,12 +174,24 @@
def = strings.Join(defs, ", ")
}
+ var envDef string
+ if option.EnvDefaultKey != "" {
+ var envPrintable string
+ if runtime.GOOS == "windows" {
+ envPrintable = "%" + option.EnvDefaultKey + "%"
+ } else {
+ envPrintable = "$" + option.EnvDefaultKey
+ }
+ envDef = fmt.Sprintf(" [%s]", envPrintable)
+ }
+
var desc string
if def != "" {
- desc = fmt.Sprintf("%s (%v)", option.Description, def)
+ desc = fmt.Sprintf("%s (%v)%s", option.Description, def,
+ envDef)
} else {
- desc = option.Description
+ desc = option.Description + envDef
}
writer.WriteString(wrapText(desc,
diff --git a/help_test.go b/help_test.go
index 72b3e81..49ee5b4 100644
--- a/help_test.go
+++ b/help_test.go
@@ -54,6 +54,8 @@
Default string `long:"default" default:"Some value" description:"Test default value"`
DefaultArray []string `long:"default-array" default:"Some value" default:"Another value" description:"Test default array value"`
DefaultMap map[string]string `long:"default-map" default:"some:value" default:"another:value" description:"Testdefault map value"`
+ EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"`
+ EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"`
OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"`
@@ -81,8 +83,11 @@
}
func TestHelp(t *testing.T) {
- var opts helpOptions
+ oldEnv := EnvSnapshot()
+ defer oldEnv.Restore()
+ os.Setenv("ENV_DEFAULT", "env-def")
+ var opts helpOptions
p := NewNamedParser("TestHelp", HelpFlag)
p.AddGroup("Application Options", "The application options", &opts)
@@ -113,6 +118,8 @@
/default: Test default value (Some value)
/default-array: Test default array value (Some value, Another value)
/default-map: Testdefault map value (some:value, another:value)
+ /env-default1: Test env-default1 value (Some value) [%ENV_DEFAULT%]
+ /env-default2: Test env-default2 value [%ENV_DEFAULT%]
Other Options:
/s: A slice of strings (some, value)
@@ -147,6 +154,8 @@
--default= Test default value (Some value)
--default-array= Test default array value (Some value, Another value)
--default-map= Testdefault map value (some:value, another:value)
+ --env-default1= Test env-default1 value (Some value) [$ENV_DEFAULT]
+ --env-default2= Test env-default2 value [$ENV_DEFAULT]
Other Options:
-s= A slice of strings (some, value)
@@ -184,8 +193,11 @@
}
func TestMan(t *testing.T) {
- var opts helpOptions
+ oldEnv := EnvSnapshot()
+ defer oldEnv.Restore()
+ os.Setenv("ENV_DEFAULT", "env-def")
+ var opts helpOptions
p := NewNamedParser("TestMan", HelpFlag)
p.ShortDescription = "Test manpage generation"
p.LongDescription = "This is a somewhat `longer' description of what this does"
@@ -229,6 +241,12 @@
\fB--default-map\fP
Testdefault map value
.TP
+\fB--env-default1\fP
+Test env-default1 value
+.TP
+\fB--env-default2\fP
+Test env-default2 value
+.TP
\fB-s\fP
A slice of strings
.TP
@@ -273,8 +291,11 @@
}
func TestHelpCommand(t *testing.T) {
- var opts helpCommandNoOptions
+ oldEnv := EnvSnapshot()
+ defer oldEnv.Restore()
+ os.Setenv("ENV_DEFAULT", "env-def")
+ var opts helpCommandNoOptions
p := NewNamedParser("TestHelpCommand", HelpFlag)
p.AddGroup("Application Options", "The application options", &opts)
diff --git a/ini_test.go b/ini_test.go
index eb752e9..cb88b64 100644
--- a/ini_test.go
+++ b/ini_test.go
@@ -9,6 +9,10 @@
)
func TestWriteIni(t *testing.T) {
+ oldEnv := EnvSnapshot()
+ defer oldEnv.Restore()
+ os.Setenv("ENV_DEFAULT", "env-def")
+
var tests = []struct {
args []string
options IniOptions
@@ -22,6 +26,12 @@
verbose = true
verbose = true
+; Test env-default1 value
+EnvDefault1 = env-def
+
+; Test env-default2 value
+EnvDefault2 = env-def
+
[Other Options]
; A map from string to int
int-map = a:2
@@ -53,6 +63,12 @@
DefaultMap = another:value
DefaultMap = some:value
+; Test env-default1 value
+EnvDefault1 = env-def
+
+; Test env-default2 value
+EnvDefault2 = env-def
+
; Option only available in ini
only-ini =
@@ -102,6 +118,12 @@
; DefaultMap = another:value
; DefaultMap = some:value
+; Test env-default1 value
+EnvDefault1 = env-def
+
+; Test env-default2 value
+EnvDefault2 = env-def
+
; Option only available in ini
; only-ini =
@@ -148,6 +170,12 @@
; Testdefault map value
DefaultMap = new:value
+; Test env-default1 value
+EnvDefault1 = env-def
+
+; Test env-default2 value
+EnvDefault2 = env-def
+
; Option only available in ini
; only-ini =
diff --git a/option.go b/option.go
index 325d9c8..26f2441 100644
--- a/option.go
+++ b/option.go
@@ -27,6 +27,12 @@
// The default value of the option.
Default []string
+ // The optional environment default value key name.
+ EnvDefaultKey string
+
+ // The optional delimiter string for EnvDefaultKey values.
+ EnvDefaultDelim string
+
// If true, specifies that the argument to an option flag is optional.
// When no argument to the flag is specified on the command line, the
// value of Default will be set in the field this option represents.
diff --git a/option_private.go b/option_private.go
index c6b7c0d..d36c841 100644
--- a/option_private.go
+++ b/option_private.go
@@ -2,6 +2,8 @@
import (
"reflect"
+ "strings"
+ "syscall"
)
// Set the value of an option to the specified value. An error will be returned
@@ -48,10 +50,24 @@
}
func (option *Option) clearDefault() {
- if len(option.Default) > 0 {
+ usedDefault := option.Default
+ if envKey := option.EnvDefaultKey; envKey != "" {
+ // os.Getenv() makes no distinction between undefined and
+ // empty values, so we use syscall.Getenv()
+ if value, ok := syscall.Getenv(envKey); ok {
+ if option.EnvDefaultDelim != "" {
+ usedDefault = strings.Split(value,
+ option.EnvDefaultDelim)
+ } else {
+ usedDefault = []string{value}
+ }
+ }
+ }
+
+ if len(usedDefault) > 0 {
option.empty()
- for _, d := range option.Default {
+ for _, d := range usedDefault {
option.set(&d)
}
} else {
diff --git a/parser_test.go b/parser_test.go
index b227244..add87d8 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -1,7 +1,9 @@
package flags
import (
+ "os"
"reflect"
+ "strings"
"testing"
"time"
)
@@ -96,3 +98,126 @@
}
}
}
+
+// envRestorer keeps a copy of a set of env variables and can restore the env from them
+type envRestorer struct {
+ env map[string]string
+}
+
+func (r *envRestorer) Restore() {
+ os.Clearenv()
+ for k, v := range r.env {
+ os.Setenv(k, v)
+ }
+}
+
+// EnvSnapshot returns a snapshot of the currently set env variables
+func EnvSnapshot() *envRestorer {
+ r := envRestorer{make(map[string]string)}
+ for _, kv := range os.Environ() {
+ parts := strings.SplitN(kv, "=", 2)
+ if len(parts) != 2 {
+ panic("got a weird env variable: " + kv)
+ }
+ r.env[parts[0]] = parts[1]
+ }
+ return &r
+}
+
+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:","`
+}
+
+func TestEnvDefaults(t *testing.T) {
+ var tests = []struct {
+ msg string
+ args []string
+ expected envDefaultOptions
+ env map[string]string
+ }{
+ {
+ msg: "no arguments, no env, expecting default values",
+ args: []string{},
+ expected: envDefaultOptions{
+ Int: 1,
+ Time: time.Minute,
+ Map: map[string]int{"a": 1},
+ Slice: []int{1, 2},
+ },
+ },
+ {
+ msg: "no arguments, env defaults, expecting env default values",
+ args: []string{},
+ expected: envDefaultOptions{
+ Int: 2,
+ Time: 2 * time.Minute,
+ Map: map[string]int{"a": 2, "b": 3},
+ Slice: []int{4, 5, 6},
+ },
+ env: map[string]string{
+ "TEST_I": "2",
+ "TEST_T": "2m",
+ "TEST_M": "a:2;b:3",
+ "TEST_S": "4,5,6",
+ },
+ },
+ {
+ msg: "non-zero value arguments, expecting overwritten arguments",
+ args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3"},
+ expected: envDefaultOptions{
+ Int: 3,
+ Time: 3 * time.Millisecond,
+ Map: map[string]int{"c": 3},
+ Slice: []int{3},
+ },
+ env: map[string]string{
+ "TEST_I": "2",
+ "TEST_T": "2m",
+ "TEST_M": "a:2;b:3",
+ "TEST_S": "4,5,6",
+ },
+ },
+ {
+ msg: "zero value arguments, expecting overwritten arguments",
+ args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0"},
+ expected: envDefaultOptions{
+ Int: 0,
+ Time: 0,
+ Map: map[string]int{"": 0},
+ Slice: []int{0},
+ },
+ env: map[string]string{
+ "TEST_I": "2",
+ "TEST_T": "2m",
+ "TEST_M": "a:2;b:3",
+ "TEST_S": "4,5,6",
+ },
+ },
+ }
+
+ oldEnv := EnvSnapshot()
+ defer oldEnv.Restore()
+
+ for _, test := range tests {
+ var opts envDefaultOptions
+ oldEnv.Restore()
+ for envKey, envValue := range test.env {
+ os.Setenv(envKey, envValue)
+ }
+ _, err := ParseArgs(&opts, test.args)
+ if err != nil {
+ t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
+ }
+
+ if opts.Slice == nil {
+ opts.Slice = []int{}
+ }
+
+ if !reflect.DeepEqual(opts, test.expected) {
+ t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
+ }
+ }
+}