Permitted flag values for Cobra
Asked Answered
B

3

11

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.

Barcroft answered 12/6, 2018 at 19:33 Comment(9)
A switch statement?Chagrin
@Chagrin Something built in.Barcroft
@MarcoBonelli already covered that in the above comments.Barcroft
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.Inness
@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.Barcroft
Looking at the file args.go it seems like there may actually be something like what you're looking for.Inness
@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.Chagrin
@Chagrin I mean, those are exported functions... so one could theoretically use them. Didn't look inside it that much thought, you're probably right.Inness
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!Chagrin
W
26

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)
Waxbill answered 31/12, 2021 at 11:26 Comment(4)
It would be nice to add an interface compliance check before interface implementation as a good coding practice. var _ pflag.Value (*myEnum)(nil). Refer hereLundell
By setting flagMyEnum = myEnumFoo are you making myEnumFoo the default value? I'm confused about how myEnumBar and myEnumMoo are used.Quinquennial
Also how do you do this with StringVarP()?Quinquennial
@Quinquennial yes, the flagMyEnum = myEnumFoo sets the default. The pflag .Var() function uses the value that you pass in as the first argument as default value. On your second question: there's a .VarP() function as well that provides a short flag, same as StringVarP()Waxbill
A
2

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.

Albertalberta answered 9/8, 2022 at 19:48 Comment(1)
Author of this package here: I only now notice that I never had a feature request for completion. Interesting.Metatarsus
B
-1

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

Barcroft answered 12/6, 2018 at 21:22 Comment(1)
I'm writing an answer using their format that you could use as a pull request in order to make it a supported feature.Chagrin

© 2022 - 2024 — McMap. All rights reserved.