Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions content/post/2025-03-12-zargs.md
Original file line number Diff line number Diff line change
@@ -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 <argparse/argparse.hpp>

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<int>("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
```

# 继续探索?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

目前看这个文章只是单纯的使用介绍,文章还是推荐有些干货分享,比如 zargs 怎么实现的,实现的难点是怎么解决的。

如果只是想推广,可以放到

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

明白,我修改补充下


阅读源代码、运行实例:

```bash
git clone [email protected]:kioz-wang/zargs.git
```

或直接访问 https://github.com/kioz-wang/zargs ,先从 README 看起!

## Command

zargs 的核心能力:编译时命令构建器,就在 src/command.zig !

## TokenIter

zargs 提供了多样的迭代器选择,可以方便地为应用的命令行接口编写测试用例。

## parseAny

zargs 提供了通用的解析函数,当命令解析器完成任务后,还可以手动处理余下的参数。

# 未来?

欢迎大家使用、宣传、提出改进建议!谢谢。