Files
uutils-args/design/problems_with_clap.md
2023-01-25 10:48:37 +01:00

9.4 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 really good library and you should probably use it over this library, unless you need compatibility with GNU utils.

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 nog 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 6: 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 7: 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 8: 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.

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-args actually uses lexopt under 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.
  • 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/tertsdiepraam/uutils-args/issues/17
  • 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.
  • 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.
  • getopt and libc::getopt
    • No long options.
  • getopt_long
    • Cumbersome to use.
    • Seems unmaintained.
    • No license.
  • getopt_long from 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.

  1. 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. ↩︎