9

Is there built-in tooling (and if so how do I use it?) to require a flag to be one of several values and throw an error if a flag is not one of the permitted values, in the Cobra library? I didn't see this on the Github page.

Bren
  • 3,516
  • 11
  • 41
  • 73
  • 1
    A switch statement? – RayfenWindspear Jun 12 '18 at 20:01
  • @RayfenWindspear Something built in. – Bren Jun 12 '18 at 20:32
  • @MarcoBonelli already covered that in the above comments. – Bren Jun 12 '18 at 20:34
  • Oh I see, sorry, so you are asking if there is a built in method that would do this automatically. I don't really think so, why would they implement it this way? I mean, you want to *potentially* crash your program if you get an unexpected flag? That doesn't sound so cool. – Marco Bonelli Jun 12 '18 at 20:38
  • @I was thinking you might be able to register an error producing function or something like that. Seems like a common enough use-case, that I thought it may be built in and wanted to use that if it did exist. – Bren Jun 12 '18 at 20:40
  • Looking at the file [`args.go`](https://github.com/spf13/cobra/blob/master/args.go) it seems like there may actually be something like what you're looking for. – Marco Bonelli Jun 12 '18 at 20:44
  • @MarcoBonelli I don't see any interfaces in that file, so I fail to see how one would add what OP is looking for. Note OP wants a single arg to be limited to a set of values. There doesn't appear to be a builtin for this. – RayfenWindspear Jun 12 '18 at 20:50
  • @RayfenWindspear I mean, those are exported functions... so one could theoretically use them. Didn't look inside it that much thought, you're probably right. – Marco Bonelli Jun 12 '18 at 21:04
  • Yeah, those all seem to operate on the full set of args. But, they give a routine way of doing the checking, so I say write one and submit a pull request! – RayfenWindspear Jun 12 '18 at 21:07

3 Answers3

19

Cobra allows you to define custom value types to be used as flags through the pflag.(*FlagSet).Var() method (from the https://github.com/spf13/pflag package, that is used by Cobra). You have to make a new type that implements the pflag.Value interface:

type Value interface {
    String() string
    Set(string) error
    Type() string
}

Example type definition:

type myEnum string

const (
    myEnumFoo myEnum = "foo"
    myEnumBar myEnum = "bar"
    myEnumMoo myEnum = "moo"
)

// String is used both by fmt.Print and by Cobra in help text
func (e *myEnum) String() string {
    return string(*e)
}

// Set must have pointer receiver so it doesn't change the value of a copy
func (e *myEnum) Set(v string) error {
    switch v {
    case "foo", "bar", "moo":
        *e = myEnum(v)
        return nil
    default:
        return errors.New(`must be one of "foo", "bar", or "moo"`)
    }
}

// Type is only used in help text
func (e *myEnum) Type() string {
    return "myEnum"
}

Example registration:

func init() {
    var flagMyEnum = myEnumFoo

    var myCmd = &cobra.Command{
        Use:   "mycmd",
        Short: "A brief description of your command",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("myenum value:", flagMyEnum)
        },
    }

    rootCmd.AddCommand(myCmd)

    myCmd.Flags().Var(&flagMyEnum, "myenum", `my custom enum. allowed: "foo", "bar", "moo"`)
}

Example usage: (notice the first line in the console output below)

$ go run . mycmd --myenum raz
Error: invalid argument "raz" for "--myenum" flag: must be one of "foo", "bar", or "moo"
Usage:
  main mycmd [flags]

Flags:
  -h, --help            help for mycmd
      --myenum myEnum   my custom enum. allowed: "foo", "bar", "moo" (default foo)

exit status 1

$ go run . mycmd --myenum bar
myenum value: bar

Completions

To add autocompletion to this, the somewhat hidden documentation page cobra/shell_completions.md#completions-for-flags is at great assistance. For our example, you would add something like this:

func init() {
    // ...

    myCmd.Flags().Var(&flagMyEnum, "myenum", `my custom enum. allowed: "foo", "bar", "moo"`)

    myCmd.RegisterFlagCompletionFunc("myenum", myEnumCompletion)
}

// myEnumCompletion should probably live next to the myEnum definition
func myEnumCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    return []string{
        "foo\thelp text for foo",
        "bar\thelp text for bar",
        "moo\thelp text for moo",
    }, cobra.ShellCompDirectiveDefault
}

Example usage:

$ go build -o main .

$ source <(main completion bash)

$ main mycmd --myenum <TAB><TAB>
bar  (help text for bar)
foo  (help text for foo)
moo  (help text for moo)
Applejag
  • 1,068
  • 10
  • 17
  • It would be nice to add an interface compliance check before interface implementation as a good coding practice. `var _ pflag.Value (*myEnum)(nil)`. Refer [here](https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance) – Avinal Jun 19 '22 at 09:22
1

It looks like an enumflag package was published as an add-on to satisfy this use case: https://pkg.go.dev/github.com/thediveo/enumflag

package main

import (
    "fmt"

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

// ① Define your new enum flag type. It can be derived from enumflag.Flag, but
// it doesn't need to be as long as it is compatible with enumflag.Flag, so
// either an int or uint.
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"},
}

// User-defined enum flag types should be derived from "enumflag.Flag"; however
// this is not strictly necessary as long as they can be converted into the
// "enumflag.Flag" type. Actually, "enumflag.Flag" is just a fancy name for an
// "uint". In order to use such user-defined enum flags, simply wrap them using
// enumflag.New.
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'")

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

Note: it does not seem to include completion logic.

Joe Atzberger
  • 3,079
  • 1
  • 18
  • 16
  • 1
    Author of this package here: I only now notice that I never had a feature request for completion. Interesting. – TheDiveO Feb 19 '23 at 20:20
-1

Though it is hard to prove a negative, it doesn't look this is currently a feature.

Bren
  • 3,516
  • 11
  • 41
  • 73