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)
+		}
+	}
+}