envflag

Configure flags from the environment
Log | Files | Refs | README | LICENSE

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:
MREADME.md | 21++++++++++++++++++++-
Mflag.go | 23++++++++++++-----------
Mflag_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) + } }) }