An old-new take on argument parsing in Rust
An old-new take on argument parsing in Rust
Rust 参数解析的一种“旧瓶装新酒”方案
Extending getopts into a small framework for Unix-style Rust command-line tools. 将 getopts 扩展为一个用于 Unix 风格 Rust 命令行工具的小型框架。
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. 多年来,我用许多不同的语言编写了数十个命令行应用程序——信不信由你,Shell 可能是其中的佼佼者——并且涵盖了各种生态系统。在此过程中,我对于它们应该如何表现形成了一些……怎么说呢……观点。但行为和实现是两个不同的话题,今天我想谈谈 Rust 生态系统中关于后者的一些想法。
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.
例如:如果你要用 Go 编写一个命令行应用程序,你会自然而然地使用内置的 flag 库来定义标志。这样做会让其他 Go 开发者觉得该工具很“正常”,也更容易阅读——但最终用户根本不在乎(我敢说,完全不在乎)你的工具是用什么语言编写的。因此,如果他们尝试在 coreutils 或 textutils 等标准 Unix 工具的上下文中使用此类工具,你的工具就会显得格格不入。这正是我所关心的:我为特定的生态系统开发工具,而不是为某种语言,我希望这些工具无论用什么语言编写,都能很好地集成在一起。
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. 我刚才提到了 Go,因为 Go 是那种以各种方式“泄露”其主观选择的典型例子。不过,本文讨论的是 Rust,所以让我们切换到 Rust 语言。
I can make your hands clap
我能让你拍手称快(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.
在编写 Rust 命令行应用程序时,现在的期望——或者更确切地说是假设——是你应该使用 clap crate 来解析选项和参数。有趣的是,这种假设在生态系统中根深蒂固,以至于当我让一个 2025 年末的编码代理审查我的代码库时,即使代码库中根本没有 clap,它也产生了幻觉,认为我正在使用它。
Here is what a simple clap-based hello world app looks like:
这是一个简单的基于 clap 的 hello world 应用程序的样子:
(Sample clap-based tool. On the left, the source code. On the right, an invocation without arguments and one invoking help.) (基于 clap 的示例工具。左侧是源代码,右侧是不带参数的调用以及调用帮助信息的示例。)
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).
我不会否认生成的应用程序看起来很漂亮,而且定义此接口的声明式惯用法既简洁又强大。但结果是……由于这些颜色(我知道可以禁用它们),它与其他程序显得格格不入。此外,代码有点太“魔法”了,这通常是 #[derive] 风格库的通病(我知道你可以选择不使用它)。
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.
然而……即使拥有所有这些花哨的功能,该库也没有提供足够的机制来“端到端”地定义一个应用程序。你看:Rust 的 main 函数可以返回一个 ExitCode,这足以向调用者报告成功或失败,但这仍然把应用程序的控制流留在了你手中。因为你必须在 main 中显式调用 clap,所以无法保证你在“正确的时间”执行它:你可能会倾向于在解析参数之前先解析配置文件,或者做其他类似糟糕的事情,这可能导致奇怪的行为,例如如果配置文件格式错误或位于不可用的网络驱动器上,--help 就无法工作。
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.
不过 clap 并不是唯一的选择。确实还有其他 Rust 库可以解析命令行,argh 是另一个流行的选择。其中一些也是围绕 derive 构建的,一些在帮助文本和输出风格上做了不同的权衡,还有一些较小的替代方案主要专注于解析选项。这些都很好,但它们仍然没有给我想要的那种小型 Unix 风格框架:一种将选项和位置参数视为统一接口、一致地验证两者,并掌控从解析到退出代码的整个启动序列的框架。
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: 出于上述所有原因,我开发了“我自己的方式”来解析 Rust 中的选项和参数,以便它们与更传统的 Unix 风格程序保持一致。在此过程中,我最终编写了自己的库。我最初是在 EndBOX 的背景下编写这个库的,当时我必须实现各种系统服务,并且希望:
- 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.
- 确保集成到由 NetBSD 基础镜像提供的宿主 Unix 风格工具生态系统中。
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.
当时我把那个库称为 libapp,在 2025 年秋天,我想到把它清理一下并发布出来。所以,今天我想迟来地宣布 getoptsargs。请注意,我早在 11 月就起草了这篇文章,但一直没发布,所以今天就是发布的日子。迟到总比不到好。
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.
你可能觉得 getoptsargs 这个名字很拗口,甚至很难听。你知道吗?确实如此。但这个名字之所以这样,是因为 getoptsargs 是在古老的 getopts 基础上构建和扩展的,这符合类 Unix 系统的传统。getopts 在今天的 Rust 生态系统中几乎无人问津——除了一个微小的事实:rustc 本身就在使用它。
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.
好吧,好吧,如果非要我诚实地说……getoptsargs 之所以存在,是因为 2016 年我开始学习 Rust 时,基于我之前对 POSIX getopt 和 GNU getopts libc 函数的了解,最初选用的就是 getopts……而且我从未改变过方向。
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:
getoptsargs 本质上是 getopts 的一个包装器,对其进行了扩展以提供参数解析功能。getopts 只留给你一堆需要手动验证的自由格式字符串,而 getoptsargs 允许你:
- 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 su…
因此,它的 API 试图遵循 getopts 提供的相同接口,这意味着我保留了原始名称,并以类似的方式对我的扩展进行了建模。这导致了一些相当晦涩的方法名称和……