authorgravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-30 08:26:12-04:00
committergravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-30 22:36:30-04:00
log96f353fc22652cefcaf24374f2e463baee237cad
tree30592a0c63e1b65f8eafd9a7e1f42148f96c021b
parent5278e4dcb8dd251142a3a0443b0391697ffc22b6

help for positional


1 files changed, 91 insertions(+), 42 deletions(-)

lib/std/cli.zig+91-42
...@@ -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(named_fields, positional_fields, writer, prog);296 printGeneratedHelp(Args, 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(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error);307 return usageError(Args, 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(named_fields, positional_fields, writer, "unexpected positional argument: {s}", .{arg}, prog, exit_on_error);317 if (positional_field_index >= positional_fields.len) return usageError(Args, 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(named_fields, positional_fields, C, arg, field.name, writer, prog, exit_on_error));321 try @field(positional_array_lists, field.name).append(allocator, try parseValue(Args, 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(named_fields, positional_fields, field.type, arg, field.name, writer, prog, exit_on_error);324 @field(result.positional, field.name) = try parseValue(Args, 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(named_fields, positional_fields, writer, "cannot specify value for bool argument: {s}", .{arg}, prog, exit_on_error);352 if (immediate_value != null) return usageError(Args, 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(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error);356 if (no_prefixed) return usageError(Args, 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(named_fields, positional_fields, writer, "expected argument after --{s}", .{field.name}, prog, exit_on_error);359 const arg_value = immediate_value orelse iter.next() orelse return usageError(Args, 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(named_fields, positional_fields, C, arg_value, field.name, writer, prog, exit_on_error));362 try @field(named_array_lists, field.name).append(allocator, try parseValue(Args, C, arg_value, field.name, writer, prog, exit_on_error));
363 } else {363 } else {
364 @field(result.named, field.name) = try parseValue(named_fields, positional_fields, field.type, arg_value, field.name, writer, prog, exit_on_error);364 @field(result.named, field.name) = try parseValue(Args, 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(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error);370 return usageError(Args, 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(named_fields, positional_fields, writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, prog, exit_on_error);387 return usageError(Args, writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, prog, exit_on_error);
388 } else {388 } else {
389 return usageError(named_fields, positional_fields, writer, "missing required argument: --" ++ field.name, .{}, prog, exit_on_error);389 return usageError(Args, 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(named_fields, positional_fields, writer, "missing required argument: " ++ field.name, .{}, prog, exit_on_error);406 return usageError(Args, 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 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 {416fn parseValue(comptime Args: type, 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(named_fields, positional_fields, writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, prog, exit_on_error);421 return usageError(Args, 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(named_fields, positional_fields, writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, prog, exit_on_error);426 return usageError(Args, 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(named_fields, positional_fields, writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, prog, exit_on_error);431 return usageError(Args, 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| {
...@@ -451,8 +451,8 @@ fn checkArgsType(comptime Args: type) struct { []const StructField, []const Stru...@@ -451,8 +451,8 @@ fn checkArgsType(comptime Args: type) struct { []const StructField, []const Stru
451 } else @compileError("unrecognized Args name: " ++ field.name);451 } else @compileError("unrecognized Args name: " ++ field.name);
452 }452 }
453453
454 const named_fields = if (has_named) @typeInfo(@TypeOf(@as(Args, undefined).named)).@"struct".fields else &.{};454 const named_fields = if (has_named) @typeInfo(@FieldType(Args, "named")).@"struct".fields else &.{};
455 const positional_fields = if (has_positional) @typeInfo(@TypeOf(@as(Args, undefined).positional)).@"struct".fields else &.{};455 const positional_fields = if (has_positional) @typeInfo(@FieldType(Args, "positional")).@"struct".fields else &.{};
456456
457 // Named arguments are more lenient.457 // Named arguments are more lenient.
458 inline for (named_fields) |field| {458 inline for (named_fields) |field| {
...@@ -508,15 +508,16 @@ fn validateField(field: StructField) void {...@@ -508,15 +508,16 @@ fn validateField(field: StructField) void {
508 // String.508 // String.
509 } else {509 } else {
510 // Array.510 // Array.
511 if (field.default_value_ptr == null) @compileError("Array arguments must have a default value: " ++ field.name);511 if (field.defaultValue()) |default| {
512 if (default.len != 0) @compileError("Array argument default value must have 0 len: " ++ field.name);
513 } else @compileError("Array arguments must have a default value: " ++ field.name);
512 switch (@typeInfo(ptrInfo.child)) {514 switch (@typeInfo(ptrInfo.child)) {
513 .bool => @compileError("Unsupported field type: " ++ @typeName(field.type)),515 .bool => @compileError("Unsupported field type: " ++ @typeName(field.type)),
514 .float => {},516 .float => {},
515 .int => {},517 .int => {},
516 .@"enum" => @compileError("Unsupported field type: " ++ @typeName(field.type)),518 .@"enum" => @compileError("Unsupported field type: " ++ @typeName(field.type)),
517 .pointer => |ptrInfo2| {519 .pointer => |ptrInfo2| {
518 if (ptrInfo2.size != .slice) @compileError("Unsupported field type: " ++ @typeName(field.type));520 if (ptrInfo2.size == .slice and ptrInfo2.child == u8) {
519 if (ptrInfo2.child == u8) {
520 // String.521 // String.
521 } else {522 } else {
522 @compileError("Unsupported field type: " ++ @typeName(field.type));523 @compileError("Unsupported field type: " ++ @typeName(field.type));
...@@ -579,7 +580,6 @@ fn ArrayListsForFields(comptime fields: []const StructField) type {...@@ -579,7 +580,6 @@ fn ArrayListsForFields(comptime fields: []const StructField) type {
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 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`.581/// This matches the default behavior of `parse`, not `parseIter` or `parseSlice`.
581pub fn @"error"(comptime Args: type, comptime msg: []const u8, msg_args: anytype, options: Options) error{Usage} {582pub fn @"error"(comptime Args: type, comptime msg: []const u8, msg_args: anytype, options: Options) error{Usage} {
582 const named_fields, const positional_fields = comptime checkArgsType(Args);
583 var buf: [0x1000]u8 = undefined;583 var buf: [0x1000]u8 = undefined;
584 const prog: ?[]const u8 = options.prog orelse blk: {584 const prog: ?[]const u8 = options.prog orelse blk: {
585 var fba: std.heap.FixedBufferAllocator = .init(&buf);585 var fba: std.heap.FixedBufferAllocator = .init(&buf);
...@@ -587,7 +587,7 @@ pub fn @"error"(comptime Args: type, comptime msg: []const u8, msg_args: anytype...@@ -587,7 +587,7 @@ pub fn @"error"(comptime Args: type, comptime msg: []const u8, msg_args: anytype
587 const argv0 = iter.next();587 const argv0 = iter.next();
588 break :blk if (argv0) |arg| std.fs.path.basename(arg) else null;588 break :blk if (argv0) |arg| std.fs.path.basename(arg) else null;
589 };589 };
590 return usageError(named_fields, positional_fields, options.writer, msg, msg_args, prog orelse "<prog>", options.exit orelse true);590 return usageError(Args, options.writer, msg, msg_args, prog orelse "<prog>", options.exit orelse true);
591}591}
592592
593test @"error" {593test @"error" {
...@@ -635,7 +635,8 @@ fn enumValuesExpr(comptime Enum: type) []const u8 {...@@ -635,7 +635,8 @@ fn enumValuesExpr(comptime Enum: type) []const u8 {
635 return values_str;635 return values_str;
636}636}
637637
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} {638fn usageError(comptime Args: type, writer: ?*Writer, comptime msg: []const u8, args: anytype, prog: []const u8, exit_on_error: bool) error{Usage} {
639 const named_fields, const positional_fields = comptime checkArgsType(Args);
639 const whole_msg =640 const whole_msg =
640 "error: " ++ msg ++ "\n" ++ //641 "error: " ++ msg ++ "\n" ++ //
641 "usage: {s} " ++ comptime usageLineFmt(named_fields, positional_fields) ++ "\n" ++642 "usage: {s} " ++ comptime usageLineFmt(named_fields, positional_fields) ++ "\n" ++
...@@ -702,17 +703,67 @@ fn usageLineFmt(comptime named_fields: []const StructField, comptime positional_...@@ -702,17 +703,67 @@ fn usageLineFmt(comptime named_fields: []const StructField, comptime positional_
702 }703 }
703 return escapeFmt(usage_str);704 return escapeFmt(usage_str);
704}705}
705fn printGeneratedHelp(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, writer: ?*Writer, prog: []const u8) void {706fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) void {
706 comptime var arguments_table: []const []const []const u8 = &.{};707 const named_fields, const positional_fields = comptime checkArgsType(Args);
707708
708 comptime var arguments_str: []const u8 = ""; // TODO: delete709 comptime var arguments_table: []const []const []const u8 = &.{};
709710
710 if (positional_fields.len > 0) {711 if (positional_fields.len > 0) {
711 arguments_table = arguments_table ++ .{&[_][]const u8{"positional arguments:"}};712 arguments_table = arguments_table ++ .{ &[_][]const u8{""}, &[_][]const u8{"positional arguments:"} };
713 }
714 inline for (positional_fields) |field| {
715 switch (@typeInfo(field.type)) {
716 .int, .float => {
717 arguments_table = arguments_table ++ .{&[_][]const u8{
718 " " ++ field.name,
719 @typeName(field.type) ++ " " ++
720 if (field.defaultValue()) |default|
721 "default: " ++ std.fmt.comptimePrint("{}", .{default})
722 else
723 "required",
724 }};
725 },
726 .@"enum" => {
727 arguments_table = arguments_table ++ .{&[_][]const u8{
728 " " ++ field.name,
729 comptime enumValuesExpr(field.type) ++ ". " ++
730 if (field.defaultValue()) |default|
731 "default: " ++ @tagName(default)
732 else
733 "required",
734 }};
735 },
736 .pointer => |ptrInfo| {
737 if (ptrInfo.size == .slice and ptrInfo.child == u8) {
738 // String
739 arguments_table = arguments_table ++ .{&[_][]const u8{
740 " " ++ field.name,
741 "string. " ++
742 if (field.defaultValue()) |default|
743 "default: " ++ quoteIfEmpty(default)
744 else
745 "required",
746 }};
747 } else {
748 // Array
749 const type_name = switch (@typeInfo(ptrInfo.child)) {
750 .bool => comptime unreachable,
751 .int, .float => @typeName(ptrInfo.child),
752 .@"enum" => comptime unreachable,
753 .pointer => "string", // The array-of-pointer that doesn't cause compile errors elsewhere.
754 else => comptime unreachable,
755 };
756 arguments_table = arguments_table ++ .{&[_][]const u8{
757 " " ++ field.name,
758 type_name ++ ". can be specified multiple times",
759 }};
760 }
761 },
762 else => comptime unreachable,
763 }
712 }764 }
713 //inline for (positional_fields) |field| {}
714765
715 arguments_table = arguments_table ++ .{&[_][]const u8{"named arguments:"}}; // The --help option is always there.766 arguments_table = arguments_table ++ .{ &[_][]const u8{""}, &[_][]const u8{"named arguments:"} };
716 inline for (named_fields) |field| {767 inline for (named_fields) |field| {
717 switch (@typeInfo(field.type)) {768 switch (@typeInfo(field.type)) {
718 .bool => {769 .bool => {
...@@ -737,11 +788,12 @@ fn printGeneratedHelp(comptime named_fields: []const StructField, comptime posit...@@ -737,11 +788,12 @@ fn printGeneratedHelp(comptime named_fields: []const StructField, comptime posit
737 },788 },
738 .@"enum" => {789 .@"enum" => {
739 arguments_table = arguments_table ++ .{&[_][]const u8{790 arguments_table = arguments_table ++ .{&[_][]const u8{
740 " --" ++ field.name ++ "=" ++ comptime enumValuesExpr(field.type),791 " --" ++ field.name ++ "=enum",
741 if (field.defaultValue()) |default|792 comptime enumValuesExpr(field.type) ++ " " ++
742 "default: " ++ @tagName(default)793 if (field.defaultValue()) |default|
743 else794 "default: " ++ @tagName(default)
744 "required",795 else
796 "required",
745 }};797 }};
746 },798 },
747 .pointer => |ptrInfo| {799 .pointer => |ptrInfo| {
...@@ -763,12 +815,9 @@ fn printGeneratedHelp(comptime named_fields: []const StructField, comptime posit...@@ -763,12 +815,9 @@ fn printGeneratedHelp(comptime named_fields: []const StructField, comptime posit
763 .pointer => "string", // The array-of-pointer that doesn't cause compile errors elsewhere.815 .pointer => "string", // The array-of-pointer that doesn't cause compile errors elsewhere.
764 else => comptime unreachable,816 else => comptime unreachable,
765 };817 };
766 arguments_str = arguments_str ++ "\n " ++ //
767 "--" ++ field.name ++ " " ++ type_name ++ " " ++ //
768 "[--" ++ field.name ++ " " ++ type_name ++ " ...]";
769 arguments_table = arguments_table ++ .{&[_][]const u8{818 arguments_table = arguments_table ++ .{&[_][]const u8{
770 " --" ++ field.name ++ "=" ++ type_name ++ " " ++ //819 " --" ++ field.name ++ "=" ++ type_name,
771 "[--" ++ field.name ++ "=" ++ type_name ++ " ...]",820 "can be specified multiple times",
772 }};821 }};
773 }822 }
774 },823 },