Skip to content

Allowing flags anywhere on the CLI

Writing CLIs in Go is great, but there's one annoyance with the stdlib flag package:

flags must be defined before any positional arguments.

For example, if you have a --debug flag, you can't use it after a positional argument, it'll be treated as a 2nd positional argument.

# ✅ Works as expeceted
$ mycli --debug run

# ❌ Does not work as expected
$ mycli run --debug

Flag parsing stops just before the first non-flag argument (- is a non-flag argument) or after the terminator --. 1


The Go project took a firm stance on this issue. For those interested, see the following GitHub issues (there are more, but these are the most relevant):

The challenge is that most users nowadays expect flag order to be irrelevant, especially since tools from other ecosystems behave this way.

The Command Line Interface Guidelines outlines best practices for authoring CLI tools — and they're pretty good. Of note, this snippet:

The order of flags, generally speaking, does not affect program semantics.

The Heroku CLI Style Guide also encourages this approach. Only mentioning it because of nostalgia, the heroku CLI had a fairly nice user experience back in the day.

This also allows the user to specify the flags in any order

There's loads more examples, guides and popular tools I could link to, but we'll leave it at that.

So what?

Imagine you're tweaking a (long) command and something isn't working as expected. You reach for a global --verbose or --debug flag, but now you have the cognitive overhead of figuring out where to slot the flag.

Or, you're a new user who just installed a shiny new CLI written in Go and it's not immediately working as you'd expect. You're not sure if you're missing a flag, or if you're using it wrong. Leading to a furstating experience.

Weak arguments? Maybe. But it bugs me, and I'm sure I'm not alone.

There's a wealth of third-party Go packages and entire CLI frameworks that handle this, but I want to do the bare minimum to get things working for 99% of cases, using only the stdlib when possible.

So here's a fairly simple solution that works for me. There's one edge case and one use of unsafe, but overall, I think it does the job right. Gotcha section for more details.

Snippet

Copied from mfridman/xflag.

func ParseToEnd(f *flag.FlagSet, arguments []string) error {
  if err := f.Parse(arguments); err != nil {
    return err
  }
  if f.NArg() == 0 {
    return nil
  }
  var args []string
  remainingArgs := f.Args()
  for i := 0; i < len(remainingArgs); i++ {
    arg := remainingArgs[i]
    if len(arg) > 1 && arg[0] == '-' {
      if arg == "--" {
        args = append(args, remainingArgs[i:]...)
        break
      }
      if err := f.Parse(remainingArgs[i:]); err != nil {
        return err
      }
      remainingArgs = f.Args()
      i = -1
      continue
    }
    args = append(args, arg)
  }
  if len(args) > 0 {
    argsField := reflect.ValueOf(f).Elem().FieldByName("args")
    argsPtr := (*[]string)(unsafe.Pointer(argsField.UnsafeAddr()))
    *argsPtr = args
  }
  return nil
}
// ParseToEnd is a drop-in replacement for flag.Parse. It improves upon the standard behavior by
// parsing flags even when they are interspersed with positional arguments. This overcomes Go's
// default limitation of stopping flag parsing upon encountering the first positional argument. For
// more details, see:
//
//   - https://github.com/golang/go/issues/4513
//   - https://github.com/golang/go/issues/63138
//
// This is a bit unforunate, but most users nowadays consuming CLI tools expect this behavior.
func ParseToEnd(f *flag.FlagSet, arguments []string) error {
  if err := f.Parse(arguments); err != nil {
    return err
  }
  if f.NArg() == 0 {
    return nil
  }
  var args []string
  remainingArgs := f.Args()
  for i := 0; i < len(remainingArgs); i++ {
    arg := remainingArgs[i]
    // If the arg looks like a flag, parses like a flag, and quacks like a flag, then it
    // probably is a flag.
    //
    // Note, there's an edge cases here which we EXPLICITLY do not handle, and quite honestly
    // 99.999% of the time you wouldn't build a CLI with this behavior.
    //
    // If you want to treat an unknown flag as a positional argument. For example:
    //
    //  $ ./cmd --valid=true arg1 --unknown-flag=foo arg2
    //
    // Right now, this will trigger an error. But *some* users might want that unknown flag to
    // be treated as a positional argument. It's trivial to add this behavior, by using VisitAll
    // to iterate over all defined flags (regardless if they are set), and then checking if the
    // flag is in the map of known flags.
    if len(arg) > 1 && arg[0] == '-' {
      // If we encounter a "--", treat all subsequent arguments as positional.
      if arg == "--" {
        args = append(args, remainingArgs[i:]...)
        break
      }
      if err := f.Parse(remainingArgs[i:]); err != nil {
        return err
      }
      remainingArgs = f.Args()
      i = -1 // Reset to handle newly parsed arguments.
      continue
    }
    args = append(args, arg)
  }
  if len(args) > 0 {
    // Access the unexported 'args' field in FlagSet using reflection and unsafe pointers.
    argsField := reflect.ValueOf(f).Elem().FieldByName("args")
    argsPtr := (*[]string)(unsafe.Pointer(argsField.UnsafeAddr()))
    *argsPtr = args
  }
  return nil
}

Gotcha

Disclaimer, there's 2 things to be aware of:

  1. It uses the unsafe package. The args field of a flag.FlagSet is unexported and there's no setter, so we need to use reflection and unsafe pointers to access it. Best case, the field name never changes, but there's never 100% guarantee with unexported fields.

  2. Some CLI author might want to treat an unknown flag as a positional argument. For example:

$ ./cmd --valid=true arg1 --unknown-flag=foo arg2
flag provided but not defined: -unknown-flag

The use case here is treating --unknown-flag=foo as a positional argument. It's fairly easy to solve, but I'm not sure if it's worth the effort.

If it looks like a flag, parses like a flag, and quacks like a flag, then it probably is a flag.