flag.go (3519B)
1 package envflag 2 3 import ( 4 "flag" 5 "fmt" 6 "os" 7 "regexp" 8 "strings" 9 ) 10 11 // Option type for configing the parsing. 12 type Option option 13 type option func(*config) 14 15 // FlagConverterFunc functions are used to update flag name for envvar lookups. 16 type FlagConverterFunc func(flag string) string 17 18 // UsageUpdaterFunc functions are used to update flag usage strings. 19 type UsageUpdaterFunc func(key, usage string) string 20 21 type config struct { 22 prefix string 23 getenv func(string) string 24 flagConverter FlagConverterFunc 25 usageUpdater UsageUpdaterFunc 26 } 27 28 // FlagConverter converts the flag name to an environment variable key. 29 func FlagConverter(f FlagConverterFunc) Option { 30 return func(cfg *config) { 31 cfg.flagConverter = f 32 } 33 } 34 35 // Prefix sets the prefix for all envvars. 36 // This is applied after the FlagConverter function. 37 func Prefix(s string) Option { 38 return func(cfg *config) { 39 cfg.prefix = s 40 } 41 } 42 43 // Getenv is used to fetch the environment, defaults to os.Getenv. 44 func Getenv(f func(string) string) Option { 45 return func(cfg *config) { 46 cfg.getenv = f 47 } 48 } 49 50 // UsageUpdater enables configurable flag usage updates. 51 func UsageUpdater(f UsageUpdaterFunc) Option { 52 return func(cfg *config) { 53 cfg.usageUpdater = f 54 } 55 } 56 57 // UsagePrefixer prefixes the flag usage with [envvar]. 58 // 59 // Example output: 60 // 61 // Usage of example: 62 // -verbose 63 // [VERBOSE] Be verbose 64 func UsagePrefixer() Option { 65 return func(cfg *config) { 66 cfg.usageUpdater = usagePrefixer 67 } 68 } 69 70 // UsageSuffixer suffixes the flag usage with [envvar]. 71 // 72 // Example output: 73 // 74 // Usage of example: 75 // -verbose 76 // Be verbose [VERBOSE] 77 func UsageSuffixer() Option { 78 return func(cfg *config) { 79 cfg.usageUpdater = usageSuffixer 80 } 81 } 82 83 // ParseWithEnv parses the command-line flags from os.Args[1:]. Must be called 84 // after all flags are defined and before flags are accessed by the program. 85 func Parse(opts ...Option) error { 86 return parseFlagSetWithEnv(flag.CommandLine, os.Args[1:], opts...) 87 } 88 89 // ParseFlagSetWithEnv parses flag definitions from the argument list, which 90 // should not include the command name. Must be called after all flags in the 91 // FlagSet are defined and before flags are accessed by the program. The return 92 // value will be ErrHelp if -help or -h were set but not defined. 93 func ParseFlagSet(fs *flag.FlagSet, arguments []string, opts ...Option) error { 94 return parseFlagSetWithEnv(fs, arguments, opts...) 95 } 96 97 func parseFlagSetWithEnv(fs *flag.FlagSet, arguments []string, opts ...Option) error { 98 if fs.Parsed() { 99 return fmt.Errorf("flag has already been parsed") 100 } 101 cfg := &config{ 102 flagConverter: flagToEnv(), 103 getenv: os.Getenv, 104 } 105 for _, o := range opts { 106 o(cfg) 107 } 108 var nerr error 109 fs.VisitAll(func(f *flag.Flag) { 110 envKey := cfg.flagConverter(f.Name) 111 envKey = cfg.prefix + envKey 112 if cfg.usageUpdater != nil { 113 f.Usage = cfg.usageUpdater(envKey, f.Usage) 114 } 115 if envVal := cfg.getenv(envKey); envVal != "" { 116 err := f.Value.Set(envVal) 117 if err != nil && nerr == nil { 118 nerr = err 119 } 120 } 121 }) 122 if nerr != nil { 123 return nerr 124 } 125 return fs.Parse(arguments) 126 } 127 128 // convert this-format to THIS_FORMAT 129 func flagToEnv() func(string) string { 130 re := regexp.MustCompile("[^a-zA-Z0-9_]+") 131 return func(f string) string { 132 return strings.ToUpper(re.ReplaceAllLiteralString(f, "_")) 133 } 134 } 135 136 func usageSuffixer(key, usage string) string { 137 return fmt.Sprintf("%s [%s]", usage, key) 138 } 139 140 func usagePrefixer(key, usage string) string { 141 return fmt.Sprintf("[%s] %s", key, usage) 142 }