Ilia Spiridonov
Software Engineer

Command Line Interfaces

  1. Parsing options
    1. Options as flags
    2. Options with required values
  2. Implicit vs explicit "literal mode"
  3. Subcommands

Personally, I love interacting with CLIs and always prefer them over GUIs, which I often find limiting and hard to use. Yeah, I still don't get why anyone would want a GUI for their Git.

With that said, unfortunately, while there are some strong conventions (e.g. that options look --like-this and short options are a single dash + a single letter), in practice, different process.argv parsers are pretty inconsistent and often lacking when dealing with trickier cases.

In this post, I'm going to list some of my expectations/wishes when it comes to parsing, and then I will probably turn this into an open-source command-line arguments parser project, because why not.

Parsing options

Options as flags

The biggest thing I don't like is not being able to just set a flag to false. Instead, people introduce inverse flags like --no-debug, which seems silly and stops making sense the moment somebody decides to make the debug mode turned off by default (and now a new flag --debug is needed).

Consider this instead:

--debug # same as --debug=true
--debug=false

# Alternative shorter syntax:
--debug=yes
--debug=no # Another Norway problem?
# Funny, but doesn't apply here

Another thing I want is being able to group multiple short options, like -abc, which should expand to -a -b -c. Doing -abc=false should be okay (but it's weird), while -abcfalse is generally ambiguous, so should fail.

Why?
Imagine that -c is a flag but -f is also defined.

Options with required values

When an option requires a value, it is not ambiguous to provide it in positional argument form:

--file foo.txt

The parser should not make me write --file=foo.txt or -Ffoo.txt.

With that said, if a value stands alone but looks like an option, it's probably a user mistake (forgot to pass a value):

# Should fail instead of consuming "--file" as a value
--file --file foo.txt

Implicit vs explicit "literal mode"

I don't like when the options I pass suddenly turn into positional arguments just because I put them after some "special" subcommand argument. There are many offenders, e.g. pnpm:

# This will send --filter to "build" script, not pnpm,
# because it comes after "run" subcommand
pnpm run build --filter @my/pkg

Of course, this issue is very old, and there's a well-known convention for working around it: passing -- argument, which means "ignore me, but treat everything that comes after as is". For example:

not-pnpm run build --filter @my/pkg -- --build-option
Obviously, when parsing -- -- the second -- must be treated as a plain positional argument and left as is, otherwise this violates the semantics of "literal mode".

Subcommands

The previous point brings us to the topic of subcommands. Sometimes, options are strictly required to be placed after the (sub)command, but before the (next) subcommand (if any). For example:

prog --verbose build # works
prog build --verbose # doesn't work,
# because --verbose doesn't belong to "build"

To me, that's not ergonomic. I like to type out all positional arguments first, and then throw in any necessary options at the end. It's frustrating to have this failing sometimes and need to shift certain options to the left in order to fix the invocation.

Here's the idea: allow putting options anywhere, as long as they come AFTER the (sub)command they belong to. And what if two or more (sub)commands define the same option? Then send it to the closest preceding one that defines it.

It may not be clean, but it is practical.


Enjoyed reading? Check out my other posts