# flagstyle: keep flags before the positional arguments

there are many schools of thought about command line flags:

as with everything with go, i found the ordering rule for the flags weird at first. but over time i learned to appreciate it. now it's my favorite style.

over time i also developed a few more rules i personally adhere to when passing flags:

when it makes sense i sometimes add checks to my tools to enforce the second rule to eliminate potential ambiguity.

but why?

# subcommands

some tools do this:

  toolname -globalflag1=value1 subcommand -subflag2=value2 arg1 arg2

in this case -subflag2 is a subcommand specific flag and must come after subcommand. i personally don't like this. as a user i can't really remember which flag is global which flag is subcommand specific. this also allows redefining the same flag (such as -help or -verbose) twice and then the confusion intensifies. the form should be this:

  toolname -globalflag1=value1 -subflag2=value2 subcommand arg1 arg2

when tool is initializing it should find the subcommand and register its flags into the global flag namespace. this should be done before all the flags are defined because the flag definitions depend on the subcommand. but extracting the subcommand without knowing which flags are bools is only possible if all non-bool flags use the "-flagname=value" form. that's why i enforce that form in my tools.

as an example let's take a hypothetical "compressor" application with two subcommands, "compress" and "decompress". running without any argument or just a -help would print a generic help message:

  $ compressor --help
  usage of compressor: compressor [flags...] [subcommand]

  subcommands:

    compress:   compress a file.
    decompress: decompress a file.

  use `compressor -help [subcommand]` to get more help.

running the help for a subcommand would print both the subcommand specific and global flags separately:

  $ compressor -help compress
  usage of the compress subcommand: compressor [flags...] compress

  compresses a file.

  subcommand flags:
    -input string
          input filename. (default "/dev/stdin")
    -level int
          compression level between 1 and 9, 9 the best but slowest. (default 5)
    -output string
          output filename. (default "/dev/stdout")

  global flags:
    -force
          auto-confirm all confirmation prompts. dangerous.
    -verbose
          print debug information.

and it would also detect incorrect usage:

  $ compressor -level 6 compress
  error: main.UnknownSubcommand subcommand=6
  exit status 1

  $ compressor compress -level=6
  error: main.BadFlagOrder arg=-level=6 (all flags must come before the subcommand and must have the -flag=value form)
  exit status 1

both global and verbose flags must come before the subcommand:

  $ compressor -verbose -level=6 compress
  compressing /dev/stdin into /dev/stdout, level=6, verbose=true.

see @/flagstyle.go for one potential (not necessarily the nicest) way to implement this. it uses reflection to magically create flags from structs. notice how the subcommand detection happens before flag.Parse(). that's only possible if all flag values use the -name=value syntax, hence the check for it.

# command wrapping

the command wrapping usecase is my primary motivation to have all flags as left as possible. take something like ssh:

  ssh [ssh_flags...] [machine-name] [command] [command-args...]
  # example:
  ssh -X myserver uname -a
  # go flag parsing:
  ssh -X jumphost ssh -X myserver uname -a
  # getopt flag parsing:
  ssh -X -- jumphost ssh -X myserver -- uname -a

you have to litter the commandline with --. some people like this sort of separation. but i am now using such commands extensively for years and i prefer to not have the -- markers. the former style gets natural very fast.

it might seem a rare usecase but at work i work with surprisingly many tools that have some sort of "pass/forward all subsequent args unchanged" needs:

i rely on these tools so much that i had to learn to keep my flags on left. then i might as well do it so everywhere. i started doing that and realized my life is much easier.

# short options

some people love short options. e.g. they can write "ls -lh" instead of "ls --long --human-readable". i don't miss short options in my tools. if that's really needed then perhaps make the first arg a short option collection like in tar or ps unix commands:

  # create tar, verbose output, output file is output.tar:
  tar cvf output.tar file1 file2 ...
  # show all processes, format nicely:
  ps auxw

ls interface could have been similar:

  # show permissions, owner, and name:
  ls pon directory1 directory2 ...

or if sacrificing the first positional argument feels too much then put all that into a single flag:

  $ ls --help
  ...
  flags:
    -show=flags: pick the fields to show for each entry.
  ...

  $ ls -show=pon directory1 directory2 ...

# takeaways

in summary my recommendation is to only allow -flag=value form of flags and all flags must be on the left before the positional arguments. it's awkward at first but one gets used to it quickly and it allows combining commands in a more natural manner. this in turn leads to a more pleasant command line experience with fewer gotchas. shells have already too many gotchas anyway.

published on 2024-11-11


posting a comment requires javascript.

to the frontpage