File: README.md

package info (click to toggle)
golang-github-thediveo-enumflag 2.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 212 kB
  • sloc: makefile: 6
file content (367 lines) | stat: -rw-r--r-- 12,995 bytes parent folder | download
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# CLI Enumeration Flags
[![Go Reference](https://pkg.go.dev/badge/github.com/thediveo/enumflag.svg)](https://pkg.go.dev/github.com/thediveo/enumflag/v2)
[![GitHub](https://img.shields.io/github/license/thediveo/enumflag)](https://img.shields.io/github/license/thediveo/enumflag)
![build and test](https://github.com/thediveo/enumflag/actions/workflows/buildandtest.yaml/badge.svg?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thediveo/enumflag/v2)](https://goreportcard.com/report/github.com/thediveo/enumflag/v2)
![Coverage](https://img.shields.io/badge/Coverage-100.0%25-brightgreen)

`enumflag/v2` is a Golang package which supplements the Golang CLI flag packages
[spf13/cobra](https://github.com/spf13/cobra) and
[spf13/pflag](https://github.com/spf13/pflag) with enumeration flags, including
support for enumeration slices. Thanks to Go generics, `enumflag/v2` now
provides type-safe enumeration flags (and thus requires Go 1.18 or later).

> The v2 API is source-compatible with v0 unless you've used the `Get()` method
> in the past. However, since the use of Go generics might be a breaking change
> to downstream projects the semantic major version of `enumflag` thus went from
> v0 straight to v2.

For instance, users can specify enum flags as `--mode=foo` or `--mode=bar`,
where `foo` and `bar` are valid enumeration values. Other values which are not
part of the set of allowed enumeration values cannot be set and raise CLI flag
errors. In case of an enumeration _slice_ flag users can specify multiple
enumeration values either with a single flag `--mode=foo,bar` or multiple flag
calls, such as `--mode=foo --mode=bar`.

Application programmers then simply deal with enumeration values in form of
uints (or ints, _erm_, anything that satisfies `comparable`s),
liberated from parsing strings and validating enumeration flags.

For devcontainer instructions, please see the [section "DevContainer"
below](#devcontainer).

## Alternatives

In case you are just interested in string-based one-of-a-set flags, then the
following packages offer you a minimalist approach:

- [hashicorp/packer/helper/enumflag](https://godoc.org/github.com/hashicorp/packer/helper/enumflag)
  really is a reduced-to-the-max version without any whistles and bells.
- [creachadair/goflags/enumflag](https://godoc.org/github.com/creachadair/goflags/enumflag)
  has a similar, but slightly more elaborate API with additional "indices" for
  enumeration values.

But if you instead want to handle one-of-a-set flags as properly typed
enumerations instead of strings, or if you need (multiple-of-a-set) slice
support, then please read on.

## Installation

To add `enumflag/v2` as a dependency, in your Go module issue:

```bash
go get github.com/thediveo/enumflag/v2
```

## How To Use

- [start with your own enum types](#start-with-your-own-enum-types),
- optional: [shell completion](#shell-completion),
- optional: [use existing enum types and non-zero defaults](#use-existing-enum-types),
- optional: [CLI flag with default](#cli-flag-with-default),
- optional: [CLI flag without a default value](#cli-flag-without-default),
- optional: [slice of enums](#slice-of-enums).

### Start With Your Own Enum Types

Without further ado, here's how to define and use enum flags in your own
applications...

```go
import (
    "fmt"

    "github.com/spf13/cobra"
    "github.com/thediveo/enumflag/v2"
)

// ① Define your new enum flag type. It can be derived from enumflag.Flag,
// but it doesn't need to be as long as it satisfies comparable.
type FooMode enumflag.Flag

// ② Define the enumeration values for FooMode.
const (
    Foo FooMode = iota
    Bar
)

// ③ Map enumeration values to their textual representations (value
// identifiers).
var FooModeIds = map[FooMode][]string{
    Foo: {"foo"},
    Bar: {"bar"},
}

// ④ Now use the FooMode enum flag. If you want a non-zero default, then
// simply set it here, such as in "foomode = Bar".
var foomode FooMode

func main() {
    rootCmd := &cobra.Command{
        Run: func(cmd *cobra.Command, _ []string) {
            fmt.Printf("mode is: %d=%q\n",
                foomode,
                cmd.PersistentFlags().Lookup("mode").Value.String())
        },
    }
    // ⑤ Define the CLI flag parameters for your wrapped enum flag.
    rootCmd.PersistentFlags().VarP(
        enumflag.New(&foomode, "mode", FooModeIds, enumflag.EnumCaseInsensitive),
        "mode", "m",
        "foos the output; can be 'foo' or 'bar'")

    rootCmd.SetArgs([]string{"--mode", "bAr"})
    _ = rootCmd.Execute()
}
```

The boilerplate pattern is always the same:

1. Define your own new enumeration type, such as `type FooMode enumflag.Flag`.
2. Define the constants in your enumeration.
3. Define the mapping of the constants onto enum values (textual
   representations).
4. Somewhere, declare a flag variable of your enum flag type.
   - If you want to use a non-zero default enum value, just go ahead and set
     it: `var foomode = Bar`. It will be used correctly.
5. Wire up your flag variable to its flag long and short names, et cetera.

### Shell Completion

Dynamic flag completion can be enabled by calling the `RegisterCompletion(...)`
receiver of an enum flag (more precise: flag value) created using
`enumflag.New(...)`. `enumflag` supports dynamic flag completion for both scalar
and slice enum flags. Unfortunately, due to the cobra API design it isn't
possible for `enumflag` to offer a fluent API. Instead, creation, adding, and
registering have to be carried out as separate instructions.

```go
    // ⑤ Define the CLI flag parameters for your wrapped enum flag.
    ef := enumflag.New(&foomode, "mode", FooModeIds, enumflag.EnumCaseInsensitive)
    rootCmd.PersistentFlags().VarP(
        ef,
        "mode", "m",
        "foos the output; can be 'foo' or 'bar'")
    // ⑥ register completion
    ef.RegisterCompletion(rootCmd, "mode", enumflag.Help[FooMode]{
		Foo: "foos the output",
		Bar: "bars the output",
	})
```

Please note for shell completion to work, your root command needs to have at
least one (explicit) sub command. Otherwise, `cobra` won't automatically add an
additional `completion` sub command. For more details, please refer to cobra's
documentation on [Generating shellcompletions](https://github.com/spf13/cobra/blob/main/site/content/completions/_index.md).

### Use Existing Enum Types

A typical example might be your application using a 3rd party logging package
and you want to offer a `-v` log level CLI flag. Here, we use the existing 3rd
party enum values and set a non-zero default for our logging CLI flag.

Considering the boiler plate shown above, we can now leave out steps ① and ②,
because these definitions come from a 3rd party package. We only need to
supply the textual enum names as ③.

```go
import (
    "fmt"
    "os"

    log "github.com/sirupsen/logrus"
    "github.com/spf13/cobra"
    "github.com/thediveo/enumflag/v2"
)

func main() {
    // ①+② skip "define your own enum flag type" and enumeration values, as we
    // already have a 3rd party one.

    // ③ Map 3rd party enumeration values to their textual representations
    var LoglevelIds = map[log.Level][]string{
        log.TraceLevel: {"trace"},
        log.DebugLevel: {"debug"},
        log.InfoLevel:  {"info"},
        log.WarnLevel:  {"warning", "warn"},
        log.ErrorLevel: {"error"},
        log.FatalLevel: {"fatal"},
        log.PanicLevel: {"panic"},
    }

    // ④ Define your enum flag value and set the your logging default value.
    var loglevel log.Level = log.WarnLevel

    rootCmd := &cobra.Command{
        Run: func(cmd *cobra.Command, _ []string) {
            fmt.Printf("logging level is: %d=%q\n",
                loglevel,
                cmd.PersistentFlags().Lookup("log").Value.String())
        },
    }

    // ⑤ Define the CLI flag parameters for your wrapped enum flag.
    rootCmd.PersistentFlags().Var(
        enumflag.New(&loglevel, "log", LoglevelIds, enumflag.EnumCaseInsensitive),
        "log",
        "sets logging level; can be 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'")

    // Defaults to what we set above: warn level.
    _ = rootCmd.Execute()

    // User specifies a specific level, such as log level. 
    rootCmd.SetArgs([]string{"--log", "debug"})
    _ = rootCmd.Execute()
}
```

### CLI Flag With Default

Sometimes you might want a CLI enum flag to have a default value when the user
just specifies the CLI flag **without its value**. A good example is the
`--color` flag of the `ls` command:

- if just specified as `--color` without a value, it
will default to the value of `auto`;
- otherwise, as specific value can be given, such as
  - `--color=always`,
  - `--color=never`,
  - or even `--color=auto`.

In such situations, use spf13/pflags's
[`NoOptDefVal`](https://godoc.org/github.com/spf13/pflag#Flag) to set the
flag's default value *as text*, if the flag is on the command line without any
options.

The gist here is as follows, please see also
[colormode.go](https://github.com/TheDiveO/lxkns/blob/master/cmd/internal/pkg/style/colormode.go)
from my [lxkns](https://github.com/TheDiveO/lxkns) Linux namespaces discovery
project:

```go
rootCmd.PersistentFlags().VarP(
    enumflag.New(&colorize, "color", colorModeIds, enumflag.EnumCaseSensitive),
    "color", "c",
    "colorize the output; can be 'always' (default if omitted), 'auto',\n"+
        "or 'never'")
rootCmd.PersistentFlags().Lookup("color").NoOptDefVal = "always"
```

### CLI Flag Without Default

In other situations you might _not_ want to have a default value set, because a
particular CLI flag is mandatory (using cobra's
[MarkFlagRequired](https://pkg.go.dev/github.com/spf13/cobra#MarkFlagRequired)).
Here, cobra's help should not show a (useless) default enum flag setting but
only the available enum values.

**Don't assign the zero value** of your enum type to any value, except the
"non-existing" default.

```go
// ② Define the enumeration values for FooMode; do not assign the zero value to
// any enum value except for the "no default" default.
const (
    NoDefault FooMode = iota // optional; must be the zero value.
    Foo                      // make sure to not use the zero value.
    Bar
)
```

Also, **don't map the zero value** of your enum type.

```go
// ③ Map enumeration values to their textual representations (value
// identifiers).
var FooModeIds = map[FooMode][]string{
    // ...do NOT include/map the "no default" zero value!
    Foo: {"foo"},
    Bar: {"bar"},
}
```

Finally, simply use `enumflag.NewWithoutDefault` instead of `enumflag.New` –
that's all.

```go
// ⑤ Define the CLI flag parameters for your wrapped enum flag.
rootCmd.PersistentFlags().VarP(
    enumflag.NewWithoutDefault(&foomode, "mode", FooModeIds, enumflag.EnumCaseInsensitive),
    "mode", "m",
    "foos the output; can be 'foo' or 'bar'")
```

### Slice of Enums

For a slice of enumerations, simply declare your variable to be a slice of your
enumeration type and then use `enumflag.NewSlice(...)` instead of
`enumflag.New(...)`.

```go
import (
    "fmt"

    "github.com/spf13/cobra"
    "github.com/thediveo/enumflag/v2"
)

// ① Define your new enum flag type. It can be derived from enumflag.Flag,
// but it doesn't need to be as long as it satisfies comparable.
type MooMode enumflag.Flag

// ② Define the enumeration values for FooMode.
const (
    Moo MooMode = (iota + 1) * 111
    Møø
    Mimimi
)

// ③ Map enumeration values to their textual representations (value
// identifiers).
var MooModeIds = map[MooMode][]string{
    Moo:    {"moo"},
    Møø:    {"møø"},
    Mimimi: {"mimimi"},
}

func Example_slice() {
    // ④ Define your enum slice flag value.
    var moomode []MooMode
    rootCmd := &cobra.Command{
        Run: func(cmd *cobra.Command, _ []string) {
            fmt.Printf("mode is: %d=%q\n",
                moomode,
                cmd.PersistentFlags().Lookup("mode").Value.String())
        },
    }
    // ⑤ Define the CLI flag parameters for your wrapped enumm slice flag.
    rootCmd.PersistentFlags().VarP(
        enumflag.NewSlice(&moomode, "mode", MooModeIds, enumflag.EnumCaseInsensitive),
        "mode", "m",
        "can be any combination of 'moo', 'møø', 'mimimi'")

    rootCmd.SetArgs([]string{"--mode", "Moo,møø"})
    _ = rootCmd.Execute()
}
```

## DevContainer

> [!CAUTION]
>
> Do **not** use VSCode's "~~Dev Containers: Clone Repository in Container
> Volume~~" command, as it is utterly broken by design, ignoring
> `.devcontainer/devcontainer.json`.

1. `git clone https://github.com/thediveo/enumflag`
2. in VSCode: Ctrl+Shift+P, "Dev Containers: Open Workspace in Container..."
3. select `enumflag.code-workspace` and off you go...

## Contributing

Please see [CONTRIBUTING.md](CONTRIBUTING.md).

## Copyright and License

`lxkns` is Copyright 2020, 2025 Harald Albrecht, and licensed under the Apache
License, Version 2.0.