I chose clap as the argument parsing crate for my book on Rust CLI programming and it's been my biggest regret. Folk sometimes complain that Rust itself is a moving target but I haven't found that to be the case at all. On the other hand, clap has gone through several, incompatible major releases over the past few years. Unfortunately, in order to keep this forward momentum, it also starts deprecating things which means that I had to pin it to a quickly outdated version. If I ever go back and update the earlier chapters I'd pick a simpler, slower-moving library.
On the other hand, this situation is a great example of why keeping things like argument parsing out of the Rust standard library is such a good idea. It's much better to let the community gel around some crates which are able to iterate (and innovate) faster than something with strict backwards-compatibility requirements. Looking at the discussion here there's clearly not one way to solve this - there's no way that something as large and complex as clap has evolved into will ever become "standard".
I'll take this opportunity to plug lexopt [0] a "pathologically simple CLI opt parser" crate. While I haven't quite had the opportunity to use it, I certainly will the next time it comes up.
structopt/Clap's derive magic is one of the first things I miss when I go to write some more-or-less trivial program in a non-Rust language these days. Being able to define all the data for a command line argument in one place (how/where to store it, what the type/valid input is, the association between the name and a variable/field, the documentation for --help...) seems like table stakes but afaict almost every other argument parsing library makes me repeat myself to the point where it takes all the joy out of writing a simple program.
I want to like docopt, but that the only data types it supports are boolean and string—if you want anything else, you have to do another round of parsing and error checking—destroys a lot of the advantage of using a high-level library for handling command-line arguments.
In my opinion, clap is a textbook example of over-engineering for a single metric (UX) at the expense of all other considerations (compilation speed, runtime cost, binary size, auditability, and maintainability). It is an 18kloc command-line parser with an additional 125kloc of dependencies that takes nearly 6 seconds to compile (‘only’ 400ms for an incremental build) and which adds nearly 690KiB to an optimised release binary (‘only’ 430KiB if you strip out most of the sugar that only clap provides).
There are many other command-line parsers to choose from that do all the key things that clap does, with half or less the build cost, and most of them with 30x less binary overhead[0]. argh is under 4kloc. gumdrop is under 2kloc. pico-args is under 700loc. What is the value of that extra 10kloc? A 10% better parser?
I am not saying there is no room for a library like clap—it is, at least, a triumphant clown car of features that can handle practically any edge-case anyone ever thought of—but if I got a nickel every time I spent 15 minutes replacing a trivial use of clap with pico-args and thus reduced the binary size and compile time of some project by at least 80%, I would have at least three nickels.
Just to try to pre-empt arguments like “disk space is cheap”, “compiler time is cheaper than human time”, etc.: there are no golden bullets in engineering, only trade-offs. Why would you default to the biggest, slowest option? This is the “every web site must be able to scale like Facebook” type logic. You don’t even have to do more work to use argh or gumdrop. If clap ends up having some magic feature that no other parser has that you absolutely need, you can switch, but I’ve yet to ever encounter such a thing. Its inertia and popularity carry it forward, but it is perhaps the last choice you should pick for a new project—not the first.
According to that link you posted, many of the other argument parsers don't even generate help, only one other offers multiple interfaces, and none of the others are noted as having color or suggested fixes.
These aren't exactly esoteric features, and you're not going to get them for free. I'm happy to pay in space and compile time for clap to gain those features.
This isn't a case of the commandline app needing to be facebook, but rather putting the exponential gains we've made in storage space to good use providing features which should be table stakes at this point.
You’re right that there are only trade-offs in engineering. But the key to evaluating trade-offs is evaluating impact, and how long my dependencies take to compile when I first check out a repo or whether it takes 1ms or 2ms to parse my command line (if we’re even talking about something above microseconds) have no discernible impact for approximately all use-cases. If you’re making some odd CLI tool that has to run on an microcontroller with 1MB of RAM or something, fine, agonize about whether your command line parser is parsimonious enough. Otherwise you’ve abjectly failed to evaluate one of the most important trade-offs in engineering: whether something is even worth your time to think about.
Hmm isn't optimizing to save 690KiB for an optimised release binary and getting incremental builds to be significantly less than 400ms actually much closer to the after-mentioned "every web site must be able to scale like Facebook” type logic" ?
I can understand this, if we were talking about JavaScript CLIs that requires GBs of dependencies. But 690KiB for modern computing is a drop in the ocean. It is not something you should base or make a consideration of unless you were doing embedded programming.
690KiB is a far compromise if Clap provided, for example, better performance or better code readability and organization. The benchmarks you provided shows the performance is practically the same which is close to no library usage.
I did do a bit of CLI work (I try to maintain https://github.com/rust-starter/rust-starter) and will always pick up clap. It just happens that even for the simplest CLIs out there, things can get hairy really fast. A good type interface that let me define my args as types and have extras on the fly (ie: Shell completion code) is worth the less than 1MiB overhead.
That 690KB savings is 1/97000th of the RAM on the machine I develop and run most of my Rust software on.
If I ever encounter a single howto or blog post or Stack Overflow answer that tells me how to use Clap to do something 5 minutes more quickly than with an alternative, it’s paid for itself.
Amdahl’s Law says you can’t optimize a system by tweaking a single component and get more than that component’s total usage back. If Clap takes 1% of a program’s resources, optimizing that down to 0 will still use 99% of the original resources.
> I got a nickel every time I spent 15 minutes replacing a trivial use of clap with pico-args and thus reduced the binary size and compile time of some project by at least 80%, I would have at least three nickels.
Hahaha, awesome. Thanks for the pico-args recommendation.
It supports the bare minimum.
I sure would like deriving-style parsing and --help auto-generation.
I think deriving-style unavoidably causes build time and complexity.
But it could be done without the dependency explosion.
Among the ones you recommend, argh supports deriving, auto-generates --help and optimizes for code size. And its syntax is very comparable to clap, so migrating is very easy. gumdrop seems very similar in its feature set (specifying help strings a little differently), but I can't find a defining feature for it.
> Why would you default to the biggest, slowest option?
Because it's not very big, nor very slow. Why wouldn't you default to the most full-featured option when its performance and space usage is adequate for the overwhelming majority of cases?
I like clap a lot, however I find that clap derive is not very easily discoverable. I always have to google for the right macro incantation to get what I want. Whereas editor completions from rust analyzer get me quite far without needing to leave my editor when I'm just using an ordinary library.
I think this is more a criticism of rust-analyzer than clap itself, any macro-heavy library I have similar issues with.
(Yes I know clap can be used without derive, but I'm willing to deal with the pain to parse directly into a struct)
I hope you don't mind me plugging my thing here, but I had the 100% same problem and made aargvark (https://docs.rs/aargvark/latest/aargvark/). When I was using clap, every time I'd need to look up how to do X, or what combination of things I needed to put an enum here, or find out that this nesting of data types wasn't supported, etc.
It's still derive macro-based, but there's only one derive (`Aargvark`) rather than `Parser`, `Subcommand`, etc, and it can handle any data structure composition orthogonally (although crazy structures may result in fairly awkward command lines).
FYI, maybe for you and other readers. My key to really understanding clap's derive macro was to understand that the macros takes as argument every methods of clap's Command struct https://docs.rs/clap/latest/clap/struct.Command.html
Looking at this resolved most of my issues about discoverability.
I feel like there's a sweet spot for complexity that the derive macro hits pretty well. When things get more complex it can feel like a maze, but below that complexity it's pretty nice.
Nice write up. “Good” CLI semantics are pretty devilish, and overall I think clap does a pretty great job of picking the right (or at least most intuitive) behavior.
(One edge case that consistently trips me up, which other argument parsers similarly struggle with: an environment variable fallback has the same “weight” as its option counterpart, so any CLI that makes use of grouping/exclusivity will eventually hit user confusions where the user passes `--exclusive` and gets a failure because of an unrelated environment variable.)
The argument / environment variable duality is a challenging one, especially when developing server software that should take security into account where you don't want to encourage users to put secrets into scripts. Do you end up with some items that can only be entered via one mechanism or another? Maybe that's where the fun of being a developer comes in is making those choices.
PowerShell has everything related to argument parsing, helptext generation, tab completion, type coercion, validation etc. built-in with a declarative DSL. Many things work even directly OOTB, without requiring special annotations.
It is by far the nicest way to create CLI tools I have ever seen. Every other shell or commandline parsing library I ever tried, feels extremely clunky in comparison.
… I don't want every program to attempt to implement argument parsing; bespoke implementations will be lower quality than clap. Not reinventing an argument parser is an extremely reasonably dependency to take.
Clap, on the non-derive side, has approximately two dependencies: anstream/anstyle (for terminal coloring, another thing that sounds deceptively simple at first pass, if you think all the world is a VT100, but really isn't; this is a reasonable dep. for a CLI arg parser) and strsim (string similarity, again, a reasonable dep for a CLI arg parser…). And that's it¹ for clap's direct deps.
(¹I'm omitting clap_lex, as an "internal" dep.)
On the derive side, there's the usual proc-macro2/quote/syn trio, but those come up frequently in derive crates due to what they do, and other than that, there's just `heck`, which is again an obvious dependency in context.
… what is so quizzical to me about the "so many dependencies!" complaint is that when we do get examples like this, they're almost always on crates that bottle up genuinely tricky functionality (like what we see here) — exactly the sort of thing that a.) is hard to get right and b.) isn't relevant to the problem I want to solve. That's like "absolutely this is a dependency" central, to me…
If all the code was crammed into the std library it'd be fine?
Functions need to build on top of simpler functions to be able to abstract problems and tackle them one at a time. There's innate complexity around and without trying to tame it into smaller functions/packages it seems you'll end up in a worse spot.
If you got a progress bar, website, and dependency tree for every
#include <argp.h>, <stdio.h>, <sstream>, or <curl.h>
it'd feel pretty crazy too. Imagine if `make` went out and pulled latest upstream changes for `pthreads` every time any one of your dependencies used it. C++ imagine it's pulling and building boost, or abseil.
C#? The entire mono/.net toolchain and system/ FFI libraries.
Imagine if we had "dot-h.io" that tracked how many separate C projects used argp. Laughable! Millions!
Every language has gobs of dependencies. So many dependencies it'd make you sick. Thousands upon thousands of lines of code, just to make something that runs on one target and says "Hello world" to the screen. Hell, some languages require you to run a runtime on your operating system that runs on real hardware _just to launch those thousands of lines of code_. And those are written using curl.h, pthreads.h, etc etc (or similar). Bananas!
At least those with package managers allow you to see it, audit it, update it seamlessly.
I have used clap to build perspt (https://github.com/eonseed/perspt). The project has extensive documentation on how it was built, as we did it as a learning exercise.
Something I’ve been working on recently is a command line tool [1] to bring clap declarative command line parsing to shell scripts. Unfinished WIP but largely functional.
The compile time guarantees + declarative nature make Clap so amazing and foolproof. This is like heaven compared to imperative, arcane incantations like getopt.
10kloc for command line parsing. TEN THOUSAND LINES. pico-args does it in 700 lines and probably handles 99% of real world use cases. compile times go to shit binary size bloats and for some edge case you'll never hit.most CLI tools need what three four flags max, maybe a subcommand or two. you don't need the swiss army knife of argument parsing for that. tried replacing clap with pico-args on three different projects last month. 80% reduction in compile time every single time. binary went from 8mb to 2mb on one of them.the "disk space is cheap" argument's acceptable partially but compile time isn't. developer experience isn't. startup time isn't. memory usage isn't
No help generation
Only flags, options, free arguments and subcommands are supported
A properer parser would knew that --arg2 is a key and will return an error, since the value is missing.
If such behavior is unacceptable to your application, then you have to use a more high-level arguments parsing library.
Yeah, no thank you. If we're talking about 700 LOC, I'm just going to write it myself rather than take on a dependency that won't even describe itself as a proper enough parser. This argument parser doesn't even handle the tedium of generating a help message for the user, and doesn't really parse the arguments -- what's the purpose of using it to do the argument parsing then?
So 700 LOC gets us a mediocre argument parser with no features. What do you get for an additional 9300 LOC? A "properer" parser (dev and user experience+). Help generation (dev experience+). Multiple interfaces (dev experience+). Color terminal output (user experience+). Suggested completions (user experience+).
Is it worth it? I dunno that's a per-project choice. If you absolutely need the smallest footprint and compile times possible, probably you don't want to go with clap. You also probably don't want to go with Rust.
Honestly, if you're doing something so small, even pico-args is a lot more than you need. Just use lexopt, and you get a very simple match-based DSL for defining your arguments, that even neatly sidesteps the limitations provided in pico.
mprovost|8 months ago
On the other hand, this situation is a great example of why keeping things like argument parsing out of the Rust standard library is such a good idea. It's much better to let the community gel around some crates which are able to iterate (and innovate) faster than something with strict backwards-compatibility requirements. Looking at the discussion here there's clearly not one way to solve this - there's no way that something as large and complex as clap has evolved into will ever become "standard".
Fluorescence|8 months ago
Major versions have good quality migration guides. If only all libraries were developed with such professionalism.
https://github.com/clap-rs/clap/blob/v4.5.0/CHANGELOG.md
packetlost|8 months ago
[0]: https://docs.rs/lexopt/latest/lexopt/
ben0x539|8 months ago
(Python's docopt is also amazing, fwiw)
ameliaquining|8 months ago
iloveyoudearly|8 months ago
It has quirks once you try to do something more complex/advanced, but for most of the simple stuff it's very nice to use.
wonger_|8 months ago
There's implementations for other languages, too.
TIL about structopt, thanks.
csnover|8 months ago
There are many other command-line parsers to choose from that do all the key things that clap does, with half or less the build cost, and most of them with 30x less binary overhead[0]. argh is under 4kloc. gumdrop is under 2kloc. pico-args is under 700loc. What is the value of that extra 10kloc? A 10% better parser?
I am not saying there is no room for a library like clap—it is, at least, a triumphant clown car of features that can handle practically any edge-case anyone ever thought of—but if I got a nickel every time I spent 15 minutes replacing a trivial use of clap with pico-args and thus reduced the binary size and compile time of some project by at least 80%, I would have at least three nickels.
Just to try to pre-empt arguments like “disk space is cheap”, “compiler time is cheaper than human time”, etc.: there are no golden bullets in engineering, only trade-offs. Why would you default to the biggest, slowest option? This is the “every web site must be able to scale like Facebook” type logic. You don’t even have to do more work to use argh or gumdrop. If clap ends up having some magic feature that no other parser has that you absolutely need, you can switch, but I’ve yet to ever encounter such a thing. Its inertia and popularity carry it forward, but it is perhaps the last choice you should pick for a new project—not the first.
[0] https://github.com/rosetta-rs/argparse-rosetta-rs
ModernMech|8 months ago
These aren't exactly esoteric features, and you're not going to get them for free. I'm happy to pay in space and compile time for clap to gain those features.
This isn't a case of the commandline app needing to be facebook, but rather putting the exponential gains we've made in storage space to good use providing features which should be table stakes at this point.
johncolanduoni|8 months ago
qaq|8 months ago
WiSaGaN|8 months ago
1vuio0pswjnm7|8 months ago
csomar|8 months ago
690KiB is a far compromise if Clap provided, for example, better performance or better code readability and organization. The benchmarks you provided shows the performance is practically the same which is close to no library usage.
I did do a bit of CLI work (I try to maintain https://github.com/rust-starter/rust-starter) and will always pick up clap. It just happens that even for the simplest CLIs out there, things can get hairy really fast. A good type interface that let me define my args as types and have extras on the fly (ie: Shell completion code) is worth the less than 1MiB overhead.
unknown|8 months ago
[deleted]
kstrauser|8 months ago
If I ever encounter a single howto or blog post or Stack Overflow answer that tells me how to use Clap to do something 5 minutes more quickly than with an alternative, it’s paid for itself.
Amdahl’s Law says you can’t optimize a system by tweaking a single component and get more than that component’s total usage back. If Clap takes 1% of a program’s resources, optimizing that down to 0 will still use 99% of the original resources.
It’s just not worth it.
sshine|8 months ago
Hahaha, awesome. Thanks for the pico-args recommendation.
It supports the bare minimum.
I sure would like deriving-style parsing and --help auto-generation.
I think deriving-style unavoidably causes build time and complexity.
But it could be done without the dependency explosion.
There's a list of options here:
https://github.com/rosetta-rs/argparse-rosetta-rs#rust-arg-p...
Among the ones you recommend, argh supports deriving, auto-generates --help and optimizes for code size. And its syntax is very comparable to clap, so migrating is very easy. gumdrop seems very similar in its feature set (specifying help strings a little differently), but I can't find a defining feature for it.
lmm|8 months ago
Because it's not very big, nor very slow. Why wouldn't you default to the most full-featured option when its performance and space usage is adequate for the overwhelming majority of cases?
pepa65|8 months ago
theknarf|8 months ago
tcfhgj|8 months ago
hamandcheese|8 months ago
I think this is more a criticism of rust-analyzer than clap itself, any macro-heavy library I have similar issues with.
(Yes I know clap can be used without derive, but I'm willing to deal with the pain to parse directly into a struct)
rendaw|8 months ago
It's still derive macro-based, but there's only one derive (`Aargvark`) rather than `Parser`, `Subcommand`, etc, and it can handle any data structure composition orthogonally (although crazy structures may result in fairly awkward command lines).
jrimbault|8 months ago
Looking at this resolved most of my issues about discoverability.
tucson-josh|8 months ago
woodruffw|8 months ago
(One edge case that consistently trips me up, which other argument parsers similarly struggle with: an environment variable fallback has the same “weight” as its option counterpart, so any CLI that makes use of grouping/exclusivity will eventually hit user confusions where the user passes `--exclusive` and gets a failure because of an unrelated environment variable.)
tucson-josh|8 months ago
fainpul|8 months ago
It is by far the nicest way to create CLI tools I have ever seen. Every other shell or commandline parsing library I ever tried, feels extremely clunky in comparison.
https://learn.microsoft.com/en-us/powershell/module/microsof...
ameliaquining|8 months ago
msgodel|8 months ago
It's like the node.js of systems languages. Touching it feels gross.
deathanatos|8 months ago
Clap, on the non-derive side, has approximately two dependencies: anstream/anstyle (for terminal coloring, another thing that sounds deceptively simple at first pass, if you think all the world is a VT100, but really isn't; this is a reasonable dep. for a CLI arg parser) and strsim (string similarity, again, a reasonable dep for a CLI arg parser…). And that's it¹ for clap's direct deps.
(¹I'm omitting clap_lex, as an "internal" dep.)
On the derive side, there's the usual proc-macro2/quote/syn trio, but those come up frequently in derive crates due to what they do, and other than that, there's just `heck`, which is again an obvious dependency in context.
… what is so quizzical to me about the "so many dependencies!" complaint is that when we do get examples like this, they're almost always on crates that bottle up genuinely tricky functionality (like what we see here) — exactly the sort of thing that a.) is hard to get right and b.) isn't relevant to the problem I want to solve. That's like "absolutely this is a dependency" central, to me…
sebtron|8 months ago
[1] https://doc.rust-lang.org/book/ch02-00-guessing-game-tutoria...
dietr1ch|8 months ago
Functions need to build on top of simpler functions to be able to abstract problems and tackle them one at a time. There's innate complexity around and without trying to tame it into smaller functions/packages it seems you'll end up in a worse spot.
jvanderbot|8 months ago
#include <argp.h>, <stdio.h>, <sstream>, or <curl.h>
it'd feel pretty crazy too. Imagine if `make` went out and pulled latest upstream changes for `pthreads` every time any one of your dependencies used it. C++ imagine it's pulling and building boost, or abseil.
C#? The entire mono/.net toolchain and system/ FFI libraries.
Imagine if we had "dot-h.io" that tracked how many separate C projects used argp. Laughable! Millions!
Every language has gobs of dependencies. So many dependencies it'd make you sick. Thousands upon thousands of lines of code, just to make something that runs on one target and says "Hello world" to the screen. Hell, some languages require you to run a runtime on your operating system that runs on real hardware _just to launch those thousands of lines of code_. And those are written using curl.h, pthreads.h, etc etc (or similar). Bananas!
At least those with package managers allow you to see it, audit it, update it seamlessly.
If it's too big, use "nostd"
tcfhgj|8 months ago
vikrantrathore|8 months ago
porphyra|8 months ago
mootoday|8 months ago
I've been building clap CLIs for a while and started to put together a template: https://github.com/mootoday/cli-template.
It also includes a crate I developed to reduce the boilerplate code for nested commands: https://crates.io/crates/clap-nested-commands
tucson-josh|8 months ago
FujiApple|8 months ago
[1] https://github.com/fujiapple852/claptrap
gametorch|8 months ago
tempodox|8 months ago
b0a04gl|8 months ago
ModernMech|8 months ago
So 700 LOC gets us a mediocre argument parser with no features. What do you get for an additional 9300 LOC? A "properer" parser (dev and user experience+). Help generation (dev experience+). Multiple interfaces (dev experience+). Color terminal output (user experience+). Suggested completions (user experience+).
Is it worth it? I dunno that's a per-project choice. If you absolutely need the smallest footprint and compile times possible, probably you don't want to go with clap. You also probably don't want to go with Rust.
MrJohz|8 months ago