alexflint / go-arg

Struct-based argument parsing in Go
BSD 2-Clause "Simplified" License
1.98k stars 99 forks source link

Choices for arguments #248

Open zekroTJA opened 3 months ago

zekroTJA commented 3 months ago

It would be awesome if you could specify choices (like possible/allowed values) for arguments and flags.

I could imagine it in two ways.

a) Via struct tags

var args struct {
    LogLevel string `arg:"-l,--log-level" choices:"panic,error,warn,info,debug"`
}

b) Via a method

In example, a parameter type could implement a Method - i.e. - called ValueChoises, which returns a list of allowed values for the argument.

type Level string

func (t Level) ValueChoises() []string {
    return []string{"panic", "error", "warn", "info", "debug"}
}

var args struct {
    LogLevel Level `arg:"-l,--log-level"`
}

I think I would prefer the second way, because it allows for more flexibility with the definition of choices.

Then, the choice values could be represented in the help text something like as following.

Usage: main [--level LEVEL]

Options:
  --level LEVEL, -l LEVEL
                         log level
                         choices: panic, error, warn, info, debug
  --help, -h             display this help and exit

Let me know what you think of the idea. If you think it would fit in the project, I would be pleased to submit an implementation for it as well. :)

alexflint commented 3 months ago

Yes, I've considered this feature in the past, and I think you're right that the dynamic approach makes more sense. Even if the choices themselves do not change in any dynamic, one generally would want to define those choices in one place -- as a set of constants, for example -- and populate the help text from there.

One very simple way to get multiple-choice behavior is using UnmarshalText like this:

type myEnum int

const (
  Choice1 myEnum = iota
  Choice2
  Choice3
)

func (e *myEnum) UnmarshalText(b []byte) error {
  switch string(b) {
    case "Choice1": *e = Choice1
    case "Choice2": *e = Choice2
    case "Choice3": *e = Choice3
    default:   return fmt.Errorf("invalid for myEnum: %q: options are Choice1, Choice2, Choice3", string(b))
  }
  return nil
}

One of the good things about this approach is that the user of the library gets to choose details such as whether the choices are case-sensitive. There are some use-cases where you really need case-sensitive choices, and others where you'd really prefer case-insensitive choices. It also leaves the door open to write "did you mean xyz?" in the error message and such.

Unfortunately this approach won't populate the help text with the choices. You would have to manually put the choices into the help text and keep them up to date there.

So then the appropriate feature is to add a dynamic help text function to go-arg, so that any type that implements, say, Help() string gets that as its help text. Then the type above would have

func (e *myEnum) Help() string {
  return "one of: " + strings.Join(", ", possibleChoices)
}
zekroTJA commented 2 months ago

Well, that makes well more sense and allows for a way more flexible implementation. I love this package and would really like to contribute an approach for an implementation of such a dynamic help text, when I find the time to do so. :)