enumflag

package module
v2.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2023 License: Apache-2.0 Imports: 6 Imported by: 37

README

CLI Enumeration Flags

Go Reference GitHub build and test Go Report Card Coverage

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

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

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

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

Alternatives

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

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

Installation

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

go get github.com/thediveo/enumflag/v2

How To Use

Start With Your Own Enum Types

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

import (
    "fmt"

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

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

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

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

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

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

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

The boilerplate pattern is always the same:

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

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

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

Please note for shell completion to work, your root command needs to have at least one (explicit) sub command. Otherwise, cobra won't automatically add an additional completion sub command. For more details, please refer to cobra's documentation on Generating shell completions.

Use Existing Enum Types

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

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

import (
    "fmt"
    "os"

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

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

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

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

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

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

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

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

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

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

In such situations, use spf13/pflags's NoOptDefVal to set the flag's default value as text, if the flag is on the command line without any options.

The gist here is as follows, please see also colormode.go from my lxkns Linux namespaces discovery project:

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

In other situations you might not want to have a default value set, because a particular CLI flag is mandatory (using cobra's MarkFlagRequired). Here, cobra's help should not show a (useless) default enum flag setting but only the availabe enum values.

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

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

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

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

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

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

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

import (
    "fmt"

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

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

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

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

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

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

VSCode Tasks

The included enumflag.code-workspace defines the following tasks:

  • View Go module documentation task: installs pkgsite, if not done already so, then starts pkgsite and opens VSCode's integrated ("simple") browser to show the go-plugger/v2 documentation.

  • Build workspace task: builds all, including the shared library test plugin.

  • Run all tests with coverage task: does what it says on the tin and runs all tests with coverage.

Aux Tasks
  • pksite service: auxilliary task to run pkgsite as a background service using scripts/pkgsite.sh. The script leverages browser-sync and nodemon to hot reload the Go module documentation on changes; many thanks to @mdaverde's Build your Golang package docs locally for paving the way. scripts/pkgsite.sh adds automatic installation of pkgsite, as well as the browser-sync and nodemon npm packages for the local user.
  • view pkgsite: auxilliary task to open the VSCode-integrated "simple" browser and pass it the local URL to open in order to show the module documentation rendered by pkgsite. This requires a detour via a task input with ID "pkgsite".

Make Targets

  • make: lists all targets.
  • make coverage: runs all tests with coverage and then updates the coverage badge in README.md.
  • make pkgsite: installs x/pkgsite, as well as the browser-sync and nodemon npm packages first, if not already done so. Then runs the pkgsite and hot reloads it whenever the documentation changes.
  • make report: installs @gojp/goreportcard if not yet done so and then runs it on the code base.
  • make test: runs all tests.

Contributing

Please see CONTRIBUTING.md.

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

Documentation

Overview

Package enumflag supplements the Golang CLI flag handling packages spf13/cobra and spf13/pflag with enumeration flags.

For instance, users can specify enum flags as “--mode=foo” or “--mode=bar”, where “foo” and “bar” are valid enumeration values. Other values which are not part of the set of allowed enumeration values cannot be set and raise CLI flag errors.

Application programmers then simply deal with enumeration values in form of uints (or ints), liberated from parsing strings and validating enumeration flags.

Example
package main

import (
	"fmt"

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

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

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

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

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

	// cobra's help will render the default enum value identifier...
	_ = rootCmd.Help()

	// parse the CLI args to set our enum flag.
	rootCmd.SetArgs([]string{"--mode", "bAr"})
	_ = rootCmd.Execute()

}
Output:

Usage:
   [flags]

Flags:
  -m, --mode mode   foos the output; can be 'foo' or 'bar' (default foo)
mode is: 1="bar"
Example (External)
package main

import (
	"fmt"
	"os"

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

func init() {
	log.SetOutput(os.Stdout)
}

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

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

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

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

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

	_ = rootCmd.Execute()
	rootCmd.SetArgs([]string{"--log", "debug"})
	_ = rootCmd.Execute()
}
Output:

logging level is: 3="warning"
logging level is: 5="debug"
Example (No_default_value)
package main

import (
	"fmt"

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

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

// ② Define the enumeration values for BarMode.
const (
	NoDefault         = iota // optional definition for "no default" zero value
	Barr      BarMode = iota
	Barz
)

// ③ Map enumeration values to their textual representations (value
// identifiers).
var BarModeIds = map[BarMode][]string{
	// ...do NOT include/map the "no default" zero value!
	Barr: {"barr"},
	Barz: {"barz"},
}

func main() {
	// ④ Define your enum flag value.
	var barmode BarMode
	rootCmd := &cobra.Command{
		Run: func(cmd *cobra.Command, _ []string) {
			fmt.Printf("mode is: %d=%q\n",
				barmode,
				cmd.PersistentFlags().Lookup("mode").Value.String())
		},
	}
	// ⑤ Define the CLI flag parameters for your wrapped enum flag.
	rootCmd.PersistentFlags().VarP(
		enumflag.NewWithoutDefault(&barmode, "mode", BarModeIds, enumflag.EnumCaseInsensitive),
		"mode", "m",
		"bars the output; can be 'barr' or 'barz'")

	// now cobra's help won't render the default enum value identifier anymore...
	_ = rootCmd.Help()

	_ = rootCmd.Execute()

}
Output:

Usage:
   [flags]

Flags:
  -m, --mode mode   bars the output; can be 'barr' or 'barz'
mode is: 0=""
Example (Slice)
package main

import (
	"fmt"

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

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

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

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

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

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

mode is: [111 222]="[moo,møø]"

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Completor added in v2.0.4

type Completor func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)

Completor tells cobra how to complete a flag. See also cobra's dynamic flag completion documentation.

type EnumCaseSensitivity

type EnumCaseSensitivity bool

EnumCaseSensitivity specifies whether the textual representations of enum values are considered to be case sensitive, or not.

const (
	EnumCaseInsensitive EnumCaseSensitivity = false
	EnumCaseSensitive   EnumCaseSensitivity = true
)

Controls whether the textual representations for enum values are case sensitive, or not.

type EnumFlagValue

type EnumFlagValue[E constraints.Integer] struct {
	// contains filtered or unexported fields
}

EnumFlagValue wraps a user-defined enum type value satisfying constraints.Integer or []constraints.Integer. It implements the github.com/spf13/pflag.Value interface, so the user-defined enum type value can directly be used with the fine pflag drop-in package for Golang CLI flags.

func New

func New[E constraints.Integer](flag *E, typename string, mapping EnumIdentifiers[E], sensitivity EnumCaseSensitivity) *EnumFlagValue[E]

New wraps a given enum variable (satisfying constraints.Integer) so that it can be used as a flag Value with github.com/spf13/pflag.Var and github.com/spf13/pflag.VarP. In case no default enum value should be set and therefore no default shown in spf13/cobra, use NewWithoutDefault instead.

func NewSlice

func NewSlice[E constraints.Integer](flag *[]E, typename string, mapping EnumIdentifiers[E], sensitivity EnumCaseSensitivity) *EnumFlagValue[E]

NewSlice wraps a given enum slice variable (satisfying constraints.Integer) so that it can be used as a flag Value with github.com/spf13/pflag.Var and github.com/spf13/pflag.VarP.

func NewWithoutDefault

func NewWithoutDefault[E constraints.Integer](flag *E, typename string, mapping EnumIdentifiers[E], sensitivity EnumCaseSensitivity) *EnumFlagValue[E]

NewWithoutDefault wraps a given enum variable (satisfying constraints.Integer) so that it can be used as a flag Value with github.com/spf13/pflag.Var and github.com/spf13/pflag.VarP. Please note that the zero enum value must not be mapped and thus not be assigned to any enum value textual representation.

spf13/cobra won't show any default value in its help for CLI enum flags created with NewWithoutDefault.

func (*EnumFlagValue[E]) Get

func (e *EnumFlagValue[E]) Get() any

Get returns the current enum value for convenience. Please note that the enum value is either scalar or slice, depending on how the enum flag was created.

func (*EnumFlagValue[E]) RegisterCompletion added in v2.0.4

func (e *EnumFlagValue[E]) RegisterCompletion(cmd *cobra.Command, name string, help Help[E]) error

RegisterCompletion registers completions for the specified (flag) name, with optional help texts.

func (*EnumFlagValue[E]) Set

func (e *EnumFlagValue[E]) Set(val string) error

Set sets the enum flag to the specified enum value. If the specified value isn't a valid enum value, then the enum flag won't be set and an error is returned instead.

func (*EnumFlagValue[E]) String

func (e *EnumFlagValue[E]) String() string

String returns the textual representation of an enumeration (flag) value. In case multiple textual representations (~identifiers) exist for the same enumeration value, then only the first textual representation is returned, which is considered to be the canonical one.

func (*EnumFlagValue[E]) Type

func (e *EnumFlagValue[E]) Type() string

Type returns the name of the flag value type. The type name is used in error messages.

type EnumIdentifiers

type EnumIdentifiers[E constraints.Integer] map[E][]string

EnumIdentifiers maps enumeration values to their corresponding textual representations (~identifiers). This mapping is a one-to-many mapping in that the same enumeration value may have more than only one associated textual representation (indentifier). If more than one textual representation exists for the same enumeration value, then the first textual representation is considered to be the canonical one.

type Flag

type Flag uint

Flag represents a CLI (enumeration) flag which can take on only a single enumeration value out of a fixed set of enumeration values. Applications using the enumflag package might want to “derive” their enumeration flags from Flag for documentation purposes; for instance:

type MyFoo enumflag.Flag

However, applications don't need to base their own enum types on Flag. The only requirement for user-defined enumeration flags is that they must be (“somewhat”) compatible with the Flag type, or more precise: user-defined enumerations must satisfy constraints.Integer.

type Help added in v2.0.4

type Help[E constraints.Integer] map[E]string

Help maps enumeration values to their corresponding help descriptions. These descriptions should contain just the description but without any "foo\t" enum value prefix. The reason is that enumflag will automatically register the correct (erm, “complete”) completion text. Please note that it isn't necessary to supply any help texts in order to register enum flag completion.

Directories

Path Synopsis
test

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL