kioz-wang/zargs
Another Comptime-argparse for Zig! Let's start to build your command line!
other language: 中文简体
Another Comptime-argparse for Zig! Let's start to build your command line!
const std = @import("std");
const zargs = @import("zargs");
const Command = zargs.Command;
const TokenIter = zargs.TokenIter;
pub fn main() !void {
comptime var cmd: Command = .{ .name = "demo", .use_subCmd = "action", .description = "This is a simple demo" };
_ = cmd.opt("verbose", u32, .{ .short = 'v' }).optArg("output", []const u8, .{ .short = 'o', .long = "out" });
comptime var install: Command = .{ .name = "install" };
comptime var remove: Command = .{ .name = "remove" };
_ = cmd.subCmd(install.posArg("name", []const u8, .{}).*).subCmd(remove.posArg("name", []const u8, .{}).*);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var it = try TokenIter.init(allocator, .{});
defer it.deinit();
_ = try it.next();
const args = cmd.parse(&it) catch |err| {
std.debug.print("Fail to parse because of {any}\n", .{err});
std.debug.print("\n{s}\n", .{cmd.usage()});
std.process.exit(1);
};
switch (args.action) {
.install => |a| {
std.debug.print("Installing {s}\n", .{a.name});
},
.remove => |a| {
std.debug.print("Removing {s}\n", .{a.name});
},
}
std.debug.print("Success to do {s}\n", .{@tagName(args.action)});
}
As a system level programming language, there should be an elegant solution for parsing command line arguments.
I've basically looked at various open source implementations on the web, both runtime parsing and compile-time parsing. Developing in a language with strong compile-time capabilities should minimize runtime overhead. For compile-time, I see two routes:
I think the latter is much cleaner to use, and what's there is basically the former, hence zargs
.
v0.13.x
only supports zig 0.13.0, support for zig 0.14.0 will be started withv0.14.0
(see release v0.14.0)
Get the latest version:
zig fetch --save git+https://github.com/kioz-wang/zargs
Get a tagged version (e.g. v0.13.0
):
# See: https://github.com/kioz-wang/zargs/releases
zig fetch --save https://github.com/kioz-wang/zargs/archive/refs/tags/v0.13.0.tar.gz
Use addImport
in your build.zig
(e.g.):
const exe = b.addExecutable(.{
.name = "your_app",
.root_source_file = b.path("src/main.zig"),
.target = b.standardTargetOptions(.{}),
.optimize = b.standardOptimizeOption(.{}),
});
exe.root_module.addImport("zargs", b.dependency("zargs", .{}).module("zargs"));
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
After importing the zargs
, you will obtain the iterator (TokenIter
), command builder (Command
), and universal parsing function (parseAny
):
const zargs = @import("zargs");
For more information and usage details about these three powerful tools, please refer to the documentation.
Flexible for real and test scenarios
init
): get real command line arguments.initGeneral
): splits command line arguments from a one-line string.initLine
): same as regular iterator, but you can specify delimiters.initList
): iterates over a list of strings.Short option prefixes (-
), long option prefixes (--
), connectors (=
), option terminators (--
) can be customized for iterators (see presentation for usage scenarios).
opt
)singleOpt
)boolOpt
), T == bool
repeatOpt
), @typeInfo(T) == .int
argOpt
)singleArgOpt
), TarrayArgOpt
), [n]const T
multiArgOpt
), []const T
arg
)optArg
) (equivalent to Option with Argument)posArg
)singlePosArg
), TarrayPosArg
), [n]const T
subCmd
)Matching and parsing are driven by an iterator. For options, the option is always matched first, and if it takes an argument, the argument is then parsed. For positional arguments, parsing is attempted directly.
For arguments, T must be the smallest parsable unit: []const u8
-> T
.int
.float
.bool
.enum
: By default, std.meta.stringToEnum
is used, but the parser method takes precedence..struct
: A struct with a parser method.If T is not parsable, a custom parser (.parseFn
) can be defined for the argument. Obviously, a parser cannot be configured for a single option, as it would be meaningless.
Options and arguments can be configured with default values (.default
). Once configured, the option or argument becomes optional.
Even if not explicitly configured, single options always have default values: boolean options default to false
, and accumulative options default to 0
. Therefore, single options are always optional.
A callback (.callBackFn
) can be configured, which will be executed after matching and parsing.
A command cannot have both positional arguments and subcommands simultaneously.
For the parser, except for accumulative options and options with a variable number of arguments, no option can appear more than once.
Various representations are primarily supported by the iterator.
Options are further divided into short options and long options:
-v
--verbose
Options with a single argument can use a connector to link the option and the argument:
-o=hello
, -o hello
--output=hello
, --output hello
Dropping the connector or whitespace for short options is not allowed, as it results in poor readability!
For options with a fixed number of arguments, connectors cannot be used, and all arguments must be provided at once. For example, with a long option:
--files f0 f1 f2 # [3]const T
Options with a variable number of arguments are similar to options with a single argument but can appear multiple times, e.g.:
--file f0 -v --file=f1 --greet # []const T
Multiple short options can share a prefix, but if an option takes an argument, it must be placed last, e.g.:
-Rns
-Rnso hello
-Rnso=hello
Once a positional argument appears, the parser informs the iterator to only return positional arguments, even if the arguments might have an option prefix, e.g.:
-o hello a b -v # -o is an option with a single argument, so a, b, -v are all positional arguments
An option terminator can be used to inform the iterator to only return positional arguments, e.g.:
--output hello -- a b -v
Double quotes can be used to avoid iterator ambiguity, e.g., to pass a negative number -1
, double quotes must be used:
--num \"-1\"
Since the shell removes double quotes, escape characters are also required! If a connector is used, escaping is unnecessary:
--num="-1"
.
comptime var cmd: Command = .{ .name = "demo" };
A command can be defined in a single line, with additional configurations like version, description, author, homepage, etc. Use chaining to add options (opt
), options with arguments (optArg
), positional arguments (posArg
), or subcommands (subCmd
).
comptime cmd.callBack(struct {
const C = cmd;
fn f(_: *C.Result()) void {
std.debug.print("CallBack of {s}\n", .{ C.name });
}
}.f);
const args = try cmd.parse(&it);
Simply call .parse
to generate the parser and argument structure. There is also parseAlloc
which supports passing a memory allocator:
cmd.destroy(&args, allocator)
to free memory.When the parser has completed its task, if you still need to handle the remaining arguments manually, you can call the iterator's nextAllBase
method.
If further parsing of the arguments is required, you can use the parseAny
function.
_ = cmd.usage();
_ = cmd.help();
See https://kioz-wang.github.io/zargs/
Look at here
To build all examples:
zig build examples
To list all examples (all step prefixed ex-
are examples):
zig build -l
To execute an example:
zig build ex-01.add -- -h
Welcome to submit PRs to link your project that use
zargs
!
More real-world examples are coming!
MIT © Kioz Wang