zopt:Zig 语言的低仪式感命令行参数解析库
zopt 是一个为 Zig 语言设计的低仪式感(low-ceremony)命令行参数解析库。它采用惰性求值和"读取时模式"(schema on read)语义——也就是说,它在你查询时才扫描 argv,而不是预先根据 schema 解析。zopt 将帮助文本、错误/缺失处理、默认值都交给用户的应用代码来处理。它是 std.process.Args 的刻意精简的封装,而不是功能齐全的框架,因此最适合选项集有限、不需要 schema 验证的小型命令行工具。
设计哲学
低仪式感(Low-Ceremony)
zopt 的核心设计理念是"低仪式感":
- 不需要预先定义 schema:不像其他 CLI 库那样需要先定义完整的参数定义
- 不需要注册回调:不需要为每个参数注册回调函数
- 不需要生成代码:不需要代码生成步骤
- 直接查询:直接查询你需要的参数,库在查询时扫描 argv
这种设计使得 zopt 的 API 非常简洁,学习成本低。
Schema on Read
zopt 采用"读取时模式"语义:
传统方式 (Schema on Write):
定义 schema → 解析 argv → 验证 → 查询参数
zopt 方式 (Schema on Read):
直接查询参数 → 库在查询时扫描 argv → 返回结果
优势:
- 灵活:不需要预先知道所有参数
- 简单:API 更简单,不需要定义 schema
- 快速启动:小型工具可以快速上手
- 按需解析:只解析实际查询的参数
刻意精简
zopt 是 std.process.Args 的刻意精简的封装:
- 不做的事:
- 不生成帮助文本(由应用代码处理)
- 不做 schema 验证(由应用代码处理)
- 不处理默认值(由应用代码用
orelse处理) - 不处理错误/缺失(由应用代码处理)
- 做的事:
- 扫描 argv,匹配 flags 和 options
- 跟踪已查询的 flags/options
- 提供位置参数
- 检测未知参数
这种设计哲学是:库只做最核心的事,其他交给应用代码。
安装
添加依赖
# 最新版本
zig fetch --save git+https://codeberg.org/hgrsd/zopt/#HEAD
# 指定版本
zig fetch --save git+https://codeberg.org/hgrsd/zopt/#v0.0.11
在 build.zig.zon 中
添加后,build.zig.zon 中会包含 zopt 的依赖。
导入
const Zopt = @import("zopt").Zopt;
API 概览
基本用法
const std = @import("std");
const Zopt = @import("zopt").Zopt;
pub fn main(init: std.process.Init) !void {
const arena = init.arena.allocator();
const z = try Zopt.from(arena, init.minimal.args);
// 查询 flag
const verbose = z.flag('v', "verbose");
// 查询 option
const output = z.option('o', "output");
// 查询位置参数
const positional = try z.positional(0);
}
Flag 查询
Flag 是布尔型的命令行开关:
// 匹配 -F, --foo, 或作为多个 flag 的一部分(如 "-FAX")
const flag = z.flag('F', "foo");
// 仅短 flag:匹配 -F,或作为多个 flag 的一部分(如 "-FAO")
const short_flag = z.flag('F', null);
// 仅长 flag:匹配 --foo
const long_flag = z.flag(null, "foo");
Flag 匹配规则:
- 短 flag:
-F(单个字符) - 长 flag:
--foo(完整单词) - 组合短 flag:
-FAX(多个短 flag 组合,等价于-F -A -X)
Option 查询
Option 是带值的命令行参数:
// 匹配 -F <value>, -F=<value>, --foo <value>, 或 --foo=<value>
const value = z.option('F', "foo");
// 仅短 option:匹配 -F <value>, -F=<value>
const short_value = z.option('F', null);
// 仅长 option:匹配 --foo <value>, --foo=<value>
const long_value = z.option(null, "foo");
Option 匹配规则:
- 空格分隔:
-F value或--foo value - 等号分隔:
-F=value或--foo=value
默认值处理
zopt 不处理默认值,而是交给应用代码用 Zig 的 orelse 处理:
// 使用 orelse 提供默认值
const value_with_default = z.option('F', "foo") orelse "my_default";
// 缺失时调用帮助函数
const value_with_help_fallback = z.option('F', "foo") orelse help();
// 缺失时报错退出
const value = z.option('F', "foo") orelse {
std.process.fatal("缺少必需参数: --foo", .{});
};
可重复 Option
有些 option 可以多次出现,收集所有匹配值:
// 可重复 option:收集所有匹配,匹配逻辑与 z.option 相同
const values = try z.optionRepeated('F', "foo");
// values 是 []const []const u8,包含所有出现的值
for (values) |v| {
std.debug.print("值: {s}\n", .{v});
}
类型转换
optionAs 允许将 option 值解析为特定类型:
const Enum = enum { Foo, Bar };
// 解析为 usize
const int_value = try z.optionAs(usize, 'F', "foo");
// 解析为枚举
const enum_value = try z.optionAs(Enum, 'E', "enum");
// 解析为浮点数
const float_value = try z.optionAs(f64, 'f', "float");
支持的类型:
- 整数类型(usize、i32、u64 等)
- 浮点数类型(f32、f64)
- 枚举类型
- 布尔类型
位置参数
位置参数是不以 - 开头的参数:
// 获取所有位置参数
const positional_args = try z.positionals();
// 获取特定位置的参数
const first_positional = try z.positional(0);
const second_positional = try z.positional(1);
重要注意:
positionals不检查值的形状- 拼错的 option 会被视为位置参数(例如
--fop而不是--foo) - 建议先验证没有未知参数,再获取位置参数
未知参数检测
// 检测未知参数
const unknown = try z.unknownArguments();
if (unknown.len > 0) {
std.process.fatal("发现未知参数: {s}", .{unknown});
}
// 现在可以安全地认为所有剩余参数都是位置参数
const positional_args = try z.positionals();
这是 zopt 的推荐用法模式:先检测未知参数,再获取位置参数。
完整示例
简单工具
const std = @import("std");
const Zopt = @import("zopt").Zopt;
pub fn main(init: std.process.Init) !void {
const arena = init.arena.allocator();
const z = try Zopt.from(arena, init.minimal.args);
// Flags
const help = z.flag('h', "help");
const verbose = z.flag('v', "verbose");
if (help) {
print_help();
return;
}
// Options
const output = z.option('o', "output") orelse "output.txt";
const count = try z.optionAs(usize, 'n', "count") orelse 10;
// 检测未知参数
const unknown = try z.unknownArguments();
if (unknown.len > 0) {
std.process.fatal("未知参数: {s}\n使用 --help 查看帮助", .{unknown});
}
// 位置参数
const input = try z.positional(0) orelse {
std.process.fatal("缺少输入文件", .{});
};
// 执行
if (verbose) {
std.debug.print("输入: {s}\n输出: {s}\n数量: {}\n", .{ input, output, count });
}
process(input, output, count);
}
fn print_help() void {
std.debug.print(
\\用法: mytool [选项] <输入文件>
\\
\\选项:
\\ -h, --help 显示帮助
\\ -v, --verbose 详细输出
\\ -o, --output 输出文件 (默认: output.txt)
\\ -n, --count 处理数量 (默认: 10)
\\
, .{});
}
带可重复 option 的工具
const std = @import("std");
const Zopt = @import("zopt").Zopt;
pub fn main(init: std.process.Init) !void {
const arena = init.arena.allocator();
const z = try Zopt.from(arena, init.minimal.args);
// 可重复 option:多个 -I 包含路径
const include_dirs = try z.optionRepeated('I', "include");
// 可重复 option:多个 -D 定义
const defines = try z.optionRepeated('D', "define");
// 检测未知参数
const unknown = try z.unknownArguments();
if (unknown.len > 0) {
std.process.fatal("未知参数: {s}", .{unknown});
}
// 位置参数:源文件列表
const source_files = try z.positionals();
// 编译
compile(source_files, include_dirs, defines);
}
与其他 Zig CLI 库的对比
| 维度 | zopt | std.process.args | clap (zig-clap) | args (zig-args) |
|---|---|---|---|---|
| 设计哲学 | 低仪式感,schema on read | 最基础,手动解析 | 功能齐全,schema 驱动 | 简单,宏驱动 |
| Schema 定义 | 不需要 | 不需要 | 需要 | 需要(编译期) |
| 帮助文本 | 应用代码处理 | 应用代码处理 | 自动生成 | 自动生成 |
| 默认值 | orelse 处理 | 手动处理 | 支持 | 支持 |
| 类型转换 | optionAs | 手动 | 自动 | 自动 |
| 可重复 option | 支持 | 手动 | 支持 | 支持 |
| 未知参数检测 | 支持 | 手动 | 支持 | 支持 |
| 代码量 | 极少 | 极少 | 中等 | 中等 |
| 学习曲线 | 很低 | 最低 | 中等 | 低 |
| 适用场景 | 小型工具 | 极简单工具 | 复杂 CLI 工具 | 中小型工具 |
适用场景
最适合
- 小型命令行工具:选项集有限的简单工具
- 快速原型:快速搭建命令行工具原型
- 脚本类工具:一次性或短期使用的工具
- 学习 Zig:学习 Zig 时的简单 CLI 库选择
- 注重简洁:偏好简洁 API、不想引入复杂依赖的项目
不太适合
- 复杂 CLI 工具:有大量选项、子命令、复杂验证需求的工具
- 需要自动帮助:需要自动生成帮助文本的工具
- 严格验证:需要严格的 schema 验证的工具
- 国际化:需要多语言帮助文本的工具
- 大型项目:有多个子命令、复杂工作流的大型 CLI 项目
注意事项
1. 拼错的 option 会被视为位置参数
这是 zopt 最需要注意的点:
# 拼错了 --output 为 --otuput
mytool --otuput result.txt input.txt
# zopt 会将 "--otuput" 和 "result.txt" 都视为位置参数
# 而不是报错
解决方案:始终在获取位置参数之前调用 unknownArguments() 进行检测。
2. 不验证 option 值
zopt 不验证 option 的值:
- 不检查值是否在允许范围内
- 不检查值的格式(如邮箱、URL)
- 不检查值是否存在(如文件路径)
解决方案:应用代码需要自行验证。
3. 帮助文本需要手动编写
zopt 不生成帮助文本:
- 需要手动编写
--help的输出 - 需要手动维护选项列表和描述
- 需要手动保持帮助文本与实际代码同步
解决方案:将帮助文本集中管理,修改选项时同步更新。
4. 子命令需要手动处理
zopt 不直接支持子命令(如 git commit、docker run):
- 需要手动解析第一个位置参数作为子命令
- 每个子命令需要单独的参数处理逻辑
- 子命令之间的参数隔离需要手动管理
解决方案:对于需要子命令的工具,考虑使用更功能齐全的 CLI 库。
项目信息
- 仓库地址:https://codeberg.org/hgrsd/zopt
- 语言:Zig
- 许可证:查看仓库
- 版本:v0.0.11(截至文章发布时)
- 示例:仓库的 examples 文件夹包含各种使用场景的示例
- 真实案例:duplik 项目的参数解析是 zopt 的"真实世界"使用示例
总结
zopt 是一个为 Zig 语言设计的低仪式感命令行参数解析库,它通过"schema on read"语义和刻意精简的设计,为小型命令行工具提供了简洁、灵活的参数解析方案。
核心要点:
- 设计哲学:低仪式感、schema on read、刻意精简
- 核心 API:flag(布尔开关)、option(带值参数)、optionRepeated(可重复参数)、optionAs(类型转换)、positionals(位置参数)、unknownArguments(未知参数检测)
- 默认值处理:使用 Zig 的
orelse运算符,由应用代码处理 - 推荐模式:先检测未知参数,再获取位置参数
- 适用场景:小型工具、快速原型、脚本类工具、注重简洁的项目
- 注意事项:拼错的 option 会被视为位置参数、不验证值、帮助文本手动编写、子命令手动处理
对于 Zig 开发者来说,zopt 提供了一个在 std.process.args 和功能齐全的 CLI 框架之间的中间选择。它不是要取代 clap 等功能齐全的库,而是为那些不需要完整框架的小型工具提供一个更轻量的选择。正如项目描述所说,zopt 是"一个精简的、美学上令人愉悦的 API",它愿意为了易用性而牺牲基于 schema 的命令行解析器的优势。
原文链接:https://codeberg.org/hgrsd/zopt