summaryrefslogtreecommitdiff
path: root/srv/src/cfg/cfg.go
blob: d87c45b94fc08b4a02a62a2476abf4505615174a (plain)
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
// Package cfg implements a simple wrapper around go's flag package, in order to
// implement initialization hooks.
package cfg

import (
	"context"
	"flag"
	"fmt"
	"os"
	"strconv"
	"strings"
)

// Cfger is a component which can be used with Cfg to setup its initialization.
type Cfger interface {
	SetupCfg(*Cfg)
}

// Params are used to initialize a Cfg instance.
type Params struct {

	// Args are the command line arguments, excluding the command-name.
	//
	// Defaults to os.Args[1:]
	Args []string

	// Env is the process's environment variables.
	//
	// Defaults to the real environment variables.
	Env map[string]string

	// EnvPrefix indicates a string to prefix to all environment variable names
	// that Cfg will read. Will be automatically suffixed with a "_" if given.
	EnvPrefix string
}

func (p Params) withDefaults() Params {

	if p.Args == nil {
		p.Args = os.Args[1:]
	}

	if p.Env == nil {

		p.Env = map[string]string{}

		for _, envVar := range os.Environ() {

			parts := strings.SplitN(envVar, "=", 2)

			if len(parts) < 2 {
				panic(fmt.Sprintf("envVar %q returned from os.Environ() somehow", envVar))
			}

			p.Env[parts[0]] = parts[1]
		}
	}

	if p.EnvPrefix != "" {
		p.EnvPrefix = strings.TrimSuffix(p.EnvPrefix, "_") + "_"
	}

	return p
}

// Cfg is a wrapper around the stdlib's FlagSet and a set of initialization
// hooks.
type Cfg struct {
	params  Params
	flagSet *flag.FlagSet

	hooks []func(ctx context.Context) error
}

// New initializes and returns a new instance of *Cfg.
func New(params Params) *Cfg {

	params = params.withDefaults()

	return &Cfg{
		params:  params,
		flagSet: flag.NewFlagSet("", flag.ExitOnError),
	}
}

// OnInit appends the given callback to the sequence of hooks which will run on
// a call to Init.
func (c *Cfg) OnInit(cb func(context.Context) error) {
	c.hooks = append(c.hooks, cb)
}

// Init runs all hooks registered using OnInit, in the same order OnInit was
// called. If one returns an error that error is returned and no further hooks
// are run.
func (c *Cfg) Init(ctx context.Context) error {
	if err := c.flagSet.Parse(c.params.Args); err != nil {
		return err
	}

	for _, h := range c.hooks {
		if err := h(ctx); err != nil {
			return err
		}
	}

	return nil
}

func (c *Cfg) envifyName(name string) string {
	name = c.params.EnvPrefix + name
	name = strings.Replace(name, "-", "_", -1)
	name = strings.ToUpper(name)
	return name
}

func envifyUsage(envName, usage string) string {
	return fmt.Sprintf("%s (overrides %s)", usage, envName)
}

// StringVar is equivalent to flag.FlagSet's StringVar method, but will
// additionally set up an environment variable for the parameter.
func (c *Cfg) StringVar(p *string, name, value, usage string) {

	envName := c.envifyName(name)

	c.flagSet.StringVar(p, name, value, envifyUsage(envName, usage))

	if val := c.params.Env[envName]; val != "" {
		*p = val
	}
}

// Args returns a pointer which will be filled with the process's positional
// arguments after Init is called. The positional arguments are all CLI
// arguments starting with the first non-flag argument.
//
// The usage argument should describe what these arguments are, and its notation
// should indicate if they are optional or variadic. For example:
//
//	// optional variadic
//	"[names...]"
//
//	// required single args
//	"<something> <something else>"
//
//	// Mixed
//	"<foo> <bar> [baz] [other...]"
//
func (c *Cfg) Args(usage string) *[]string {

	args := new([]string)

	c.flagSet.Usage = func() {
		fmt.Fprintf(os.Stderr, "USAGE [flags...] %s\n", usage)
		fmt.Fprintf(os.Stderr, "\nFLAGS\n\n")
		c.flagSet.PrintDefaults()
	}

	c.OnInit(func(ctx context.Context) error {
		*args = c.flagSet.Args()
		return nil
	})

	return args
}

// String is equivalent to flag.FlagSet's String method, but will additionally
// set up an environment variable for the parameter.
func (c *Cfg) String(name, value, usage string) *string {
	p := new(string)
	c.StringVar(p, name, value, usage)
	return p
}

// IntVar is equivalent to flag.FlagSet's IntVar method, but will additionally
// set up an environment variable for the parameter.
func (c *Cfg) IntVar(p *int, name string, value int, usage string) {

	envName := c.envifyName(name)

	c.flagSet.IntVar(p, name, value, envifyUsage(envName, usage))

	// if we can't parse the envvar now then just hold onto the error until
	// Init, otherwise we'd have to panic here and that'd be ugly.
	var err error

	if valStr := c.params.Env[envName]; valStr != "" {

		var val int
		val, err = strconv.Atoi(valStr)

		if err != nil {
			err = fmt.Errorf(
				"parsing envvar %q with value %q: %w",
				envName, valStr, err,
			)

		} else {
			*p = val
		}
	}

	c.OnInit(func(context.Context) error { return err })
}

// Int is equivalent to flag.FlagSet's Int method, but will additionally set up
// an environment variable for the parameter.
func (c *Cfg) Int(name string, value int, usage string) *int {
	p := new(int)
	c.IntVar(p, name, value, usage)
	return p
}

// BoolVar is equivalent to flag.FlagSet's BoolVar method, but will additionally
// set up an environment variable for the parameter.
func (c *Cfg) BoolVar(p *bool, name string, value bool, usage string) {

	envName := c.envifyName(name)

	c.flagSet.BoolVar(p, name, value, envifyUsage(envName, usage))

	if valStr := c.params.Env[envName]; valStr != "" {
		*p = valStr != "" && valStr != "0" && valStr != "false"
	}
}

// Bool is equivalent to flag.FlagSet's Bool method, but will additionally set
// up an environment variable for the parameter.
func (c *Cfg) Bool(name string, value bool, usage string) *bool {
	p := new(bool)
	c.BoolVar(p, name, value, usage)
	return p
}

// SubCmd should be called _after_ Init. Init will have consumed all arguments
// up until the first non-flag argument. This non-flag argument is a
// sub-command, and is returned by this method. This method also resets Cfg's
// internal state so that new options can be added to it.
//
// If there is no sub-command following the initial set of flags then this will
// return empty string.
func (c *Cfg) SubCmd() string {
	c.params.Args = c.flagSet.Args()
	if len(c.params.Args) == 0 {
		return ""
	}

	subCmd := c.params.Args[0]

	c.flagSet = flag.NewFlagSet(subCmd, flag.ExitOnError)
	c.hooks = nil
	c.params.Args = c.params.Args[1:]

	return subCmd
}