commit 87e4f8cc1cc88929700d0fd01d142816ff3c15c6
parent 9eada7f9f6df2e72c71c079c2ce795ae79963b40
Author: Felix Hanley <felix@userspace.com.au>
Date: Fri, 29 Aug 2025 15:10:35 +1000
Add option to override os.Getenv
This can be useful for testing
Diffstat:
| M | README.md | | | 21 | ++++++++++++++++++++- |
| M | flag.go | | | 23 | ++++++++++++----------- |
| M | flag_test.go | | | 61 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++------ |
3 files changed, 87 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
@@ -1,3 +1,22 @@
# Environment variables and flags
-Set flags using Go's `flag` package, and complement them with environment variables.
+Set flags using Go's `flag` package, and complement them with environment
+variables. Flags, if set, take precedence over the environment.
+
+Go's standard `flag.FlagSet` can be initialised as normal and parse errors are
+returned as expected. Environment variables can have custom prefixes, suffixes
+etc. as can the usage displayed with the standard `-h` flag.
+
+```go
+fs := flag.NewFlagSet("test", flag.ContinueOnError)
+fs.String("db", "", "Database thing")
+envflag.ParseFlagSet(fs, envflag.Prefix("APP_"), envflag.UsageSuffixer())
+```
+
+produces the following usage
+
+```sh
+Usage of test:
+ -db string
+ Database thing [APP_DB]
+```
diff --git a/flag.go b/flag.go
@@ -20,6 +20,7 @@ type UsageUpdaterFunc func(key, usage string) string
type config struct {
prefix string
+ getenv func(string) string
flagConverter FlagConverterFunc
usageUpdater UsageUpdaterFunc
}
@@ -39,6 +40,13 @@ func Prefix(s string) Option {
}
}
+// Getenv is used to fetch the environment, defaults to os.Getenv.
+func Getenv(f func(string) string) Option {
+ return func(cfg *config) {
+ cfg.getenv = f
+ }
+}
+
// UsageUpdater enables configurable flag usage updates.
func UsageUpdater(f UsageUpdaterFunc) Option {
return func(cfg *config) {
@@ -80,6 +88,7 @@ func parseFlagSetWithEnv(fs *flag.FlagSet, arguments []string, opts ...Option) e
}
cfg := &config{
flagConverter: flagToEnv(),
+ getenv: os.Getenv,
}
for _, o := range opts {
o(cfg)
@@ -91,7 +100,7 @@ func parseFlagSetWithEnv(fs *flag.FlagSet, arguments []string, opts ...Option) e
if cfg.usageUpdater != nil {
f.Usage = cfg.usageUpdater(envKey, f.Usage)
}
- if envVal := os.Getenv(envKey); envVal != "" {
+ if envVal := cfg.getenv(envKey); envVal != "" {
err := f.Value.Set(envVal)
if err != nil && nerr == nil {
nerr = err
@@ -113,17 +122,9 @@ func flagToEnv() func(string) string {
}
func usageSuffixer(key, usage string) string {
- envSuffix := fmt.Sprintf("[%s]", key)
- if strings.HasSuffix(usage, envSuffix) {
- return usage
- }
- return fmt.Sprintf("%s %s", usage, envSuffix)
+ return fmt.Sprintf("%s [%s]", usage, key)
}
func usagePrefixer(key, usage string) string {
- envPrefix := fmt.Sprintf("[%s]", key)
- if strings.HasPrefix(usage, envPrefix) {
- return usage
- }
- return fmt.Sprintf("%s %s", envPrefix, usage)
+ return fmt.Sprintf("[%s] %s", key, usage)
}
diff --git a/flag_test.go b/flag_test.go
@@ -1,6 +1,7 @@
package envflag
import (
+ "bytes"
"flag"
"os"
"testing"
@@ -49,13 +50,13 @@ func TestParseFlagSet(t *testing.T) {
expected: "passed",
},
"no flags with env": {
- // Flags take precedence
name: "testing-123",
env: map[string]string{"TESTING_123": "fromenv"},
expected: "fromenv",
},
"flags with env and suffix": {
name: "testing-123",
+ // Flags take precedence
values: []string{"-testing-123", "passed"},
opts: []Option{UsageSuffixer()},
env: map[string]string{"TESTING_123": "fromenv"},
@@ -63,10 +64,38 @@ func TestParseFlagSet(t *testing.T) {
},
"no flags with env and suffix": {
name: "testing-123",
- def: "defaultvalue",
+ values: []string{"-h"},
opts: []Option{UsageSuffixer()},
env: map[string]string{"TESTING_123": "fromenv"},
expected: "fromenv",
+ usage: `Usage of test:
+ -testing-123 string
+ usage [TESTING_123]
+`,
+ },
+ "usage prefix": {
+ name: "testing-123",
+ values: []string{"-h"},
+ opts: []Option{UsagePrefixer()},
+ env: map[string]string{"TESTING_123": "fromenv"},
+ expected: "fromenv",
+ usage: `Usage of test:
+ -testing-123 string
+ [TESTING_123] usage
+`,
+ },
+ "usage updater": {
+ name: "testing-123",
+ values: []string{"-h"},
+ opts: []Option{UsageUpdater(func(key,usage string)string{
+ return "thine usage"
+ })},
+ env: map[string]string{"TESTING_123": "fromenv"},
+ expected: "fromenv",
+ usage: `Usage of test:
+ -testing-123 string
+ thine usage
+`,
},
"a variety of characters": {
// Flags take precedence
@@ -74,7 +103,7 @@ func TestParseFlagSet(t *testing.T) {
env: map[string]string{"TES_T_ING_123": "fromenv"},
expected: "fromenv",
},
- "prefixed-no-flag": {
+ "prefixed no flag": {
// Flags take precedence
name: "testing-123",
values: []string{},
@@ -82,7 +111,7 @@ func TestParseFlagSet(t *testing.T) {
env: map[string]string{"APP_TESTING_123": "fromenv"},
expected: "fromenv",
},
- "prefixed-with-flag": {
+ "prefixed with flag": {
// Flags take precedence
name: "testing-123",
values: []string{"-testing-123", "passed"},
@@ -90,11 +119,29 @@ func TestParseFlagSet(t *testing.T) {
env: map[string]string{"APP_TESTING_123": "fromenv"},
expected: "passed",
},
+ "no flags custom env": {
+ name: "testing-123",
+ opts: []Option{Getenv(func(s string) string {
+ return "fake env"
+ })},
+ env: map[string]string{"TESTING_123": "fromenv"},
+ expected: "fake env",
+ },
+ "no flags with converter": {
+ name: "testing-123",
+ opts: []Option{FlagConverter(func(f string) string {
+ return "conVERTed"
+ })},
+ env: map[string]string{"conVERTed": "fromenv"},
+ expected: "fromenv",
+ },
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
- fs := flag.NewFlagSet("test", flag.ExitOnError)
+ fs := flag.NewFlagSet("test", flag.ContinueOnError)
+ buf := new(bytes.Buffer)
+ fs.SetOutput(buf)
for k, v := range tt.env {
os.Setenv(k, v)
}
@@ -105,10 +152,12 @@ func TestParseFlagSet(t *testing.T) {
}()
actual := fs.String(tt.name, tt.def, "usage")
ParseFlagSet(fs, tt.values, tt.opts...)
-
if *actual != tt.expected {
t.Errorf("got %q, want %q", *actual, tt.expected)
}
+ if tt.usage != buf.String() {
+ t.Errorf("got %q, want %q", buf.String(), tt.usage)
+ }
})
}