diff --git a/content/post/2025-03-12-zargs.md b/content/post/2025-03-12-zargs.md new file mode 100644 index 0000000..992561a --- /dev/null +++ b/content/post/2025-03-12-zargs.md @@ -0,0 +1,160 @@ +--- +title: 分享一个编译时命令构建库-zargs +author: KiozWang +date: 2025-03-12T20:12:00+08:00 +--- + +See [zargs](https://github.com/kioz-wang/zargs)! + +# 为什么要再造一个轮子 + +重度命令行用户,工作中喜欢手搓一些小工具。命令行参数的设计决定了工具的易用程度,语言的表达能力决定了命令行参数设计的上限。 + +Rust 有 [clap](https://docs.rs/clap/latest/clap/):可以通过注解直接获得命令解析器,也可以通过方法的链式调用显式构建命令。可以生成良好的帮助信息,生成 shell 补全规则…… +```rust +use clap::Parser; + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Name of the person to greet + #[arg(short, long)] + name: String, + /// Number of times to greet + #[arg(short, long, default_value_t = 1)] + count: u8, +} + +fn main() { + let args = Args::parse(); + for _ in 0..args.count { + println!("Hello {}!", args.name); + } +} +``` + +Python 有 [argparse](https://docs.python.org/zh-cn/3.13/library/argparse.html):动态弱类型语言的解析器,通过方法调用显式构建命令,运行时动态生成参数结构体,十分轻松的参数访问…… +```python +parser = argparse.ArgumentParser(prog='PROG', allow_abbrev=False) +parser.add_argument('--foobar', action='store_true') +parser.add_argument('--foonley', action='store_false') +parser.parse_args(['--foon']) +``` + +Cpp 有 [argparse](https://github.com/p-ranav/argparse):通过方法调用显式构建命令,方便的参数访问 +```cpp +#include + +int main(int argc, char *argv[]) { + argparse::ArgumentParser program("program_name"); + + program.add_argument("square") + .help("display the square of a given integer") + .scan<'i', int>(); + + try { + program.parse_args(argc, argv); + } + catch (const std::exception& err) { + std::cerr << err.what() << std::endl; + std::cerr << program; + return 1; + } + + auto input = program.get("square"); + std::cout << (input * input) << std::endl; + + return 0; +} +``` + +这些参数解析方案都有一些共同点:充分利用语言特性,尽可能集中的表达,方便的参数访问…… + +Zig 是一个具有强大 comptime 能力的新系统级语言,为何还没有一个类似的方案? + +# zargs 做了什么努力? + +[zig-clap](https://github.com/Hejsil/zig-clap) 使用 comptime 解析 help message 来生成命令,独特而奇怪…… 为什么不能使用方法调用来实现?调用`clap.parse`返回解析结果,但位置参数混在Slice中且没有得到解析。 + +[flags](https://github.com/joegm/flags) 要求用户直接定义出参数结构体,再在另外的地方描述参数配置,如选项、帮助信息等…… 有点太凌乱了 + +zargs 想要为 Zig 实现一个充分利用 comptime 特性、尽可能集中描述参数配置、方便访问解析后的参数的参数解析方案。自认为 zargs 做到了! + +以下语句创建一个编译时变量,用于构建命令 Command。其中,`.use_subCmd = "action"`表示该命令(demo)会包含子命令,同时可以通过参数结构体中名为`action`的联合枚举成员访问到: + +```zig +comptime var cmd: Command = .{ .name = "demo", .use_subCmd = "action", .description = "This is a simple demo" }; +``` + +使用链式的方法调用,为 Command 添加选项或参数,在一个位置,完成选项或参数的新增和配置,包括默认值、自定义解析器、回调等……: + +```zig +_ = cmd.opt("verbose", u32, .{ .short = 'v' }).optArg("output", []const u8, .{ .short = 'o', .long = "out", .default = "demo.bin" }); +``` + +运行时调用编译时生成的解析器,得到参数结构体: + +```zig +const args = cmd.parseAlloc(&it, allocator) catch |e| { + std.debug.print("\nError => {}\n", .{e}); + std.debug.print("{s}\n", .{cmd.usage()}); + std.process.exit(1); + }; +``` + +访问解析得到的参数,就像访问自己在某处定义的结构体成员一样简单明了: + +```zig +switch (args.action) { + .install => |a| { + std.debug.print("Installing {s}\n", .{a.name}); + }, + .remove => |a| { + std.debug.print("Removing {s}\n", .{a.name}); + }, +} +``` + +内置的 help 选项,当传入`-h`或`--help`时自动输出生成的帮助信息: + +```bash +$ zig build ex-02.simple -- -h +Usage: demo [-h|--help] [-v]... -o|--out {OUTPUT} [--] {install|remove} + +This is a simple demo + +[-h|--help] Show this help then exit +[-v]... + +-o|--out {OUTPUT} + +install +remove +``` + +# 继续探索? + +阅读源代码、运行实例: + +```bash +git clone git@github.com:kioz-wang/zargs.git +``` + +或直接访问 https://github.com/kioz-wang/zargs ,先从 README 看起! + +## Command + +zargs 的核心能力:编译时命令构建器,就在 src/command.zig ! + +## TokenIter + +zargs 提供了多样的迭代器选择,可以方便地为应用的命令行接口编写测试用例。 + +## parseAny + +zargs 提供了通用的解析函数,当命令解析器完成任务后,还可以手动处理余下的参数。 + +# 未来? + +欢迎大家使用、宣传、提出改进建议!谢谢。