|
| 1 | +--- |
| 2 | +title: 分享一个编译时命令构建库-zargs |
| 3 | +author: KiozWang |
| 4 | +date: 2025-03-12T20:12:00+08:00 |
| 5 | +--- |
| 6 | + |
| 7 | +See [zargs](https://github.com/kioz-wang/zargs)! |
| 8 | + |
| 9 | +# 为什么要再造一个轮子 |
| 10 | + |
| 11 | +重度命令行用户,工作中喜欢手搓一些小工具。命令行参数的设计决定了工具的易用程度,语言的表达能力决定了命令行参数设计的上限。 |
| 12 | + |
| 13 | +Rust 有 [clap](https://docs.rs/clap/latest/clap/):可以通过注解直接获得命令解析器,也可以通过方法的链式调用显式构建命令。可以生成良好的帮助信息,生成 shell 补全规则…… |
| 14 | +```rust |
| 15 | +use clap::Parser; |
| 16 | + |
| 17 | +/// Simple program to greet a person |
| 18 | +#[derive(Parser, Debug)] |
| 19 | +#[command(version, about, long_about = None)] |
| 20 | +struct Args { |
| 21 | + /// Name of the person to greet |
| 22 | + #[arg(short, long)] |
| 23 | + name: String, |
| 24 | + /// Number of times to greet |
| 25 | + #[arg(short, long, default_value_t = 1)] |
| 26 | + count: u8, |
| 27 | +} |
| 28 | + |
| 29 | +fn main() { |
| 30 | + let args = Args::parse(); |
| 31 | + for _ in 0..args.count { |
| 32 | + println!("Hello {}!", args.name); |
| 33 | + } |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +Python 有 [argparse](https://docs.python.org/zh-cn/3.13/library/argparse.html):动态弱类型语言的解析器,通过方法调用显式构建命令,运行时动态生成参数结构体,十分轻松的参数访问…… |
| 38 | +```python |
| 39 | +parser = argparse.ArgumentParser(prog='PROG', allow_abbrev=False) |
| 40 | +parser.add_argument('--foobar', action='store_true') |
| 41 | +parser.add_argument('--foonley', action='store_false') |
| 42 | +parser.parse_args(['--foon']) |
| 43 | +``` |
| 44 | + |
| 45 | +Cpp 有 [argparse](https://github.com/p-ranav/argparse):通过方法调用显式构建命令,方便的参数访问 |
| 46 | +```cpp |
| 47 | +#include <argparse/argparse.hpp> |
| 48 | + |
| 49 | +int main(int argc, char *argv[]) { |
| 50 | + argparse::ArgumentParser program("program_name"); |
| 51 | + |
| 52 | + program.add_argument("square") |
| 53 | + .help("display the square of a given integer") |
| 54 | + .scan<'i', int>(); |
| 55 | + |
| 56 | + try { |
| 57 | + program.parse_args(argc, argv); |
| 58 | + } |
| 59 | + catch (const std::exception& err) { |
| 60 | + std::cerr << err.what() << std::endl; |
| 61 | + std::cerr << program; |
| 62 | + return 1; |
| 63 | + } |
| 64 | + |
| 65 | + auto input = program.get<int>("square"); |
| 66 | + std::cout << (input * input) << std::endl; |
| 67 | + |
| 68 | + return 0; |
| 69 | +} |
| 70 | +``` |
| 71 | +
|
| 72 | +这些参数解析方案都有一些共同点:充分利用语言特性,尽可能集中的表达,方便的参数访问…… |
| 73 | +
|
| 74 | +Zig 是一个具有强大 comptime 能力的新系统级语言,为何还没有一个类似的方案? |
| 75 | +
|
| 76 | +# zargs 做了什么努力? |
| 77 | +
|
| 78 | +[zig-clap](https://github.com/Hejsil/zig-clap) 使用 comptime 解析 help message 来生成命令,独特而奇怪…… 为什么不能使用方法调用来实现?调用`clap.parse`返回解析结果,但位置参数混在Slice中且没有得到解析。 |
| 79 | +
|
| 80 | +[flags](https://github.com/joegm/flags) 要求用户直接定义出参数结构体,再在另外的地方描述参数配置,如选项、帮助信息等…… 有点太凌乱了 |
| 81 | +
|
| 82 | +zargs 想要为 Zig 实现一个充分利用 comptime 特性、尽可能集中描述参数配置、方便访问解析后的参数的参数解析方案。自认为 zargs 做到了! |
| 83 | +
|
| 84 | +以下语句创建一个编译时变量,用于构建命令 Command。其中,`.use_subCmd = "action"`表示该命令(demo)会包含子命令,同时可以通过参数结构体中名为`action`的联合枚举成员访问到: |
| 85 | +
|
| 86 | +```zig |
| 87 | +comptime var cmd: Command = .{ .name = "demo", .use_subCmd = "action", .description = "This is a simple demo" }; |
| 88 | +``` |
| 89 | + |
| 90 | +使用链式的方法调用,为 Command 添加选项或参数,在一个位置,完成选项或参数的新增和配置,包括默认值、自定义解析器、回调等……: |
| 91 | + |
| 92 | +```zig |
| 93 | +_ = cmd.opt("verbose", u32, .{ .short = 'v' }).optArg("output", []const u8, .{ .short = 'o', .long = "out", .default = "demo.bin" }); |
| 94 | +``` |
| 95 | + |
| 96 | +运行时调用编译时生成的解析器,得到参数结构体: |
| 97 | + |
| 98 | +```zig |
| 99 | +const args = cmd.parseAlloc(&it, allocator) catch |e| { |
| 100 | + std.debug.print("\nError => {}\n", .{e}); |
| 101 | + std.debug.print("{s}\n", .{cmd.usage()}); |
| 102 | + std.process.exit(1); |
| 103 | + }; |
| 104 | +``` |
| 105 | + |
| 106 | +访问解析得到的参数,就像访问自己在某处定义的结构体成员一样简单明了: |
| 107 | + |
| 108 | +```zig |
| 109 | +switch (args.action) { |
| 110 | + .install => |a| { |
| 111 | + std.debug.print("Installing {s}\n", .{a.name}); |
| 112 | + }, |
| 113 | + .remove => |a| { |
| 114 | + std.debug.print("Removing {s}\n", .{a.name}); |
| 115 | + }, |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +内置的 help 选项,当传入`-h`或`--help`时自动输出生成的帮助信息: |
| 120 | + |
| 121 | +```bash |
| 122 | +$ zig build ex-02.simple -- -h |
| 123 | +Usage: demo [-h|--help] [-v]... -o|--out {OUTPUT} [--] {install|remove} |
| 124 | + |
| 125 | +This is a simple demo |
| 126 | + |
| 127 | +[-h|--help] Show this help then exit |
| 128 | +[-v]... |
| 129 | + |
| 130 | +-o|--out {OUTPUT} |
| 131 | + |
| 132 | +install |
| 133 | +remove |
| 134 | +``` |
| 135 | + |
| 136 | +# 继续探索? |
| 137 | + |
| 138 | +阅读源代码、运行实例: |
| 139 | + |
| 140 | +```bash |
| 141 | +git clone [email protected]:kioz-wang/zargs.git |
| 142 | +``` |
| 143 | + |
| 144 | +或直接访问 https://github.com/kioz-wang/zargs ,先从 README 看起! |
| 145 | + |
| 146 | +## Command |
| 147 | + |
| 148 | +zargs 的核心能力:编译时命令构建器,就在 src/command.zig ! |
| 149 | + |
| 150 | +## TokenIter |
| 151 | + |
| 152 | +zargs 提供了多样的迭代器选择,可以方便地为应用的命令行接口编写测试用例。 |
| 153 | + |
| 154 | +## parseAny |
| 155 | + |
| 156 | +zargs 提供了通用的解析函数,当命令解析器完成任务后,还可以手动处理余下的参数。 |
| 157 | + |
| 158 | +# 未来? |
| 159 | + |
| 160 | +欢迎大家使用、宣传、提出改进建议!谢谢。 |
0 commit comments