22const std = @import ("std" );
33const parser = @import ("parser.zig" );
44const context = @import ("context.zig" );
5- const utils = @import ("utils .zig" );
5+ const styles = @import ("styles .zig" );
66const types = @import ("types.zig" );
77const errors = @import ("errors.zig" );
88
9+ /// Defines the configuration for a `Command`.
10+ ///
11+ /// All string-slice fields (`name`, `description`, `version`, `section`, and
12+ /// the entries of `aliases`) are borrowed by the `Command`, not copied. They
13+ /// must remain valid for the lifetime of the command tree.
14+ pub const CommandOptions = struct {
15+ /// The primary name of the command, used to invoke it.
16+ name : []const u8 ,
17+ /// A short description of the command's purpose, shown in help messages.
18+ description : []const u8 ,
19+ /// The function to execute when this command is run.
20+ exec : * const fn (ctx : context.CommandContext ) anyerror ! void ,
21+ /// An optional list of alternative names for the command.
22+ aliases : ? []const []const u8 = null ,
23+ /// An optional single-character shortcut for the command (e.g., 'c').
24+ shortcut : ? u8 = null ,
25+ /// An optional version string for the application. If provided on the root command,
26+ /// an automatic `--version` flag will be available.
27+ version : ? []const u8 = null ,
28+ /// The name of the section under which this command should be grouped in a parent's help message.
29+ section : []const u8 = "Commands" ,
30+ };
31+
932/// Represents a single command in a CLI application.
1033///
1134/// A `Command` can have its own flags, positional arguments, and an execution function.
@@ -19,7 +42,7 @@ const errors = @import("errors.zig");
1942/// threads on the same `Command` instance concurrently will result in a data race
2043/// and undefined behavior.
2144pub const Command = struct {
22- options : types. CommandOptions ,
45+ options : CommandOptions ,
2346 subcommands : std .ArrayList (* Command ),
2447 flags : std .ArrayList (types .Flag ),
2548 positional_args : std .ArrayList (types .PositionalArg ),
@@ -30,7 +53,7 @@ pub const Command = struct {
3053
3154 /// Initializes a new command.
3255 /// Panics if the provided command name is empty.
33- pub fn init (allocator : std.mem.Allocator , options : types. CommandOptions ) ! * Command {
56+ pub fn init (allocator : std.mem.Allocator , options : CommandOptions ) ! * Command {
3457 if (options .name .len == 0 ) {
3558 std .debug .panic ("Command name cannot be empty." , .{});
3659 }
@@ -271,8 +294,8 @@ pub const Command = struct {
271294 failed_cmd : ? * const Command ,
272295 writer : anytype ,
273296 ) void {
274- const red = utils . styles .s (utils . styles .RED );
275- const reset = utils . styles .s (utils . styles .RESET );
297+ const red = styles .s (styles .RED );
298+ const reset = styles .s (styles .RESET );
276299
277300 switch (err ) {
278301 error .BrokenPipe = > return , // Exit silently on broken pipe
@@ -442,9 +465,9 @@ pub const Command = struct {
442465 var buf : [4096 ]u8 = undefined ;
443466 var file_writer = std .Io .File .stdout ().writer (io , & buf );
444467 const stdout = & file_writer .interface ;
445- const bold = utils . styles .s (utils . styles .BOLD );
446- const dim = utils . styles .s (utils . styles .DIM );
447- const reset = utils . styles .s (utils . styles .RESET );
468+ const bold = styles .s (styles .BOLD );
469+ const dim = styles .s (styles .DIM );
470+ const reset = styles .s (styles .RESET );
448471
449472 try stdout .print ("{s}{s}{s}\n " , .{ bold , self .options .description , reset });
450473
@@ -454,27 +477,203 @@ pub const Command = struct {
454477 try stdout .print ("\n " , .{});
455478
456479 try stdout .print ("{s}Usage:{s}\n " , .{ bold , reset });
457- try utils . printUsageLine (self , stdout );
480+ try printUsageLine (self , stdout );
458481
459482 if (self .positional_args .items .len > 0 ) {
460483 try stdout .print ("{s}Arguments:{s}\n " , .{ bold , reset });
461- try utils . printAlignedPositionalArgs (self , stdout );
484+ try printAlignedPositionalArgs (self , stdout );
462485 try stdout .print ("\n " , .{});
463486 }
464487
465488 if (self .flags .items .len > 0 ) {
466489 try stdout .print ("{s}Flags:{s}\n " , .{ bold , reset });
467- try utils . printAlignedFlags (self , stdout );
490+ try printAlignedFlags (self , stdout );
468491 try stdout .print ("\n " , .{});
469492 }
470493
471494 if (self .subcommands .items .len > 0 ) {
472- try utils . printSubcommands (self , stdout );
495+ try printSubcommands (self , stdout );
473496 }
474497 try file_writer .flush ();
475498 }
476499};
477500
501+ // ============================================================================
502+ // Help-output printers (private)
503+ // ============================================================================
504+ // These used to live in utils.zig. They were moved here because they depend
505+ // on Command's internals (flags, positional_args, subcommands, parent chain),
506+ // and keeping them in a separate module created a utils <-> command import
507+ // cycle that the refactor set out to eliminate. They are intentionally not
508+ // `pub` — help output is a Command concern, not a public extension point.
509+
510+ fn printAlignedCommands (commands : []* Command , writer : anytype ) ! void {
511+ var max_width : usize = 0 ;
512+ for (commands ) | cmd | {
513+ var len = cmd .options .name .len ;
514+ if (cmd .options .shortcut != null ) {
515+ len += 4 ; // " (c)"
516+ }
517+ if (len > max_width ) max_width = len ;
518+ }
519+
520+ for (commands ) | cmd | {
521+ try writer .print (" {s}" , .{cmd .options .name });
522+ var current_width = cmd .options .name .len ;
523+ if (cmd .options .shortcut ) | sc | {
524+ try writer .print (" ({c})" , .{sc });
525+ current_width += 4 ;
526+ }
527+
528+ for (0.. max_width - current_width + 2 ) | _ | try writer .writeByte (' ' );
529+ try writer .print ("{s}\n " , .{cmd .options .description });
530+ }
531+ }
532+
533+ fn printAlignedFlags (cmd : * const Command , writer : anytype ) ! void {
534+ var max_width : usize = 0 ;
535+ for (cmd .flags .items ) | flag | {
536+ if (flag .hidden ) continue ;
537+ const len : usize = if (flag .shortcut != null )
538+ // " -c, --name"
539+ flag .name .len + 8
540+ else
541+ // " --name"
542+ flag .name .len + 8 ;
543+ if (len > max_width ) max_width = len ;
544+ }
545+
546+ for (cmd .flags .items ) | flag | {
547+ if (flag .hidden ) continue ;
548+
549+ var current_width : usize = undefined ;
550+ if (flag .shortcut ) | sc | {
551+ try writer .print (" -{c}, --{s}" , .{ sc , flag .name });
552+ current_width = flag .name .len + 8 ;
553+ } else {
554+ try writer .print (" --{s}" , .{flag .name });
555+ current_width = flag .name .len + 8 ;
556+ }
557+
558+ for (0.. max_width - current_width + 2 ) | _ | try writer .writeByte (' ' );
559+ try writer .print ("{s} [{s}]" , .{ flag .description , @tagName (flag .type ) });
560+
561+ switch (flag .default_value ) {
562+ .Bool = > | v | try writer .print (" (default: {})" , .{v }),
563+ .Int = > | v | try writer .print (" (default: {})" , .{v }),
564+ .Float = > | v | try writer .print (" (default: {})" , .{v }),
565+ .String = > | v | try writer .print (" (default: \" {s}\" )" , .{v }),
566+ }
567+ try writer .print ("\n " , .{});
568+ }
569+ }
570+
571+ fn printAlignedPositionalArgs (cmd : * const Command , writer : anytype ) ! void {
572+ var max_width : usize = 0 ;
573+ for (cmd .positional_args .items ) | arg | {
574+ if (arg .name .len > max_width ) max_width = arg .name .len ;
575+ }
576+
577+ for (cmd .positional_args .items ) | arg | {
578+ try writer .print (" {s}" , .{arg .name });
579+ for (0.. max_width - arg .name .len + 2 ) | _ | try writer .writeByte (' ' );
580+ try writer .print ("{s}" , .{arg .description });
581+
582+ if (arg .variadic ) {
583+ try writer .print (" (variadic)\n " , .{});
584+ } else if (arg .is_required ) {
585+ try writer .print (" (required)\n " , .{});
586+ } else {
587+ try writer .print (" (optional)\n " , .{});
588+ }
589+ }
590+ }
591+
592+ fn printUsageLine (cmd : * const Command , writer : anytype ) ! void {
593+ var parents : std .ArrayList (* Command ) = .empty ;
594+ defer parents .deinit (cmd .allocator );
595+
596+ var current_parent = cmd .parent ;
597+ while (current_parent ) | p | {
598+ try parents .append (cmd .allocator , p );
599+ current_parent = p .parent ;
600+ }
601+ std .mem .reverse (* Command , parents .items );
602+
603+ if (parents .items .len > 0 ) {
604+ try writer .print (" {s}" , .{parents .items [0 ].options .name });
605+ for (parents .items [1.. ]) | p | {
606+ try writer .print (" {s}" , .{p .options .name });
607+ }
608+ try writer .print (" {s}" , .{cmd .options .name });
609+ } else {
610+ try writer .print (" {s}" , .{cmd .options .name });
611+ }
612+
613+ if (cmd .flags .items .len > 0 ) {
614+ try writer .print (" [flags]" , .{});
615+ }
616+
617+ for (cmd .positional_args .items ) | arg | {
618+ if (arg .variadic ) {
619+ try writer .print (" [{s}...]" , .{arg .name });
620+ } else if (arg .is_required ) {
621+ try writer .print (" <{s}>" , .{arg .name });
622+ } else {
623+ try writer .print (" [{s}]" , .{arg .name });
624+ }
625+ }
626+
627+ if (cmd .subcommands .items .len > 0 ) {
628+ try writer .print (" [command]" , .{});
629+ }
630+
631+ try writer .print ("\n\n " , .{});
632+ }
633+
634+ const CommandSortContext = struct {
635+ pub fn lessThan (_ : @This (), a : * Command , b : * Command ) bool {
636+ return std .mem .order (u8 , a .options .name , b .options .name ) == .lt ;
637+ }
638+ };
639+
640+ const StringSortContext = struct {
641+ pub fn lessThan (_ : @This (), a : []const u8 , b : []const u8 ) bool {
642+ return std .mem .order (u8 , a , b ) == .lt ;
643+ }
644+ };
645+
646+ fn printSubcommands (cmd : * const Command , writer : anytype ) ! void {
647+ var section_map = std .StringHashMap (std .ArrayList (* Command )).init (cmd .allocator );
648+ defer {
649+ var it = section_map .iterator ();
650+ while (it .next ()) | entry | entry .value_ptr .* .deinit (cmd .allocator );
651+ section_map .deinit ();
652+ }
653+
654+ for (cmd .subcommands .items ) | sub | {
655+ const list = try section_map .getOrPut (sub .options .section );
656+ if (! list .found_existing ) {
657+ list .value_ptr .* = .empty ;
658+ }
659+ try list .value_ptr .* .append (cmd .allocator , sub );
660+ }
661+
662+ var sorted_sections : std .ArrayList ([]const u8 ) = .empty ;
663+ defer sorted_sections .deinit (cmd .allocator );
664+ var it = section_map .keyIterator ();
665+ while (it .next ()) | key | try sorted_sections .append (cmd .allocator , key .* );
666+ std .sort .pdq ([]const u8 , sorted_sections .items , StringSortContext {}, StringSortContext .lessThan );
667+
668+ for (sorted_sections .items ) | section_name | {
669+ try writer .print ("{s}{s}{s}:\n " , .{ styles .s (styles .BOLD ), section_name , styles .s (styles .RESET ) });
670+ const cmds_list = section_map .get (section_name ).? ;
671+ std .sort .pdq (* Command , cmds_list .items , CommandSortContext {}, CommandSortContext .lessThan );
672+ try printAlignedCommands (cmds_list .items , writer );
673+ try writer .print ("\n " , .{});
674+ }
675+ }
676+
478677// Tests for the `command` module
479678
480679const testing = std .testing ;
@@ -1004,3 +1203,67 @@ test "command: Args.Iterator with only argv0 produces no user args" {
10041203 const user_args = if (args_list .items .len > 1 ) args_list .items [1.. ] else args_list .items [0.. 0];
10051204 try std .testing .expectEqual (@as (usize , 0 ), user_args .len );
10061205}
1206+
1207+ // ============================================================================
1208+ // Tests for the help-output printers (moved from utils.zig)
1209+ // ============================================================================
1210+
1211+ test "help: printAlignedFlags produces correct padding" {
1212+ const allocator = std .testing .allocator ;
1213+ var cmd = try Command .init (allocator , .{ .name = "test" , .description = "" , .exec = dummyExec });
1214+ defer cmd .deinit ();
1215+
1216+ try cmd .addFlag (.{
1217+ .name = "verbose" ,
1218+ .shortcut = 'v' ,
1219+ .description = "Enable verbose output" ,
1220+ .type = .Bool ,
1221+ .default_value = .{ .Bool = false },
1222+ });
1223+
1224+ var buf : [2048 ]u8 = undefined ;
1225+ var writer = TestBufWriter { .buf = & buf };
1226+ try printAlignedFlags (cmd , & writer );
1227+
1228+ const output = writer .getWritten ();
1229+ try std .testing .expect (std .mem .indexOf (u8 , output , "--help" ) != null );
1230+ try std .testing .expect (std .mem .indexOf (u8 , output , "--verbose" ) != null );
1231+ try std .testing .expect (std .mem .indexOf (u8 , output , "Enable verbose output" ) != null );
1232+ try std .testing .expect (std .mem .indexOf (u8 , output , " " ) != null );
1233+ }
1234+
1235+ test "help: printAlignedPositionalArgs produces correct padding" {
1236+ const allocator = std .testing .allocator ;
1237+ var cmd = try Command .init (allocator , .{ .name = "test" , .description = "" , .exec = dummyExec });
1238+ defer cmd .deinit ();
1239+
1240+ try cmd .addPositional (.{ .name = "input" , .description = "Input file" , .is_required = true });
1241+ try cmd .addPositional (.{ .name = "output-file" , .description = "Output file" , .is_required = false , .default_value = .{ .String = "out.txt" } });
1242+
1243+ var buf : [2048 ]u8 = undefined ;
1244+ var writer = TestBufWriter { .buf = & buf };
1245+ try printAlignedPositionalArgs (cmd , & writer );
1246+
1247+ const output = writer .getWritten ();
1248+ try std .testing .expect (std .mem .indexOf (u8 , output , "input" ) != null );
1249+ try std .testing .expect (std .mem .indexOf (u8 , output , "output-file" ) != null );
1250+ try std .testing .expect (std .mem .indexOf (u8 , output , "(required)" ) != null );
1251+ try std .testing .expect (std .mem .indexOf (u8 , output , "(optional)" ) != null );
1252+ }
1253+
1254+ test "help: printUsageLine produces correct output" {
1255+ const allocator = std .testing .allocator ;
1256+ var cmd = try Command .init (allocator , .{ .name = "app" , .description = "" , .exec = dummyExec });
1257+ defer cmd .deinit ();
1258+
1259+ try cmd .addPositional (.{ .name = "file" , .description = "A file" , .is_required = true });
1260+
1261+ var buf : [2048 ]u8 = undefined ;
1262+ var writer = TestBufWriter { .buf = & buf };
1263+ try printUsageLine (cmd , & writer );
1264+
1265+ const output = writer .getWritten ();
1266+ try std .testing .expect (std .mem .indexOf (u8 , output , "app" ) != null );
1267+ try std .testing .expect (std .mem .indexOf (u8 , output , "<file>" ) != null );
1268+ try std .testing .expect (std .mem .indexOf (u8 , output , "[flags]" ) != null );
1269+ }
0 commit comments