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{...@@ -40,15 +40,37 @@ pub const Error = error{
40/// `Args` is a struct that you define looking like this:40/// `Args` is a struct that you define looking like this:
41/// ```41/// ```
42/// const Args = struct {42/// const Args = struct {
43/// pub const description = "this program does a thing";
43/// named: struct {44/// named: struct {
44/// // ...45/// verbose: bool = false,
46/// output: [:0]const u8,
47/// pub const output_help = "path to output file";
45/// },48/// },
46/// positional: struct {49/// positional: struct {
47/// // ...50/// input: []const u8,
51/// args: []const []const u8 = &.{},
48/// },52/// },
49/// };53/// };
50/// ```54/// ```
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.
52///74///
53/// The sequence of arg strings from the `ArgIterator` is parsed to determine named and positional arguments.75/// The sequence of arg strings from the `ArgIterator` is parsed to determine named and positional arguments.
54///76///
...@@ -160,7 +182,7 @@ test parse {...@@ -160,7 +182,7 @@ test parse {
160 /// First positional (non-named) argument:182 /// First positional (non-named) argument:
161 input: [:0]const u8 = "",183 input: [:0]const u8 = "",
162 /// Second positional argument is declared as optional:184 /// Second positional argument is declared as optional:
163 repititions: u32 = 1,185 repetitions: u32 = 1,
164 /// Receives the rest of the positional arguments.186 /// Receives the rest of the positional arguments.
165 @"the-rest": []const [:0]const u8 = &.{},187 @"the-rest": []const [:0]const u8 = &.{},
166 },188 },
...@@ -717,20 +739,20 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v...@@ -717,20 +739,20 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
717 arguments_table = arguments_table ++ .{&[_][]const u8{739 arguments_table = arguments_table ++ .{&[_][]const u8{
718 " " ++ field.name,740 " " ++ field.name,
719 @typeName(field.type) ++ " " ++741 @typeName(field.type) ++ " " ++
720 if (field.defaultValue()) |default|742 (if (field.defaultValue()) |default|
721 "default: " ++ std.fmt.comptimePrint("{}", .{default})743 "default: " ++ std.fmt.comptimePrint("{}", .{default})
722 else744 else
723 "required",745 "required") ++ argHelp(Args, "positional", field.name),
724 }};746 }};
725 },747 },
726 .@"enum" => {748 .@"enum" => {
727 arguments_table = arguments_table ++ .{&[_][]const u8{749 arguments_table = arguments_table ++ .{&[_][]const u8{
728 " " ++ field.name,750 " " ++ field.name,
729 comptime enumValuesExpr(field.type) ++ ". " ++751 comptime enumValuesExpr(field.type) ++ ". " ++
730 if (field.defaultValue()) |default|752 (if (field.defaultValue()) |default|
731 "default: " ++ @tagName(default)753 "default: " ++ @tagName(default)
732 else754 else
733 "required",755 "required") ++ argHelp(Args, "positional", field.name),
734 }};756 }};
735 },757 },
736 .pointer => |ptrInfo| {758 .pointer => |ptrInfo| {
...@@ -739,10 +761,10 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v...@@ -739,10 +761,10 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
739 arguments_table = arguments_table ++ .{&[_][]const u8{761 arguments_table = arguments_table ++ .{&[_][]const u8{
740 " " ++ field.name,762 " " ++ field.name,
741 "string. " ++763 "string. " ++
742 if (field.defaultValue()) |default|764 (if (field.defaultValue()) |default|
743 "default: " ++ quoteIfEmpty(default)765 "default: " ++ quoteIfEmpty(default)
744 else766 else
745 "required",767 "required") ++ argHelp(Args, "positional", field.name),
746 }};768 }};
747 } else {769 } else {
748 // Array770 // Array
...@@ -755,7 +777,7 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v...@@ -755,7 +777,7 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
755 };777 };
756 arguments_table = arguments_table ++ .{&[_][]const u8{778 arguments_table = arguments_table ++ .{&[_][]const u8{
757 " " ++ field.name,779 " " ++ field.name,
758 type_name ++ ". can be specified multiple times",780 type_name ++ ". can be specified multiple times" ++ argHelp(Args, "positional", field.name),
759 }};781 }};
760 }782 }
761 },783 },
...@@ -769,31 +791,31 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v...@@ -769,31 +791,31 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
769 .bool => {791 .bool => {
770 if (field.defaultValue()) |default| {792 if (field.defaultValue()) |default| {
771 if (default) {793 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) }};
773 } else {795 } 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) }};
775 }797 }
776 } else {798 } 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) }};
778 }800 }
779 },801 },
780 .int, .float => {802 .int, .float => {
781 arguments_table = arguments_table ++ .{&[_][]const u8{803 arguments_table = arguments_table ++ .{&[_][]const u8{
782 " --" ++ field.name ++ "=" ++ @typeName(field.type),804 " --" ++ field.name ++ "=" ++ @typeName(field.type),
783 if (field.defaultValue()) |default|805 (if (field.defaultValue()) |default|
784 "default: " ++ std.fmt.comptimePrint("{}", .{default})806 "default: " ++ std.fmt.comptimePrint("{}", .{default})
785 else807 else
786 "required",808 "required") ++ argHelp(Args, "named", field.name),
787 }};809 }};
788 },810 },
789 .@"enum" => {811 .@"enum" => {
790 arguments_table = arguments_table ++ .{&[_][]const u8{812 arguments_table = arguments_table ++ .{&[_][]const u8{
791 " --" ++ field.name ++ "=enum",813 " --" ++ field.name ++ "=enum",
792 comptime enumValuesExpr(field.type) ++ " " ++814 comptime enumValuesExpr(field.type) ++ ". " ++
793 if (field.defaultValue()) |default|815 (if (field.defaultValue()) |default|
794 "default: " ++ @tagName(default)816 "default: " ++ @tagName(default)
795 else817 else
796 "required",818 "required") ++ argHelp(Args, "named", field.name),
797 }};819 }};
798 },820 },
799 .pointer => |ptrInfo| {821 .pointer => |ptrInfo| {
...@@ -801,10 +823,10 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v...@@ -801,10 +823,10 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
801 // String823 // String
802 arguments_table = arguments_table ++ .{&[_][]const u8{824 arguments_table = arguments_table ++ .{&[_][]const u8{
803 " --" ++ field.name ++ "=string",825 " --" ++ field.name ++ "=string",
804 if (field.defaultValue()) |default|826 (if (field.defaultValue()) |default|
805 "default: " ++ quoteIfEmpty(default)827 "default: " ++ quoteIfEmpty(default)
806 else828 else
807 "required",829 "required") ++ argHelp(Args, "named", field.name),
808 }};830 }};
809 } else {831 } else {
810 // Array832 // Array
...@@ -817,7 +839,7 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v...@@ -817,7 +839,7 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
817 };839 };
818 arguments_table = arguments_table ++ .{&[_][]const u8{840 arguments_table = arguments_table ++ .{&[_][]const u8{
819 " --" ++ field.name ++ "=" ++ type_name,841 " --" ++ field.name ++ "=" ++ type_name,
820 "can be specified multiple times",842 "can be specified multiple times" ++ argHelp(Args, "named", field.name),
821 }};843 }};
822 }844 }
823 },845 },
...@@ -833,6 +855,9 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v...@@ -833,6 +855,9 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
833 }855 }
834856
835 comptime var help_str: []const u8 = "";857 comptime var help_str: []const u8 = "";
858 if (@hasDecl(Args, "description")) {
859 help_str = "\n\n" ++ Args.description;
860 }
836 inline for (arguments_table) |row| {861 inline for (arguments_table) |row| {
837 help_str = help_str ++ "\n";862 help_str = help_str ++ "\n";
838 inline for (row, 0..) |cell, c| {863 inline for (row, 0..) |cell, c| {
...@@ -856,6 +881,13 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v...@@ -856,6 +881,13 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v
856 }881 }
857}882}
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
859inline fn quoteIfEmpty(comptime s: []const u8) []const u8 {891inline fn quoteIfEmpty(comptime s: []const u8) []const u8 {
860 if (s.len == 0) return "''";892 if (s.len == 0) return "''";
861 return s;893 return s;