authorchloe caruso <git@paperclover.net> 2024-09-16 03:40:59-04:00
committerchloe caruso <git@paperclover.net> 2024-09-16 03:40:59-04:00
loga298f902e7a56c5c2205870920fbfa2144b14efa
treebb80e580c2b7d9968db8183639bffb96dadb6a41
parenta1c102b80d39c733eec219da352304c4eff8ab8e

chore: rename cli -> Cli.zig

this was missed due to macOS having a case-insensitive filesystem, so renaming the file's casing did not cause git to track the change.

3 files changed, 660 insertions(+), 659 deletions(-)

src/Cli.zig created+659
......@@ -0,0 +1,659 @@
1//! Configuration for an command line interface, which can be executed against
2//! an argument slice using `.parse`. None of the parsing strategies require an
3//! allocator, however it expects incoming arguments to be UTF-8 formatted which
4//! on Windows will require an allocator.
5//!
6//! See `fn main` for a basic usage example.
7options: []const Option = &.{},
8positionals: Positionals = .none,
9help: HelpConfig = .{},
10
11pub const Option = struct {
12 /// Should be formatted as the flag name: '--dump-ir'
13 name: []const u8,
14 /// Displayed in generated help menu
15 desc: []const u8,
16 kind: Kind = .boolean,
17 /// A short code allows single dash argument parsing.
18 short: ?u8 = null,
19
20 const Kind = union(enum) {
21 /// UTF-8 validated
22 string,
23 boolean,
24 @"enum": type,
25
26 pub fn Type(k: Kind) type {
27 return switch (k) {
28 .string => []const u8,
29 .boolean => bool,
30 .@"enum" => |t| t,
31 };
32 }
33 };
34
35 pub fn optional(comptime cond: bool, opts: []const Option) []const Option {
36 return if (cond) opts else &.{};
37 }
38
39 const help: Option = .{
40 .name = "--help",
41 .short = 'h',
42 .desc = "Print this help and exit",
43 };
44};
45
46pub const Positionals = union(enum) {
47 none,
48 /// Store a single positional: ?[]const u8
49 single,
50 /// Store the start and count of positionals. Call
51 /// 'parsed.positionals.iterator(args)' with the
52 /// original argument array to read positionals.
53 unlimited,
54 /// Same strategy as .unlimited, with a limit
55 limited: usize,
56 /// Use a bounded array to store positionals, using
57 /// a stack allocated buffer that can hold enough
58 /// '[]const u8's as allowed by the bound.
59 bounded: usize,
60
61 pub fn Type(p: Positionals) type {
62 return switch (p) {
63 .none => void,
64 .single => ?[]const u8,
65 .limited => |limit| switch (limit) {
66 0 => void,
67 else => Multi,
68 },
69 .unlimited => Multi,
70 .bounded => |n| std.BoundedArray([]const u8, n),
71 };
72 }
73
74 pub inline fn maxCount(p: Positionals) usize {
75 return switch (p) {
76 .none => 0,
77 .single => 1,
78 inline .limited, .bounded => |limit| limit,
79 .unlimited => std.math.maxInt(usize),
80 };
81 }
82
83 pub const Multi = struct {
84 /// The index of the first positional
85 first: usize,
86 /// The number of total positionals. This can
87 /// be used to pre-allocate resources.
88 len: usize,
89
90 first_is_after_double_dash: bool,
91
92 pub const empty = .{
93 .first = 0,
94 .len = 0,
95 .first_is_after_double_dash = false,
96 };
97
98 pub fn iterator(pos: @This(), all_args: []const []const u8) PositionalIterator {
99 return .{
100 .args = all_args,
101 .i = pos.first,
102 .left = pos.len,
103 .after_double_dash = pos.first_is_after_double_dash,
104 };
105 }
106 };
107};
108
109pub const HelpConfig = struct {
110 /// If set true, registers an addition '--help' / '-h' flag
111 /// These will show the help menu and exit the program.
112 ///
113 /// Help menu can always be displayed manually with .showHelp()
114 option: bool = true,
115
116 /// Override the usage string in the first line.
117 usage: ?[]const u8 = null,
118
119 /// Hide a list of flag names.
120 hide_options: []const []const u8 = &.{},
121};
122
123pub const PositionalIterator = struct {
124 args: []const []const u8,
125 i: usize,
126 left: usize,
127 after_double_dash: bool,
128
129 pub fn peek(it: PositionalIterator) ?[]const u8 {
130 return if (it.left > 0) it.args[it.i] else null;
131 }
132
133 pub fn next(it: *PositionalIterator) ?[]const u8 {
134 if (it.left > 0) {
135 defer it.advance();
136 return it.args[it.i];
137 } else {
138 return null;
139 }
140 }
141
142 pub fn advance(it: *PositionalIterator) void {
143 it.left -= 1;
144 if (it.left == 0) return;
145 if (it.after_double_dash) {
146 it.i += 1;
147 } else while (mem.startsWith(u8, it.args[it.i], "-")) {
148 defer it.i += 1;
149 if (it.args[it.i].len == 2 and it.args[it.i][1] == '-') {
150 it.after_double_dash = true;
151 return;
152 }
153 }
154 }
155};
156
157inline fn fieldName(comptime name: []const u8) [:0]const u8 {
158 std.debug.assert(name[0] == '-' and name[1] == '-');
159 for (name[2..]) |char| switch (char) {
160 'a'...'z', '-' => {},
161 else => |ch| @compileError(std.fmt.comptimePrint("Unexpected char '{c}' in option name '{s}'", .{ ch, name })),
162 };
163
164 const replacement_size = mem.replacementSize(u8, name[2..], "-", "_");
165 const replaced = brk: {
166 var buf: [replacement_size:0]u8 = undefined;
167 _ = mem.replace(u8, name[2..], "-", "_", &buf);
168 break :brk buf;
169 };
170 return &replaced;
171}
172
173/// Every option name is converted into snake case, then
174/// an additional field name 'positionals' is added.
175pub fn Parsed(p: Cli) type {
176 const fields = brk: {
177 var fields: [p.options.len + 1]Type.StructField = undefined;
178
179 for (p.options, fields[0..p.options.len]) |def, *field| {
180 const Wrapped = ?def.kind.Type();
181 field.* = .{
182 .name = fieldName(def.name),
183 .alignment = @alignOf(Wrapped),
184 .default_value = &@as(Wrapped, null),
185 .is_comptime = false,
186 .type = Wrapped,
187 };
188 }
189
190 const Pos = p.positionals.Type();
191 fields[fields.len - 1] = .{
192 .name = "positionals",
193 .alignment = @alignOf(Pos),
194 .type = Pos,
195 .default_value = &@as(Pos, switch (p.positionals) {
196 .none => {},
197 .single => null,
198 .limited, .unlimited => .empty,
199 .bounded => .{},
200 }),
201 .is_comptime = false,
202 };
203
204 break :brk fields;
205 };
206
207 return @Type(.{ .@"struct" = .{
208 .layout = .auto,
209 .fields = &fields,
210 .decls = &.{},
211 .is_tuple = false,
212 } });
213}
214
215const StdErrWriter = struct {
216 file: std.fs.File,
217 buffered_writer: std.io.BufferedWriter(4096, std.fs.File.Writer),
218 tty: std.io.tty.Config,
219 writer_ptr: std.io.BufferedWriter(4096, std.fs.File.Writer).Writer,
220
221 pub fn init() StdErrWriter {
222 const stderr = std.io.getStdErr();
223 const tty = std.io.tty.detectConfig(stderr);
224 return .{
225 .file = stderr,
226 .tty = tty,
227 .buffered_writer = std.io.bufferedWriter(stderr.writer()),
228 .writer_ptr = undefined,
229 };
230 }
231
232 pub fn get(w: *StdErrWriter) std.io.AnyWriter {
233 if (@import("builtin").os.tag == .windows and w.tty == .windows_api) {
234 // all data must be flushed before changing colors using
235 // the Windows API. thankfully, modern windows terminals
236 // will not hit this path.
237 return w.buffered_writer.unbuffered_writer.any();
238 } else {
239 w.writer_ptr = w.buffered_writer.writer(); // uaf unless stored
240 return w.writer_ptr.any();
241 }
242 }
243};
244
245const ParseError = error{ InvalidArguments, DisplayedHelpMenu };
246
247/// This will quit the program with usage information if invalid arguments are
248/// received and the `--help` option is enabled. This is the most common use
249/// case. If manual error handling is desired, `.parseWithErrorWriter` can
250/// be used instead.
251pub fn parse(
252 comptime p: Cli,
253 program_name: []const u8,
254 args: []const []const u8,
255) p.Parsed() {
256 var stderr = StdErrWriter.init();
257 defer stderr.buffered_writer.flush() catch {};
258 return parseWithErrorWriter(
259 p,
260 program_name,
261 args,
262 stderr.get(),
263 stderr.tty,
264 true,
265 true,
266 ) catch |e| switch (e) {
267 error.InvalidArguments,
268 error.DisplayedHelpMenu,
269 => {
270 stderr.buffered_writer.flush() catch {};
271 std.process.exit(1);
272 },
273 };
274}
275
276pub fn parseWithErrorWriter(
277 comptime p: Cli,
278 program_name: []const u8,
279 args: []const []const u8,
280 err: std.io.AnyWriter,
281 tty: std.io.tty.Config,
282 comptime show_help_on_error: bool,
283 comptime exit_on_error: bool,
284) ParseError!p.Parsed() {
285 var out: p.Parsed() = .{};
286 var had_errors = false;
287 var positionals_counted: usize = 0;
288 var first_positional: usize = 0;
289 var double_dash: ?usize = null;
290
291 var show_help = false;
292
293 const max_positionals = p.positionals.maxCount();
294
295 outer: for (args, 0..) |arg, i| {
296 if (mem.startsWith(u8, arg, "--")) {
297 if (arg.len == 2) {
298 if (max_positionals == 0) {
299 writeError(err, tty);
300 err.writeAll("unexpected end-of-parameter mark: --\n") catch {};
301 }
302
303 double_dash = i;
304 const remain = args[i + 1 ..];
305 if (positionals_counted == 0 and remain.len > 0)
306 first_positional = i + 1;
307
308 if (p.positionals == .bounded) {
309 out.positionals.appendSliceAssumeCapacity(remain[0..@min(remain.len, max_positionals -| positionals_counted)]);
310 }
311
312 positionals_counted += remain.len;
313
314 if (positionals_counted > max_positionals) {
315 had_errors = true;
316 const diff = @min(remain.len, positionals_counted - max_positionals);
317 for (remain[remain.len - diff ..]) |pos| {
318 writeError(err, tty);
319 err.print("unrecognized argument '{s}'\n", .{pos}) catch {};
320 }
321 }
322
323 break;
324 }
325
326 inline for (p.options) |def| try_this_def: {
327 const prefix = def.name;
328 const kind = def.kind;
329 const dest = &@field(out, fieldName(def.name));
330
331 const is_blank = arg.len == prefix.len;
332 if (!(mem.startsWith(u8, arg[2..], prefix[2..]) and (is_blank or arg[prefix.len] == '=')))
333 break :try_this_def;
334
335 if (kind != .boolean and is_blank) {
336 had_errors = true;
337 writeError(err, tty);
338 err.print(prefix ++ " requires a value\n", .{}) catch {};
339 continue :outer;
340 }
341
342 const value = if (is_blank) "" else arg[prefix.len + 1 ..];
343 switch (kind) {
344 .boolean => if (value.len == 0) {
345 dest.* = true;
346 } else if (mem.eql(u8, value, "false")) {
347 dest.* = false;
348 } else if (mem.eql(u8, value, "true")) {
349 dest.* = true;
350 } else {
351 had_errors = true;
352 writeError(err, tty);
353 err.print(prefix ++ " only accepts true or false, got '{s}'\n", .{
354 arg[prefix.len + 1 ..],
355 }) catch {};
356 },
357
358 .string => if (std.unicode.utf8ValidateSlice(value)) {
359 dest.* = value;
360 } else {
361 had_errors = true;
362 writeError(err, tty);
363 err.print(prefix ++ " requires valid UTF-8 input\n", .{}) catch {};
364 },
365
366 .@"enum" => |E| {
367 dest.* = std.meta.stringToEnum(E, value) orelse {
368 had_errors = true;
369 const values_list = comptime brk: {
370 var str: []const u8 = "";
371 for (std.enums.values(E)) |val| {
372 str = str ++ " " ++ @tagName(val) ++ "\n";
373 }
374 break :brk str;
375 };
376 writeError(err, tty);
377 err.print(prefix ++ " must be one of the following values, got {s}:\n{s}", .{
378 arg[prefix.len + 1 ..],
379 values_list,
380 }) catch {};
381 continue :outer;
382 };
383 },
384 }
385
386 continue :outer;
387 }
388
389 if (p.help.option) {
390 if (mem.startsWith(u8, arg[2..], "help") and (arg.len == "--help".len or arg["--help".len] == '=')) {
391 show_help = true;
392 if (arg.len > "--help".len) {
393 had_errors = true;
394 err.print("--help does not take a value\n", .{}) catch {};
395 }
396 continue :outer;
397 }
398 }
399
400 had_errors = true;
401 writeError(err, tty);
402 err.print("unrecognized argument '{s}'\n", .{arg}) catch {};
403 } else if (arg.len > 1 and arg[0] == '-') {
404 per_char_in_short: for (arg[1..]) |ch| {
405 inline for (p.options) |def| try_this_def: {
406 const short = def.short orelse break :try_this_def;
407 if (def.kind != .boolean) {
408 @compileError("TODO: non-boolean shorthand arguments");
409 }
410 if (ch == short) {
411 const dest = &@field(out, fieldName(def.name));
412 dest.* = true;
413 continue :per_char_in_short;
414 }
415 if (p.help.option and ch == 'h') {
416 show_help = true;
417 }
418 }
419 }
420 } else {
421 if (positionals_counted == 0) first_positional = i;
422 positionals_counted += 1;
423 if (positionals_counted > max_positionals) {
424 had_errors = true;
425 writeError(err, tty);
426 err.print("unrecognized argument '{s}'\n", .{arg}) catch {};
427 } else if (p.positionals == .bounded) {
428 out.positionals.appendAssumeCapacity(arg);
429 }
430 }
431 }
432
433 if (had_errors) {
434 if (positionals_counted > max_positionals) {
435 writeNote(err, tty);
436 err.writeAll(switch (max_positionals) {
437 0 => "this program does not take positional arguments\n",
438 1 => "this program accepts one positional argument\n",
439 else => |n| std.fmt.comptimePrint(
440 "this program accepts up to {d} positional arguments\n",
441 .{n},
442 ),
443 }) catch {};
444 }
445 if (show_help or show_help_on_error) {
446 p.showHelpWithWriter(program_name, err, tty);
447 }
448 if (exit_on_error) {
449 return error.DisplayedHelpMenu;
450 }
451 return error.InvalidArguments;
452 }
453
454 if (show_help) {
455 p.showHelpWithWriter(program_name, err, tty);
456 return error.DisplayedHelpMenu;
457 }
458
459 out.positionals = switch (p.positionals) {
460 .none => {},
461 .single => if (positionals_counted > 0)
462 args[first_positional]
463 else
464 null,
465 .bounded => out.positionals, // already correct
466 .unlimited, .limited => if (max_positionals > 0) .{
467 .first = first_positional,
468 .len = positionals_counted,
469 .first_is_after_double_dash = if (double_dash) |dd|
470 dd < first_positional
471 else
472 false,
473 },
474 };
475
476 return out;
477}
478
479pub fn showHelp(comptime p: Cli, program_name: []const u8) void {
480 var stderr = StdErrWriter.init();
481 defer stderr.buffered_writer.flush() catch {};
482 return showHelpWithWriter(
483 p,
484 program_name,
485 stderr.get(),
486 stderr.tty,
487 );
488}
489
490pub fn showHelpWithWriter(
491 comptime p: Cli,
492 program_name: []const u8,
493 w: std.io.AnyWriter,
494 tty: std.io.tty.Config,
495) void {
496 const usage = p.help.usage orelse default: {
497 const opts_usage = if (p.options.len > 0 or p.help.option) "[options...]" else "";
498 break :default switch (p.positionals.maxCount()) {
499 0 => opts_usage,
500 1 => mem.trimLeft(u8, opts_usage ++ " [argument]", " "),
501 else => mem.trimLeft(u8, opts_usage ++ " [args...]", " "),
502 };
503 };
504
505 w.print("usage: {s} {s}\n", .{ program_name, usage }) catch return;
506
507 if (p.options.len > 0 or p.help.option) {
508 w.writeAll("\n") catch return;
509 tty.setColor(w, .bold) catch return;
510 w.writeAll("options:") catch return;
511 tty.setColor(w, .reset) catch return;
512 w.writeAll(comptime brk: {
513 var width = 0;
514 var any_short_options = false;
515
516 const all_opts = if (p.help.option)
517 p.options ++ [_]Option{.help}
518 else
519 p.options;
520 for (all_opts) |opt| {
521 width = @max(width, opt.name.len);
522 any_short_options = any_short_options or opt.short != null;
523 }
524
525 width += " ".len;
526 if (any_short_options) width += "-h ".len;
527 const sub = if (any_short_options) 4 else 1;
528
529 var out: []const u8 = "\n";
530 for (all_opts) |opt| {
531 out = out ++ " ";
532 if (any_short_options) {
533 out = out ++ if (opt.short) |s| [_]u8{ '-', s, ' ' } else " ";
534 }
535 out = out ++ opt.name ++ (" " ** (width - opt.name.len - sub));
536 // TODO: wrapping at 72 columns
537 out = out ++ opt.desc ++ "\n";
538 }
539 break :brk out ++ "\n";
540 }) catch return;
541 }
542}
543
544pub fn showHelpAndExit(comptime p: Cli, program_name: []const u8) noreturn {
545 p.showHelp(program_name);
546 std.process.exit(1);
547}
548
549fn writeError(err: std.io.AnyWriter, tty: std.io.tty.Config) void {
550 tty.setColor(err, .red) catch return;
551 tty.setColor(err, .bold) catch return;
552 err.writeAll("error") catch return;
553 tty.setColor(err, .reset) catch return;
554 tty.setColor(err, .dim) catch return;
555 err.writeAll(": ") catch return;
556 tty.setColor(err, .reset) catch return;
557}
558
559fn writeNote(err: std.io.AnyWriter, tty: std.io.tty.Config) void {
560 tty.setColor(err, .blue) catch return;
561 tty.setColor(err, .bold) catch return;
562 err.writeAll("note") catch return;
563 tty.setColor(err, .reset) catch return;
564 tty.setColor(err, .dim) catch return;
565 err.writeAll(": ") catch return;
566 tty.setColor(err, .reset) catch return;
567}
568
569/// Splits args[0] containing the program name
570///
571/// const program_name, const args = programArgsAlloc();
572pub fn programArgsAlloc(allocator: std.mem.Allocator, fallback_name: []const u8) !struct { []const u8, [][:0]u8 } {
573 const magic_number = @alignOf([*][:0]u8);
574 var raw_args = try std.process.argsAlloc(allocator);
575 switch (raw_args.len) {
576 0 => {
577 raw_args.ptr = @ptrFromInt(magic_number);
578 return .{ fallback_name, raw_args };
579 },
580 else => return .{
581 std.fs.path.basename(raw_args[0]),
582 raw_args[1..],
583 },
584 }
585}
586
587pub fn programArgsFree(allocator: std.mem.Allocator, args: [][:0]u8) void {
588 const magic_number = @alignOf([*][:0]u8);
589 if (args.len == 0 and @intFromPtr(args.ptr) == magic_number) return;
590 const raw_args = (args.ptr - 1)[0 .. args.len + 1];
591 std.process.argsFree(allocator, raw_args);
592}
593
594pub fn fatalWithHelp(p: Cli, program_name: []const u8, comptime fmt: []const u8, args: anytype) noreturn {
595 print: {
596 const stderr = std.io.getStdErr();
597 const tty = std.io.tty.detectConfig(stderr);
598 var bufw = std.io.bufferedWriter(stderr.writer());
599 writeError(bufw.writer().any(), tty);
600 bufw.writer().print(fmt ++ "\n", args) catch break :print;
601 p.showHelpWithWriter(program_name, bufw.writer().any(), tty);
602 bufw.flush() catch break :print;
603 }
604 std.process.exit(1);
605}
606
607pub fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
608 const stderr = std.io.getStdErr();
609 const tty = std.io.tty.detectConfig(stderr);
610 var bufw = std.io.bufferedWriter(stderr.writer());
611 writeError(bufw.writer().any(), tty);
612 bufw.writer().print(fmt ++ "\n", args);
613 bufw.flush() catch {}; // ignoring I/O errors
614 std.process.exit(1);
615}
616
617pub fn main() !void {
618 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
619 defer _ = gpa.deinit();
620 const alloc = gpa.allocator();
621
622 const config: Cli = .{
623 .options = &.{
624 .{ .name = "--flag", .desc = "check for something" },
625 .{ .name = "--str", .desc = "pass a string", .kind = .string },
626 .{
627 .name = "--enum",
628 .desc = "enum",
629 .kind = .{ .@"enum" = std.builtin.OptimizeMode },
630 },
631 },
632 .positionals = .{ .bounded = 2 },
633 };
634
635 const program_name, const args = try Cli.programArgsAlloc(alloc);
636 defer Cli.programArgsFree(alloc, args);
637
638 const parsed = config.parse(program_name, args[1..]);
639
640 std.debug.print("--flag: {any}\n", .{parsed.flag});
641 std.debug.print("--str: {?s}\n", .{parsed.str});
642 std.debug.print("--enum: {any}\n", .{parsed.@"enum"});
643 std.debug.print("positionals({d}):\n", .{parsed.positionals.len});
644
645 // if using '.positionals = .unlimited'
646 // var it = parsed.positionals.iterator(args[1..]);
647 // while (it.next()) |item| {
648 // std.debug.print(" - {s}\n", .{item});
649 // }
650
651 for (parsed.positionals.slice()) |item| {
652 std.debug.print(" - {s}\n", .{item});
653 }
654}
655
656const Cli = @This();
657const std = @import("std");
658const mem = std.mem;
659const Type = std.builtin.Type;
src/cli.zig deleted-659
......@@ -1,659 +0,0 @@
1//! Configuration for an command line interface, which can be executed against
2//! an argument slice using `.parse`. None of the parsing strategies require an
3//! allocator, however it expects incoming arguments to be UTF-8 formatted which
4//! on Windows will require an allocator.
5//!
6//! See `fn main` for a basic usage example.
7options: []const Option = &.{},
8positionals: Positionals = .none,
9help: HelpConfig = .{},
10
11pub const Option = struct {
12 /// Should be formatted as the flag name: '--dump-ir'
13 name: []const u8,
14 /// Displayed in generated help menu
15 desc: []const u8,
16 kind: Kind = .boolean,
17 /// A short code allows single dash argument parsing.
18 short: ?u8 = null,
19
20 const Kind = union(enum) {
21 /// UTF-8 validated
22 string,
23 boolean,
24 @"enum": type,
25
26 pub fn Type(k: Kind) type {
27 return switch (k) {
28 .string => []const u8,
29 .boolean => bool,
30 .@"enum" => |t| t,
31 };
32 }
33 };
34
35 pub fn optional(comptime cond: bool, opts: []const Option) []const Option {
36 return if (cond) opts else &.{};
37 }
38
39 const help: Option = .{
40 .name = "--help",
41 .short = 'h',
42 .desc = "Print this help and exit",
43 };
44};
45
46pub const Positionals = union(enum) {
47 none,
48 /// Store a single positional: ?[]const u8
49 single,
50 /// Store the start and count of positionals. Call
51 /// 'parsed.positionals.iterator(args)' with the
52 /// original argument array to read positionals.
53 unlimited,
54 /// Same strategy as .unlimited, with a limit
55 limited: usize,
56 /// Use a bounded array to store positionals, using
57 /// a stack allocated buffer that can hold enough
58 /// '[]const u8's as allowed by the bound.
59 bounded: usize,
60
61 pub fn Type(p: Positionals) type {
62 return switch (p) {
63 .none => void,
64 .single => ?[]const u8,
65 .limited => |limit| switch (limit) {
66 0 => void,
67 else => Multi,
68 },
69 .unlimited => Multi,
70 .bounded => |n| std.BoundedArray([]const u8, n),
71 };
72 }
73
74 pub inline fn maxCount(p: Positionals) usize {
75 return switch (p) {
76 .none => 0,
77 .single => 1,
78 inline .limited, .bounded => |limit| limit,
79 .unlimited => std.math.maxInt(usize),
80 };
81 }
82
83 pub const Multi = struct {
84 /// The index of the first positional
85 first: usize,
86 /// The number of total positionals. This can
87 /// be used to pre-allocate resources.
88 len: usize,
89
90 first_is_after_double_dash: bool,
91
92 pub const empty = .{
93 .first = 0,
94 .len = 0,
95 .first_is_after_double_dash = false,
96 };
97
98 pub fn iterator(pos: @This(), all_args: []const []const u8) PositionalIterator {
99 return .{
100 .args = all_args,
101 .i = pos.first,
102 .left = pos.len,
103 .after_double_dash = pos.first_is_after_double_dash,
104 };
105 }
106 };
107};
108
109pub const HelpConfig = struct {
110 /// If set true, registers an addition '--help' / '-h' flag
111 /// These will show the help menu and exit the program.
112 ///
113 /// Help menu can always be displayed manually with .showHelp()
114 option: bool = true,
115
116 /// Override the usage string in the first line.
117 usage: ?[]const u8 = null,
118
119 /// Hide a list of flag names.
120 hide_options: []const []const u8 = &.{},
121};
122
123pub const PositionalIterator = struct {
124 args: []const []const u8,
125 i: usize,
126 left: usize,
127 after_double_dash: bool,
128
129 pub fn peek(it: PositionalIterator) ?[]const u8 {
130 return if (it.left > 0) it.args[it.i] else null;
131 }
132
133 pub fn next(it: *PositionalIterator) ?[]const u8 {
134 if (it.left > 0) {
135 defer it.advance();
136 return it.args[it.i];
137 } else {
138 return null;
139 }
140 }
141
142 pub fn advance(it: *PositionalIterator) void {
143 it.left -= 1;
144 if (it.left == 0) return;
145 if (it.after_double_dash) {
146 it.i += 1;
147 } else while (mem.startsWith(u8, it.args[it.i], "-")) {
148 defer it.i += 1;
149 if (it.args[it.i].len == 2 and it.args[it.i][1] == '-') {
150 it.after_double_dash = true;
151 return;
152 }
153 }
154 }
155};
156
157inline fn fieldName(comptime name: []const u8) [:0]const u8 {
158 std.debug.assert(name[0] == '-' and name[1] == '-');
159 for (name[2..]) |char| switch (char) {
160 'a'...'z', '-' => {},
161 else => |ch| @compileError(std.fmt.comptimePrint("Unexpected char '{c}' in option name '{s}'", .{ ch, name })),
162 };
163
164 const replacement_size = mem.replacementSize(u8, name[2..], "-", "_");
165 const replaced = brk: {
166 var buf: [replacement_size:0]u8 = undefined;
167 _ = mem.replace(u8, name[2..], "-", "_", &buf);
168 break :brk buf;
169 };
170 return &replaced;
171}
172
173/// Every option name is converted into snake case, then
174/// an additional field name 'positionals' is added.
175pub fn Parsed(p: Cli) type {
176 const fields = brk: {
177 var fields: [p.options.len + 1]Type.StructField = undefined;
178
179 for (p.options, fields[0..p.options.len]) |def, *field| {
180 const Wrapped = ?def.kind.Type();
181 field.* = .{
182 .name = fieldName(def.name),
183 .alignment = @alignOf(Wrapped),
184 .default_value = &@as(Wrapped, null),
185 .is_comptime = false,
186 .type = Wrapped,
187 };
188 }
189
190 const Pos = p.positionals.Type();
191 fields[fields.len - 1] = .{
192 .name = "positionals",
193 .alignment = @alignOf(Pos),
194 .type = Pos,
195 .default_value = &@as(Pos, switch (p.positionals) {
196 .none => {},
197 .single => null,
198 .limited, .unlimited => .empty,
199 .bounded => .{},
200 }),
201 .is_comptime = false,
202 };
203
204 break :brk fields;
205 };
206
207 return @Type(.{ .@"struct" = .{
208 .layout = .auto,
209 .fields = &fields,
210 .decls = &.{},
211 .is_tuple = false,
212 } });
213}
214
215const StdErrWriter = struct {
216 file: std.fs.File,
217 buffered_writer: std.io.BufferedWriter(4096, std.fs.File.Writer),
218 tty: std.io.tty.Config,
219 writer_ptr: std.io.BufferedWriter(4096, std.fs.File.Writer).Writer,
220
221 pub fn init() StdErrWriter {
222 const stderr = std.io.getStdErr();
223 const tty = std.io.tty.detectConfig(stderr);
224 return .{
225 .file = stderr,
226 .tty = tty,
227 .buffered_writer = std.io.bufferedWriter(stderr.writer()),
228 .writer_ptr = undefined,
229 };
230 }
231
232 pub fn get(w: *StdErrWriter) std.io.AnyWriter {
233 if (@import("builtin").os.tag == .windows and w.tty == .windows_api) {
234 // all data must be flushed before changing colors using
235 // the Windows API. thankfully, modern windows terminals
236 // will not hit this path.
237 return w.buffered_writer.unbuffered_writer.any();
238 } else {
239 w.writer_ptr = w.buffered_writer.writer(); // uaf unless stored
240 return w.writer_ptr.any();
241 }
242 }
243};
244
245const ParseError = error{ InvalidArguments, DisplayedHelpMenu };
246
247/// This will quit the program with usage information if invalid arguments are
248/// received and the `--help` option is enabled. This is the most common use
249/// case. If manual error handling is desired, `.parseWithErrorWriter` can
250/// be used instead.
251pub fn parse(
252 comptime p: Cli,
253 program_name: []const u8,
254 args: []const []const u8,
255) p.Parsed() {
256 var stderr = StdErrWriter.init();
257 defer stderr.buffered_writer.flush() catch {};
258 return parseWithErrorWriter(
259 p,
260 program_name,
261 args,
262 stderr.get(),
263 stderr.tty,
264 true,
265 true,
266 ) catch |e| switch (e) {
267 error.InvalidArguments,
268 error.DisplayedHelpMenu,
269 => {
270 stderr.buffered_writer.flush() catch {};
271 std.process.exit(1);
272 },
273 };
274}
275
276pub fn parseWithErrorWriter(
277 comptime p: Cli,
278 program_name: []const u8,
279 args: []const []const u8,
280 err: std.io.AnyWriter,
281 tty: std.io.tty.Config,
282 comptime show_help_on_error: bool,
283 comptime exit_on_error: bool,
284) ParseError!p.Parsed() {
285 var out: p.Parsed() = .{};
286 var had_errors = false;
287 var positionals_counted: usize = 0;
288 var first_positional: usize = 0;
289 var double_dash: ?usize = null;
290
291 var show_help = false;
292
293 const max_positionals = p.positionals.maxCount();
294
295 outer: for (args, 0..) |arg, i| {
296 if (mem.startsWith(u8, arg, "--")) {
297 if (arg.len == 2) {
298 if (max_positionals == 0) {
299 writeError(err, tty);
300 err.writeAll("unexpected end-of-parameter mark: --\n") catch {};
301 }
302
303 double_dash = i;
304 const remain = args[i + 1 ..];
305 if (positionals_counted == 0 and remain.len > 0)
306 first_positional = i + 1;
307
308 if (p.positionals == .bounded) {
309 out.positionals.appendSliceAssumeCapacity(remain[0..@min(remain.len, max_positionals -| positionals_counted)]);
310 }
311
312 positionals_counted += remain.len;
313
314 if (positionals_counted > max_positionals) {
315 had_errors = true;
316 const diff = @min(remain.len, positionals_counted - max_positionals);
317 for (remain[remain.len - diff ..]) |pos| {
318 writeError(err, tty);
319 err.print("unrecognized argument '{s}'\n", .{pos}) catch {};
320 }
321 }
322
323 break;
324 }
325
326 inline for (p.options) |def| try_this_def: {
327 const prefix = def.name;
328 const kind = def.kind;
329 const dest = &@field(out, fieldName(def.name));
330
331 const is_blank = arg.len == prefix.len;
332 if (!(mem.startsWith(u8, arg[2..], prefix[2..]) and (is_blank or arg[prefix.len] == '=')))
333 break :try_this_def;
334
335 if (kind != .boolean and is_blank) {
336 had_errors = true;
337 writeError(err, tty);
338 err.print(prefix ++ " requires a value\n", .{}) catch {};
339 continue :outer;
340 }
341
342 const value = if (is_blank) "" else arg[prefix.len + 1 ..];
343 switch (kind) {
344 .boolean => if (value.len == 0) {
345 dest.* = true;
346 } else if (mem.eql(u8, value, "false")) {
347 dest.* = false;
348 } else if (mem.eql(u8, value, "true")) {
349 dest.* = true;
350 } else {
351 had_errors = true;
352 writeError(err, tty);
353 err.print(prefix ++ " only accepts true or false, got '{s}'\n", .{
354 arg[prefix.len + 1 ..],
355 }) catch {};
356 },
357
358 .string => if (std.unicode.utf8ValidateSlice(value)) {
359 dest.* = value;
360 } else {
361 had_errors = true;
362 writeError(err, tty);
363 err.print(prefix ++ " requires valid UTF-8 input\n", .{}) catch {};
364 },
365
366 .@"enum" => |E| {
367 dest.* = std.meta.stringToEnum(E, value) orelse {
368 had_errors = true;
369 const values_list = comptime brk: {
370 var str: []const u8 = "";
371 for (std.enums.values(E)) |val| {
372 str = str ++ " " ++ @tagName(val) ++ "\n";
373 }
374 break :brk str;
375 };
376 writeError(err, tty);
377 err.print(prefix ++ " must be one of the following values, got {s}:\n{s}", .{
378 arg[prefix.len + 1 ..],
379 values_list,
380 }) catch {};
381 continue :outer;
382 };
383 },
384 }
385
386 continue :outer;
387 }
388
389 if (p.help.option) {
390 if (mem.startsWith(u8, arg[2..], "help") and (arg.len == "--help".len or arg["--help".len] == '=')) {
391 show_help = true;
392 if (arg.len > "--help".len) {
393 had_errors = true;
394 err.print("--help does not take a value\n", .{}) catch {};
395 }
396 continue :outer;
397 }
398 }
399
400 had_errors = true;
401 writeError(err, tty);
402 err.print("unrecognized argument '{s}'\n", .{arg}) catch {};
403 } else if (arg.len > 1 and arg[0] == '-') {
404 per_char_in_short: for (arg[1..]) |ch| {
405 inline for (p.options) |def| try_this_def: {
406 const short = def.short orelse break :try_this_def;
407 if (def.kind != .boolean) {
408 @compileError("TODO: non-boolean shorthand arguments");
409 }
410 if (ch == short) {
411 const dest = &@field(out, fieldName(def.name));
412 dest.* = true;
413 continue :per_char_in_short;
414 }
415 if (p.help.option and ch == 'h') {
416 show_help = true;
417 }
418 }
419 }
420 } else {
421 if (positionals_counted == 0) first_positional = i;
422 positionals_counted += 1;
423 if (positionals_counted > max_positionals) {
424 had_errors = true;
425 writeError(err, tty);
426 err.print("unrecognized argument '{s}'\n", .{arg}) catch {};
427 } else if (p.positionals == .bounded) {
428 out.positionals.appendAssumeCapacity(arg);
429 }
430 }
431 }
432
433 if (had_errors) {
434 if (positionals_counted > max_positionals) {
435 writeNote(err, tty);
436 err.writeAll(switch (max_positionals) {
437 0 => "this program does not take positional arguments\n",
438 1 => "this program accepts one positional argument\n",
439 else => |n| std.fmt.comptimePrint(
440 "this program accepts up to {d} positional arguments\n",
441 .{n},
442 ),
443 }) catch {};
444 }
445 if (show_help or show_help_on_error) {
446 p.showHelpWithWriter(program_name, err, tty);
447 }
448 if (exit_on_error) {
449 return error.DisplayedHelpMenu;
450 }
451 return error.InvalidArguments;
452 }
453
454 if (show_help) {
455 p.showHelpWithWriter(program_name, err, tty);
456 return error.DisplayedHelpMenu;
457 }
458
459 out.positionals = switch (p.positionals) {
460 .none => {},
461 .single => if (positionals_counted > 0)
462 args[first_positional]
463 else
464 null,
465 .bounded => out.positionals, // already correct
466 .unlimited, .limited => if (max_positionals > 0) .{
467 .first = first_positional,
468 .len = positionals_counted,
469 .first_is_after_double_dash = if (double_dash) |dd|
470 dd < first_positional
471 else
472 false,
473 },
474 };
475
476 return out;
477}
478
479pub fn showHelp(comptime p: Cli, program_name: []const u8) void {
480 var stderr = StdErrWriter.init();
481 defer stderr.buffered_writer.flush() catch {};
482 return showHelpWithWriter(
483 p,
484 program_name,
485 stderr.get(),
486 stderr.tty,
487 );
488}
489
490pub fn showHelpWithWriter(
491 comptime p: Cli,
492 program_name: []const u8,
493 w: std.io.AnyWriter,
494 tty: std.io.tty.Config,
495) void {
496 const usage = p.help.usage orelse default: {
497 const opts_usage = if (p.options.len > 0 or p.help.option) "[options...]" else "";
498 break :default switch (p.positionals.maxCount()) {
499 0 => opts_usage,
500 1 => mem.trimLeft(u8, opts_usage ++ " [argument]", " "),
501 else => mem.trimLeft(u8, opts_usage ++ " [args...]", " "),
502 };
503 };
504
505 w.print("usage: {s} {s}\n", .{ program_name, usage }) catch return;
506
507 if (p.options.len > 0 or p.help.option) {
508 w.writeAll("\n") catch return;
509 tty.setColor(w, .bold) catch return;
510 w.writeAll("options:") catch return;
511 tty.setColor(w, .reset) catch return;
512 w.writeAll(comptime brk: {
513 var width = 0;
514 var any_short_options = false;
515
516 const all_opts = if (p.help.option)
517 p.options ++ [_]Option{.help}
518 else
519 p.options;
520 for (all_opts) |opt| {
521 width = @max(width, opt.name.len);
522 any_short_options = any_short_options or opt.short != null;
523 }
524
525 width += " ".len;
526 if (any_short_options) width += "-h ".len;
527 const sub = if (any_short_options) 4 else 1;
528
529 var out: []const u8 = "\n";
530 for (all_opts) |opt| {
531 out = out ++ " ";
532 if (any_short_options) {
533 out = out ++ if (opt.short) |s| [_]u8{ '-', s, ' ' } else " ";
534 }
535 out = out ++ opt.name ++ (" " ** (width - opt.name.len - sub));
536 // TODO: wrapping at 72 columns
537 out = out ++ opt.desc ++ "\n";
538 }
539 break :brk out ++ "\n";
540 }) catch return;
541 }
542}
543
544pub fn showHelpAndExit(comptime p: Cli, program_name: []const u8) noreturn {
545 p.showHelp(program_name);
546 std.process.exit(1);
547}
548
549fn writeError(err: std.io.AnyWriter, tty: std.io.tty.Config) void {
550 tty.setColor(err, .red) catch return;
551 tty.setColor(err, .bold) catch return;
552 err.writeAll("error") catch return;
553 tty.setColor(err, .reset) catch return;
554 tty.setColor(err, .dim) catch return;
555 err.writeAll(": ") catch return;
556 tty.setColor(err, .reset) catch return;
557}
558
559fn writeNote(err: std.io.AnyWriter, tty: std.io.tty.Config) void {
560 tty.setColor(err, .blue) catch return;
561 tty.setColor(err, .bold) catch return;
562 err.writeAll("note") catch return;
563 tty.setColor(err, .reset) catch return;
564 tty.setColor(err, .dim) catch return;
565 err.writeAll(": ") catch return;
566 tty.setColor(err, .reset) catch return;
567}
568
569/// Splits args[0] containing the program name
570///
571/// const program_name, const args = programArgsAlloc();
572pub fn programArgsAlloc(allocator: std.mem.Allocator, fallback_name: []const u8) !struct { []const u8, [][:0]u8 } {
573 const magic_number = @alignOf([*][:0]u8);
574 var raw_args = try std.process.argsAlloc(allocator);
575 switch (raw_args.len) {
576 0 => {
577 raw_args.ptr = @ptrFromInt(magic_number);
578 return .{ fallback_name, raw_args };
579 },
580 else => return .{
581 std.fs.path.basename(raw_args[0]),
582 raw_args[1..],
583 },
584 }
585}
586
587pub fn programArgsFree(allocator: std.mem.Allocator, args: [][:0]u8) void {
588 const magic_number = @alignOf([*][:0]u8);
589 if (args.len == 0 and @intFromPtr(args.ptr) == magic_number) return;
590 const raw_args = (args.ptr - 1)[0 .. args.len + 1];
591 std.process.argsFree(allocator, raw_args);
592}
593
594pub fn fatalWithHelp(p: Cli, program_name: []const u8, comptime fmt: []const u8, args: anytype) noreturn {
595 print: {
596 const stderr = std.io.getStdErr();
597 const tty = std.io.tty.detectConfig(stderr);
598 var bufw = std.io.bufferedWriter(stderr.writer());
599 writeError(bufw.writer().any(), tty);
600 bufw.writer().print(fmt ++ "\n", args) catch break :print;
601 p.showHelpWithWriter(program_name, bufw.writer().any(), tty);
602 bufw.flush() catch break :print;
603 }
604 std.process.exit(1);
605}
606
607pub fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
608 const stderr = std.io.getStdErr();
609 const tty = std.io.tty.detectConfig(stderr);
610 var bufw = std.io.bufferedWriter(stderr.writer());
611 writeError(bufw.writer().any(), tty);
612 bufw.writer().print(fmt ++ "\n", args);
613 bufw.flush() catch {}; // ignoring I/O errors
614 std.process.exit(1);
615}
616
617pub fn main() !void {
618 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
619 defer _ = gpa.deinit();
620 const alloc = gpa.allocator();
621
622 const config: Cli = .{
623 .options = &.{
624 .{ .name = "--flag", .desc = "check for something" },
625 .{ .name = "--str", .desc = "pass a string", .kind = .string },
626 .{
627 .name = "--enum",
628 .desc = "enum",
629 .kind = .{ .@"enum" = std.builtin.OptimizeMode },
630 },
631 },
632 .positionals = .{ .bounded = 2 },
633 };
634
635 const program_name, const args = try Cli.programArgsAlloc(alloc);
636 defer Cli.programArgsFree(alloc, args);
637
638 const parsed = config.parse(program_name, args[1..]);
639
640 std.debug.print("--flag: {any}\n", .{parsed.flag});
641 std.debug.print("--str: {?s}\n", .{parsed.str});
642 std.debug.print("--enum: {any}\n", .{parsed.@"enum"});
643 std.debug.print("positionals({d}):\n", .{parsed.positionals.len});
644
645 // if using '.positionals = .unlimited'
646 // var it = parsed.positionals.iterator(args[1..]);
647 // while (it.next()) |item| {
648 // std.debug.print(" - {s}\n", .{item});
649 // }
650
651 for (parsed.positionals.slice()) |item| {
652 std.debug.print(" - {s}\n", .{item});
653 }
654}
655
656const Cli = @This();
657const std = @import("std");
658const mem = std.mem;
659const Type = std.builtin.Type;
src/cli_compiler.zig+1
......@@ -8,6 +8,7 @@ const arg_config: Cli = .{
88 .{ .short = 'i', .name = "--dump-ir", .desc = "Dump intermediate representations" },
99 .{ .name = "--dump-state", .desc = "Dump compiler state to JSON", .kind = .string },
1010 .{ .short = 'r', .name = "--report-trace", .desc = "Print stack traces to errors" },
11 .{ .short = 't', .name = "--tests", .desc = "Build unit tests instead of the main function" },
1112 .{ .short = 'L', .name = "--no-leak-checks", .desc = "" },
1213 },
1314 .positionals = .single,