9.9 KiB
Problems with clap and other parsers
To ensure that this library is an improvement over the current situation, we
need to investigate what we want to change and what to keep from clap. In the
process, I'll also discuss some other parsers to see if we can take some
inspiration from them.
Before I continue, I want to note that these are not (always) general problems
with clap. They are problems that show up when you want to implement the
coreutils with it. The coreutils have some weird behaviour that you won't have
to deal with in a new project. clap is still a great library, and you
should probably use it over this library, unless you need compatibility with GNU
utilities.
Problem 1: No many-to-many relationship between arguments and settings
This is the biggest issue we have with clap. In clap, it is assumed that
options do not interfere with each other. This means that partially overriding
options are really hard to support. rm has --interactive and -f, which
mostly just override each other, because they set the interactive mode and
decide whether to print warnings. However, --interactive=never does not change
whether warnings are printed. Hence, they cannot override completely, because
then these two are not identical:
rm -f --interactive=never
rm --interactive=never
The only way we've come up with to support this in clap is by manually
comparing the indices between these options, which is not very nice.
This can get very complicated, as is the case in ls, where the
parsing of the format is very strange and error-prone.
Problem 2: We can't use the derive API
This is mostly due to the previous problem, but because arguments usually change
multiple settings, we cannot use the derive API of clap in most cases. We
could go for a hybrid between the derive and builder APIs, which clap does
support, but that feels overly complicated. Hence, we stuck with the builder
API.
Problem 3: Wrong defaults
The defaults of clap are often not what we want them to be. One might argue
clap's defaults are better, but we're aiming for compatibility with coreutils,
so we have no choice but to override them. Here are a few examples:
clap defaults |
coreutils | |
|---|---|---|
| help flags | -h and --help |
--help |
| version flags | -V and --version |
--version |
| Long option inference | Optional | Always |
| Conflicting options | Must be set to override | Always override |
| Leading hyphens | Must be set per argument1 | Always accepted |
Changing these defaults is sometimes just a single line, but other times it becomes quite verbose. In particular, setting the options to override becomes quite verbose in some cases.
Problem 4: Subtle differences
clap parses differently than getopt. This can be seen with optional values:
clap does not require a = between the flag and the value, but getopt does.
Instead, clap checks whether the next argument starts with a hyphen to check
whether the value is the value of the previous option or a new option.
Therefore, unless we tell clap that a = is required it will parse foo.txt
as the value to --color instead of as a file here:
ls --color foo.txt
Now assume there is some argument -f, --foo with an optional value. If we do
require =, then the behaviour is still not correct, because now clap also
requires a = for the short option. In the coreutils, however, = is never
used for a short option. Hence, the only way to get the desired behaviour is to
create multiple arguments.
But even then, there is no way to tell clap to consider the = as part of the
value. E.g. cut -d= will be parsed as cut -d'', which we have to work
around.
It happens quite often that we miss these subtle differences and therefore end up not being compatible with GNU coreutils. If we do want to do this correctly, it usually takes changing multiple settings to get the desired result.
Problem 5: Deprecated syntax of head, tail and uniq
As discussed in the argument types document, these utils support a shorthand
syntax for some options (e.g. -5 is short for -s 5). We have not managed to
implement these nicely with clap. Our best efforts try to filter these values
out of the arguments before passing them to clap, but it is extremely
difficult to handle all edge-cases.
Problem 6: Exit codes
In coreutils, different utils have different exit codes when they fail to parse.
For example, timeout returns 125, because the command it calls probably uses
1 or 2. There is no way to customize this in clap, so we work around it in
uutils and when we opened as issue for it, it was discarded. This makes sense
from clap's perspective, but it shows that the priorities between clap and
uutils diverge.
Problem 7: It's stringly typed
clap's arguments are identified by strings. This leads to code like this:
const OPT_NAME: &'static str = "name";
// -- snip --
fn main() {
let cmd = Command::new(...)
.arg(
Arg::new(OPT_NAME)
);
// -- snip --
let name = matches.get_one<String>(OPT_NAME);
}
There is no checking at compile time whether OPT_NAME has been registered as
an argument and if we wouldn't use a constant, it would be prone to typos. It
also leads to a big list of strings at the top of the file, which is not a big
deal, but a bit annoying.
Of course, we wouldn't have this problem if we were able to use the derive API.
Problem 8: Reading help string from a file
In uutils our help strings can get quite long. Therefore, we like to extract
those to an external file. With clap this means that we need to do some custom
preprocessing on this file to extract the information for the several pieces of
the help string that clap supports.
Problem 9: No markdown support
Granted, this is not really a problem, but more of a nice-to-have. We have online documentation for the utils, based on the help strings and these are rendered from markdown. Ideally, our argument parser supports markdown too, so that we can have nicely rendered help strings which have (roughly) the same appearance in the terminal and online.
Problem 10: No position-dependent argument-error prioritization
This is the question of which error to print if both -A and -B are given,
and both are individually an error somehow. In case of the GNU tools, only the
first error is printed, and then the program is aborted.
This also is not really a problem, but since it can be reasonably easily achieved by simply raising an error during argument application, this enables matching more closely the exact behavior of the GNU tools.
Good things about clap
Alright, enough problems. Let's praise clap a bit, because it's an excellent
library.
- The help text looks great (although I think we should turn off the textwrap feature).
- The error messages are very informative and provide a lot of context.
- It has no trouble dealing with invalid UTF-8.
- It is very configurable. The fact that we were able to work around most of our
issues at all, even though it might have been quite verbose is a great
accomplishment of
clap's developers. - It has support for generating completion information for many shells.
- We can access its internals to generate our online docs.
Other parsers
I'll do a quick rundown of other parsers and why they are not well-suited to the uutils project. I've included anything I could find, including obscure libraries.
lexopt- Great but very low-level.
- No help generation or other fancy features.
uutils-argsactually useslexoptunder the hood.
clap_lex- As discussed above,
clap's lexing is slightly different from coreutils. - Otherwise, it would be interesting to build on top of.
- As discussed above,
argh- Does not handle invalid UTF-8.
- It is also not configurable enough.
- Does not support a many-to-many relationship.
bpaf- Extremely flexible, even supports
dd-style. - A different configuration between short and long options requires a workaround.
- A many-to-many relation ship is possible, though not very ergonomic.
- For more information, see: https://github.com/uutils/uutils-args/issues/17
- Extremely flexible, even supports
gumdrop- Does not handle invalid UTF-8.
- Not configurable enough.
- Does not have the many-to-many relationship (options map directly to fields in a struct).
pico_args- Interesting, but does not seem to provide much over
lexopt.
- Interesting, but does not seem to provide much over
xflags- No different configuration between short and long options.
- Does not have the many-to-many relationship (options map directly to fields).
getopts- Was once used by uutils.
- No help generation.
- No many-to-many relationship.
getoptandlibc::getopt- No long options.
getopt_long- Cumbersome to use.
- Seems unmaintained.
- No license.
getopt_longfrom GNUlib- We can't use GNU code, because of the GPL.
- We also do not want to do this, because we don't want to depend on GNUlib.
-
There is a setting to set it for all arguments, but it behaves differently than setting it individually and leads to some troubles, due to the differences mentioned in the next section. ↩︎