mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-03-08 01:24:49 +01:00
cli: Simplify API by removing Config field
- Remove Config struct (.sorted/.default presets) - partition() now handles multi-value flags correctly - Add Iterator.partitionRemaining() for deferred partitioning - parseStruct calls partitionRemaining() automatically - Rework tests Signed-off-by: Pablo Alessandro Santos Hugen <phugen@redhat.com>
This commit is contained in:
parent
2dd7956105
commit
2e53b865ea
1 changed files with 266 additions and 322 deletions
588
lib/std/cli.zig
588
lib/std/cli.zig
|
|
@ -1,23 +1,22 @@
|
|||
//! Type-driven command-line argument parser.
|
||||
//!
|
||||
//! Structs map to named flags, tuples to positionals, unions to subcommands.
|
||||
//! Use `.sorted` config to allow flags anywhere; `.default` requires flags first.
|
||||
//!
|
||||
//! ```
|
||||
//! const Args = struct {
|
||||
//! struct { verbose: bool = false, output: ?[]const u8 = null }, // flags
|
||||
//! []const u8, // positional
|
||||
//! struct { verbose: ?void = null, output: ?[]const u8 = null },
|
||||
//! []const u8,
|
||||
//! };
|
||||
//! const flags, const input = try std.cli.parse(Args, .sorted, args, allocator);
|
||||
//! const named, const positionals = try std.cli.parse(Args, args, allocator);
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
const heap = std.heap;
|
||||
const mem = std.mem;
|
||||
const meta = std.meta;
|
||||
const testing = std.testing;
|
||||
const fmt = std.fmt;
|
||||
const Allocator = mem.Allocator;
|
||||
const Args = std.process.Args;
|
||||
const EnumSet = std.EnumSet;
|
||||
|
||||
/// Parsing errors returned by `parse`.
|
||||
|
|
@ -30,40 +29,21 @@ pub const Error = error{
|
|||
UnknownFlag,
|
||||
};
|
||||
|
||||
/// Parser configuration presets.
|
||||
pub const Config = struct {
|
||||
/// Reorders args so flags come before positionals. Allows `<program> file.txt --verbose`.
|
||||
pub const sorted: @This() = .{ .sort = true };
|
||||
/// Expects flags before positionals. Requires strictly `<program> --verbose file.txt`.
|
||||
pub const default: @This() = .{ .sort = false };
|
||||
|
||||
sort: bool = false,
|
||||
};
|
||||
|
||||
/// Parse command-line arguments into type `T`. Skips first arg (program name).
|
||||
///
|
||||
/// - `T`: struct for flags, tuple for positionals, union for subcommands
|
||||
/// - `config`: `.sorted` allows flags after positionals, `.default` requires flags first
|
||||
/// - `args`: Argument slice. see `std.process.Args.toSlice`
|
||||
/// - `allocator`: for string/slice allocations
|
||||
///
|
||||
/// Returns parsed `T` or `Error`.
|
||||
pub fn parse(
|
||||
comptime T: type,
|
||||
comptime config: Config,
|
||||
args: if (config.sort) [][:0]const u8 else []const [:0]const u8,
|
||||
args: [][:0]const u8,
|
||||
allocator: Allocator,
|
||||
) (Error || Allocator.Error)!T {
|
||||
if (config.sort and args.len > 1) partition(args[1..]);
|
||||
var iter: Iterator = .{ .args = args };
|
||||
return parseInner(T, &iter, allocator);
|
||||
}
|
||||
|
||||
// This indirection is just to enable selective dropping of the first
|
||||
// arg (program name, subcommand, etc.)
|
||||
inline fn parseInner(comptime T: type, iter: *Iterator, allocator: Allocator) (Error || Allocator.Error)!T {
|
||||
var iter: Iterator = .init(args);
|
||||
iter.skip() orelse return Error.MissingValue;
|
||||
return parseValue(T, iter, allocator);
|
||||
return parseValue(T, &iter, allocator);
|
||||
}
|
||||
|
||||
fn parseValue(comptime T: type, iter: *Iterator, allocator: Allocator) (Error || Allocator.Error)!T {
|
||||
|
|
@ -85,44 +65,37 @@ fn parseValue(comptime T: type, iter: *Iterator, allocator: Allocator) (Error ||
|
|||
};
|
||||
}
|
||||
|
||||
// @"struct" rules:
|
||||
// struct { foo: u32, bar: bool }, expect args like --foo 42 --bar 1
|
||||
// field types can be void (presence-only flag), optional, or have default values
|
||||
// fields without default values or optional types are required
|
||||
fn parseStruct(comptime T: type, iter: *Iterator, allocator: Allocator) (Error || Allocator.Error)!T {
|
||||
const info = @typeInfo(T);
|
||||
const fields = info.@"struct".fields;
|
||||
const fields = @typeInfo(T).@"struct".fields;
|
||||
var result: T = undefined;
|
||||
|
||||
// First pass for setting defaults
|
||||
inline for (fields) |field| @field(result, field.name) = field.defaultValue() orelse
|
||||
if (@typeInfo(field.type) == .optional) null else continue;
|
||||
iter.partitionRemaining();
|
||||
|
||||
inline for (fields) |f| @field(result, f.name) = f.defaultValue() orelse
|
||||
if (@typeInfo(f.type) == .optional) null else continue;
|
||||
|
||||
const FieldEnum = meta.FieldEnum(T);
|
||||
var fieldsSeen = EnumSet(FieldEnum).initEmpty();
|
||||
var seen = EnumSet(FieldEnum).initEmpty();
|
||||
|
||||
// Parse args - stop on first non-flag (positional)
|
||||
while (iter.peek()) |arg| {
|
||||
const field_name = flagName(arg) orelse break;
|
||||
const name = flagName(arg) orelse break;
|
||||
_ = iter.skip();
|
||||
|
||||
inline for (fields) |field| {
|
||||
if (mem.eql(u8, field.name, field_name)) {
|
||||
fieldsSeen.insert(@field(FieldEnum, field.name));
|
||||
@field(result, field.name) = try parseValue(field.type, iter, allocator);
|
||||
inline for (fields) |f| {
|
||||
if (mem.eql(u8, f.name, name)) {
|
||||
seen.insert(@field(FieldEnum, f.name));
|
||||
@field(result, f.name) = try parseValue(f.type, iter, allocator);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return Error.UnknownFlag;
|
||||
}
|
||||
} else return Error.UnknownFlag;
|
||||
}
|
||||
|
||||
// Required fields were set?
|
||||
inline for (fields) |field|
|
||||
if (field.default_value_ptr == null and
|
||||
!(@typeInfo(field.type) == .optional) and
|
||||
!fieldsSeen.contains(@field(FieldEnum, field.name)))
|
||||
return Error.MissingValue;
|
||||
if (iter.peek()) |a| {
|
||||
if (mem.eql(u8, a, "--")) _ = iter.skip();
|
||||
}
|
||||
|
||||
inline for (fields) |f|
|
||||
if (f.default_value_ptr == null and @typeInfo(f.type) != .optional and
|
||||
!seen.contains(@field(FieldEnum, f.name))) return Error.MissingValue;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -130,22 +103,25 @@ fn parseStruct(comptime T: type, iter: *Iterator, allocator: Allocator) (Error |
|
|||
fn parseTuple(comptime T: type, iter: *Iterator, allocator: Allocator) (Error || Allocator.Error)!T {
|
||||
const fields = @typeInfo(T).@"struct".fields;
|
||||
var result: T = undefined;
|
||||
inline for (fields) |field| {
|
||||
|
||||
inline for (fields) |field|
|
||||
@field(result, field.name) = try parseValue(field.type, iter, allocator);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn parseUnion(comptime T: type, iter: *Iterator, allocator: Allocator) (Error || Allocator.Error)!T {
|
||||
const info = @typeInfo(T).@"union";
|
||||
const Tag = info.tag_type orelse @compileError("Non-tagged unions are not supported");
|
||||
const arg = iter.peek() orelse return Error.MissingValue;
|
||||
const tag = meta.stringToEnum(Tag, arg) orelse return Error.InvalidValue;
|
||||
|
||||
inline for (info.fields) |field| if (@field(Tag, field.name) == tag) {
|
||||
const value = try parseInner(field.type, iter, allocator);
|
||||
return @unionInit(T, field.name, value);
|
||||
};
|
||||
const arg = iter.next() orelse
|
||||
return Error.MissingValue;
|
||||
|
||||
const tag = meta.stringToEnum(Tag, arg) orelse
|
||||
return Error.InvalidValue;
|
||||
|
||||
inline for (info.fields) |field| if (@field(Tag, field.name) == tag)
|
||||
return @unionInit(T, field.name, try parseValue(field.type, iter, allocator));
|
||||
|
||||
unreachable;
|
||||
}
|
||||
|
|
@ -153,7 +129,9 @@ fn parseUnion(comptime T: type, iter: *Iterator, allocator: Allocator) (Error ||
|
|||
fn parseEnum(comptime T: type, iter: *Iterator) Error!T {
|
||||
const arg = iter.peek() orelse return Error.MissingValue;
|
||||
const val = meta.stringToEnum(T, arg) orelse return Error.InvalidValue;
|
||||
|
||||
_ = iter.skip();
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +142,9 @@ fn parseScalar(comptime T: type, iter: *Iterator) Error!T {
|
|||
.float => fmt.parseFloat(T, arg) catch return Error.InvalidValue,
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
_ = iter.skip();
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
|
|
@ -197,58 +177,49 @@ fn parsePointer(comptime T: type, iter: *Iterator, allocator: Allocator) (Error
|
|||
fn parseArray(comptime T: type, iter: *Iterator, allocator: Allocator) (Error || Allocator.Error)!T {
|
||||
const info = @typeInfo(T).array;
|
||||
var result: T = undefined;
|
||||
|
||||
inline for (0..info.len) |i| result[i] =
|
||||
try parseValue(info.child, iter, allocator);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn parseVector(comptime T: type, iter: *Iterator, allocator: Allocator) (Error || Allocator.Error)!T {
|
||||
const info = @typeInfo(T).vector;
|
||||
var result: T = undefined;
|
||||
|
||||
inline for (0..info.len) |i| result[i] =
|
||||
try parseValue(info.child, iter, allocator);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// bool rules:
|
||||
// maps {1/0, true/false, yes/no and on/off} to bool
|
||||
// NOTE: bool expects a value. For presence-only flags, use ?void type in struct
|
||||
fn parseBool(comptime T: type, iter: *Iterator) Error!T {
|
||||
const arg = iter.peek() orelse return Error.MissingValue;
|
||||
|
||||
if (mem.eql(u8, arg, "1") or
|
||||
mem.eql(u8, arg, "true") or
|
||||
mem.eql(u8, arg, "yes") or
|
||||
mem.eql(u8, arg, "on"))
|
||||
{
|
||||
inline for (.{ "1", "true", "yes", "on" }) |s| if (mem.eql(u8, arg, s)) {
|
||||
_ = iter.skip();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
if (mem.eql(u8, arg, "0") or
|
||||
mem.eql(u8, arg, "false") or
|
||||
mem.eql(u8, arg, "no") or
|
||||
mem.eql(u8, arg, "off"))
|
||||
{
|
||||
inline for (.{ "0", "false", "no", "off" }) |s| if (mem.eql(u8, arg, s)) {
|
||||
_ = iter.skip();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return Error.InvalidValue;
|
||||
}
|
||||
|
||||
fn parseOptional(comptime T: type, iter: *Iterator, allocator: Allocator) (Error || Allocator.Error)!T {
|
||||
const ChildT = @typeInfo(T).optional.child;
|
||||
|
||||
return parseValue(ChildT, iter, allocator) catch |err| switch (err) {
|
||||
return parseValue(@typeInfo(T).optional.child, iter, allocator) catch |err| switch (err) {
|
||||
Error.MissingValue => null,
|
||||
else => err,
|
||||
};
|
||||
}
|
||||
|
||||
// If arg is a flag, return its name without the prefix, otherwise return null
|
||||
inline fn flagName(arg: []const u8) ?[]const u8 {
|
||||
if (mem.startsWith(u8, arg, "--")) return arg[2..];
|
||||
if (mem.startsWith(u8, arg, "--")) return if (arg.len == 2) null else arg[2..];
|
||||
|
||||
if (mem.startsWith(u8, arg, "-") or
|
||||
mem.startsWith(u8, arg, "+")) return arg[1..];
|
||||
|
|
@ -257,19 +228,20 @@ inline fn flagName(arg: []const u8) ?[]const u8 {
|
|||
}
|
||||
|
||||
// Stable in-place partition: moves all flag units before positionals
|
||||
// Stops at "--" (everything after is positional)
|
||||
fn partition(args: [][:0]const u8) void {
|
||||
var write: usize = 0;
|
||||
var read: usize = 0;
|
||||
|
||||
while (read < args.len) {
|
||||
if (flagName(args[read]) != null) {
|
||||
const has_value = mem.indexOfScalar(u8, args[read], '=') == null and
|
||||
read + 1 < args.len and flagName(args[read + 1]) == null;
|
||||
const unit_size = @as(usize, @intFromBool(has_value)) + 1;
|
||||
|
||||
mem.rotate([:0]const u8, args[write .. read + unit_size], read - write);
|
||||
write += unit_size;
|
||||
read += unit_size;
|
||||
if (mem.eql(u8, args[read], "--")) break;
|
||||
if (flagName(args[read])) |_| {
|
||||
var end = read + 1;
|
||||
if (mem.indexOfScalar(u8, args[read], '=') == null)
|
||||
while (end < args.len and flagName(args[end]) == null) : (end += 1) {};
|
||||
mem.rotate([:0]const u8, args[write..end], read - write);
|
||||
write += end - read;
|
||||
read = end;
|
||||
} else read += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -278,282 +250,254 @@ fn partition(args: [][:0]const u8) void {
|
|||
// index position or in the same index position separated by '=', serving a rather
|
||||
// different purpose than std.process.Args.Iterator
|
||||
const Iterator = struct {
|
||||
args: []const [:0]const u8,
|
||||
args: [][:0]const u8,
|
||||
index: usize = 0,
|
||||
@"=": ?[:0]const u8 = null, // stores value after '=' to return on next call
|
||||
@"=": ?[:0]const u8 = null,
|
||||
|
||||
pub fn init(args: []const [:0]const u8) Iterator {
|
||||
pub fn init(args: [][:0]const u8) @This() {
|
||||
return .{ .args = args };
|
||||
}
|
||||
|
||||
pub fn next(self: *Iterator) ?[]const u8 {
|
||||
if (self.@"=") |value| {
|
||||
self.@"=" = null;
|
||||
return value;
|
||||
pub fn next(this: *@This()) ?[]const u8 {
|
||||
if (this.@"=") |v| {
|
||||
this.@"=" = null;
|
||||
return v;
|
||||
}
|
||||
if (this.index >= this.args.len) return null;
|
||||
|
||||
if (self.index >= self.args.len) return null;
|
||||
const arg = this.args[this.index];
|
||||
this.index += 1;
|
||||
|
||||
const arg = self.args[self.index];
|
||||
self.index += 1;
|
||||
|
||||
if (flagName(arg)) |_| if (mem.findScalar(u8, arg, '=')) |eq_pos| {
|
||||
self.@"=" = arg[eq_pos + 1 .. :0];
|
||||
return arg[0..eq_pos];
|
||||
if (flagName(arg) != null) if (mem.indexOfScalar(u8, arg, '=')) |i| {
|
||||
this.@"=" = arg[i + 1 .. :0];
|
||||
return arg[0..i];
|
||||
};
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
pub fn peek(self: *Iterator) ?[]const u8 {
|
||||
const prev = .{ self.index, self.@"=" };
|
||||
pub fn peek(this: *@This()) ?[]const u8 {
|
||||
const s = .{ this.index, this.@"=" };
|
||||
defer {
|
||||
self.index = prev[0];
|
||||
self.@"=" = prev[1];
|
||||
this.index = s[0];
|
||||
this.@"=" = s[1];
|
||||
}
|
||||
|
||||
return self.next();
|
||||
return this.next();
|
||||
}
|
||||
|
||||
pub fn skip(self: *Iterator) ?void {
|
||||
_ = self.next() orelse return null;
|
||||
pub fn skip(this: *@This()) ?void {
|
||||
_ = this.next() orelse return null;
|
||||
}
|
||||
|
||||
pub fn collect(this: *@This(), allocator: Allocator) ![]const []const u8 {
|
||||
var list: std.ArrayList([]const u8) = .empty;
|
||||
while (this.next()) |arg| try list.append(allocator, arg);
|
||||
return list.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
pub fn partitionRemaining(this: *@This()) void {
|
||||
if (this.index < this.args.len) partition(this.args[this.index..]);
|
||||
}
|
||||
};
|
||||
|
||||
test "std.cli: Iterator" {
|
||||
var iter1: Iterator = .{ .args = &.{ "--foo", "42", "--bar", "1" } };
|
||||
try testing.expectEqualStrings("--foo", iter1.next().?);
|
||||
try testing.expectEqualStrings("42", iter1.next().?);
|
||||
try testing.expectEqualStrings("--bar", iter1.next().?);
|
||||
try testing.expectEqualStrings("1", iter1.next().?);
|
||||
try testing.expectEqual(null, iter1.next());
|
||||
var args = [_][:0]const u8{ "--foo=42", "--bar", "1", "--foobar=💔", "🥀" };
|
||||
const expected: []const []const u8 = &.{ "--foo", "42", "--bar", "1", "--foobar", "💔", "🥀" };
|
||||
|
||||
var iter2: Iterator = .{ .args = &.{ "--foo=42", "--bar=hello" } };
|
||||
try testing.expectEqualStrings("--foo", iter2.next().?);
|
||||
try testing.expectEqualStrings("42", iter2.next().?);
|
||||
try testing.expectEqualStrings("--bar", iter2.next().?);
|
||||
try testing.expectEqualStrings("hello", iter2.next().?);
|
||||
try testing.expectEqual(null, iter2.next());
|
||||
var it: Iterator = .init(&args);
|
||||
|
||||
const actual = try it.collect(testing.allocator);
|
||||
defer testing.allocator.free(actual);
|
||||
|
||||
try testing.expectEqualDeep(expected, actual);
|
||||
}
|
||||
|
||||
test "std.cli: partition" {
|
||||
// Positionals appear BEFORE flags - partition reorders them
|
||||
var args = [_][:0]const u8{ "file1.txt", "file2.txt", "--verbose", "true", "--output", "result.txt" };
|
||||
partition(&args);
|
||||
var actual = [_][:0]const u8{ "output.mp4", "--resolution", "1920", "1080", "--fps", "60", "input.mp4" };
|
||||
const expected: []const []const u8 = &.{ "--resolution", "1920", "1080", "--fps", "60", "input.mp4", "output.mp4" };
|
||||
|
||||
try testing.expectEqualStrings("--verbose", args[0]);
|
||||
try testing.expectEqualStrings("true", args[1]);
|
||||
try testing.expectEqualStrings("--output", args[2]);
|
||||
try testing.expectEqualStrings("result.txt", args[3]);
|
||||
try testing.expectEqualStrings("file1.txt", args[4]);
|
||||
try testing.expectEqualStrings("file2.txt", args[5]);
|
||||
partition(&actual);
|
||||
try testing.expectEqualDeep(expected, &actual);
|
||||
}
|
||||
|
||||
// zig build -Doptimize=ReleaseFast -Dtarget=x86_64-linux -j8
|
||||
test "std.cli: zig build" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
test "std.cli: simple" {
|
||||
var arena: heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const Optimize = enum { Debug, ReleaseSafe, ReleaseFast, ReleaseSmall };
|
||||
const BuildOptions = struct {
|
||||
optimize: Optimize = .Debug,
|
||||
target: []const u8 = "native",
|
||||
j: u32 = 1,
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const types = .{
|
||||
bool,
|
||||
i32,
|
||||
f64,
|
||||
void,
|
||||
?bool,
|
||||
?i32,
|
||||
?f64,
|
||||
enum { red, green, blue },
|
||||
?enum { one, many },
|
||||
};
|
||||
|
||||
var arguments = [_][2][:0]const u8{
|
||||
.{ "<arg0>", "true" },
|
||||
.{ "<arg1>", "42" },
|
||||
.{ "<arg2>", "3.14" },
|
||||
.{ "<arg3>", "" },
|
||||
.{ "<arg4>", "false" },
|
||||
.{ "<arg5>", "-123" },
|
||||
.{ "<arg6>", "2.718" },
|
||||
.{ "<arg7>", "green" },
|
||||
.{ "<arg8>", "one" },
|
||||
};
|
||||
const expected = .{
|
||||
true,
|
||||
42,
|
||||
3.14,
|
||||
{},
|
||||
false,
|
||||
@as(?i32, -123),
|
||||
@as(?f64, 2.718),
|
||||
.green,
|
||||
.one,
|
||||
};
|
||||
|
||||
inline for (types, &arguments, expected) |T, *a, e|
|
||||
try testing.expectEqual(e, try parse(T, a, allocator));
|
||||
}
|
||||
|
||||
test "std.cli: sequences" {
|
||||
var arena: heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const types = .{
|
||||
[3]i32,
|
||||
@Vector(3, f64),
|
||||
struct { i32, bool, f64 },
|
||||
[]const []const u8,
|
||||
};
|
||||
|
||||
var arguments = [_][4][:0]const u8{
|
||||
.{ "<arg0>", "1", "2", "3" },
|
||||
.{ "<arg0>", "1.5", "2.5", "3.5" },
|
||||
.{ "<arg0>", "42", "true", "3.14" },
|
||||
.{ "<arg0>", "hello", "🦎", "good" },
|
||||
};
|
||||
|
||||
const expected = .{
|
||||
[3]i32{ 1, 2, 3 },
|
||||
@Vector(3, f64){ 1.5, 2.5, 3.5 },
|
||||
.{ 42, true, 3.14 },
|
||||
@as([]const []const u8, &.{ "hello", "🦎", "good" }),
|
||||
};
|
||||
|
||||
inline for (types, &arguments, expected) |T, *a, e|
|
||||
try testing.expectEqualDeep(e, try parse(T, a, allocator));
|
||||
}
|
||||
|
||||
test "std.cli: struct" {
|
||||
var arena: heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const Point = struct { x: i32 = 0, y: i32 = 0 };
|
||||
const Config = struct { name: []const u8, verbose: bool = false };
|
||||
const Color = struct { r: u8 = 0, g: u8 = 0, b: u8 = 0 };
|
||||
|
||||
const types = .{ Point, Config, Color };
|
||||
|
||||
var arguments = [_][5][:0]const u8{
|
||||
.{ "<arg0>", "--x", "10", "--y", "20" },
|
||||
.{ "<arg0>", "--verbose", "true", "--name", "alice" },
|
||||
.{ "<arg0>", "--r", "255", "--g", "128" },
|
||||
};
|
||||
|
||||
const expected = .{
|
||||
Point{ .x = 10, .y = 20 },
|
||||
Config{ .name = "alice", .verbose = true },
|
||||
Color{ .r = 255, .g = 128 },
|
||||
};
|
||||
|
||||
inline for (types, &arguments, expected) |T, *a, e|
|
||||
try testing.expectEqualDeep(e, try parse(T, a, allocator));
|
||||
}
|
||||
|
||||
test "std.cli: union" {
|
||||
var arena: heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const Op = union(enum) { add: i32, sub: i32, nop: void };
|
||||
|
||||
const types = .{ Op, Op, Op };
|
||||
|
||||
var arguments = [_][3][:0]const u8{
|
||||
.{ "<arg0>", "add", "5" },
|
||||
.{ "<arg0>", "sub", "-3" },
|
||||
.{ "<arg0>", "nop", "" },
|
||||
};
|
||||
|
||||
const expected = .{
|
||||
Op{ .add = 5 },
|
||||
Op{ .sub = -3 },
|
||||
Op{ .nop = {} },
|
||||
};
|
||||
|
||||
inline for (types, &arguments, expected) |T, *a, e|
|
||||
try testing.expectEqualDeep(e, try parse(T, a, allocator));
|
||||
}
|
||||
|
||||
test "std.cli: combined" {
|
||||
var arena: heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const EncodeOptions = struct {
|
||||
input: []const u8,
|
||||
output: []const u8,
|
||||
format: enum { mp4, avi, mkv } = .mp4,
|
||||
resolution: [2]u32,
|
||||
quality: ?enum { low, medium, high } = null,
|
||||
verbose: bool = false,
|
||||
};
|
||||
|
||||
const parsed = try parse(BuildOptions, .default, &.{
|
||||
"zig-build", "--optimize=ReleaseFast", "--target=x86_64-linux", "-j=8",
|
||||
}, arena.allocator());
|
||||
|
||||
try testing.expectEqual(.ReleaseFast, parsed.optimize);
|
||||
try testing.expectEqualStrings("x86_64-linux", parsed.target);
|
||||
try testing.expectEqual(@as(u32, 8), parsed.j);
|
||||
try testing.expectEqual(false, parsed.verbose);
|
||||
}
|
||||
|
||||
// git clone --depth 1 --branch main https://github.com/ziglang/zig
|
||||
// git commit --message "fix: resolve memory leak" --amend true
|
||||
// git log --oneline true --count 10
|
||||
test "std.cli: git" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const Command = union(enum) {
|
||||
clone: struct {
|
||||
depth: ?u32 = null,
|
||||
branch: []const u8 = "master",
|
||||
url: []const u8,
|
||||
},
|
||||
commit: struct {
|
||||
message: []const u8,
|
||||
amend: bool = false,
|
||||
signoff: bool = false,
|
||||
},
|
||||
log: struct {
|
||||
oneline: bool = false,
|
||||
count: u32 = 20,
|
||||
const GitCommand = union(enum) {
|
||||
remote: union(enum) {
|
||||
add: struct { name: []const u8, url: []const u8 },
|
||||
remove: []const u8,
|
||||
},
|
||||
status: void,
|
||||
};
|
||||
|
||||
const clone = try parse(Command, .default, &.{
|
||||
"git", "clone", "--depth", "1", "--branch", "main", "--url", "https://github.com/ziglang/zig",
|
||||
}, allocator);
|
||||
try testing.expectEqual(@as(?u32, 1), clone.clone.depth);
|
||||
try testing.expectEqualStrings("main", clone.clone.branch);
|
||||
try testing.expectEqualStrings("https://github.com/ziglang/zig", clone.clone.url);
|
||||
|
||||
const commit = try parse(Command, .default, &.{
|
||||
"git", "commit", "--message", "fix: resolve memory leak", "--amend", "true",
|
||||
}, allocator);
|
||||
try testing.expectEqualStrings("fix: resolve memory leak", commit.commit.message);
|
||||
try testing.expectEqual(true, commit.commit.amend);
|
||||
|
||||
const log = try parse(Command, .default, &.{ "git", "log", "--oneline", "true", "--count", "10" }, allocator);
|
||||
try testing.expectEqual(true, log.log.oneline);
|
||||
try testing.expectEqual(@as(u32, 10), log.log.count);
|
||||
}
|
||||
|
||||
// curl --request POST --header "Content-Type: application/json" --data '{"key":"value"}' https://api.example.com
|
||||
test "std.cli: curl" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const Method = enum { GET, POST, PUT, DELETE, PATCH };
|
||||
const CurlOptions = struct {
|
||||
request: Method = .GET,
|
||||
header: []const u8 = "",
|
||||
data: []const u8 = "",
|
||||
output: []const u8 = "-",
|
||||
silent: bool = false,
|
||||
url: []const u8,
|
||||
};
|
||||
|
||||
const parsed = try parse(CurlOptions, .default, &.{
|
||||
"curl", "--request", "POST",
|
||||
"--header", "Content-Type: application/json", "--data",
|
||||
"{\"key\":\"value\"}", "--url", "https://api.example.com/endpoint",
|
||||
}, arena.allocator());
|
||||
|
||||
try testing.expectEqual(.POST, parsed.request);
|
||||
try testing.expectEqualStrings("Content-Type: application/json", parsed.header);
|
||||
try testing.expectEqualStrings("{\"key\":\"value\"}", parsed.data);
|
||||
try testing.expectEqualStrings("https://api.example.com/endpoint", parsed.url);
|
||||
}
|
||||
|
||||
// ffmpeg -i input.mp4 --codec h264 --bitrate 4500 --resolution 1920 1080 --fps 60 output.mp4
|
||||
test "std.cli: ffmpeg" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const Codec = enum { h264, h265, vp9, av1 };
|
||||
const FfmpegOptions = struct {
|
||||
const GrepArgs = struct {
|
||||
struct {
|
||||
i: []const u8,
|
||||
codec: Codec = .h264,
|
||||
bitrate: u32 = 2500,
|
||||
resolution: @Vector(2, u32) = .{ 1280, 720 },
|
||||
fps: u8 = 30,
|
||||
@"ignore-case": ?void = null,
|
||||
@"line-number": ?void = null,
|
||||
},
|
||||
[]const u8, // output file
|
||||
[]const []const u8,
|
||||
};
|
||||
|
||||
const opts, const output = try parse(FfmpegOptions, .default, &.{
|
||||
"ffmpeg", "-i", "input.mp4", "--codec", "h265",
|
||||
"--bitrate", "4500", "--resolution", "1920", "1080",
|
||||
"--fps", "60", "output.mp4",
|
||||
}, arena.allocator());
|
||||
const types = .{ EncodeOptions, GitCommand, GitCommand, GitCommand, GrepArgs };
|
||||
|
||||
try testing.expectEqualStrings("input.mp4", opts.i);
|
||||
try testing.expectEqual(.h265, opts.codec);
|
||||
try testing.expectEqual(@as(u32, 4500), opts.bitrate);
|
||||
try testing.expectEqual(@Vector(2, u32){ 1920, 1080 }, opts.resolution);
|
||||
try testing.expectEqual(@as(u8, 60), opts.fps);
|
||||
try testing.expectEqualStrings("output.mp4", output);
|
||||
}
|
||||
|
||||
// cc -O2 --target x86_64-linux --output program main.c utils.c lib.c
|
||||
test "std.cli: cc" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const OptLevel = enum { O0, O1, O2, O3, Os, Oz };
|
||||
const CompilerOptions = struct {
|
||||
struct {
|
||||
O: OptLevel = .O0,
|
||||
target: []const u8 = "native",
|
||||
output: []const u8 = "a.out",
|
||||
g: bool = false,
|
||||
},
|
||||
[]const []const u8, // input files
|
||||
var arguments = [_][10][:0]const u8{
|
||||
.{ "<arg0>", "--input", "video.mp4", "--output", "result.mkv", "--format", "mkv", "--resolution", "1920", "1080" },
|
||||
.{ "<arg0>", "remote", "add", "--name", "origin", "--url", "git@github.com:user/repo", "", "", "" },
|
||||
.{ "<arg0>", "remote", "remove", "upstream", "", "", "", "", "", "" },
|
||||
.{ "<arg0>", "status", "", "", "", "", "", "", "", "" },
|
||||
.{ "<arg0>", "--ignore-case", "pattern", "file1.txt", "file2.txt", "", "", "", "", "" },
|
||||
};
|
||||
|
||||
const opts, const files = try parse(CompilerOptions, .default, &.{
|
||||
"cc", "-O", "O2", "--target", "x86_64-linux", "--output", "program", "main.c", "utils.c", "lib.c",
|
||||
}, arena.allocator());
|
||||
|
||||
try testing.expectEqual(.O2, opts.O);
|
||||
try testing.expectEqualStrings("x86_64-linux", opts.target);
|
||||
try testing.expectEqualStrings("program", opts.output);
|
||||
try testing.expectEqual(@as(usize, 3), files.len);
|
||||
try testing.expectEqualStrings("main.c", files[0]);
|
||||
try testing.expectEqualStrings("utils.c", files[1]);
|
||||
try testing.expectEqualStrings("lib.c", files[2]);
|
||||
}
|
||||
|
||||
// docker run --detach true --publish 8080 80 --env KEY=value --name myapp nginx:latest
|
||||
test "std.cli: docker run" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const DockerRun = struct {
|
||||
struct {
|
||||
detach: bool = false,
|
||||
publish: @Vector(2, u16) = .{ 0, 0 },
|
||||
env: []const u8 = "",
|
||||
name: []const u8 = "",
|
||||
volume: []const u8 = "",
|
||||
},
|
||||
[]const u8, // image
|
||||
const expected = .{
|
||||
EncodeOptions{ .input = "video.mp4", .output = "result.mkv", .format = .mkv, .resolution = .{ 1920, 1080 }, .quality = null },
|
||||
GitCommand{ .remote = .{ .add = .{ .name = "origin", .url = "git@github.com:user/repo" } } },
|
||||
GitCommand{ .remote = .{ .remove = "upstream" } },
|
||||
GitCommand{ .status = {} },
|
||||
GrepArgs{ .{ .@"ignore-case" = {} }, @as([]const []const u8, &.{ "pattern", "file1.txt", "file2.txt", "", "", "", "", "" }) },
|
||||
};
|
||||
|
||||
const opts, const image = try parse(DockerRun, .default, &.{
|
||||
"docker-run", "--detach", "true", "--publish", "8080", "80", "--env", "KEY=value", "--name", "myapp", "nginx:latest",
|
||||
}, arena.allocator());
|
||||
|
||||
try testing.expectEqual(true, opts.detach);
|
||||
try testing.expectEqual(@Vector(2, u16){ 8080, 80 }, opts.publish);
|
||||
try testing.expectEqualStrings("KEY=value", opts.env);
|
||||
try testing.expectEqualStrings("myapp", opts.name);
|
||||
try testing.expectEqualStrings("nginx:latest", image);
|
||||
}
|
||||
|
||||
// tar --create true --gzip true --file archive.tar.gz src/ lib/ include/
|
||||
test "std.cli: tar" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const TarOptions = struct {
|
||||
struct {
|
||||
create: bool = false,
|
||||
extract: bool = false,
|
||||
gzip: bool = false,
|
||||
verbose: bool = false,
|
||||
file: []const u8,
|
||||
},
|
||||
[]const []const u8, // paths
|
||||
};
|
||||
|
||||
const opts, const paths = try parse(TarOptions, .default, &.{
|
||||
"tar", "--create", "true", "--gzip", "true", "--file", "archive.tar.gz", "src/", "lib/", "include/",
|
||||
}, arena.allocator());
|
||||
|
||||
try testing.expectEqual(true, opts.create);
|
||||
try testing.expectEqual(true, opts.gzip);
|
||||
try testing.expectEqualStrings("archive.tar.gz", opts.file);
|
||||
try testing.expectEqual(@as(usize, 3), paths.len);
|
||||
try testing.expectEqualStrings("src/", paths[0]);
|
||||
try testing.expectEqualStrings("lib/", paths[1]);
|
||||
try testing.expectEqualStrings("include/", paths[2]);
|
||||
inline for (types, &arguments, expected) |T, *a, e|
|
||||
try testing.expectEqualDeep(e, try parse(T, a, allocator));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue