authorgravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-30 06:08:21-04:00
committergravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-30 22:36:30-04:00
log9df8a48667af45f67cd12c02d2e77f1dd67ea373
treebf7ed479138f3346363347d44c005725d9d361bd
parentf2753f5910b320b53c26fa6e0e145bc934119cb8

nicer usage


1 files changed, 183 insertions(+), 75 deletions(-)

lib/std/cli.zig+183-75
...@@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
293 file_writer.interface.flush() catch {};293 file_writer.interface.flush() catch {};
294 }294 }
295 } else {295 } else {
296 printGeneratedHelp(writer, prog, named_fields);296 printGeneratedHelp(named_fields, positional_fields, writer, prog);
297 }297 }
298 if (exit_on_error) {298 if (exit_on_error) {
299 std.process.exit(0);299 std.process.exit(0);
...@@ -304,7 +304,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -304,7 +304,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
304 if (!the_rest_is_positional and arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) {304 if (!the_rest_is_positional and arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) {
305 // Always invalid.305 // Always invalid.
306 // Examples: -h, -flag, -I/path306 // Examples: -h, -flag, -I/path
307 return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);307 return usageError(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error);
308 }308 }
309 if (!the_rest_is_positional and mem.eql(u8, arg, "--")) {309 if (!the_rest_is_positional and mem.eql(u8, arg, "--")) {
310 // Stop recognizing named arguments. Everything else is positional.310 // Stop recognizing named arguments. Everything else is positional.
...@@ -314,14 +314,14 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -314,14 +314,14 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
314 if (the_rest_is_positional or !(arg.len >= 3 and arg[0] == '-' and arg[1] == '-')) {314 if (the_rest_is_positional or !(arg.len >= 3 and arg[0] == '-' and arg[1] == '-')) {
315 // Positional.315 // Positional.
316 // Examples: "", "a", "-", "-1", "other"316 // Examples: "", "a", "-", "-1", "other"
317 if (positional_field_index >= positional_fields.len) return usageError(writer, "unexpected positional argument: {s}", .{arg}, exit_on_error);317 if (positional_field_index >= positional_fields.len) return usageError(named_fields, positional_fields, writer, "unexpected positional argument: {s}", .{arg}, prog, exit_on_error);
318 inline for (positional_fields, 0..) |field, i| {318 inline for (positional_fields, 0..) |field, i| {
319 if (positional_field_index == i) {319 if (positional_field_index == i) {
320 if (getArrayChild(field.type)) |C| {320 if (getArrayChild(field.type)) |C| {
321 try @field(positional_array_lists, field.name).append(allocator, try parseValue(C, arg, field.name, writer, exit_on_error));321 try @field(positional_array_lists, field.name).append(allocator, try parseValue(named_fields, positional_fields, C, arg, field.name, writer, prog, exit_on_error));
322 // Don't increment positional_field_index.322 // Don't increment positional_field_index.
323 } else {323 } else {
324 @field(result.positional, field.name) = try parseValue(field.type, arg, field.name, writer, exit_on_error);324 @field(result.positional, field.name) = try parseValue(named_fields, positional_fields, field.type, arg, field.name, writer, prog, exit_on_error);
325 positional_field_index += 1;325 positional_field_index += 1;
326 }326 }
327 break;327 break;
...@@ -349,25 +349,25 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -349,25 +349,25 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
349 if (mem.eql(u8, field.name, arg_name)) {349 if (mem.eql(u8, field.name, arg_name)) {
350 named_fields_seen[i] = true;350 named_fields_seen[i] = true;
351 if (field.type == bool) {351 if (field.type == bool) {
352 if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error);352 if (immediate_value != null) return usageError(named_fields, positional_fields, writer, "cannot specify value for bool argument: {s}", .{arg}, prog, exit_on_error);
353 @field(result.named, field.name) = !no_prefixed;353 @field(result.named, field.name) = !no_prefixed;
354 break;354 break;
355 }355 }
356 if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);356 if (no_prefixed) return usageError(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error);
357357
358 // All other argument types require a value.358 // All other argument types require a value.
359 const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}, exit_on_error);359 const arg_value = immediate_value orelse iter.next() orelse return usageError(named_fields, positional_fields, writer, "expected argument after --{s}", .{field.name}, prog, exit_on_error);
360360
361 if (getArrayChild(field.type)) |C| {361 if (getArrayChild(field.type)) |C| {
362 try @field(named_array_lists, field.name).append(allocator, try parseValue(C, arg_value, field.name, writer, exit_on_error));362 try @field(named_array_lists, field.name).append(allocator, try parseValue(named_fields, positional_fields, C, arg_value, field.name, writer, prog, exit_on_error));
363 } else {363 } else {
364 @field(result.named, field.name) = try parseValue(field.type, arg_value, field.name, writer, exit_on_error);364 @field(result.named, field.name) = try parseValue(named_fields, positional_fields, field.type, arg_value, field.name, writer, prog, exit_on_error);
365 }365 }
366 break;366 break;
367 }367 }
368 } else {368 } else {
369 // Didn't match anything.369 // Didn't match anything.
370 return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);370 return usageError(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error);
371 }371 }
372 }372 }
373373
...@@ -384,9 +384,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -384,9 +384,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
384 @field(result.named, field.name) = default;384 @field(result.named, field.name) = default;
385 } else {385 } else {
386 if (field.type == bool) {386 if (field.type == bool) {
387 return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error);387 return usageError(named_fields, positional_fields, writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, prog, exit_on_error);
388 } else {388 } else {
389 return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error);389 return usageError(named_fields, positional_fields, writer, "missing required argument: --" ++ field.name, .{}, prog, exit_on_error);
390 }390 }
391 }391 }
392 }392 }
...@@ -403,7 +403,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -403,7 +403,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
403 if (field.defaultValue()) |default| {403 if (field.defaultValue()) |default| {
404 @field(result.positional, field.name) = default;404 @field(result.positional, field.name) = default;
405 } else {405 } else {
406 return usageError(writer, "missing required argument: " ++ field.name, .{}, exit_on_error);406 return usageError(named_fields, positional_fields, writer, "missing required argument: " ++ field.name, .{}, prog, exit_on_error);
407 }407 }
408 }408 }
409 }409 }
...@@ -413,22 +413,22 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -413,22 +413,22 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
413}413}
414414
415/// arg_value is []const u8 or [:0]const u8.415/// arg_value is []const u8 or [:0]const u8.
416fn parseValue(comptime T: type, arg_value: anytype, comptime field_name: []const u8, writer: ?*Writer, exit_on_error: bool) !T {416fn parseValue(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, comptime T: type, arg_value: anytype, comptime field_name: []const u8, writer: ?*Writer, prog: []const u8, exit_on_error: bool) !T {
417 switch (@typeInfo(T)) {417 switch (@typeInfo(T)) {
418 .bool => comptime unreachable, // Handled elsewhere.418 .bool => comptime unreachable, // Handled elsewhere.
419 .float => {419 .float => {
420 return std.fmt.parseFloat(T, arg_value) catch |err| {420 return std.fmt.parseFloat(T, arg_value) catch |err| {
421 return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, exit_on_error);421 return usageError(named_fields, positional_fields, writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, prog, exit_on_error);
422 };422 };
423 },423 },
424 .int => {424 .int => {
425 return std.fmt.parseInt(T, arg_value, 0) catch |err| {425 return std.fmt.parseInt(T, arg_value, 0) catch |err| {
426 return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, exit_on_error);426 return usageError(named_fields, positional_fields, writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, prog, exit_on_error);
427 };427 };
428 },428 },
429 .@"enum" => {429 .@"enum" => {
430 return std.meta.stringToEnum(T, arg_value) orelse {430 return std.meta.stringToEnum(T, arg_value) orelse {
431 return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, exit_on_error);431 return usageError(named_fields, positional_fields, writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, prog, exit_on_error);
432 };432 };
433 },433 },
434 .pointer => |ptrInfo| {434 .pointer => |ptrInfo| {
...@@ -574,12 +574,20 @@ fn ArrayListsForFields(comptime fields: []const StructField) type {...@@ -574,12 +574,20 @@ fn ArrayListsForFields(comptime fields: []const StructField) type {
574/// call this function to produce the same error behavior as if this API's validation failed.574/// call this function to produce the same error behavior as if this API's validation failed.
575/// An error message will be written to `options.writer` or stderr by default, and `error.Usage` is returned.575/// An error message will be written to `options.writer` or stderr by default, and `error.Usage` is returned.
576/// The given `msg` template is prefixed by `"error: "` and suffixed by a newline and a prompt to try passing in `--help`.576/// The given `msg` template is prefixed by `"error: "` and suffixed by a newline and a prompt to try passing in `--help`.
577/// `options.prog` is not used by this function, but could be in the future.577/// `options.prog` is not used by this function, but could be in the future. TODO: yes it is.
578///578///
579/// This function calls `std.process.exit` with an error status unless `options.exit` is set to `false`, in which case it returns `error.Usage`.579/// This function calls `std.process.exit` with an error status unless `options.exit` is set to `false`, in which case it returns `error.Usage`.
580/// This matches the default behavior of `parse`, not `parseIter` or `parseSlice`.580/// This matches the default behavior of `parse`, not `parseIter` or `parseSlice`.
581pub fn @"error"(comptime msg: []const u8, args: anytype, options: Options) error{Usage} {581pub fn @"error"(comptime Args: type, comptime msg: []const u8, msg_args: anytype, options: Options) error{Usage} {
582 return usageError(options.writer, msg, args, options.exit orelse true);582 const named_fields, const positional_fields = comptime checkArgsType(Args);
583 var buf: [0x1000]u8 = undefined;
584 const prog: ?[]const u8 = options.prog orelse blk: {
585 var fba: std.heap.FixedBufferAllocator = .init(&buf);
586 var iter = ArgIterator.initWithAllocator(fba.allocator()) catch break :blk null;
587 const argv0 = iter.next();
588 break :blk if (argv0) |arg| std.fs.path.basename(arg) else null;
589 };
590 return usageError(named_fields, positional_fields, options.writer, msg, msg_args, prog orelse "<prog>", options.exit orelse true);
583}591}
584592
585test @"error" {593test @"error" {
...@@ -597,25 +605,8 @@ test @"error" {...@@ -597,25 +605,8 @@ test @"error" {
597 const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ "--output=o.txt", "i.txt" }, .{});605 const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ "--output=o.txt", "i.txt" }, .{});
598606
599 if (std.fs.path.isAbsolute(args.named.output)) {607 if (std.fs.path.isAbsolute(args.named.output)) {
600 return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false });608 return std.cli.@"error"(Args, "--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false });
601 }
602}
603
604fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} {
605 const whole_msg =
606 "error: " ++ msg ++ "\n" ++
607 \\try --help for full help info
608 \\
609 ;
610 if (writer) |w| {
611 w.print(whole_msg, args) catch {};
612 } else {
613 std.debug.print(whole_msg, args);
614 }609 }
615 if (exit_on_error) {
616 std.process.exit(1);
617 }
618 return error.Usage;
619}610}
620611
621fn ArgIteratorSlice(comptime String: type) type {612fn ArgIteratorSlice(comptime String: type) type {
...@@ -644,53 +635,125 @@ fn enumValuesExpr(comptime Enum: type) []const u8 {...@@ -644,53 +635,125 @@ fn enumValuesExpr(comptime Enum: type) []const u8 {
644 return values_str;635 return values_str;
645}636}
646637
647fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_fields: []const StructField) void {638fn usageError(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, writer: ?*Writer, comptime msg: []const u8, args: anytype, prog: []const u8, exit_on_error: bool) error{Usage} {
648 const msg = //639 const whole_msg =
649 \\usage: {s} [options] [arg...]640 "error: " ++ msg ++ "\n" ++ //
650 \\641 "usage: {s} " ++ comptime usageLineFmt(named_fields, positional_fields) ++ "\n" ++
651 \\arguments:{s}642 \\try --help for full help info
652 \\ --help643 \\
653 \\644 ;
654 ;645 if (writer) |w| {
655 comptime var arguments_str: []const u8 = "";646 w.print(whole_msg, args ++ .{prog}) catch {};
647 } else {
648 std.debug.print(whole_msg, args ++ .{prog});
649 }
650 if (exit_on_error) {
651 std.process.exit(1);
652 }
653 return error.Usage;
654}
655
656/// returns a string with all "{" escaped for passing into std.fmt.
657fn usageLineFmt(comptime named_fields: []const StructField, comptime positional_fields: []const StructField) []const u8 {
658 comptime var usage_parts: []const []const u8 = &.{};
659 var at_least_one_optional_named_argument = false;
660 inline for (named_fields) |field| {
661 if (field.default_value_ptr != null) {
662 // Don't mention optional named arguments.
663 at_least_one_optional_named_argument = true;
664 continue;
665 }
666 usage_parts = usage_parts ++ .{switch (@typeInfo(field.type)) {
667 .bool => "--[no-]" ++ field.name,
668 .int, .float => "--" ++ field.name ++ "=" ++ @typeName(field.type),
669 .@"enum" => "--" ++ field.name ++ "=" ++ enumValuesExpr(field.type),
670 else => blk: {
671 comptime assert(@typeInfo(field.type).pointer.size == .slice and @typeInfo(field.type).pointer.child == u8);
672 break :blk "--" ++ field.name ++ "=string";
673 },
674 }};
675 }
676
677 if (at_least_one_optional_named_argument) {
678 // Prepend with an [options] placeholder.
679 usage_parts = [_][]const u8{"[options]"} ++ usage_parts;
680 }
681
682 inline for (positional_fields) |field| {
683 if (field.default_value_ptr != null) {
684 if (getArrayChild(field.type) != null) {
685 // Array
686 usage_parts = usage_parts ++ .{"[" ++ field.name ++ "...]"};
687 } else {
688 // Scalar
689 usage_parts = usage_parts ++ .{"[" ++ field.name ++ "]"};
690 }
691 } else {
692 usage_parts = usage_parts ++ .{field.name};
693 }
694 }
695
696 comptime var usage_str: []const u8 = "";
697 inline for (usage_parts) |part| {
698 if (usage_str.len > 0) {
699 usage_str = usage_str ++ " ";
700 }
701 usage_str = usage_str ++ part;
702 }
703 return escapeFmt(usage_str);
704}
705fn printGeneratedHelp(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, writer: ?*Writer, prog: []const u8) void {
706 comptime var arguments_table: []const []const []const u8 = &.{};
707
708 comptime var arguments_str: []const u8 = ""; // TODO: delete
709
710 if (positional_fields.len > 0) {
711 arguments_table = arguments_table ++ .{&[_][]const u8{"positional arguments:"}};
712 }
713 //inline for (positional_fields) |field| {}
714
715 arguments_table = arguments_table ++ .{&[_][]const u8{"named arguments:"}}; // The --help option is always there.
656 inline for (named_fields) |field| {716 inline for (named_fields) |field| {
657 switch (@typeInfo(field.type)) {717 switch (@typeInfo(field.type)) {
658 .bool => {718 .bool => {
659 if (field.defaultValue()) |default| {719 if (field.defaultValue()) |default| {
660 if (default) {720 if (default) {
661 arguments_str = arguments_str ++ "\n --no-" ++ field.name ++ " default: --" ++ field.name;721 arguments_table = arguments_table ++ .{&[_][]const u8{ " --no-" ++ field.name, "default: --" ++ field.name }};
662 } else {722 } else {
663 arguments_str = arguments_str ++ "\n --" ++ field.name ++ " default: --no-" ++ field.name;723 arguments_table = arguments_table ++ .{&[_][]const u8{ " --" ++ field.name, "default: --no-" ++ field.name }};
664 }724 }
665 } else {725 } else {
666 arguments_str = arguments_str ++ "\n --" ++ field.name ++ " or --no-" ++ field.name ++ " required";726 arguments_table = arguments_table ++ .{&[_][]const u8{ " --[no-]" ++ field.name, "required" }};
667 }727 }
668 },728 },
669 .int, .float => {729 .int, .float => {
670 arguments_str = arguments_str ++ "\n --" ++ field.name ++ " " ++ @typeName(field.type);730 arguments_table = arguments_table ++ .{&[_][]const u8{
671 if (field.defaultValue()) |default| {731 " --" ++ field.name ++ "=" ++ @typeName(field.type),
672 arguments_str = arguments_str ++ " default: " ++ std.fmt.comptimePrint("{}", .{default});732 if (field.defaultValue()) |default|
673 } else {733 "default: " ++ std.fmt.comptimePrint("{}", .{default})
674 arguments_str = arguments_str ++ " required";734 else
675 }735 "required",
736 }};
676 },737 },
677 .@"enum" => {738 .@"enum" => {
678 arguments_str = arguments_str ++ "\n --" ++ field.name ++ " " ++ comptime enumValuesExpr(field.type);739 arguments_table = arguments_table ++ .{&[_][]const u8{
679 if (field.defaultValue()) |default| {740 " --" ++ field.name ++ "=" ++ comptime enumValuesExpr(field.type),
680 arguments_str = arguments_str ++ " default: " ++ quoteIfEmpty(@tagName(default));741 if (field.defaultValue()) |default|
681 } else {742 "default: " ++ @tagName(default)
682 arguments_str = arguments_str ++ " required";743 else
683 }744 "required",
745 }};
684 },746 },
685 .pointer => |ptrInfo| {747 .pointer => |ptrInfo| {
686 if (ptrInfo.size == .slice and ptrInfo.child == u8) {748 if (ptrInfo.size == .slice and ptrInfo.child == u8) {
687 // String.749 // String
688 arguments_str = arguments_str ++ "\n --" ++ field.name ++ " string";750 arguments_table = arguments_table ++ .{&[_][]const u8{
689 if (field.defaultValue()) |default| {751 " --" ++ field.name ++ "=string",
690 arguments_str = arguments_str ++ " default: " ++ quoteIfEmpty(default);752 if (field.defaultValue()) |default|
691 } else {753 "default: " ++ quoteIfEmpty(default)
692 arguments_str = arguments_str ++ " required";754 else
693 }755 "required",
756 }};
694 } else {757 } else {
695 // Array758 // Array
696 const type_name = switch (@typeInfo(ptrInfo.child)) {759 const type_name = switch (@typeInfo(ptrInfo.child)) {
...@@ -703,18 +766,43 @@ fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_fields:...@@ -703,18 +766,43 @@ fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_fields:
703 arguments_str = arguments_str ++ "\n " ++ //766 arguments_str = arguments_str ++ "\n " ++ //
704 "--" ++ field.name ++ " " ++ type_name ++ " " ++ //767 "--" ++ field.name ++ " " ++ type_name ++ " " ++ //
705 "[--" ++ field.name ++ " " ++ type_name ++ " ...]";768 "[--" ++ field.name ++ " " ++ type_name ++ " ...]";
769 arguments_table = arguments_table ++ .{&[_][]const u8{
770 " --" ++ field.name ++ "=" ++ type_name ++ " " ++ //
771 "[--" ++ field.name ++ "=" ++ type_name ++ " ...]",
772 }};
706 }773 }
707 },774 },
708 else => @compileError("Unsupported field type: " ++ @typeName(field.type)),775 else => comptime unreachable,
776 }
777 }
778
779 arguments_table = arguments_table ++ .{&[_][]const u8{ " --help", "print this help and exit" }};
780
781 comptime var width = 0;
782 inline for (arguments_table) |row| {
783 width = @max(width, row[0].len);
784 }
785
786 comptime var help_str: []const u8 = "";
787 inline for (arguments_table) |row| {
788 help_str = help_str ++ "\n";
789 inline for (row, 0..) |cell, c| {
790 help_str = help_str ++ cell;
791 if (c == 0 and row.len > 1) {
792 help_str = help_str ++ " " ** (width + 2 - cell.len);
793 }
709 }794 }
710 }795 }
796
797 const msg = "usage: {s} " ++ comptime usageLineFmt(named_fields, positional_fields) ++ //
798 escapeFmt(help_str) ++ "\n";
711 if (writer) |w| {799 if (writer) |w| {
712 w.print(msg, .{ prog, arguments_str }) catch {};800 w.print(msg, .{prog}) catch {};
713 w.flush() catch {};801 w.flush() catch {};
714 } else {802 } else {
715 var buffer: [0x100]u8 = undefined;803 var buffer: [0x100]u8 = undefined;
716 var file_writer = std.fs.File.stdout().writer(&buffer);804 var file_writer = std.fs.File.stdout().writer(&buffer);
717 file_writer.interface.print(msg, .{ prog, arguments_str }) catch {};805 file_writer.interface.print(msg, .{prog}) catch {};
718 file_writer.interface.flush() catch {};806 file_writer.interface.flush() catch {};
719 }807 }
720}808}
...@@ -724,6 +812,26 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 {...@@ -724,6 +812,26 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 {
724 return s;812 return s;
725}813}
726814
815inline fn escapeFmt(comptime s: []const u8) []const u8 {
816 var result: []const u8 = "";
817 comptime var cursor = 0;
818 for (s, 0..) |c, i| {
819 switch (c) {
820 '{' => {
821 result = result ++ s[cursor..i] ++ "{{";
822 cursor = i + 1;
823 },
824 '}' => {
825 result = result ++ s[cursor..i] ++ "}}";
826 cursor = i + 1;
827 },
828 else => {},
829 }
830 }
831 result = result ++ s[cursor..];
832 return result;
833}
834
727var failing_writer: Writer = .failing;835var failing_writer: Writer = .failing;
728const silent_options = Options{ .writer = &failing_writer, .exit = false };836const silent_options = Options{ .writer = &failing_writer, .exit = false };
729837
...@@ -1310,8 +1418,8 @@ test "actually calling error" {...@@ -1310,8 +1418,8 @@ test "actually calling error" {
1310 "--output=/absolute/path", "too", "many", "other", "args",1418 "--output=/absolute/path", "too", "many", "other", "args",
1311 }, .{});1419 }, .{});
13121420
1313 try testing.expectEqual(error.Usage, std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, silent_options));1421 try testing.expectEqual(error.Usage, @"error"(Args, "--output must not be absolute: {s}", .{args.named.output}, silent_options));
1314 try testing.expectEqual(error.Usage, std.cli.@"error"("expected exactly 1 positional arg", .{}, silent_options));1422 try testing.expectEqual(error.Usage, @"error"(Args, "expected exactly 1 positional arg", .{}, silent_options));
1315}1423}
13161424
1317test "custom help" {1425test "custom help" {