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?
-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
-- --
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