authorgravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-30 11:57:23-04:00
committergravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-30 22:36:30-04:00
log473c1d6fa5bea6dcb03151a304af645910826be8
tree989a3901f793b4ce9abed5687338cccb9a0aa57f
parent96f353fc22652cefcaf24374f2e463baee237cad

support extra help decoration


1 files changed, 54 insertions(+), 22 deletions(-)

lib/std/cli.zig+54-22
......@@ -40,15 +40,37 @@ pub const Error = error{
4040/// `Args` is a struct that you define looking like this:
4141/// ```
4242/// const Args = struct {
43/// pub const description = "this program does a thing";
4344/// named: struct {
44/// // ...
45/// verbose: bool = false,
46/// output: [:0]const u8,
47/// pub const output_help = "path to output file";
4548/// },
4649/// positional: struct {
47/// // ...
50/// input: []const u8,
51/// args: []const []const u8 = &.{},
4852/// },
4953/// };
5054/// ```
51/// Either or both of `named` and `positional` may be omitted, which is effectively equivalent to them having no fields.
55/// Which results in this generated `--help` output:
56/// ```
57/// usage: <prog> [options] --output=string input [args...]
58///
59/// this program does a thing
60///
61/// positional arguments:
62/// input string. required
63/// args string. can be specified multiple times
64///
65/// named arguments:
66/// --verbose default: --no-verbose
67/// --output=string required. path to output file
68/// --help print this help and exit
69/// ```
70/// Either or both of `named` and `positional` may be omitted, which is effectively equivalent to declaring them as `struct {}`.
71/// If `description` is declared, it is concatenated into the help output.
72/// If any `pub const <name>_help` accompanies a field `<name>` in either `named` or `positional`,
73/// it is included in that argument's help text.
5274///
5375/// The sequence of arg strings from the `ArgIterator` is parsed to determine named and positional arguments.
5476///
......@@ -160,7 +182,7 @@ test parse {
160182 /// First positional (non-named) argument:
161183 input: [:0]const u8 = "",
162184 /// Second positional argument is declared as optional:
163 repititions: u32 = 1,
185 repetitions: u32 = 1,
164186 /// Receives the rest of the positional arguments.
165187 @"the-rest": []const [:0]const u8 = &.{},
166188 },
......@@ -717,20 +739,20 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
717739 arguments_table = arguments_table ++ .{&[_][]const u8{
718740 " " ++ field.name,
719741 @typeName(field.type) ++ " " ++
720 if (field.defaultValue()) |default|
742 (if (field.defaultValue()) |default|
721743 "default: " ++ std.fmt.comptimePrint("{}", .{default})
722744 else
723 "required",
745 "required") ++ argHelp(Args, "positional", field.name),
724746 }};
725747 },
726748 .@"enum" => {
727749 arguments_table = arguments_table ++ .{&[_][]const u8{
728750 " " ++ field.name,
729751 comptime enumValuesExpr(field.type) ++ ". " ++
730 if (field.defaultValue()) |default|
752 (if (field.defaultValue()) |default|
731753 "default: " ++ @tagName(default)
732754 else
733 "required",
755 "required") ++ argHelp(Args, "positional", field.name),
734756 }};
735757 },
736758 .pointer => |ptrInfo| {
......@@ -739,10 +761,10 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
739761 arguments_table = arguments_table ++ .{&[_][]const u8{
740762 " " ++ field.name,
741763 "string. " ++
742 if (field.defaultValue()) |default|
764 (if (field.defaultValue()) |default|
743765 "default: " ++ quoteIfEmpty(default)
744766 else
745 "required",
767 "required") ++ argHelp(Args, "positional", field.name),
746768 }};
747769 } else {
748770 // Array
......@@ -755,7 +777,7 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
755777 };
756778 arguments_table = arguments_table ++ .{&[_][]const u8{
757779 " " ++ field.name,
758 type_name ++ ". can be specified multiple times",
780 type_name ++ ". can be specified multiple times" ++ argHelp(Args, "positional", field.name),
759781 }};
760782 }
761783 },
......@@ -769,31 +791,31 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
769791 .bool => {
770792 if (field.defaultValue()) |default| {
771793 if (default) {
772 arguments_table = arguments_table ++ .{&[_][]const u8{ " --no-" ++ field.name, "default: --" ++ field.name }};
794 arguments_table = arguments_table ++ .{&[_][]const u8{ " --no-" ++ field.name, "default: --" ++ field.name ++ argHelp(Args, "named", field.name) }};
773795 } else {
774 arguments_table = arguments_table ++ .{&[_][]const u8{ " --" ++ field.name, "default: --no-" ++ field.name }};
796 arguments_table = arguments_table ++ .{&[_][]const u8{ " --" ++ field.name, "default: --no-" ++ field.name ++ argHelp(Args, "named", field.name) }};
775797 }
776798 } else {
777 arguments_table = arguments_table ++ .{&[_][]const u8{ " --[no-]" ++ field.name, "required" }};
799 arguments_table = arguments_table ++ .{&[_][]const u8{ " --[no-]" ++ field.name, "required" ++ argHelp(Args, "named", field.name) }};
778800 }
779801 },
780802 .int, .float => {
781803 arguments_table = arguments_table ++ .{&[_][]const u8{
782804 " --" ++ field.name ++ "=" ++ @typeName(field.type),
783 if (field.defaultValue()) |default|
805 (if (field.defaultValue()) |default|
784806 "default: " ++ std.fmt.comptimePrint("{}", .{default})
785807 else
786 "required",
808 "required") ++ argHelp(Args, "named", field.name),
787809 }};
788810 },
789811 .@"enum" => {
790812 arguments_table = arguments_table ++ .{&[_][]const u8{
791813 " --" ++ field.name ++ "=enum",
792 comptime enumValuesExpr(field.type) ++ " " ++
793 if (field.defaultValue()) |default|
814 comptime enumValuesExpr(field.type) ++ ". " ++
815 (if (field.defaultValue()) |default|
794816 "default: " ++ @tagName(default)
795817 else
796 "required",
818 "required") ++ argHelp(Args, "named", field.name),
797819 }};
798820 },
799821 .pointer => |ptrInfo| {
......@@ -801,10 +823,10 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
801823 // String
802824 arguments_table = arguments_table ++ .{&[_][]const u8{
803825 " --" ++ field.name ++ "=string",
804 if (field.defaultValue()) |default|
826 (if (field.defaultValue()) |default|
805827 "default: " ++ quoteIfEmpty(default)
806828 else
807 "required",
829 "required") ++ argHelp(Args, "named", field.name),
808830 }};
809831 } else {
810832 // Array
......@@ -817,7 +839,7 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
817839 };
818840 arguments_table = arguments_table ++ .{&[_][]const u8{
819841 " --" ++ field.name ++ "=" ++ type_name,
820 "can be specified multiple times",
842 "can be specified multiple times" ++ argHelp(Args, "named", field.name),
821843 }};
822844 }
823845 },
......@@ -833,6 +855,9 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
833855 }
834856
835857 comptime var help_str: []const u8 = "";
858 if (@hasDecl(Args, "description")) {
859 help_str = "\n\n" ++ Args.description;
860 }
836861 inline for (arguments_table) |row| {
837862 help_str = help_str ++ "\n";
838863 inline for (row, 0..) |cell, c| {
......@@ -856,6 +881,13 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
856881 }
857882}
858883
884inline fn argHelp(comptime Args: type, comptime named_or_positional: []const u8, comptime field_name: []const u8) []const u8 {
885 const N = @FieldType(Args, named_or_positional);
886 comptime assert(@hasField(N, field_name));
887 if (!@hasDecl(N, field_name ++ "_help")) return "";
888 return ". " ++ @field(N, field_name ++ "_help");
889}
890
859891inline fn quoteIfEmpty(comptime s: []const u8) []const u8 {
860892 if (s.len == 0) return "''";
861893 return s;