Skip to content

[std] add minimal CLI parser std.cli #24881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

[std] add minimal CLI parser std.cli #24881

wants to merge 5 commits into from

Conversation

thejoshwolfe
Copy link
Contributor

Closes #24601

Comment on lines +127 to +130
/// --seed=0x<something> is actually passed in by the `zig test` system (as of 0.14.1), which we receive here.
seed: u32 = 0,
@"cache-dir": []const u8 = "",
listen: []const u8 = "",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is questionable.


fn checkArgsType(comptime Args: type) void {
const args_fields = @typeInfo(Args).@"struct".fields;
if (!(args_fields.len == 2 and mem.eql(u8, args_fields[0].name, "named") and mem.eql(u8, args_fields[1].name, "positional"))) @compileError("expected Args to have exactly these fields in this order: named, positional");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

requiring the order here seems questionable maybe.

Choose a reason for hiding this comment

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

It is. People WILL stumble upon it since they (understandably) think of structs more similar to a hashmap than an array.

But using std.sort.block and std.sort.binarySearch (there is no other search function in the standard library where you can define the predicate afaik) could help here.

@andrewrk
Copy link
Member

andrewrk commented Aug 17, 2025

I recommend giving it a trial run by porting all the files in tools/ to use this.

You can test at least that those compile successfully with zig build test-standalone.

@thejoshwolfe
Copy link
Contributor Author

thejoshwolfe commented Aug 17, 2025

Currently, just trying the parse() call results in an error return trace:

josh@nixos:~/dev/zig$ zig run --zig-lib-dir lib/ tools/docgen.zig -- --help
Usage: docgen [options] input output

   Generates an HTML document from a docgen template.

Options:
   --code-dir dir         Path to directory containing code example outputs
   --help                 Print this help and exit
error: Help
/home/josh/dev/zig/lib/std/cli.zig:275:13: 0x118c7af in innerParse__anon_25090 (std.zig)
            return error.Help;
            ^
/home/josh/dev/zig/lib/std/cli.zig:159:5: 0x118399b in parseIter__anon_24026 (std.zig)
    return innerParse(Args, arena, iter, prog, options.writer);
    ^
/home/josh/dev/zig/lib/std/cli.zig:115:5: 0x1169426 in parse__anon_22563 (std.zig)
    return parseIter(Args, arena, &iter, options);
    ^
/home/josh/dev/zig/tools/docgen.zig:41:18: 0x1166557 in main (docgen.zig)
    const args = try std.cli.parse(Args, arena, .{});
                 ^

This appears to be done unconditionally for all error codes:

zig/lib/std/start.zig

Lines 638 to 640 in 623290e

if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}

Is there any appetite for special casing the cli errors error{Help,Usage} to bypass the tracing? This would be similar to how python's sys.exit() i.e. raise SystemExit() bypasses the typical traceback.

If we don't want to modify callMain() for this cli feature, then the design of this PR needs to be adjusted to fully call std.process.exit() by default, and have an option for returning errors. i'll proceed in this direction unless someone stops me.

@andrewrk
Copy link
Member

Is there any appetite for special casing the cli errors error{Help,Usage} to bypass the tracing?

sounds like #24510

@rohlem
Copy link
Contributor

rohlem commented Aug 17, 2025

Is there any appetite for special casing the cli errors error{Help,Usage} to bypass the tracing?

Just my 2c, but if I propagated error.Help or error.Usage anywhere else in my program, I'd be very confused if it caused a program exit without an error trace.
(iiuc, since the code that triggers the help/usage printing is within std.cli, this would just be silent, unexplained program termination.)
If this is chosen as strategy, I'd recommend at least making the names more specific, f.e. error.StdCliHelp and error.StdCliUsage, since users aren't expected to intercept them anyway.

adjusted to fully call std.process.exit() by default, and have an option for returning errors.

I think that would be the simplest, most local solution. Options can gain fields exit_on_help: bool = true, exit_on_usage: bool = true (or potentially a combined one).

@thejoshwolfe
Copy link
Contributor Author

thejoshwolfe commented Aug 17, 2025

exit_on_help: bool = true, exit_on_usage: bool = true

I went with exit_on_error, and i'm calling --help an error, but it turns out that's not obviously the right design. i've started a discussion here: #24601 (comment)

EDIT: options.exit controls both the error and non-error exits.

@thejoshwolfe
Copy link
Contributor Author

i'm calling --help an error,

I'm no longer calling --help an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

std: minimal CLI parsing driven by struct fields
4 participants