-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathenvflags.go
189 lines (170 loc) · 4.9 KB
/
envflags.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// envflags provides a way to set flags from environment variables,
// working in conjunction with the flag package; it adds the
// option to look for an environment variable in between the flag and
// its defsult value, and also adds environment variable names to the
// usage string.
//
// The precedence order for values is explicit flag -> environment
// variable -> default.
package envflags
import (
"encoding"
"flag"
"fmt"
"log/slog"
"os"
"reflect"
"strconv"
"strings"
"time"
)
var (
EnvPrefix = ""
// Will be added to usage strings for flags that have an
// environment variable. The format string substitutes the
// environment variable name.
EnvUsageTemplate = "\nEnvironment: %s"
)
type Value[T any] struct {
envName string
flagValue T
converter func(string) (T, error)
isBoolFlag bool
}
// NewEnvFlagValue creates a new Value[T] with the given environment
// variable name. Pass "" to the envName to disable environment variables
// for this flag. The defaultValue is used if the environment variable or
// explicit flag are not set or cannot be converted.
func NewEnvFlagValue[T any](
envName string,
defaultValue T,
converter func(string) (T, error),
) *Value[T] {
envFlag := &Value[T]{
converter: converter,
envName: envName,
}
envFlag.setDefault(envName, defaultValue)
return envFlag
}
// AddTo adds the flag to the given flag.FlagSet with the given name.
// usage is appended with environment flag info if applicable.
func (p *Value[T]) AddTo(flags *flag.FlagSet, name, usage string) {
usage += p.envUsage()
flags.Var(p, name, usage)
}
func (p Value[T]) EnvName() string {
if p.envName == "" {
return ""
}
return EnvPrefix + p.envName
}
func (p Value[T]) envUsage() string {
if p.envName == "" {
return ""
}
return fmt.Sprintf(EnvUsageTemplate, p.EnvName())
}
func (p Value[T]) IsBoolFlag() bool {
return p.isBoolFlag
}
func (p *Value[T]) setDefault(envName string, defaultValue T) {
if env := os.Getenv(EnvPrefix + envName); env != "" {
converted, err := p.converter(env)
if err == nil {
//defaultValue = converted
p.flagValue = converted
return
} else {
slog.Warn("error converting environment variable, ignoring", "env", EnvPrefix+envName, "error", err, "default", defaultValue)
}
}
p.flagValue = defaultValue
}
func (p Value[T]) String() string {
return fmt.Sprintf("%v", p.flagValue)
}
// Get the value of the flag.
func (p Value[T]) Get() T {
return p.flagValue
}
// Implements flag.Setter
func (p *Value[T]) Set(value string) error {
if p.converter == nil {
return fmt.Errorf("no converter for type %T", p.flagValue)
}
converted, err := p.converter(value)
if err != nil {
return err
}
p.flagValue = converted
return nil
}
func NewString(env, defaultValue string) *Value[string] {
converter := func(s string) (string, error) {
return s, nil
}
pflag := NewEnvFlagValue(env, defaultValue, converter)
return pflag
}
func NewBool(env string, defaultValue bool) *Value[bool] {
eflag := NewEnvFlagValue(env, defaultValue, strconv.ParseBool)
eflag.isBoolFlag = true
return eflag
}
func NewInt(env string, defaultValue int) *Value[int] {
pflag := NewEnvFlagValue(env, defaultValue, strconv.Atoi)
return pflag
}
func NewDuration(env string, defaultValue time.Duration) *Value[time.Duration] {
pflag := NewEnvFlagValue(env, defaultValue, time.ParseDuration)
return pflag
}
func NewLogLevel(env string, defaultValue slog.Level) *Value[slog.Level] {
converter := func(s string) (slog.Level, error) {
switch strings.ToLower(s) {
case "debug":
return slog.LevelDebug, nil
case "info":
return slog.LevelInfo, nil
case "warn":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelInfo, fmt.Errorf("invalid log level: %s", s)
}
}
pflag := NewEnvFlagValue(env, defaultValue, converter)
return pflag
}
func NewUint64(env string, defaultValue uint64) *Value[uint64] {
converter := func(s string) (uint64, error) {
return strconv.ParseUint(s, 10, strconv.IntSize)
}
pflag := NewEnvFlagValue(env, defaultValue, converter)
return pflag
}
func NewText[S encoding.TextUnmarshaler](env string, defaultValue S) *Value[S] {
// It seems not to be possible to create a new S without reflection.
// If we don't create a new instance of S and use defaultValue as the UnmarshalText target,
// defaultValue will be modified by the UnmarshalText call. This is only materially a problem
// when the Unmarshal fails, because it can still update the thing's value.
defVal := reflect.ValueOf(defaultValue)
if defVal.Kind() == reflect.Ptr {
defVal = defVal.Elem()
}
defType := defVal.Type()
converter := func(s string) (S, error) {
text := reflect.New(defType).Interface().(S)
if s == "" {
return text, fmt.Errorf("empty string for text value")
}
if err := text.UnmarshalText([]byte(s)); err != nil {
return text, err
}
return text, nil
}
pflag := NewEnvFlagValue(env, defaultValue, converter)
return pflag
}