├── .gitignore ├── enumflag.code-workspace ├── .vscode ├── launch.json └── tasks.json ├── doc.go ├── Makefile ├── .github └── workflows │ └── buildandtest.yaml ├── test └── enumflag-testing │ ├── package_test.go │ ├── main.go │ └── main_test.go ├── go.mod ├── .devcontainer └── devcontainer.json ├── example_slice_test.go ├── package_test.go ├── completion.go ├── example_test.go ├── example_external_test.go ├── example_nodefault_test.go ├── mapper_test.go ├── CONTRIBUTING.md ├── flag_test.go ├── value_scalar.go ├── mapper.go ├── value_slice.go ├── value_test.go ├── completion_test.go ├── flag.go ├── go.sum ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.* 2 | *.log 3 | __debug_bin 4 | -------------------------------------------------------------------------------- /enumflag.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Test", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "test", 9 | "program": "${fileDirname}", 10 | "env": {}, 11 | "args": [] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package enumflag supplements the Golang CLI flag handling packages spf13/cobra 3 | and spf13/pflag with enumeration flags. 4 | 5 | For instance, users can specify enum flags as “--mode=foo” or “--mode=bar”, 6 | where “foo” and “bar” are valid enumeration values. Other values which are not 7 | part of the set of allowed enumeration values cannot be set and raise CLI flag 8 | errors. 9 | 10 | Application programmers then simply deal with enumeration values in form of 11 | uints (or ints), liberated from parsing strings and validating enumeration 12 | flags. 13 | */ 14 | package enumflag 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help clean coverage pkgsite report test vuln chores 2 | 3 | help: ## list available targets 4 | @# Shamelessly stolen from Gomega's Makefile 5 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-16s\033[0m %s\n", $$1, $$2}' 6 | 7 | clean: ## cleans up build and testing artefacts 8 | rm -f coverage.* 9 | 10 | test: ## run unit tests 11 | go test -v -p=1 -race ./... 12 | 13 | report: ## run goreportcard-cli on this module 14 | # from ghcr.io/thediveo/devcontainer-features/goreportcard 15 | goreportcard-cli -v ./.. 16 | 17 | coverage: ## gathers coverage and updates README badge 18 | # from ghcr.io/thediveo/devcontainer-features/gocover 19 | gocover 20 | -------------------------------------------------------------------------------- /.github/workflows/buildandtest.yaml: -------------------------------------------------------------------------------- 1 | name: build and test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | 12 | buildandtest: 13 | name: Build and Test on Go ${{matrix.go}} 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | go: [ 'stable', 'oldstable' ] 18 | steps: 19 | 20 | - name: Set up Go ${{matrix.go}} 21 | uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@v4 22 | with: 23 | go-version: ${{matrix.go}} 24 | id: go 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3 28 | 29 | - name: Test 30 | run: go test -v -p=1 -race ./... 31 | -------------------------------------------------------------------------------- /test/enumflag-testing/package_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "testing" 19 | 20 | . "github.com/onsi/ginkgo/v2" 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | func TestEnumFlag(t *testing.T) { 25 | RegisterFailHandler(Fail) 26 | RunSpecs(t, "test/enumflag-testing") 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thediveo/enumflag/v2 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/onsi/ginkgo/v2 v2.22.2 9 | github.com/onsi/gomega v1.36.2 10 | github.com/sirupsen/logrus v1.9.3 11 | github.com/spf13/cobra v1.8.1 12 | ) 13 | 14 | require ( 15 | github.com/go-logr/logr v1.4.2 // indirect 16 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 17 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 18 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 19 | golang.org/x/tools v0.28.0 // indirect 20 | ) 21 | 22 | require ( 23 | github.com/google/go-cmp v0.6.0 // indirect 24 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 25 | github.com/spf13/pflag v1.0.5 // indirect 26 | github.com/thediveo/success v1.0.2 27 | golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 28 | golang.org/x/net v0.33.0 // indirect 29 | golang.org/x/sys v0.28.0 // indirect 30 | golang.org/x/text v0.21.0 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enumflag", 3 | "portsAttributes": { 4 | "6060": { 5 | "label": "enumflag package documentation", 6 | "onAutoForward": "notify", 7 | "protocol": "http" 8 | } 9 | }, 10 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", 11 | "features": { 12 | "ghcr.io/thediveo/devcontainer-features/local-pkgsite:0": {}, 13 | "ghcr.io/thediveo/devcontainer-features/goreportcard:0": {}, 14 | "ghcr.io/thediveo/devcontainer-features/go-mod-upgrade:0": {}, 15 | "ghcr.io/thediveo/devcontainer-features/gocover:0": { 16 | "num-programs": "1", 17 | "race": true, 18 | "verbose": true, 19 | "html": true 20 | }, 21 | "ghcr.io/thediveo/devcontainer-features/pin-github-action:0": {} 22 | }, 23 | "remoteEnv": { 24 | "GOPATH": "/home/vscode/go", 25 | "PATH": "/home/vscode/go/bin:/go/bin:/usr/local/go/bin:${localEnv:PATH}" 26 | }, 27 | "customizations": { 28 | "vscode": { 29 | "extensions": [ 30 | "stkb.rewrap", 31 | "brunnerh.insert-unicode", 32 | "mhutchie.git-graph", 33 | "ms-vscode.makefile-tools" 34 | ] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /example_slice_test.go: -------------------------------------------------------------------------------- 1 | package enumflag_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/thediveo/enumflag/v2" 8 | ) 9 | 10 | // ① Define your new enum flag type. It can be derived from enumflag.Flag, 11 | // but it doesn't need to be as long as it satisfies constraints.Integer. 12 | type MooMode enumflag.Flag 13 | 14 | // ② Define the enumeration values for FooMode. 15 | const ( 16 | Moo MooMode = (iota + 1) * 111 17 | Møø 18 | Mimimi 19 | ) 20 | 21 | // ③ Map enumeration values to their textual representations (value 22 | // identifiers). 23 | var MooModeIds = map[MooMode][]string{ 24 | Moo: {"moo"}, 25 | Møø: {"møø"}, 26 | Mimimi: {"mimimi"}, 27 | } 28 | 29 | func Example_slice() { 30 | // ④ Define your enum slice flag value. 31 | var moomode []MooMode 32 | rootCmd := &cobra.Command{ 33 | Run: func(cmd *cobra.Command, _ []string) { 34 | fmt.Printf("mode is: %d=%q\n", 35 | moomode, 36 | cmd.PersistentFlags().Lookup("mode").Value.String()) 37 | }, 38 | } 39 | // ⑤ Define the CLI flag parameters for your wrapped enum slice flag. 40 | rootCmd.PersistentFlags().VarP( 41 | enumflag.NewSlice(&moomode, "mode", MooModeIds, enumflag.EnumCaseInsensitive), 42 | "mode", "m", 43 | "can be any combination of 'moo', 'møø', 'mimimi'") 44 | 45 | rootCmd.SetArgs([]string{"--mode", "Moo,møø"}) 46 | _ = rootCmd.Execute() 47 | // Output: mode is: [111 222]="[moo,møø]" 48 | } 49 | -------------------------------------------------------------------------------- /package_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | "testing" 19 | 20 | . "github.com/onsi/ginkgo/v2" 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | // Our new enumeration type. 25 | type FooModeTest Flag 26 | 27 | // Enumeration constants/values. 28 | const ( 29 | fmFoo FooModeTest = iota + 1 30 | fmBar 31 | fmBaz 32 | ) 33 | 34 | // Enumeration identifiers mapped to their corresponding constants. 35 | var FooModeIdentifiersTest = map[FooModeTest][]string{ 36 | fmFoo: {"foo"}, 37 | fmBar: {"bar", "Bar"}, 38 | fmBaz: {"baz"}, 39 | } 40 | 41 | var FooModeHelp = map[FooModeTest]string{ 42 | fmFoo: "foo it", 43 | fmBar: "bar IT!", 44 | fmBaz: "baz nit!!", 45 | } 46 | 47 | func TestEnumFlag(t *testing.T) { 48 | RegisterFailHandler(Fail) 49 | RunSpecs(t, "enumflag") 50 | } 51 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "go build (debug)", 8 | "type": "shell", 9 | "command": "go", 10 | "args": [ 11 | "build", 12 | "-o", 13 | "${fileDirname}/__debug_bin" 14 | ], 15 | "options": { 16 | "cwd": "${fileDirname}", 17 | "env": { 18 | "PATH": "${env:PATH}:/snap/bin" 19 | } 20 | }, 21 | "problemMatcher": [], 22 | "group": { 23 | "kind": "build", 24 | "isDefault": true 25 | } 26 | }, 27 | { 28 | "label": "go test (debug)", 29 | "type": "shell", 30 | "command": "go", 31 | "args": [ 32 | "test", 33 | "-c", 34 | "-o", 35 | "${fileDirname}/__debug_bin" 36 | ], 37 | "options": { 38 | "cwd": "${fileDirname}", 39 | "env": { 40 | "PATH": "${env:PATH}:/snap/bin" 41 | } 42 | }, 43 | "problemMatcher": [], 44 | "group": { 45 | "kind": "build", 46 | "isDefault": true 47 | } 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /completion.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | "golang.org/x/exp/constraints" 20 | ) 21 | 22 | // Help maps enumeration values to their corresponding help descriptions. These 23 | // descriptions should contain just the description but without any "foo\t" enum 24 | // value prefix. The reason is that enumflag will automatically register the 25 | // correct (erm, “complete”) completion text. Please note that it isn't 26 | // necessary to supply any help texts in order to register enum flag completion. 27 | type Help[E constraints.Integer] map[E]string 28 | 29 | // Completor tells cobra how to complete a flag. See also cobra's [dynamic flag 30 | // completion] documentation. 31 | // 32 | // [dynamic flag completion]: https://github.com/spf13/cobra/blob/main/shell_completions.md#specify-dynamic-flag-completion 33 | type Completor func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) 34 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package enumflag_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/thediveo/enumflag/v2" 8 | ) 9 | 10 | // ① Define your new enum flag type. It can be derived from enumflag.Flag, 11 | // but it doesn't need to be as long as it satisfies constraints.Integer. 12 | type FooMode enumflag.Flag 13 | 14 | // ② Define the enumeration values for FooMode. 15 | const ( 16 | Foo FooMode = iota 17 | Bar 18 | ) 19 | 20 | // ③ Map enumeration values to their textual representations (value 21 | // identifiers). 22 | var FooModeIds = map[FooMode][]string{ 23 | Foo: {"foo"}, 24 | Bar: {"bar"}, 25 | } 26 | 27 | func Example() { 28 | // ④ Define your enum flag value. 29 | var foomode FooMode 30 | rootCmd := &cobra.Command{ 31 | Run: func(cmd *cobra.Command, _ []string) { 32 | fmt.Printf("mode is: %d=%q\n", 33 | foomode, 34 | cmd.PersistentFlags().Lookup("mode").Value.String()) 35 | }, 36 | } 37 | // ⑤ Define the CLI flag parameters for your wrapped enum flag. 38 | rootCmd.PersistentFlags().VarP( 39 | enumflag.New(&foomode, "mode", FooModeIds, enumflag.EnumCaseInsensitive), 40 | "mode", "m", 41 | "foos the output; can be 'foo' or 'bar'") 42 | 43 | // cobra's help will render the default enum value identifier... 44 | _ = rootCmd.Help() 45 | 46 | // parse the CLI args to set our enum flag. 47 | rootCmd.SetArgs([]string{"--mode", "bAr"}) 48 | _ = rootCmd.Execute() 49 | 50 | // Output: 51 | // Usage: 52 | // [flags] 53 | // 54 | // Flags: 55 | // -m, --mode mode foos the output; can be 'foo' or 'bar' (default foo) 56 | // mode is: 1="bar" 57 | } 58 | -------------------------------------------------------------------------------- /example_external_test.go: -------------------------------------------------------------------------------- 1 | package enumflag_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | "github.com/thediveo/enumflag/v2" 10 | ) 11 | 12 | func init() { 13 | log.SetOutput(os.Stdout) 14 | } 15 | 16 | func Example_external() { 17 | // ①+② skip "define your own enum flag type" and enumeration values, as we 18 | // already have a 3rd party one. 19 | 20 | // ③ Map 3rd party enumeration values to their textual representations 21 | var LoglevelIds = map[log.Level][]string{ 22 | log.TraceLevel: {"trace"}, 23 | log.DebugLevel: {"debug"}, 24 | log.InfoLevel: {"info"}, 25 | log.WarnLevel: {"warning", "warn"}, 26 | log.ErrorLevel: {"error"}, 27 | log.FatalLevel: {"fatal"}, 28 | log.PanicLevel: {"panic"}, 29 | } 30 | 31 | // ④ Define your enum flag value and set the your logging default value. 32 | var loglevel log.Level = log.WarnLevel 33 | 34 | rootCmd := &cobra.Command{ 35 | Run: func(cmd *cobra.Command, _ []string) { 36 | fmt.Printf("logging level is: %d=%q\n", 37 | loglevel, 38 | cmd.PersistentFlags().Lookup("log").Value.String()) 39 | }, 40 | } 41 | 42 | // ⑤ Define the CLI flag parameters for your wrapped enum flag. 43 | rootCmd.PersistentFlags().Var( 44 | enumflag.New(&loglevel, "log", LoglevelIds, enumflag.EnumCaseInsensitive), 45 | "log", 46 | "sets logging level; can be 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'") 47 | 48 | _ = rootCmd.Execute() 49 | rootCmd.SetArgs([]string{"--log", "debug"}) 50 | _ = rootCmd.Execute() 51 | // Output: 52 | // logging level is: 3="warning" 53 | // logging level is: 5="debug" 54 | } 55 | -------------------------------------------------------------------------------- /example_nodefault_test.go: -------------------------------------------------------------------------------- 1 | package enumflag_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/thediveo/enumflag/v2" 8 | ) 9 | 10 | // ① Define your new enum flag type. It can be derived from enumflag.Flag, 11 | // but it doesn't need to be as long as it satisfies constraints.Integer. 12 | type BarMode enumflag.Flag 13 | 14 | // ② Define the enumeration values for BarMode. 15 | const ( 16 | NoDefault = iota // optional definition for "no default" zero value 17 | Barr BarMode = iota 18 | Barz 19 | ) 20 | 21 | // ③ Map enumeration values to their textual representations (value 22 | // identifiers). 23 | var BarModeIds = map[BarMode][]string{ 24 | // ...do NOT include/map the "no default" zero value! 25 | Barr: {"barr"}, 26 | Barz: {"barz"}, 27 | } 28 | 29 | func Example_no_default_value() { 30 | // ④ Define your enum flag value. 31 | var barmode BarMode 32 | rootCmd := &cobra.Command{ 33 | Run: func(cmd *cobra.Command, _ []string) { 34 | fmt.Printf("mode is: %d=%q\n", 35 | barmode, 36 | cmd.PersistentFlags().Lookup("mode").Value.String()) 37 | }, 38 | } 39 | // ⑤ Define the CLI flag parameters for your wrapped enum flag. 40 | rootCmd.PersistentFlags().VarP( 41 | enumflag.NewWithoutDefault(&barmode, "mode", BarModeIds, enumflag.EnumCaseInsensitive), 42 | "mode", "m", 43 | "bars the output; can be 'barr' or 'barz'") 44 | 45 | // now cobra's help won't render the default enum value identifier anymore... 46 | _ = rootCmd.Help() 47 | 48 | _ = rootCmd.Execute() 49 | 50 | // Output: 51 | // Usage: 52 | // [flags] 53 | // 54 | // Flags: 55 | // -m, --mode mode bars the output; can be 'barr' or 'barz' 56 | // mode is: 0="" 57 | } 58 | -------------------------------------------------------------------------------- /mapper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | . "github.com/onsi/ginkgo/v2" 19 | . "github.com/onsi/gomega" 20 | ) 21 | 22 | var _ = Describe("enum name-value mapper", func() { 23 | 24 | DescribeTable("looks up name for value", 25 | func(value FooModeTest, expectedNames []string) { 26 | mapper := newEnumMapper(FooModeIdentifiersTest, EnumCaseSensitive) 27 | Expect(mapper.Lookup(value)).To(Equal(expectedNames)) 28 | }, 29 | Entry("fmBar", fmBar, FooModeIdentifiersTest[fmBar]), 30 | Entry("fool", FooModeTest(0), nil), 31 | ) 32 | 33 | DescribeTable("looks up value for name", 34 | func(name string, sensitivity EnumCaseSensitivity, expectedValue FooModeTest) { 35 | mapper := newEnumMapper(FooModeIdentifiersTest, sensitivity) 36 | Expect(mapper.ValueOf(name)).To(Equal(expectedValue)) 37 | }, 38 | Entry("baz", "baz", EnumCaseSensitive, fmBaz), 39 | Entry("Baz/i", "Baz", EnumCaseInsensitive, fmBaz), 40 | Entry("bar", "bar", EnumCaseSensitive, fmBar), 41 | Entry("Bar", "Bar", EnumCaseSensitive, fmBar), 42 | ) 43 | 44 | DescribeTable("returns helpful error when lookup fails", 45 | func(name string, sensitivity EnumCaseSensitivity) { 46 | mapper := newEnumMapper(FooModeIdentifiersTest, sensitivity) 47 | Expect(mapper.ValueOf(name)).Error().To(MatchError("must be 'bar'/'Bar', 'baz', 'foo'")) 48 | }, 49 | Entry("fool", "fool", EnumCaseSensitive), 50 | Entry("fool/i", "fool", EnumCaseInsensitive), 51 | Entry("BAr", "BAr", EnumCaseSensitive), 52 | ) 53 | 54 | }) 55 | -------------------------------------------------------------------------------- /test/enumflag-testing/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "io" 19 | "os" 20 | 21 | "github.com/spf13/cobra" 22 | "github.com/thediveo/enumflag/v2" 23 | ) 24 | 25 | const Name = "enumflag-testing" 26 | 27 | type FooMode enumflag.Flag 28 | 29 | var fooMode FooMode 30 | 31 | const ( 32 | Foo FooMode = iota 33 | Bar 34 | Baz 35 | ) 36 | 37 | var FooModeNames = map[FooMode][]string{ 38 | Foo: {"foo"}, 39 | Bar: {"bar"}, 40 | Baz: {"baz"}, 41 | } 42 | 43 | func newRootCmd(wout, werr io.Writer) *cobra.Command { 44 | rootCmd := &cobra.Command{ 45 | Use: Name, 46 | Run: func(*cobra.Command, []string) {}, 47 | } 48 | // https://github.com/spf13/cobra/issues/2214#issuecomment-2571424842 49 | rootCmd.SetOut(wout) 50 | rootCmd.SetErr(werr) 51 | 52 | testCmd := &cobra.Command{ 53 | Use: "test the canary", 54 | Long: "test the canary", 55 | Args: cobra.NoArgs, 56 | Run: func(*cobra.Command, []string) {}, 57 | } 58 | 59 | ef := enumflag.New(&fooMode, "FooMode", FooModeNames, enumflag.EnumCaseInsensitive) 60 | testCmd.PersistentFlags().Var(ef, "mode", "sets foo mode") 61 | ef.RegisterCompletion(testCmd, "mode", enumflag.Help[FooMode]{ 62 | Foo: "foos the output", 63 | Bar: "bars the output", 64 | Baz: "bazs the output", 65 | }) 66 | 67 | rootCmd.AddCommand(testCmd) 68 | return rootCmd 69 | } 70 | 71 | func main() { 72 | // Cobra automatically adds a "__complete" command to our root command 73 | // behind the scenes, unless we specify one explicitly. It also adds a 74 | // "complete" sub command if we're adding at least one sub command. 75 | if err := newRootCmd(stdout, stderr).Execute(); err != nil { 76 | osExit(1) 77 | } 78 | } 79 | 80 | // To 100% and beyond!!! 81 | var osExit = os.Exit 82 | var stdout io.Writer = os.Stdout 83 | var stderr io.Writer = os.Stderr 84 | -------------------------------------------------------------------------------- /test/enumflag-testing/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "io" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | . "github.com/onsi/ginkgo/v2" 25 | . "github.com/onsi/gomega" 26 | ) 27 | 28 | var _ = Describe("enumflag-testing canary", func() { 29 | 30 | var rootCmd *cobra.Command 31 | var outbuff, errbuff *bytes.Buffer 32 | 33 | BeforeEach(func() { 34 | outbuff = &bytes.Buffer{} 35 | errbuff = &bytes.Buffer{} 36 | rootCmd = newRootCmd(outbuff, errbuff) 37 | }) 38 | 39 | It("has a hidden __complete command", func() { 40 | rootCmd.SetArgs([]string{"__complete", "t"}) 41 | Expect(rootCmd.Execute()).To(Succeed()) 42 | Expect(outbuff.String()).To(MatchRegexp(`test\n:\d+\n`)) 43 | Expect(errbuff.String()).To(MatchRegexp(`Completion ended with directive: .+`)) 44 | }) 45 | 46 | It("lists the completion command", func() { 47 | rootCmd.SetArgs([]string{"-h"}) 48 | Expect(rootCmd.Execute()).To(Succeed()) 49 | Expect(outbuff.String()).To(MatchRegexp(`Available Commands:\n\s+completion\s+ Generate .* shell`)) 50 | Expect(errbuff.String()).To(BeEmpty()) 51 | }) 52 | 53 | It("generates a shell completion script", func() { 54 | rootCmd.SetArgs([]string{"completion", "bash"}) 55 | Expect(rootCmd.Execute()).To(Succeed()) 56 | Expect(outbuff.String()).To(MatchRegexp(`^# bash completion V2 for`)) 57 | Expect(errbuff.String()).To(BeEmpty()) 58 | }) 59 | 60 | It("reaches 100% :p", func() { 61 | exitCode := -1 62 | defer func(old func(int), oldargs []string, wout, werr io.Writer) { 63 | osExit = old 64 | os.Args = oldargs 65 | stdout = wout 66 | stderr = werr 67 | }(osExit, os.Args, os.Stdout, os.Stderr) 68 | osExit = func(code int) { exitCode = code } 69 | os.Args = []string{os.Args[0], "froobz"} 70 | stdout = outbuff 71 | stderr = errbuff 72 | main() 73 | Expect(exitCode).To(Equal(1)) 74 | Expect(outbuff.String()).To(BeEmpty()) 75 | Expect(errbuff.String()).To(MatchRegexp(`Error: unknown command .+\n.+ for usage.\n`)) 76 | }) 77 | 78 | }) 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions in several forms, for instance: 4 | 5 | * Documenting 6 | * Testing / Bug reports 7 | * Coding 8 | * etc. 9 | 10 | Please read [14 Ways to Contribute to Open Source without Being a Programming 11 | Genius or a Rock 12 | Star](https://smartbear.com/blog/test-and-monitor/14-ways-to-contribute-to-open-source-without-being/). 13 | 14 | ## Developer Certificate of Origin 15 | 16 | All commits must be signed-off by their author when contributing. 17 | 18 | When signing-off a patch for this project like this 19 | 20 | Signed-off-by: Random J Developer 21 | 22 | using your real name (no pseudonyms or anonymous contributions), 23 | you declare the following: 24 | 25 | By making a contribution to this project, I certify that: 26 | 27 | (a) The contribution was created in whole or in part by me and 28 | I have the right to submit it under the open source license 29 | indicated in the file; or 30 | 31 | (b) The contribution is based upon previous work that, to the best 32 | of my knowledge, is covered under an appropriate open source 33 | license and I have the right under that license to submit that 34 | work with modifications, whether created in whole or in part 35 | by me, under the same open source license (unless I am 36 | permitted to submit under a different license), as indicated 37 | in the file; or 38 | 39 | (c) The contribution was provided directly to me by some other 40 | person who certified (a), (b) or (c) and I have not modified it. 41 | 42 | (d) I understand and agree that this project and the contribution 43 | are public and that a record of the contribution (including all 44 | personal information I submit with it, including my sign-off) is 45 | maintained indefinitely and may be redistributed consistent with 46 | this project or the open source license(s) involved. 47 | 48 | ## Workflow 49 | 50 | We appreciate any contributions, so please use the [Forking 51 | Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow) 52 | and send us `Merge Requests`. 53 | 54 | ### Commit Message 55 | 56 | Commit messages shall follow the conventions defined by [conventional 57 | commits](https://www.conventionalcommits.org/en/v1.0.0/). 58 | 59 | > **HINT**: A good way to create commit messages is by using the tool `git gui`. 60 | 61 | ### What to use as scope 62 | 63 | In most cases the changed component is a good choice as scope 64 | e.g. if the change is done in the documentation, the scope should be *doc*. 65 | 66 | For documentation changes the section that was changed makes a good scope name 67 | e.g. use *FAQ* if you changed that section. -------------------------------------------------------------------------------- /flag_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | . "github.com/onsi/ginkgo/v2" 19 | . "github.com/onsi/gomega" 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | var _ = Describe("flag", func() { 24 | 25 | Context("scalar enum flag", func() { 26 | 27 | It("returns the canonical textual representation", func() { 28 | foomode := fmBar 29 | val := New(&foomode, "mode", FooModeIdentifiersTest, EnumCaseInsensitive) 30 | Expect(val.String()).To(Equal("bar")) 31 | Expect(val.Type()).To(Equal("mode")) 32 | }) 33 | 34 | It("rejects setting invalid values", func() { 35 | var foomode FooModeTest 36 | val := New(&foomode, "mode", FooModeIdentifiersTest, EnumCaseSensitive) 37 | Expect(val.Set("FOOBAR")).To(MatchError("must be 'bar'/'Bar', 'baz', 'foo'")) 38 | }) 39 | 40 | It("sets the enumeration value from text", func() { 41 | var foomode FooModeTest 42 | val := New(&foomode, "mode", FooModeIdentifiersTest, EnumCaseSensitive) 43 | 44 | Expect(val.Set("foo")).NotTo(HaveOccurred()) 45 | Expect(val.Set("Bar")).NotTo(HaveOccurred()) 46 | Expect(foomode).To(Equal(fmBar)) 47 | Expect(val.Get()).To(Equal(fmBar)) 48 | }) 49 | 50 | }) 51 | 52 | Context("slice enum flag", func() { 53 | 54 | It("returns the canonical textual representation", func() { 55 | foomodes := []FooModeTest{fmBar, fmFoo} 56 | val := NewSlice(&foomodes, "modes", FooModeIdentifiersTest, EnumCaseInsensitive) 57 | Expect(val.String()).To(Equal("[bar,foo]")) 58 | Expect(val.Type()).To(Equal("modes")) 59 | }) 60 | 61 | }) 62 | 63 | When("passing nil", func() { 64 | 65 | It("panics", func() { 66 | Expect(func() { 67 | _ = New[FooModeTest](nil, "foo", nil, EnumCaseInsensitive) 68 | }).To(PanicWith(MatchRegexp("New requires flag to be a non-nil pointer to .*"))) 69 | Expect(func() { 70 | var f FooModeTest 71 | _ = New(&f, "foo", nil, EnumCaseInsensitive) 72 | }).To(PanicWith(MatchRegexp("New requires mapping not to be nil"))) 73 | 74 | Expect(func() { 75 | _ = NewSlice[FooModeTest](nil, "foo", nil, EnumCaseInsensitive) 76 | }).To(PanicWith(MatchRegexp("NewSlice requires flag to be a non-nil pointer to .*"))) 77 | Expect(func() { 78 | var f []FooModeTest 79 | _ = NewSlice(&f, "foo", nil, EnumCaseInsensitive) 80 | }).To(PanicWith(MatchRegexp("NewSlice requires mapping not to be nil"))) 81 | }) 82 | 83 | }) 84 | 85 | It("returns completors", func() { 86 | cmd := &cobra.Command{} 87 | foomodes := []FooModeTest{fmBar, fmFoo} 88 | val := NewSlice(&foomodes, "modes", FooModeIdentifiersTest, EnumCaseInsensitive) 89 | cmd.PersistentFlags().Var(val, "mode", "blahblah") 90 | Expect(val.RegisterCompletion(cmd, "mode", Help[FooModeTest]{ 91 | fmFoo: "gives a foo", 92 | fmBar: "gives a bar", 93 | fmBaz: "gives a baz", 94 | })).To(Succeed()) 95 | }) 96 | 97 | }) 98 | -------------------------------------------------------------------------------- /value_scalar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | "golang.org/x/exp/constraints" 20 | ) 21 | 22 | // unknown is the textual representation of an unknown enum value, that is, when 23 | // the enum value to name mapping doesn't have any idea about a particular enum 24 | // value. 25 | const unknown = "" 26 | 27 | // enumScalar represents a mutable, single enumeration value that can be 28 | // retrieved, set, and stringified. 29 | type enumScalar[E constraints.Integer] struct { 30 | v *E 31 | nodefault bool // opts in to accepting a zero enum value as the "none" 32 | } 33 | 34 | // Get returns the scalar enum value. 35 | func (s *enumScalar[E]) Get() any { return *s.v } 36 | 37 | // Set the value to the new scalar enum value corresponding to the passed 38 | // textual representation, using the additionally specified text-to-value 39 | // mapping. If the specified textual representation doesn't match any of the 40 | // defined ones, an error is returned instead and the value isn't changed. 41 | func (s *enumScalar[E]) Set(val string, names enumMapper[E]) error { 42 | enumcode, err := names.ValueOf(val) 43 | if err != nil { 44 | return err 45 | } 46 | *s.v = enumcode 47 | return nil 48 | } 49 | 50 | // String returns the textual representation of the scalar enum value, using the 51 | // specified text-to-value mapping. 52 | // 53 | // String will return "" for undefined/unmapped enum values. If the 54 | // enum flag has been created using [NewWithoutDefault], then an empty string is 55 | // returned instead: in this case [spf13/cobra] will not show any default for 56 | // the corresponding CLI flag. 57 | // 58 | // [spf13/cobra]: https://github.com/spf13/cobra 59 | func (s *enumScalar[E]) String(names enumMapper[E]) string { 60 | if ids := names.Lookup(*s.v); len(ids) > 0 { 61 | return ids[0] 62 | } 63 | if *s.v == 0 && s.nodefault { 64 | return "" 65 | } 66 | return unknown 67 | } 68 | 69 | // NewCompletor returns a cobra Completor that completes enum flag values. 70 | // Please note that shell completion hasn't the notion of case sensitivity or 71 | // insensitivity, so we cannot take this into account but instead return all 72 | // available enum value names in their original form. 73 | func (s *enumScalar[E]) NewCompletor(enums EnumIdentifiers[E], help Help[E]) Completor { 74 | completions := []string{} 75 | for enumval, enumnames := range enums { 76 | helptext := "" 77 | if text, ok := help[enumval]; ok { 78 | helptext = "\t" + text 79 | } 80 | // complete not only the canonical enum value name, but also all other 81 | // (alias) names. 82 | for _, name := range enumnames { 83 | completions = append(completions, name+helptext) 84 | } 85 | } 86 | return func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { 87 | return completions, cobra.ShellCompDirectiveNoFileComp 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /mapper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | "fmt" 19 | "sort" 20 | "strings" 21 | 22 | "golang.org/x/exp/constraints" 23 | "golang.org/x/exp/slices" 24 | ) 25 | 26 | // EnumIdentifiers maps enumeration values to their corresponding textual 27 | // representations (~identifiers). This mapping is a one-to-many mapping in that 28 | // the same enumeration value may have more than only one associated textual 29 | // representation (identifier). If more than one textual representation exists 30 | // for the same enumeration value, then the first textual representation is 31 | // considered to be the canonical one. 32 | type EnumIdentifiers[E constraints.Integer] map[E][]string 33 | 34 | // enumMapper is an optionally case insensitive map from enum values to their 35 | // corresponding textual representations. 36 | type enumMapper[E constraints.Integer] struct { 37 | m EnumIdentifiers[E] 38 | sensitivity EnumCaseSensitivity 39 | } 40 | 41 | // newEnumMapper returns a new enumMapper for the given mapping and case 42 | // sensitivity or insensitivity. 43 | func newEnumMapper[E constraints.Integer](mapping EnumIdentifiers[E], sensitivity EnumCaseSensitivity) enumMapper[E] { 44 | return enumMapper[E]{ 45 | m: mapping, 46 | sensitivity: sensitivity, 47 | } 48 | } 49 | 50 | // Lookup returns the enum textual representations (identifiers) for the 51 | // specified enum value, if any; otherwise, returns a zero string slice. 52 | func (m enumMapper[E]) Lookup(enum E) (names []string) { 53 | return m.m[enum] 54 | } 55 | 56 | // ValueOf returns the enumeration value corresponding with the specified 57 | // textual representation (identifier), or an error if no match is found. 58 | func (m enumMapper[E]) ValueOf(name string) (E, error) { 59 | comparefn := func(s string) bool { return s == name } 60 | if m.sensitivity == EnumCaseInsensitive { 61 | name = strings.ToLower(name) 62 | comparefn = func(s string) bool { return strings.ToLower(s) == name } 63 | } 64 | // Try to find a matching enum value textual representation, and then take 65 | // its enumeration value ("code"). 66 | for enumval, ids := range m.m { 67 | if slices.IndexFunc(ids, comparefn) >= 0 { 68 | return enumval, nil 69 | } 70 | } 71 | // Oh no! An invalid textual enum value was specified, so let's generate 72 | // some useful error explaining which textual representations are valid. 73 | // We're ordering values by their canonical names in order to achieve a 74 | // stable error message. 75 | allids := []string{} 76 | for _, ids := range m.m { 77 | s := []string{} 78 | for _, id := range ids { 79 | s = append(s, "'"+id+"'") 80 | } 81 | allids = append(allids, strings.Join(s, "/")) 82 | } 83 | sort.Strings(allids) 84 | return 0, fmt.Errorf("must be %s", strings.Join(allids, ", ")) 85 | } 86 | 87 | // Mapping returns the mapping of enum values to their names. 88 | func (m enumMapper[E]) Mapping() EnumIdentifiers[E] { 89 | return m.m 90 | } 91 | -------------------------------------------------------------------------------- /value_slice.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/spf13/cobra" 21 | "golang.org/x/exp/constraints" 22 | "golang.org/x/exp/slices" 23 | ) 24 | 25 | // enumSlice represents a slice of enumeration values that can be retrieved, 26 | // set, and stringified. 27 | type enumSlice[E constraints.Integer] struct { 28 | v *[]E 29 | merge bool // replace the complete slice or merge values? 30 | } 31 | 32 | // Get returns the slice enum values. 33 | func (s *enumSlice[E]) Get() any { return *s.v } 34 | 35 | // Set or merge one or more values of the new scalar enum value corresponding to 36 | // the passed textual representation, using the additionally specified 37 | // text-to-value mapping. If the specified textual representation doesn't match 38 | // any of the defined ones, an error is returned instead and the value isn't 39 | // changed. The first call to Set will always clear any previous default value. 40 | // All subsequent calls to Set will merge the specified enum values with the 41 | // current enum values. 42 | func (s *enumSlice[E]) Set(val string, names enumMapper[E]) error { 43 | // First parse and convert the textual enum values into their 44 | // program-internal codes. 45 | ids := strings.Split(val, ",") 46 | enumvals := make([]E, 0, len(ids)) // ...educated guess 47 | for _, id := range ids { 48 | enumval, err := names.ValueOf(id) 49 | if err != nil { 50 | return err 51 | } 52 | enumvals = append(enumvals, enumval) 53 | } 54 | if !s.merge { 55 | // Replace any existing default enum value set on first Set(). 56 | *s.v = enumvals 57 | s.merge = true // ...and next time: merge. 58 | return nil 59 | } 60 | // Later, merge with the existing enum values. 61 | for _, enumval := range enumvals { 62 | if slices.Index(*s.v, enumval) >= 0 { 63 | continue 64 | } 65 | *s.v = append(*s.v, enumval) 66 | } 67 | return nil 68 | } 69 | 70 | // String returns the textual representation of the slice enum value, using the 71 | // specified text-to-value mapping. 72 | func (s *enumSlice[E]) String(names enumMapper[E]) string { 73 | n := make([]string, 0, len(*s.v)) 74 | for _, enumval := range *s.v { 75 | if enumnames := names.Lookup(enumval); len(enumnames) > 0 { 76 | n = append(n, enumnames[0]) 77 | continue 78 | } 79 | n = append(n, unknown) 80 | } 81 | return "[" + strings.Join(n, ",") + "]" 82 | } 83 | 84 | // NewCompletor returns a cobra Completor that completes enum flag values. 85 | func (s *enumSlice[E]) NewCompletor(enums EnumIdentifiers[E], help Help[E]) Completor { 86 | completions := []string{} 87 | for enumval, enumnames := range enums { 88 | helptext := "" 89 | if text, ok := help[enumval]; ok { 90 | helptext = "\t" + text 91 | } 92 | // complete not only the canonical enum value name, but also all other 93 | // (alias) names. 94 | for _, name := range enumnames { 95 | completions = append(completions, name+helptext) 96 | } 97 | } 98 | return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 99 | prefix := "" 100 | completes := []string{} 101 | if lastComma := strings.LastIndex(toComplete, ","); lastComma >= 0 { 102 | prefix = toComplete[:lastComma+1] // ...Prof J. won't ever like this variable name 103 | completes = strings.Split(prefix, ",") 104 | completes = completes[:len(completes)-1] // remove last empty element 105 | } 106 | filteredCompletions := make([]string, 0, len(completions)) 107 | for _, completion := range completions { 108 | if slices.Contains(completes, strings.Split(completion, "\t")[0]) { 109 | continue 110 | } 111 | filteredCompletions = append(filteredCompletions, prefix+completion) 112 | } 113 | return filteredCompletions, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | . "github.com/onsi/ginkgo/v2" 19 | . "github.com/onsi/gomega" 20 | ) 21 | 22 | var _ = Describe("enum values", func() { 23 | 24 | Context("scalars", func() { 25 | 26 | It("retrieves the current enum value", func() { 27 | f := fmFoo 28 | es := enumScalar[FooModeTest]{v: &f} 29 | Expect(es.Get()).To(Equal(fmFoo)) 30 | }) 31 | 32 | DescribeTable("stringifies", 33 | func(e FooModeTest, expected string) { 34 | es := enumScalar[FooModeTest]{v: &e} 35 | m := newEnumMapper(FooModeIdentifiersTest, EnumCaseInsensitive) 36 | Expect(es.String(m)).To(Equal(expected)) 37 | }, 38 | Entry("fmBar", fmBar, "bar"), // sic! returns canonical name, which is "bar" 39 | Entry("unknown", FooModeTest(0), ""), 40 | ) 41 | 42 | It("sets a new enum value", func() { 43 | f := fmFoo 44 | es := enumScalar[FooModeTest]{v: &f} 45 | m := newEnumMapper(FooModeIdentifiersTest, EnumCaseInsensitive) 46 | Expect(es.Set("Bar", m)).To(Succeed()) 47 | Expect(es.Get()).To(Equal(fmBar)) 48 | }) 49 | 50 | It("rejects setting an unknown textual representation", func() { 51 | f := fmFoo 52 | es := enumScalar[FooModeTest]{v: &f} 53 | m := newEnumMapper(FooModeIdentifiersTest, EnumCaseInsensitive) 54 | Expect(es.Set("Barumph", m)).NotTo(Succeed()) 55 | }) 56 | 57 | DescribeTable("completion", 58 | func(toc string, expected []string) { 59 | c := (&enumScalar[FooModeTest]{}).NewCompletor(FooModeIdentifiersTest, nil) 60 | actual, _ := c(nil, nil, toc) 61 | Expect(actual).To(ConsistOf(expected)) 62 | }, 63 | Entry("returns all values", "", []string{"foo", "bar", "Bar", "baz"}), 64 | Entry("always returns all values without filtering", "b", []string{"foo", "bar", "Bar", "baz"}), 65 | ) 66 | 67 | It("completes with help", func() { 68 | c := (&enumScalar[FooModeTest]{}).NewCompletor(FooModeIdentifiersTest, FooModeHelp) 69 | actual, _ := c(nil, nil, "") 70 | Expect(actual).To(ConsistOf([]string{ 71 | "foo\tfoo it", 72 | "bar\tbar IT!", 73 | "Bar\tbar IT!", 74 | "baz\tbaz nit!!", 75 | })) 76 | }) 77 | 78 | }) 79 | 80 | Context("slices", func() { 81 | 82 | m := newEnumMapper(FooModeIdentifiersTest, EnumCaseInsensitive) 83 | var es enumSlice[FooModeTest] 84 | 85 | BeforeEach(func() { 86 | sf := []FooModeTest{fmFoo, fmBar, 0} 87 | es = enumSlice[FooModeTest]{v: &sf} 88 | }) 89 | 90 | It("retrieves the current enum value", func() { 91 | Expect(es.Get()).To(ConsistOf(fmFoo, fmBar, FooModeTest(0))) 92 | }) 93 | 94 | It("stringifies", func() { 95 | Expect(es.String(m)).To(Equal("[foo,bar,]")) 96 | sf := []FooModeTest{} 97 | es := enumSlice[FooModeTest]{v: &sf} 98 | Expect(es.String(m)).To(Equal("[]")) 99 | }) 100 | 101 | It("sets a new enum value", func() { 102 | Expect(es.Set("baz", m)).To(Succeed()) 103 | Expect(es.Get()).To(ConsistOf(fmBaz)) 104 | Expect(es.Set("foo", m)).To(Succeed()) 105 | Expect(es.Get()).To(ConsistOf(fmBaz, fmFoo)) 106 | Expect(es.Set("Baz", m)).To(Succeed()) 107 | Expect(es.Get()).To(ConsistOf(fmBaz, fmFoo)) 108 | }) 109 | 110 | DescribeTable("rejects setting an unknown textual representation", 111 | func(value string) { 112 | Expect(es.Set(value, m)).NotTo(Succeed()) 113 | }, 114 | Entry(nil, "bajazzo"), 115 | Entry(nil, "foo,bajazzo"), 116 | Entry(`""`, ""), 117 | ) 118 | 119 | DescribeTable("completion", 120 | func(toc string, expected []string) { 121 | c := (&enumSlice[FooModeTest]{}).NewCompletor(FooModeIdentifiersTest, nil) 122 | actual, _ := c(nil, nil, toc) 123 | Expect(actual).To(ConsistOf(expected)) 124 | }, 125 | Entry("returns all values", "", []string{"foo", "bar", "Bar", "baz"}), 126 | Entry("always returns all values without filtering", "b", 127 | []string{"foo", "bar", "Bar", "baz"}), 128 | Entry("returns all remaining values", "f", 129 | []string{"foo", "bar", "Bar", "baz"}), 130 | Entry(nil, "foo,", []string{"foo,bar", "foo,Bar", "foo,baz"}), 131 | Entry(nil, "foo,bar,", []string{"foo,bar,Bar", "foo,bar,baz"}), 132 | Entry(nil, "bar,baz,f", []string{"bar,baz,foo", "bar,baz,Bar"}), 133 | Entry("ignores non-existing elements", "foo,koo,", 134 | []string{"foo,koo,bar", "foo,koo,Bar", "foo,koo,baz"}), 135 | ) 136 | 137 | }) 138 | 139 | }) 140 | -------------------------------------------------------------------------------- /completion_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | "io" 19 | "os" 20 | "os/exec" 21 | "path/filepath" 22 | "syscall" 23 | "time" 24 | 25 | "github.com/onsi/gomega/gbytes" 26 | "github.com/onsi/gomega/gexec" 27 | "golang.org/x/exp/slices" 28 | 29 | . "github.com/onsi/ginkgo/v2" 30 | . "github.com/onsi/gomega" 31 | . "github.com/thediveo/success" 32 | ) 33 | 34 | const dummyCommandName = "enumflag-testing" 35 | 36 | // See: 37 | // https://serverfault.com/questions/506612/standard-place-for-user-defined-bash-completion-d-scripts/1013395#1013395 38 | const bashComplDirEnv = "BASH_COMPLETION_USER_DIR" 39 | 40 | type writer struct { 41 | io.WriteCloser 42 | } 43 | 44 | func (w *writer) WriteString(s string) { 45 | GinkgoHelper() 46 | Expect(w.WriteCloser.Write([]byte(s))).Error().NotTo(HaveOccurred()) 47 | } 48 | 49 | var _ = Describe("flag enum completions end-to-end", Ordered, func() { 50 | 51 | var enumflagTestingPath string 52 | var completionsUserDir string 53 | 54 | BeforeAll(func() { 55 | By("building a CLI binary for testing") 56 | enumflagTestingPath = Successful(gexec.Build("./test/enumflag-testing")) 57 | DeferCleanup(func() { 58 | gexec.CleanupBuildArtifacts() 59 | }) 60 | 61 | By("creating a temporary directory for storing completion scripts") 62 | completionsUserDir = Successful(os.MkdirTemp("", "bash-completions-*")) 63 | DeferCleanup(func() { 64 | os.RemoveAll(completionsUserDir) 65 | }) 66 | // Notice how the bash-completion FAQ 67 | // https://github.com/scop/bash-completion/blob/master/README.md#faq 68 | // says that the completions must be inside a "completions" sub 69 | // directory of $BASH_COMPLETION_USER_DIR, and not inside 70 | // $BASH_COMPLETION_USER_DIR itself ... yeah, 🤷 71 | Expect(os.Mkdir(filepath.Join(completionsUserDir, "completions"), 0770)).To(Succeed()) 72 | 73 | By("telling the CLI binary to give us a completion script that we then store away") 74 | session := Successful( 75 | gexec.Start(exec.Command(enumflagTestingPath, "completion", "bash"), 76 | GinkgoWriter, GinkgoWriter)) 77 | Eventually(session).Within(5 * time.Second).ProbeEvery(100 * time.Millisecond). 78 | Should(gexec.Exit(0)) 79 | completionScript := session.Out.Contents() 80 | Expect(completionScript).To(MatchRegexp(`^# bash completion V2 for`)) 81 | Expect(os.WriteFile(filepath.Join(completionsUserDir, "completions", "enumflag-testing"), 82 | completionScript, 0770)). 83 | To(Succeed()) 84 | }) 85 | 86 | Bash := func() (*gexec.Session, *writer) { 87 | GinkgoHelper() 88 | By("creating a new test bash session") 89 | bashCmd := exec.Command("/bin/bash", "--rcfile", "/etc/profile", "-i") 90 | // Run the silly interactive subshell in its own session so we don't get 91 | // funny surprises such as the subshell getting suspended by its parent 92 | // shell... 93 | bashCmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} 94 | bashCmd.Env = append(slices.Clone(os.Environ()), 95 | bashComplDirEnv+"="+completionsUserDir, 96 | "PATH="+filepath.Dir(enumflagTestingPath)+":"+os.Getenv("PATH"), 97 | ) 98 | stdin := &writer{Successful(bashCmd.StdinPipe())} 99 | session := Successful( 100 | gexec.Start(bashCmd, GinkgoWriter, GinkgoWriter)) 101 | DeferCleanup(func() { 102 | By("killing the test bash session") 103 | stdin.Close() 104 | session.Kill().Wait(2 * time.Second) 105 | }) 106 | return session, stdin 107 | } 108 | 109 | var bash *gexec.Session 110 | var bashin *writer 111 | 112 | BeforeEach(func() { 113 | bash, bashin = Bash() 114 | // Gomega hack: when trying to run the interactive subshell in its own 115 | // session, creating a new session might be ignored and we get an error 116 | // message on stderr from the subshell. So we need to ignore this 117 | // unwanted "heckling", but Gomega's gbytes does not directly support 118 | // ignoring output. So we resort to (asynchronous) detection of bash's 119 | // heckling and then simply ignore it. This will fast forward over the 120 | // heckling, so the tests then can still correctly proceed. 121 | ch := bash.Err.Detect(`(bash: .*\n)+`) 122 | // Make sure to drain the unbuffered detection channel. 123 | done := make(chan struct{}) 124 | defer close(done) 125 | go func() { 126 | for { 127 | select { 128 | case <-ch: 129 | // nothing; just drain 130 | case <-done: 131 | break 132 | } 133 | } 134 | }() 135 | DeferCleanup(func() { 136 | // Note: Gomega currently crashes in CancelDetects() if no 137 | // Detect()'s have been done before. 138 | bash.Err.CancelDetects() 139 | }) 140 | }) 141 | 142 | It("tab-completes the canary's name in $PATH", func() { 143 | By("checking BASH_COMPLETION_USER_DIR") 144 | bashin.WriteString("echo $" + bashComplDirEnv + "\n") 145 | Eventually(bash.Out).Should(gbytes.Say(completionsUserDir)) 146 | 147 | By("listing the canary in the first search PATH directory") 148 | bashin.WriteString("ls -l ${PATH%%:*}\n") 149 | Eventually(bash.Out).Should(gbytes.Say(dummyCommandName)) 150 | 151 | By("ensuring the canary is in the PATH and gets completed") 152 | bashin.WriteString(dummyCommandName[:len(dummyCommandName)-4] + "\t") 153 | Eventually(bash.Err).Should(gbytes.Say(dummyCommandName)) 154 | }) 155 | 156 | It("completes canary's test subcommand", func() { 157 | bashin.WriteString(dummyCommandName + " t\t") 158 | Eventually(bash.Err).Should(gbytes.Say(dummyCommandName + " test")) 159 | }) 160 | 161 | It("completes canary's \"mode\" enum flag name", func() { 162 | bashin.WriteString(dummyCommandName + " test --\t\t") 163 | Eventually(bash.Err).Should(gbytes.Say( 164 | `--help\s+\(help for test\)\s+--mode\s+\(sets foo mode\)`)) 165 | }) 166 | 167 | It("lists enum flag's values", func() { 168 | bashin.WriteString(dummyCommandName + " test --mode \t\t") 169 | Eventually(bash.Err).Should(gbytes.Say( 170 | `bar\s+\(bars the output\)\s+baz\s+\(bazs the output\)\s+foo\s+\(foos the output\)`)) 171 | bashin.WriteString("\b=\t\t") 172 | Eventually(bash.Err).Should(gbytes.Say( 173 | `bar\s+\(bars the output\)\s+baz\s+\(bazs the output\)\s+foo\s+\(foos the output\)`)) 174 | }) 175 | 176 | It("completes enum flag's values", func() { 177 | bashin.WriteString(dummyCommandName + " test --mode ba\t\t") 178 | Eventually(bash.Err).Should(gbytes.Say( 179 | `bar\s+\(bars the output\)\s+baz\s+\(bazs the output\)`)) 180 | bashin.WriteString("\b\bf\t") 181 | Eventually(bash.Err).Should(gbytes.Say( 182 | `oo `)) 183 | }) 184 | 185 | }) 186 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020, 2022 Harald Albrecht. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package enumflag 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/spf13/cobra" 21 | "golang.org/x/exp/constraints" 22 | ) 23 | 24 | // Flag represents a CLI (enumeration) flag which can take on only a single 25 | // enumeration value out of a fixed set of enumeration values. Applications 26 | // using the enumflag package might want to “derive” their enumeration flags 27 | // from Flag for documentation purposes; for instance: 28 | // 29 | // type MyFoo enumflag.Flag 30 | // 31 | // However, applications don't need to base their own enum types on Flag. The 32 | // only requirement for user-defined enumeration flags is that they must be 33 | // (“somewhat”) compatible with the Flag type, or more precise: user-defined 34 | // enumerations must satisfy [constraints.Integer]. 35 | type Flag uint 36 | 37 | // EnumCaseSensitivity specifies whether the textual representations of enum 38 | // values are considered to be case sensitive, or not. 39 | type EnumCaseSensitivity bool 40 | 41 | // Controls whether the textual representations for enum values are case 42 | // sensitive, or not. 43 | const ( 44 | EnumCaseInsensitive EnumCaseSensitivity = false 45 | EnumCaseSensitive EnumCaseSensitivity = true 46 | ) 47 | 48 | // EnumFlagValue wraps a user-defined enum type value satisfying 49 | // [constraints.Integer] or [][constraints.Integer]. It implements the 50 | // [github.com/spf13/pflag.Value] interface, so the user-defined enum type value 51 | // can directly be used with the fine pflag drop-in package for Golang CLI 52 | // flags. 53 | type EnumFlagValue[E constraints.Integer] struct { 54 | value enumValue[E] // enum value of a user-defined enum scalar or slice type. 55 | enumtype string // user-friendly name of the user-defined enum type. 56 | names enumMapper[E] // enum value names. 57 | } 58 | 59 | // enumValue supports getting, setting, and stringifying an scalar or slice enum 60 | // enumValue. 61 | // 62 | // Do I smell preemptive interfacing here...? Now watch the magic of “cleanest 63 | // code”: by just moving the interface type from the source file with the struct 64 | // types to the source file with the consumer we achieve immediate Go 65 | // perfectness! Strike! 66 | type enumValue[E constraints.Integer] interface { 67 | Get() any 68 | Set(val string, names enumMapper[E]) error 69 | String(names enumMapper[E]) string 70 | NewCompletor(enums EnumIdentifiers[E], help Help[E]) Completor 71 | } 72 | 73 | // New wraps a given enum variable (satisfying [constraints.Integer]) so that it 74 | // can be used as a flag Value with [github.com/spf13/pflag.Var] and 75 | // [github.com/spf13/pflag.VarP]. In case no default enum value should be set 76 | // and therefore no default shown in [spf13/cobra], use [NewWithoutDefault] 77 | // instead. 78 | // 79 | // [spf13/cobra]: https://github.com/spf13/cobra 80 | func New[E constraints.Integer](flag *E, typename string, mapping EnumIdentifiers[E], sensitivity EnumCaseSensitivity) *EnumFlagValue[E] { 81 | return new("New", flag, typename, mapping, sensitivity, false) 82 | } 83 | 84 | // NewWithoutDefault wraps a given enum variable (satisfying 85 | // [constraints.Integer]) so that it can be used as a flag Value with 86 | // [github.com/spf13/pflag.Var] and [github.com/spf13/pflag.VarP]. Please note 87 | // that the zero enum value must not be mapped and thus not be assigned to any 88 | // enum value textual representation. 89 | // 90 | // [spf13/cobra] won't show any default value in its help for CLI enum flags 91 | // created with NewWithoutDefault. 92 | // 93 | // [spf13/cobra]: https://github.com/spf13/cobra 94 | func NewWithoutDefault[E constraints.Integer](flag *E, typename string, mapping EnumIdentifiers[E], sensitivity EnumCaseSensitivity) *EnumFlagValue[E] { 95 | return new("NewWithoutDefault", flag, typename, mapping, sensitivity, true) 96 | } 97 | 98 | // new returns a new enum variable to be used with pflag.Var and pflag.VarP. 99 | func new[E constraints.Integer](ctor string, flag *E, typename string, mapping EnumIdentifiers[E], sensitivity EnumCaseSensitivity, nodefault bool) *EnumFlagValue[E] { 100 | if flag == nil { 101 | panic(fmt.Sprintf("%s requires flag to be a non-nil pointer to an enum value satisfying constraints.Integer", ctor)) 102 | } 103 | if mapping == nil { 104 | panic(fmt.Sprintf("%s requires mapping not to be nil", ctor)) 105 | } 106 | return &EnumFlagValue[E]{ 107 | value: &enumScalar[E]{v: flag, nodefault: nodefault}, 108 | enumtype: typename, 109 | names: newEnumMapper(mapping, sensitivity), 110 | } 111 | } 112 | 113 | // NewSlice wraps a given enum slice variable (satisfying [constraints.Integer]) 114 | // so that it can be used as a flag Value with [github.com/spf13/pflag.Var] and 115 | // [github.com/spf13/pflag.VarP]. 116 | func NewSlice[E constraints.Integer](flag *[]E, typename string, mapping EnumIdentifiers[E], sensitivity EnumCaseSensitivity) *EnumFlagValue[E] { 117 | if flag == nil { 118 | panic("NewSlice requires flag to be a non-nil pointer to an enum value slice satisfying []constraints.Integer") 119 | } 120 | if mapping == nil { 121 | panic("NewSlice requires mapping not to be nil") 122 | } 123 | return &EnumFlagValue[E]{ 124 | value: &enumSlice[E]{v: flag}, 125 | enumtype: typename, 126 | names: newEnumMapper(mapping, sensitivity), 127 | } 128 | } 129 | 130 | // Set sets the enum flag to the specified enum value. If the specified value 131 | // isn't a valid enum value, then the enum flag won't be set and an error is 132 | // returned instead. 133 | func (e *EnumFlagValue[E]) Set(val string) error { 134 | return e.value.Set(val, e.names) 135 | } 136 | 137 | // String returns the textual representation of an enumeration (flag) value. In 138 | // case multiple textual representations (~identifiers) exist for the same 139 | // enumeration value, then only the first textual representation is returned, 140 | // which is considered to be the canonical one. 141 | func (e *EnumFlagValue[E]) String() string { return e.value.String(e.names) } 142 | 143 | // Type returns the name of the flag value type. The type name is used in error 144 | // messages. 145 | func (e *EnumFlagValue[E]) Type() string { return e.enumtype } 146 | 147 | // Get returns the current enum value for convenience. Please note that the enum 148 | // value is either scalar or slice, depending on how the enum flag was created. 149 | func (e *EnumFlagValue[E]) Get() any { return e.value.Get() } 150 | 151 | // RegisterCompletion registers completions for the specified (flag) name, with 152 | // optional help texts. 153 | func (e *EnumFlagValue[E]) RegisterCompletion(cmd *cobra.Command, name string, help Help[E]) error { 154 | return cmd.RegisterFlagCompletionFunc( 155 | name, e.value.NewCompletor(e.names.Mapping(), help)) 156 | } 157 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 10 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 11 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 12 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 13 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 14 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 15 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 16 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 17 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 18 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 19 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 20 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 21 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 22 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 23 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 24 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 25 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 26 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 27 | github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= 28 | github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= 29 | github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= 30 | github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= 31 | github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= 32 | github.com/onsi/gomega v1.28.1/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= 33 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 34 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 38 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 39 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 40 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 41 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 42 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 43 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 44 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 45 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 48 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 49 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/thediveo/success v1.0.1 h1:NVwUOwKUwaN8szjkJ+vsiM2L3sNBFscldoDJ2g2tAPg= 51 | github.com/thediveo/success v1.0.1/go.mod h1:AZ8oUArgbIsCuDEWrzWNQHdKnPbDOLQsWOFj9ynwLt0= 52 | github.com/thediveo/success v1.0.2 h1:w+r3RbSjLmd7oiNnlCblfGqItcsaShcuAorRVh/+0xk= 53 | github.com/thediveo/success v1.0.2/go.mod h1:hdPJB77k70w764lh8uLUZgNhgeTl3DYeZ4d4bwMO2CU= 54 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 55 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 56 | golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= 57 | golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 58 | golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= 59 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 60 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 61 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 62 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 63 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 66 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 68 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 69 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 70 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 71 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 72 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 73 | golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= 74 | golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 75 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 76 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 77 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 78 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 83 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLI Enumeration Flags 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/thediveo/enumflag.svg)](https://pkg.go.dev/github.com/thediveo/enumflag/v2) 3 | [![GitHub](https://img.shields.io/github/license/thediveo/enumflag)](https://img.shields.io/github/license/thediveo/enumflag) 4 | ![build and test](https://github.com/thediveo/enumflag/actions/workflows/buildandtest.yaml/badge.svg?branch=master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/thediveo/enumflag/v2)](https://goreportcard.com/report/github.com/thediveo/enumflag/v2) 6 | ![Coverage](https://img.shields.io/badge/Coverage-100.0%25-brightgreen) 7 | 8 | `enumflag/v2` is a Golang package which supplements the Golang CLI flag packages 9 | [spf13/cobra](https://github.com/spf13/cobra) and 10 | [spf13/pflag](https://github.com/spf13/pflag) with enumeration flags, including 11 | support for enumeration slices. Thanks to Go generics, `enumflag/v2` now 12 | provides type-safe enumeration flags (and thus requires Go 1.18 or later). 13 | 14 | > The v2 API is source-compatible with v0 unless you've used the `Get()` method 15 | > in the past. However, since the use of Go generics might be a breaking change 16 | > to downstream projects the semantic major version of `enumflag` thus went from 17 | > v0 straight to v2. 18 | 19 | For instance, users can specify enum flags as `--mode=foo` or `--mode=bar`, 20 | where `foo` and `bar` are valid enumeration values. Other values which are not 21 | part of the set of allowed enumeration values cannot be set and raise CLI flag 22 | errors. In case of an enumeration _slice_ flag users can specify multiple 23 | enumeration values either with a single flag `--mode=foo,bar` or multiple flag 24 | calls, such as `--mode=foo --mode=bar`. 25 | 26 | Application programmers then simply deal with enumeration values in form of 27 | uints (or ints, _erm_, anything that satisfies `constraints.Integer`s), 28 | liberated from parsing strings and validating enumeration flags. 29 | 30 | For devcontainer instructions, please see the [section "DevContainer" 31 | below](#devcontainer). 32 | 33 | ## Alternatives 34 | 35 | In case you are just interested in string-based one-of-a-set flags, then the 36 | following packages offer you a minimalist approach: 37 | 38 | - [hashicorp/packer/helper/enumflag](https://godoc.org/github.com/hashicorp/packer/helper/enumflag) 39 | really is a reduced-to-the-max version without any whistles and bells. 40 | - [creachadair/goflags/enumflag](https://godoc.org/github.com/creachadair/goflags/enumflag) 41 | has a similar, but slightly more elaborate API with additional "indices" for 42 | enumeration values. 43 | 44 | But if you instead want to handle one-of-a-set flags as properly typed 45 | enumerations instead of strings, or if you need (multiple-of-a-set) slice 46 | support, then please read on. 47 | 48 | ## Installation 49 | 50 | To add `enumflag/v2` as a dependency, in your Go module issue: 51 | 52 | ```bash 53 | go get github.com/thediveo/enumflag/v2 54 | ``` 55 | 56 | ## How To Use 57 | 58 | - [start with your own enum types](#start-with-your-own-enum-types), 59 | - optional: [shell completion](#shell-completion), 60 | - optional: [use existing enum types and non-zero defaults](#use-existing-enum-types), 61 | - optional: [CLI flag with default](#cli-flag-with-default), 62 | - optional: [CLI flag without a default value](#cli-flag-without-default), 63 | - optional: [slice of enums](#slice-of-enums). 64 | 65 | ### Start With Your Own Enum Types 66 | 67 | Without further ado, here's how to define and use enum flags in your own 68 | applications... 69 | 70 | ```go 71 | import ( 72 | "fmt" 73 | 74 | "github.com/spf13/cobra" 75 | "github.com/thediveo/enumflag/v2" 76 | ) 77 | 78 | // ① Define your new enum flag type. It can be derived from enumflag.Flag, 79 | // but it doesn't need to be as long as it satisfies constraints.Integer. 80 | type FooMode enumflag.Flag 81 | 82 | // ② Define the enumeration values for FooMode. 83 | const ( 84 | Foo FooMode = iota 85 | Bar 86 | ) 87 | 88 | // ③ Map enumeration values to their textual representations (value 89 | // identifiers). 90 | var FooModeIds = map[FooMode][]string{ 91 | Foo: {"foo"}, 92 | Bar: {"bar"}, 93 | } 94 | 95 | // ④ Now use the FooMode enum flag. If you want a non-zero default, then 96 | // simply set it here, such as in "foomode = Bar". 97 | var foomode FooMode 98 | 99 | func main() { 100 | rootCmd := &cobra.Command{ 101 | Run: func(cmd *cobra.Command, _ []string) { 102 | fmt.Printf("mode is: %d=%q\n", 103 | foomode, 104 | cmd.PersistentFlags().Lookup("mode").Value.String()) 105 | }, 106 | } 107 | // ⑤ Define the CLI flag parameters for your wrapped enum flag. 108 | rootCmd.PersistentFlags().VarP( 109 | enumflag.New(&foomode, "mode", FooModeIds, enumflag.EnumCaseInsensitive), 110 | "mode", "m", 111 | "foos the output; can be 'foo' or 'bar'") 112 | 113 | rootCmd.SetArgs([]string{"--mode", "bAr"}) 114 | _ = rootCmd.Execute() 115 | } 116 | ``` 117 | 118 | The boilerplate pattern is always the same: 119 | 120 | 1. Define your own new enumeration type, such as `type FooMode enumflag.Flag`. 121 | 2. Define the constants in your enumeration. 122 | 3. Define the mapping of the constants onto enum values (textual 123 | representations). 124 | 4. Somewhere, declare a flag variable of your enum flag type. 125 | - If you want to use a non-zero default enum value, just go ahead and set 126 | it: `var foomode = Bar`. It will be used correctly. 127 | 5. Wire up your flag variable to its flag long and short names, et cetera. 128 | 129 | ### Shell Completion 130 | 131 | Dynamic flag completion can be enabled by calling the `RegisterCompletion(...)` 132 | receiver of an enum flag (more precise: flag value) created using 133 | `enumflag.New(...)`. `enumflag` supports dynamic flag completion for both scalar 134 | and slice enum flags. Unfortunately, due to the cobra API design it isn't 135 | possible for `enumflag` to offer a fluent API. Instead, creation, adding, and 136 | registering have to be carried out as separate instructions. 137 | 138 | ```go 139 | // ⑤ Define the CLI flag parameters for your wrapped enum flag. 140 | ef := enumflag.New(&foomode, "mode", FooModeIds, enumflag.EnumCaseInsensitive) 141 | rootCmd.PersistentFlags().VarP( 142 | ef, 143 | "mode", "m", 144 | "foos the output; can be 'foo' or 'bar'") 145 | // ⑥ register completion 146 | ef.RegisterCompletion(rootCmd, "mode", enumflag.Help[FooMode]{ 147 | Foo: "foos the output", 148 | Bar: "bars the output", 149 | }) 150 | ``` 151 | 152 | Please note for shell completion to work, your root command needs to have at 153 | least one (explicit) sub command. Otherwise, `cobra` won't automatically add an 154 | additional `completion` sub command. For more details, please refer to cobra's 155 | documentation on [Generating shellcompletions](https://github.com/spf13/cobra/blob/main/site/content/completions/_index.md). 156 | 157 | ### Use Existing Enum Types 158 | 159 | A typical example might be your application using a 3rd party logging package 160 | and you want to offer a `-v` log level CLI flag. Here, we use the existing 3rd 161 | party enum values and set a non-zero default for our logging CLI flag. 162 | 163 | Considering the boiler plate shown above, we can now leave out steps ① and ②, 164 | because these definitions come from a 3rd party package. We only need to 165 | supply the textual enum names as ③. 166 | 167 | ```go 168 | import ( 169 | "fmt" 170 | "os" 171 | 172 | log "github.com/sirupsen/logrus" 173 | "github.com/spf13/cobra" 174 | "github.com/thediveo/enumflag/v2" 175 | ) 176 | 177 | func main() { 178 | // ①+② skip "define your own enum flag type" and enumeration values, as we 179 | // already have a 3rd party one. 180 | 181 | // ③ Map 3rd party enumeration values to their textual representations 182 | var LoglevelIds = map[log.Level][]string{ 183 | log.TraceLevel: {"trace"}, 184 | log.DebugLevel: {"debug"}, 185 | log.InfoLevel: {"info"}, 186 | log.WarnLevel: {"warning", "warn"}, 187 | log.ErrorLevel: {"error"}, 188 | log.FatalLevel: {"fatal"}, 189 | log.PanicLevel: {"panic"}, 190 | } 191 | 192 | // ④ Define your enum flag value and set the your logging default value. 193 | var loglevel log.Level = log.WarnLevel 194 | 195 | rootCmd := &cobra.Command{ 196 | Run: func(cmd *cobra.Command, _ []string) { 197 | fmt.Printf("logging level is: %d=%q\n", 198 | loglevel, 199 | cmd.PersistentFlags().Lookup("log").Value.String()) 200 | }, 201 | } 202 | 203 | // ⑤ Define the CLI flag parameters for your wrapped enum flag. 204 | rootCmd.PersistentFlags().Var( 205 | enumflag.New(&loglevel, "log", LoglevelIds, enumflag.EnumCaseInsensitive), 206 | "log", 207 | "sets logging level; can be 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'") 208 | 209 | // Defaults to what we set above: warn level. 210 | _ = rootCmd.Execute() 211 | 212 | // User specifies a specific level, such as log level. 213 | rootCmd.SetArgs([]string{"--log", "debug"}) 214 | _ = rootCmd.Execute() 215 | } 216 | ``` 217 | 218 | ### CLI Flag With Default 219 | 220 | Sometimes you might want a CLI enum flag to have a default value when the user 221 | just specifies the CLI flag **without its value**. A good example is the 222 | `--color` flag of the `ls` command: 223 | 224 | - if just specified as `--color` without a value, it 225 | will default to the value of `auto`; 226 | - otherwise, as specific value can be given, such as 227 | - `--color=always`, 228 | - `--color=never`, 229 | - or even `--color=auto`. 230 | 231 | In such situations, use spf13/pflags's 232 | [`NoOptDefVal`](https://godoc.org/github.com/spf13/pflag#Flag) to set the 233 | flag's default value *as text*, if the flag is on the command line without any 234 | options. 235 | 236 | The gist here is as follows, please see also 237 | [colormode.go](https://github.com/TheDiveO/lxkns/blob/master/cmd/internal/pkg/style/colormode.go) 238 | from my [lxkns](https://github.com/TheDiveO/lxkns) Linux namespaces discovery 239 | project: 240 | 241 | ```go 242 | rootCmd.PersistentFlags().VarP( 243 | enumflag.New(&colorize, "color", colorModeIds, enumflag.EnumCaseSensitive), 244 | "color", "c", 245 | "colorize the output; can be 'always' (default if omitted), 'auto',\n"+ 246 | "or 'never'") 247 | rootCmd.PersistentFlags().Lookup("color").NoOptDefVal = "always" 248 | ``` 249 | 250 | ### CLI Flag Without Default 251 | 252 | In other situations you might _not_ want to have a default value set, because a 253 | particular CLI flag is mandatory (using cobra's 254 | [MarkFlagRequired](https://pkg.go.dev/github.com/spf13/cobra#MarkFlagRequired)). 255 | Here, cobra's help should not show a (useless) default enum flag setting but 256 | only the available enum values. 257 | 258 | **Don't assign the zero value** of your enum type to any value, except the 259 | "non-existing" default. 260 | 261 | ```go 262 | // ② Define the enumeration values for FooMode; do not assign the zero value to 263 | // any enum value except for the "no default" default. 264 | const ( 265 | NoDefault FooMode = iota // optional; must be the zero value. 266 | Foo // make sure to not use the zero value. 267 | Bar 268 | ) 269 | ``` 270 | 271 | Also, **don't map the zero value** of your enum type. 272 | 273 | ```go 274 | // ③ Map enumeration values to their textual representations (value 275 | // identifiers). 276 | var FooModeIds = map[FooMode][]string{ 277 | // ...do NOT include/map the "no default" zero value! 278 | Foo: {"foo"}, 279 | Bar: {"bar"}, 280 | } 281 | ``` 282 | 283 | Finally, simply use `enumflag.NewWithoutDefault` instead of `enumflag.New` – 284 | that's all. 285 | 286 | ```go 287 | // ⑤ Define the CLI flag parameters for your wrapped enum flag. 288 | rootCmd.PersistentFlags().VarP( 289 | enumflag.NewWithoutDefault(&foomode, "mode", FooModeIds, enumflag.EnumCaseInsensitive), 290 | "mode", "m", 291 | "foos the output; can be 'foo' or 'bar'") 292 | ``` 293 | 294 | ### Slice of Enums 295 | 296 | For a slice of enumerations, simply declare your variable to be a slice of your 297 | enumeration type and then use `enumflag.NewSlice(...)` instead of 298 | `enumflag.New(...)`. 299 | 300 | ```go 301 | import ( 302 | "fmt" 303 | 304 | "github.com/spf13/cobra" 305 | "github.com/thediveo/enumflag/v2" 306 | ) 307 | 308 | // ① Define your new enum flag type. It can be derived from enumflag.Flag, 309 | // but it doesn't need to be as long as it satisfies constraints.Integer. 310 | type MooMode enumflag.Flag 311 | 312 | // ② Define the enumeration values for FooMode. 313 | const ( 314 | Moo MooMode = (iota + 1) * 111 315 | Møø 316 | Mimimi 317 | ) 318 | 319 | // ③ Map enumeration values to their textual representations (value 320 | // identifiers). 321 | var MooModeIds = map[MooMode][]string{ 322 | Moo: {"moo"}, 323 | Møø: {"møø"}, 324 | Mimimi: {"mimimi"}, 325 | } 326 | 327 | func Example_slice() { 328 | // ④ Define your enum slice flag value. 329 | var moomode []MooMode 330 | rootCmd := &cobra.Command{ 331 | Run: func(cmd *cobra.Command, _ []string) { 332 | fmt.Printf("mode is: %d=%q\n", 333 | moomode, 334 | cmd.PersistentFlags().Lookup("mode").Value.String()) 335 | }, 336 | } 337 | // ⑤ Define the CLI flag parameters for your wrapped enumm slice flag. 338 | rootCmd.PersistentFlags().VarP( 339 | enumflag.NewSlice(&moomode, "mode", MooModeIds, enumflag.EnumCaseInsensitive), 340 | "mode", "m", 341 | "can be any combination of 'moo', 'møø', 'mimimi'") 342 | 343 | rootCmd.SetArgs([]string{"--mode", "Moo,møø"}) 344 | _ = rootCmd.Execute() 345 | } 346 | ``` 347 | 348 | ## DevContainer 349 | 350 | > [!CAUTION] 351 | > 352 | > Do **not** use VSCode's "~~Dev Containers: Clone Repository in Container 353 | > Volume~~" command, as it is utterly broken by design, ignoring 354 | > `.devcontainer/devcontainer.json`. 355 | 356 | 1. `git clone https://github.com/thediveo/enumflag` 357 | 2. in VSCode: Ctrl+Shift+P, "Dev Containers: Open Workspace in Container..." 358 | 3. select `enumflag.code-workspace` and off you go... 359 | 360 | ## VSCode Tasks 361 | 362 | The included `enumflag.code-workspace` defines the following tasks: 363 | 364 | - **Build workspace** task: builds all, including the shared library test 365 | plugin. 366 | 367 | - **Run all tests with coverage** task: does what it says on the tin and runs 368 | all tests with coverage. 369 | 370 | ## Make Targets 371 | 372 | - `make`: lists available targets. 373 | - `make test`: runs all tests. 374 | - `make coverage`: deprecated, use the `gocover` CLI command in the devcontainer 375 | instead. 376 | - `make report`: deprecated, use the `goreportcard-cli` CLI command in the 377 | devcontainer instead. 378 | 379 | ## Contributing 380 | 381 | Please see [CONTRIBUTING.md](CONTRIBUTING.md). 382 | 383 | ## Copyright and License 384 | 385 | `lxkns` is Copyright 2020, 2025 Harald Albrecht, and licensed under the Apache 386 | License, Version 2.0. 387 | --------------------------------------------------------------------------------