Extending getopts into a small framework for Unix-style Rust command-line tools.
Over the years, I’ve written tens of command-line applications in many different languages—shell is probably the top contender, believe it or not—and for various ecosystems. Along the way, I’ve developed… let’s say… opinions on how they should behave. But behavior and implementation are different topics, and today I would like to talk a little about the latter in the Rust ecosystem.
The ecosystem
A big theme behind those opinions is that consistency usually wins: when designing an application, you should target an ecosystem and make sure the tool feels “at home” within it instead of reinventing the way it accepts arguments or presents help.
But what is the ecosystem?
- Is it the language the tool is written in, or…
- is it the set of tools with which it plays?
For example: if you were to write a command-line application in Go, you’d naturally reach for the built-in flag library to define flags. Doing so would make the tool feel normal to other Go developers and would make it easier to “read” to them—but the end user does not care, dare I say… at all, which language your tool is written in. So if they try to use such a tool in the context of standard Unix tools like those provided by coreutils or textutils, your tool will feel out of place. And that is what matters to me: I develop tools for a certain ecosystem, not for a language, and I want those tools to integrate well no matter which language they are written in.
I mentioned Go right above because Go is the prime example of opinionated choices that “leak” in various ways. This article is about Rust, however, so let’s switch languages.
I can make your hands clap
When writing Rust command-line applications, the expectation nowadays—or rather, assumption—is that you’ll use the clap crate to parse options and arguments. Funnily enough, this assumption is so ingrained in the ecosystem that, when I asked a late-2025 coding agent to review a codebase of mine, it hallucinated that I was using clap even when such crate was nowhere to be found.
Here is what a simple clap-based hello world app looks like:

I will not deny that the resulting app looks nice and that the declarative idiom to define this interface is concise and very powerful. But the result is… out of place with other programs because of all these colors (I know they can be disabled). Also, the code is a bit too magical, as usually happens with #[derive]-style libraries (I know you can opt out of that).
And yet… even with all the bells and whistles, the library doesn’t provide enough mechanisms to define an app “end to end”. You see: Rust’s main can return an ExitCode, which is enough to report success or failure to the caller, but this still leaves the application’s control flow in your hands. Because you have to explicitly call clap within main, there is no guarantee that you do it at “the right time”: you might be tempted to parse config files before parsing arguments and other nasty things like that, which can then lead to weird behavior like --help not working if the config file is malformed or on an unavailable network drive.
clap isn’t the only game in town though. There are indeed other Rust libraries to parse command lines, with argh being another popular choice. Some of these are also built around derives, some make different tradeoffs around help text and output style, and some smaller alternatives focus mostly on parsing options. This is all fine, but it still doesn’t give me the small Unix-y framework I wanted: something that treats options and positional arguments as one interface, validates both consistently, and owns the startup sequence from parsing to exit code.
Enter simpler times
For all of the reasons above, I’ve developed “my own ways” to parse options and arguments in Rust so that they align with more traditional Unix-y programs. In doing so, I ended up writing my own library. I initially wrote this library in the context of the EndBOX where I had to implement various system services and wanted:
- to enforce consistency among them with as little code duplication as possible, and
- to ensure integration into the host’s ecosystem of Unix-y tools provided by the NetBSD base image.
I called that library libapp at the time and, in the fall of 2025, I thought of cleaning it up a little and publishing it. So, today, I want to belatedly announce getoptsargs. Mind you, I had drafted this article back in November but never published it, so today is the day. Better late than never.
You might be thinking that getoptsargs is quite a mouthful, and even an ugly name. And you know what? That’s true. But the name is what it is because getoptsargs builds on and extends the ancient getopts, in the tradition of Unix-like systems. getopts is largely unused today in the Rust ecosystem—except for the tiny little fact that rustc itself uses it.
OK, OK, if you want me to be perfectly honest… the reason getoptsargs exists at all is because getopts is what I originally picked up in 2016 when I started learning Rust based on my previous knowledge of the POSIX getopt and the GNU getopts libc functions… and I never switched gears.
The basics
getoptsargs is basically a wrapper over getopts, extending it to offer argument-parsing facilities. Where getopts leaves you with a list of free-form strings to validate by hand, getoptsargs lets you:
- declare positional arguments with cardinality constraints,
- validates those constraints for you,
- prints them as a distinct section in the generated help, and
- provides helpers for common application metadata such as version, bug-reporting, home page, and manual-page information.
As such, its API tries to follow the same interfaces that getopts offers, which means I’ve kept original names intact and modeled my own extensions in a similar fashion. This leads to rather cryptic method names and suboptimal Rust interfaces, but again, I tried to mimic getopts as much as possible.
What I’ve changed, however, is the way in which you should use the library. getoptsargs provides an “end-to-end” framework to define the main method of an application, and it does so with three pieces. The first is the application Builder, which is a struct that implements the builder pattern to register application metadata, options, and arguments. The second is the command-line Matches, which extends the getopts struct for parsed options with access to parsed arguments. And the third is an app macro that facilitates writing the scaffolding for main, delegating to a couple of functions.
A sample, full-featured getoptsargs program looks like this:
use getoptsargs::prelude::*;
fn app_setup(builder: Builder) -> Builder {
builder
.copyright("Copyright 2026 Julio Merino")
.bugs("https://example.com/hello/issues/")
.homepage("https://hello.example.com/")
.manpage("hello", "1")
.optflag("y", "yell", "yell in uppercase")
.trailarg("name", 1, usize::MAX, "names of the people to say hello to")
}
fn app_main(matches: Matches) -> Result<i32> {
let mut message = format!("Hello, {}!", matches.arg_trail().join(", "));
if matches.opt_present("yell") {
message = message.to_ascii_uppercase();
}
println!("{}", message);
Ok(0)
}
app!("Hello", app_setup, app_main);
And then we can run it in various ways:
$ ./target/debug/hello
Usage error: Trailing argument `name` requires at least 1 value
Type `hello --help` or `man 1 hello` for more information
$ ./target/debug/hello -h
Usage: hello [options] name1 [.. nameN]
Options:
-h, --help show command-line usage information and exit
--version show version information and exit
-y, --yell yell in uppercase
Arguments:
name1 [.. nameN] names of the people to say hello to
Report bugs to: https://example.com/hello/issues/
Hello home page: https://hello.example.com/
$ ./target/debug/hello --version
Hello 0.1.0
Copyright 2026 Julio Merino
$ ./target/debug/hello -y julio
HELLO, JULIO!
$ █
You might still say: there is too much magic in those macros and builders! And you’re right, so you can also define the same app using imperative code and avoid all of that. To see how, I’ll refer you to the various upstream examples.
The future
I do not intend for this crate to replace the nicer clap or argh or the other libraries that exist out there. Heck, I do not even expect any of you to want to use it. But I still have a use for something like this in my own programs, and I wanted to factor out the code I had already written into a cohesive standalone piece, so I had to publish the crate.
Head to https://github.com/jmmv/getoptsargs/ for more details!