// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . const std = @import("std"); const Allocator = std.mem.Allocator; const lp = @import("lightpanda"); const log = lp.log; /// Comptime CLI builder that generates a tagged union parser from a /// declarative command recipe. Each command becomes a union variant whose /// payload is a struct with one field per option. A `help` variant is added /// automatically; do not include it in the recipe. /// /// ## Parsing behavior /// /// `parse` reads `std.process.args`, picks a command by the first non-exec /// argument, then walks the rest as `--flag value` pairs. Quirks: /// /// - When no command is given, the parser defaults to `serve`. /// - `help`, `help `, ` help`, and ` --help` all /// yield the `help` union variant. When a command is named (in either /// position), the variant carries that command's enum tag so callers can /// print command-specific help; bare `help` and `help help` carry the /// `.help` tag. An unknown name after `help` returns /// `error.UnknownCommand`. /// - Legacy fallback: if the first argument starts with `--` and matches a /// known fetch/serve flag, the parser sniffs the command from it and /// re-parses argv. Only exists for backwards compatibility. /// /// ## Command descriptor fields /// /// - `name: []const u8` — canonical command name on the command line. /// - `options: tuple` — tuple of option descriptors (see below). Use `.{}` /// for none. /// - `shared_options: tuple` (optional) — extra options merged into this /// command. Useful for common flags shared across commands. An option /// that appears in both with the same field name and type is collapsed /// into one field (the command's own option wins); reusing a name with /// a different type is a compile error. /// - `before_parse: fn () void` (optional) — called once the command is /// known (by name or sniffed from a legacy flag), before any option is /// read. For mode-level process defaults that must already hold while /// the options themselves are parsed, e.g. log settings; an explicit /// option parsed later still wins. /// - `positional: struct` (optional) — a positional argument with `.name` /// and `.type` that may appear anywhere in argv. By default it holds a /// single value: `.type` must be an optional pointer-to-u8 slice (e.g. /// `?[:0]const u8`), it defaults to `null`, and passing it more than once /// returns `error.TooManyPositionalArguments`. With `.multiple = true`, /// `.type` is the (non-optional) element slice (e.g. `[:0]const u8`), the /// field becomes a `std.ArrayList(type)`, and each occurrence appends. /// /// ## Option descriptor fields /// /// - `name: []const u8` — snake_case field name. Both `--snake_case` and /// `--kebab-case` are accepted on the command line. /// - `field_name: []const u8` (optional) — name of the struct field the /// value is written to, when it should differ from the flag name. /// - `type` — the Zig type of the parsed value (see supported types below). /// When the stored representation differs from the CLI-facing one, use /// `.{ .cli = T, .memory = T }`; a `validator` must then produce the /// memory type. /// - `default` (optional) — compile-time default when the flag is absent. /// Rules vary by type; see the defaults section below. /// - `multiple: bool` (optional) — when `true`, the field becomes a /// `std.ArrayList(type)` and each occurrence appends. Not supported for /// `bool` or packed-struct options. /// - `validator: fn` (optional) — custom parse function that replaces the /// built-in type switch. See the validator section below. /// - `variants: tuple` (optional) — alternate flag names that write into /// the same field. See the variants section below. /// - `deprecated: []const u8` (optional) — the option still parses, but /// each use logs a warning carrying this note. /// /// ## Supported types and their defaults /// /// - `bool` — presence flips the field to the opposite of its `default` /// (so a flag with `default = true` acts as a disable switch). Defaults /// to `false` when no `default` is given. `?bool` is not allowed. /// - Integers (`u8`, `u16`, `u31`, `usize`, etc.) — parsed with /// `std.fmt.parseInt`. Requires `default` unless wrapped in `?`. /// - `[]const u8`, `[:0]const u8` (and mutable variants) — string slices /// duped from argv. Sentinel is preserved. Requires `default` unless `?`. /// - Enums — parsed via `std.meta.stringToEnum`. Returns /// `error.InvalidArgument` on a bad value. Requires `default` unless `?`. /// - Packed structs of `bool` fields — parsed from a comma-separated list /// (e.g. `--strip-mode js,css`). The literal `"full"` sets every field. /// Unknown names return `error.InvalidArgument`. Requires `default`. /// `multiple` is not supported. /// - Optional types default to `null` when `default` is omitted. /// /// ## Validators /// /// A `validator` is a custom parse function that takes over argument /// consumption for an option. It receives a pointer to the generated struct /// field and writes through it; the pointee depends on whether `multiple` /// is set: /// /// - Single: `fn (Allocator, *ArgIterator, *T) !void` — writes the parsed /// value through the field pointer. /// - Multiple: `fn (Allocator, *ArgIterator, *std.ArrayList(T)) !void` — /// appends directly into the list. /// /// When a validator is present, the built-in type switch is skipped entirely. /// The validator owns advancing the iterator and is free to peek ahead. /// Because the validator sees the field itself, a repeated flag can /// accumulate state in place (e.g. add each certificate to a store the /// field points at) instead of collecting values in a list. /// /// ## Variants /// /// A `variants` tuple lets multiple flag names write into the same field /// using different parse logic. Each variant has its own `.name` and an /// optional `.validator` (with the same signatures as above); the option's /// `type` and `multiple` are inherited. Useful for "value or file" pairs: /// e.g. `--wait-script "code"` vs `--wait-script-file path/to/script.js`, /// both populating the same `wait_script` field. /// /// ## Example /// /// ```zig /// const StripMode = packed struct(u2) { /// js: bool = false, /// css: bool = false, /// }; /// /// const WaitUntil = enum { load, domcontentloaded, networkidle }; /// /// const CommonOptions = .{ /// .{ .name = "verbose", .type = bool }, /// .{ .name = "log_level", .type = ?log.Level }, /// .{ .name = "timeout", .type = u31, .default = 30 }, /// }; /// /// const Cli = cli.Builder(.{ /// .{ /// .name = "serve", /// .options = .{ /// .{ .name = "host", .type = []const u8, .default = "127.0.0.1" }, /// .{ .name = "port", .type = u16, .default = 9222 }, /// }, /// .shared_options = CommonOptions, /// }, /// .{ /// .name = "fetch", /// .positional = .{ .name = "url", .type = ?[:0]const u8 }, /// .options = .{ /// .{ .name = "dump", .type = ?DumpFormat, .validator = dumpValidator }, /// .{ .name = "strip_mode", .type = StripMode, .default = .{} }, /// .{ .name = "wait_until", .type = ?WaitUntil }, /// .{ .name = "extra_header", .type = []const u8, .multiple = true }, /// .{ /// .name = "wait_script", /// .type = ?[:0]const u8, /// .variants = .{ /// .{ .name = "wait_script_file", .validator = readScriptFile }, /// }, /// }, /// }, /// .shared_options = CommonOptions, /// }, /// .{ .name = "version", .options = .{} }, /// }); /// /// const _, const cmd = try Cli.parse(arena); /// switch (cmd) { /// .serve => |opts| listen(opts.host, opts.port), /// .fetch => |opts| fetch(opts.url orelse return error.UrlRequired, opts.dump), /// .version => printVersion(), /// .help => |tag| printHelp(tag), /// } /// ``` pub fn Builder(comptime commands: anytype) type { return struct { const Self = @This(); /// Enum type for provided commands. pub const Enum = blk: { const len = commands.len + 1; const Tag = std.math.IntFittingRange(0, len); var names: [len][:0]const u8 = undefined; var i: usize = 0; while (i < commands.len) : (i += 1) { names[i] = commands[i].name; } // Entry for help. names[i] = "help"; break :blk @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, len)); }; /// Creates an array of `StructField` out of given options. fn optionsToStructFields(comptime options: anytype) [options.len]std.builtin.Type.StructField { var fields: [options.len]std.builtin.Type.StructField = undefined; inline for (options, 0..) |option, j| { // Whether prefer `ArrayList` for the option. const is_multiple = @hasField(@TypeOf(option), "multiple") and option.multiple; // Whether option has a default value. const has_default = @hasField(@TypeOf(option), "default"); const Type = blk: { if (@typeInfo(@TypeOf(option.type)) == .@"struct") { break :blk option.type.memory; } break :blk option.type; }; const T = if (is_multiple) std.ArrayList(Type) else Type; // Prefer `option.field_name` if memory representation will differ. const name = blk: { if (@hasField(@TypeOf(option), "field_name")) { break :blk option.field_name; } break :blk option.name; }; const default = blk: { if (is_multiple) { // We currently don't allow default values for lists. if (has_default) { @compileError("`default` is not allowed for lists"); } // Multiples are always initialized the same. break :blk @as(*const anyopaque, @ptrCast(&@as(T, .empty))); } switch (@typeInfo(Type)) { .optional => |optional| { if (optional.child == bool) { @compileError("?bool is not supported, prefer enum"); } // If type is an optional type without default value, prefer null. if (!has_default) { break :blk @as(*const anyopaque, @ptrCast(&@as(T, null))); } // We have default value for an optional. break :blk @as(*const anyopaque, @ptrCast(&@as(T, option.default))); }, .bool => { // Prefer `false` if no default. const default = if (has_default) option.default else false; break :blk @as(*const anyopaque, @ptrCast(&@as(T, default))); }, inline else => { if (!has_default) { @compileError("option `" ++ name ++ "` is not optional type and has no default value"); } break :blk @as(*const anyopaque, @ptrCast(&@as(T, option.default))); }, } }; fields[j] = .{ .name = name, .type = T, .default_value_ptr = default, .is_comptime = false, .alignment = @alignOf(T), }; } return fields; } /// Drops duplicate fields, keeping the first occurrence. Only an exact /// duplicate (same name and type) is deduplicated; a name that /// reappears with a different type is a conflict. fn dedupeStructFields(comptime fields: []const std.builtin.Type.StructField) []const std.builtin.Type.StructField { // The pairwise name comparisons blow the default 1000-branch quota. @setEvalBranchQuota(1000 + fields.len * fields.len * 100); var out: [fields.len]std.builtin.Type.StructField = undefined; var len: usize = 0; outer: for (fields) |field| { for (out[0..len]) |existing| { if (!std.mem.eql(u8, existing.name, field.name)) continue; if (existing.type != field.type) { @compileError("field `" ++ field.name ++ "` is duplicated with a different type"); } continue :outer; } out[len] = field; len += 1; } const frozen = out; return frozen[0..len]; } /// Union type for provided commands. pub const Union = blk: { const len = commands.len + 1; var union_fields: [len]std.builtin.Type.UnionField = undefined; var i: usize = 0; while (i < commands.len) : (i += 1) { const command = commands[i]; const Command = @TypeOf(command); const options = command.options; const all_fields = optionsToStructFields(options) ++ (if (@hasField(Command, "shared_options")) optionsToStructFields(command.shared_options) else .{}) ++ (if (@hasField(Command, "positional")) [1]std.builtin.Type.StructField{positionalField(command.positional)} else .{}); const T = StructFromFields(dedupeStructFields(&all_fields)); union_fields[i] = .{ .name = command.name, .type = T, .alignment = @alignOf(T) }; } // Entry for help; just takes `Enum` itself. const Help = Enum; union_fields[i] = .{ .name = "help", .type = Help, .alignment = @alignOf(Help) }; var names: [len][:0]const u8 = undefined; var types: [len]type = undefined; var attrs: [len]std.builtin.Type.UnionField.Attributes = undefined; for (union_fields, 0..) |f, j| { names[j] = f.name; types[j] = f.type; attrs[j] = .{ .@"align" = f.alignment }; } break :blk @Union(.auto, Enum, &names, &types, &attrs); }; fn StructFromFields(comptime fields: []const std.builtin.Type.StructField) type { var names: [fields.len][:0]const u8 = undefined; var types: [fields.len]type = undefined; var attrs: [fields.len]std.builtin.Type.StructField.Attributes = undefined; for (fields, 0..) |f, i| { names[i] = f.name; types[i] = f.type; attrs[i] = .{ .@"comptime" = f.is_comptime, .@"align" = f.alignment, .default_value_ptr = f.default_value_ptr }; } return @Struct(.auto, null, &names, &types, &attrs); } /// Builds the `StructField` for a command's positional argument. A plain /// positional is an optional that defaults to `null`; a `multiple` /// positional collects every occurrence into an `ArrayList` that /// defaults to empty. fn positionalField(comptime positional: anytype) std.builtin.Type.StructField { const is_multiple = @hasField(@TypeOf(positional), "multiple") and positional.multiple; const T = if (is_multiple) std.ArrayList(positional.type) else positional.type; const default: *const anyopaque = if (is_multiple) @ptrCast(&@as(T, .empty)) else @ptrCast(&@as(T, null)); return .{ .name = positional.name, .type = T, .default_value_ptr = default, .is_comptime = false, .alignment = @alignOf(T), }; } /// Parses executable name, command and options via single call. pub fn parse(allocator: Allocator, proc_args: std.process.Args) !struct { []const u8, Union } { var args = std.process.Args.Iterator.init(proc_args); defer args.deinit(); const exec_name = std.fs.path.basename(args.next().?); const cmd_str: []const u8 = args.next() orelse "serve"; inline for (commands) |command| { // Match a command. if (std.mem.eql(u8, cmd_str, command.name)) { const cmd_parsed = try parseCommand(allocator, command, &args); return .{ exec_name, cmd_parsed }; } } // Help is not in `commands`; so, we have to special case it. if (std.mem.eql(u8, cmd_str, "help")) { // Check if we're followed by a command name. const command_name: []const u8 = args.next() orelse { // "lightpanda help"; short-circuit. return .{ exec_name, @unionInit(Union, "help", .help) }; }; inline for (commands) |command| { if (std.mem.eql(u8, command_name, command.name)) { return .{ exec_name, @unionInit(Union, "help", std.meta.stringToEnum(Enum, command.name).?), }; } } // Treat `help help` as the full help. if (std.mem.eql(u8, command_name, "help")) { return .{ exec_name, @unionInit(Union, "help", .help) }; } log.fatal(.app, "unknown command", .{ .arg = command_name }); return error.UnknownCommand; } // Last resort, try sniffing. const command_enum = try sniffCommand(cmd_str); // Legacy `--help` situation. if (command_enum == .help) { return .{ exec_name, @unionInit(Union, "help", .help) }; } // "cmd_str" wasn't a command but an option. We can't reset args, but // we can create a new one. Not great, but this fallback is temporary // as we transition to this command mode approach. args.deinit(); args = std.process.Args.Iterator.init(proc_args); // Skip the `exec_name`. _ = args.skip(); inline for (commands) |command| { if (std.mem.eql(u8, @tagName(command_enum), command.name)) { const cmd_parsed = try parseCommand(allocator, command, &args); return .{ exec_name, cmd_parsed }; } } unreachable; } /// Try to sniff the command out of given option. /// Only exists for legacy reasons; hence hardcoded. fn sniffCommand(cmd_str: []const u8) error{UnknownCommand}!Enum { if (std.mem.startsWith(u8, cmd_str, "--") == false) { return .fetch; } // Fetch heuristics. inline for (.{ "--dump", "--strip-mode", "--strip_mode", "--with-base", "--with_base", "--with-frames", "--with_frames", }) |heuristic| { if (std.mem.eql(u8, cmd_str, heuristic)) { return .fetch; } } // Serve heuristics. inline for (.{ "--host", "--port", }) |heuristic| { if (std.mem.eql(u8, cmd_str, heuristic)) { return .serve; } } // Legacy `--help` flag maps to the `help` command. if (std.mem.eql(u8, cmd_str, "--help")) { return .help; } return error.UnknownCommand; } /// Returns the type for validator function. pub fn ValidatorFn(comptime T: type, comptime is_multiple: bool) type { if (is_multiple) { return *const fn (Allocator, *std.process.Args.Iterator, *std.ArrayList(T)) anyerror!void; } return *const fn (Allocator, *std.process.Args.Iterator, *T) anyerror!void; } /// Turns a snake_case string to kebab-case in comptime. fn toKebabCase(comptime str: []const u8) [str.len]u8 { var output: [str.len]u8 = str[0..str.len].*; for (&output) |*c| if (c.* == '_') { c.* = '-'; }; return output; } fn parseValue( allocator: Allocator, args: *std.process.Args.Iterator, /// Pointer to field; *T. target: anytype, /// `Option` doesn't have a concrete type; this field expects: /// ```zig /// Option{ /// .name = "option_name", /// // If provided, names the struct field written to instead of `name`. /// .field_name = "struct_field_name", /// .type = T, // or .{ .cli = T, .memory = T } /// .multiple = ?bool, /// .validator = ?ValidatorFn(T, is_multiple), /// }; /// ``` option: anytype, ) !void { const kebab_cased = "--" ++ comptime toKebabCase(option.name); const OptionType = @TypeOf(option); const is_multiple = @hasField(OptionType, "multiple") and option.multiple; const has_validator = @hasField(OptionType, "validator"); if (@hasField(OptionType, "deprecated")) { log.warn(.app, "deprecated CLI parameter", .{ .name = option.name, .note = option.deprecated }); } // Prefer validator for parsing if provided. The validator writes // through the field pointer (the list itself for multiples). if (has_validator) { return @call(.auto, option.validator, .{ allocator, args, target }); } // Extract type info. We need the type that's used in the CLI. const T = blk: { if (@typeInfo(@TypeOf(option.type)) == .@"struct") { break :blk option.type.cli; } break :blk option.type; }; const option_info = blk: { const info = @typeInfo(T); // If wrapped in optional, prefer the child type. if (info == .optional) break :blk @typeInfo(info.optional.child); break :blk info; }; // Parse by type. return switch (option_info) { .int => |int| { const Int = std.meta.Int(int.signedness, int.bits); const str = args.next() orelse return error.MissingArgument; const v = std.fmt.parseInt(Int, str, 10) catch |err| { switch (err) { error.Overflow => log.fatal(.app, "range overflow", .{ .arg = kebab_cased, .value = str }), error.InvalidCharacter => log.fatal(.app, "invalid character", .{ .arg = kebab_cased, .value = str }), } return error.InvalidArgument; }; if (is_multiple) { // Push to ArrayList. try target.append(allocator, v); } else { target.* = v; } }, .pointer => |pointer| { const not_u8_slice = pointer.child != u8 or pointer.size != .slice; if (not_u8_slice) { @compileError("Only []u8, []const u8, [:sentinel]u8 and [:sentinel]const u8 pointers are supported"); } const v = blk: { const str = args.next() orelse return error.MissingArgument; // DupeZ branch. if (comptime pointer.sentinel()) |sentinel| { const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment orelse @alignOf(u8)), str.len + 1); @memcpy(buf[0..str.len], str); buf[str.len] = sentinel; break :blk buf[0..str.len :sentinel]; } // Dupe branch. const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment orelse @alignOf(u8)), str.len); @memcpy(buf, str); break :blk buf; }; if (is_multiple) { try target.append(allocator, v); } else { target.* = v; } }, .@"struct" => |_struct| { // Don't support multiple for structs for now. if (is_multiple) { @compileError("multiple option is not supported for structs"); } const not_packed = _struct.layout != .@"packed"; if (not_packed) { @compileError("only packed structs are allowed"); } const str = args.next() orelse return error.MissingArgument; if (std.mem.eql(u8, str, "full")) { // "full" sets all the fields of packed struct. const Int = _struct.backing_integer orelse @compileError("packed struct must provide a backing integer"); target.* = @bitCast(@as(Int, std.math.maxInt(Int))); } else { // Parse given args. var it = std.mem.tokenizeScalar(u8, str, ','); outer: while (it.next()) |part| { const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace); inline for (_struct.fields) |f| { lp.assert(f.type == bool, "all fields of packed struct must be boolean", .{ .option = option.name, .field = f.name, }); if (std.mem.eql(u8, trimmed, @as([]const u8, f.name))) { @field(target, f.name) = true; continue :outer; } } // Invalid option choice. log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = trimmed }); return error.InvalidArgument; } } }, .@"enum" => { const E = switch (@typeInfo(T)) { .optional => |optional| optional.child, inline else => T, }; const str = args.next() orelse return error.MissingArgument; const v = std.meta.stringToEnum(E, str) orelse { log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = str }); return error.InvalidArgument; }; if (is_multiple) { try target.append(allocator, v); } else { target.* = v; } }, .bool => { if (is_multiple) { @compileError("multiple option is not supported for booleans"); } const default = blk: { if (@hasField(@TypeOf(option), "default")) { break :blk option.default; } break :blk false; }; // Set opposite of the default. target.* = !default; }, else => unreachable, }; } /// Parses the command with its options. fn parseCommand( allocator: Allocator, command: anytype, args: *std.process.Args.Iterator, ) !Union { const Command = @FieldType(Union, command.name); if (@hasField(@TypeOf(command), "before_parse")) { command.before_parse(); } var c = Command{}; const options = blk: { if (@hasField(@TypeOf(command), "shared_options")) { break :blk command.options ++ command.shared_options; } break :blk command.options; }; iter_args: while (args.next()) |option_name| { inline for (options) |option| { const name = option.name; const field_name = blk: { if (@hasField(@TypeOf(option), "field_name")) { break :blk option.field_name; } break :blk option.name; }; // We allow both `--my-option` and `--my_option` variants; // assuming given `option` struct prefer snake_case for `name`. // Match an option. const matches_short = comptime @hasField(@TypeOf(option), "short"); if (std.mem.eql(u8, option_name, "--" ++ name) or std.mem.eql(u8, option_name, "--" ++ comptime toKebabCase(name)) or (matches_short and std.mem.eql(u8, option_name, "-" ++ [_]u8{option.short}))) { try parseValue(allocator, args, &@field(c, field_name), option); continue :iter_args; } const is_multiple = @hasField(@TypeOf(option), "multiple") and option.multiple; // Parse for variants if there are. const has_variants = @hasField(@TypeOf(option), "variants"); if (has_variants) { inline for (option.variants) |variant| { if (std.mem.eql(u8, option_name, "--" ++ variant.name) or std.mem.eql(u8, option_name, "--" ++ comptime toKebabCase(variant.name))) { const opts = blk: { if (@hasField(@TypeOf(variant), "validator")) { break :blk .{ .name = variant.name, .type = option.type, .multiple = is_multiple, .validator = variant.validator, }; } break :blk .{ .name = variant.name, .type = option.type, .multiple = is_multiple }; }; try parseValue(allocator, args, &@field(c, field_name), opts); continue :iter_args; } } } } // Subcommand help: `lightpanda fetch help` or `lightpanda fetch --help`. if (std.mem.eql(u8, option_name, "help") or std.mem.eql(u8, option_name, "--help")) { return @unionInit(Union, "help", std.meta.stringToEnum(Enum, command.name).?); } // Encountered an option we don't know of. if (std.mem.startsWith(u8, option_name, "--")) { log.fatal(.app, "unknown argument", .{ .mode = command.name, .arg = option_name }); return error.UnknownOption; } // Parse positional arg if provided; can be given out of order: // // lightpanda fetch --wait-ms 2_000 "https://lightpanda.io" --dump "html" // ---------------------------------^ if (comptime @hasField(@TypeOf(command), "positional")) { const positional = command.positional; const is_multiple = comptime @hasField(@TypeOf(positional), "multiple") and positional.multiple; // A single (non-multiple) positional may only be given once. if (!is_multiple and @field(c, positional.name) != null) { return error.TooManyPositionalArguments; } // Element type: the optional's child for a single positional, // the slice element itself for a `multiple` one. const Child = if (is_multiple) positional.type else @typeInfo(positional.type).optional.child; const info = @typeInfo(Child); const str = @as([]const u8, option_name); switch (info) { .pointer => |pointer| { const not_u8_slice = pointer.child != u8 or pointer.size != .slice; if (not_u8_slice) { @compileError("Only []u8, []const u8, [:sentinel]u8 and [:sentinel]const u8 pointers are supported"); } const v = blk: { // DupeZ branch. if (comptime pointer.sentinel()) |sentinel| { const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment orelse @alignOf(u8)), str.len + 1); @memcpy(buf[0..str.len], str); buf[str.len] = sentinel; break :blk buf[0..str.len :sentinel]; } // Dupe branch. const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment orelse @alignOf(u8)), str.len); @memcpy(buf, str); break :blk buf; }; if (is_multiple) { try @field(c, positional.name).append(allocator, v); } else { @field(c, positional.name) = v; } }, inline else => @compileError("not supported"), } } else { log.fatal(.app, "unknown argument", .{ .mode = command.name, .arg = option_name }); return error.UnknownOption; } } // A non-optional, single positional that is still null after parsing // is missing. A `multiple` positional is never required here — the // caller decides whether an empty list is acceptable. if (comptime @hasField(@TypeOf(command), "positional")) { const is_multiple = @hasField(@TypeOf(command.positional), "multiple") and command.positional.multiple; const is_optional = @typeInfo(command.positional.type) == .optional; if (!is_multiple and !is_optional and @field(c, command.positional.name) == null) { return error.MissingArgument; } } return @unionInit(Union, command.name, c); } }; }