@@ -153,21 +153,33 @@ pub const Command = struct {
153153 var arg_iterator = parser .ArgIterator .init (user_args );
154154
155155 var current_cmd : * Command = self ;
156+ out_failed_cmd .* = current_cmd ;
157+
158+ // Reset root command state for re-entering.
159+ current_cmd .parsed_flags .shrinkRetainingCapacity (0 );
160+ current_cmd .parsed_positionals .shrinkRetainingCapacity (0 );
161+
162+ // Resolve the subcommand chain, parsing flags at each level.
163+ // Flags before a subcommand name are stored on the command at that level.
156164 while (arg_iterator .peek ()) | arg | {
157- if (std .mem .startsWith (u8 , arg , "-" )) break ;
165+ if (std .mem .eql (u8 , arg , "--" )) break ;
166+ if (std .mem .startsWith (u8 , arg , "-" )) {
167+ try parser .parseFlagsOnly (current_cmd , & arg_iterator );
168+ continue ;
169+ }
158170 if (current_cmd .findSubcommand (arg )) | found_sub | {
159171 current_cmd = found_sub ;
172+ out_failed_cmd .* = current_cmd ;
173+ // Reset subcommand state for re-entrancy.
174+ current_cmd .parsed_flags .shrinkRetainingCapacity (0 );
175+ current_cmd .parsed_positionals .shrinkRetainingCapacity (0 );
160176 arg_iterator .next ();
161177 } else {
162178 break ;
163179 }
164180 }
165- out_failed_cmd .* = current_cmd ;
166-
167- // Reset state from any previous run, making the command re-entrant.
168- current_cmd .parsed_flags .shrinkRetainingCapacity (0 );
169- current_cmd .parsed_positionals .shrinkRetainingCapacity (0 );
170181
182+ // Parse remaining flags and positional arguments for the final resolved command.
171183 try parser .parseArgsAndFlags (current_cmd , & arg_iterator );
172184
173185 // Check for --help and --version flags BEFORE validation
@@ -344,10 +356,16 @@ pub const Command = struct {
344356 return null ;
345357 }
346358
347- /// (Internal) Retrieves the parsed value of a flag for the current command.
359+ /// (Internal) Retrieves the parsed value of a flag, searching upwards through
360+ /// parent commands. This mirrors `findFlag` and allows subcommand exec functions
361+ /// to access flags that were parsed at a parent command level.
348362 pub fn getFlagValue (self : * const Command , name : []const u8 ) ? types.FlagValue {
349- for (self .parsed_flags .items ) | flag | {
350- if (std .mem .eql (u8 , flag .name , name )) return flag .value ;
363+ var current : ? * const Command = self ;
364+ while (current ) | cmd | {
365+ for (cmd .parsed_flags .items ) | flag | {
366+ if (std .mem .eql (u8 , flag .name , name )) return flag .value ;
367+ }
368+ current = cmd .parent ;
351369 }
352370 return null ;
353371 }
@@ -443,6 +461,114 @@ test "command: execute with args and flags" {
443461 try testing .expectEqualStrings ("input.txt" , integration_arg_val );
444462}
445463
464+ // -- Tests for flags before subcommand (issue #12) --
465+
466+ var parent_flag_from_sub : []const u8 = "" ;
467+
468+ fn parentFlagExec (ctx : context.CommandContext ) ! void {
469+ parent_flag_from_sub = try ctx .getFlag ("config" , []const u8 );
470+ integration_arg_val = try ctx .getArg ("file" , []const u8 );
471+ }
472+
473+ test "command: root flag before subcommand resolves subcommand" {
474+ const allocator = testing .allocator ;
475+ var root = try Command .init (allocator , .{ .name = "app" , .description = "" , .exec = dummyExec });
476+ defer root .deinit ();
477+
478+ try root .addFlag (.{ .name = "config" , .type = .String , .default_value = .{ .String = "default.conf" }, .description = "" });
479+
480+ var sub = try Command .init (allocator , .{ .name = "run" , .description = "" , .exec = parentFlagExec });
481+ try root .addSubcommand (sub );
482+ try sub .addPositional (.{ .name = "file" , .is_required = true , .description = "" });
483+
484+ // The exact pattern from the bug report: --config <value> run <arg>
485+ var failed_cmd : ? * const Command = null ;
486+ const args = &[_ ][]const u8 { "--config" , "custom.conf" , "run" , "input.txt" };
487+ try root .execute (args , null , & failed_cmd );
488+
489+ try testing .expect (failed_cmd == null );
490+ try testing .expectEqualStrings ("input.txt" , integration_arg_val );
491+ }
492+
493+ test "command: root short flag before subcommand resolves subcommand" {
494+ const allocator = testing .allocator ;
495+ var root = try Command .init (allocator , .{ .name = "app" , .description = "" , .exec = dummyExec });
496+ defer root .deinit ();
497+
498+ try root .addFlag (.{ .name = "verbose" , .shortcut = 'v' , .type = .Bool , .default_value = .{ .Bool = false }, .description = "" });
499+
500+ exec_called_on = null ;
501+ const sub = try Command .init (allocator , .{ .name = "run" , .description = "" , .exec = trackingExec });
502+ try root .addSubcommand (sub );
503+
504+ var failed_cmd : ? * const Command = null ;
505+ const args = &[_ ][]const u8 { "-v" , "run" };
506+ try root .execute (args , null , & failed_cmd );
507+
508+ try testing .expect (failed_cmd == null );
509+ try testing .expectEqualStrings ("run" , exec_called_on .? );
510+ }
511+
512+ test "command: multiple root flags before subcommand" {
513+ const allocator = testing .allocator ;
514+ var root = try Command .init (allocator , .{ .name = "app" , .description = "" , .exec = dummyExec });
515+ defer root .deinit ();
516+
517+ try root .addFlag (.{ .name = "verbose" , .shortcut = 'v' , .type = .Bool , .default_value = .{ .Bool = false }, .description = "" });
518+ try root .addFlag (.{ .name = "config" , .type = .String , .default_value = .{ .String = "default.conf" }, .description = "" });
519+
520+ exec_called_on = null ;
521+ const sub = try Command .init (allocator , .{ .name = "run" , .description = "" , .exec = trackingExec });
522+ try root .addSubcommand (sub );
523+
524+ var failed_cmd : ? * const Command = null ;
525+ const args = &[_ ][]const u8 { "-v" , "--config=custom.conf" , "run" };
526+ try root .execute (args , null , & failed_cmd );
527+
528+ try testing .expect (failed_cmd == null );
529+ try testing .expectEqualStrings ("run" , exec_called_on .? );
530+ }
531+
532+ test "command: getFlagValue traverses parents" {
533+ const allocator = testing .allocator ;
534+ var root = try Command .init (allocator , .{ .name = "app" , .description = "" , .exec = dummyExec });
535+ defer root .deinit ();
536+
537+ try root .addFlag (.{ .name = "config" , .type = .String , .default_value = .{ .String = "default.conf" }, .description = "" });
538+
539+ var sub = try Command .init (allocator , .{ .name = "run" , .description = "" , .exec = parentFlagExec });
540+ try root .addSubcommand (sub );
541+ try sub .addPositional (.{ .name = "file" , .is_required = true , .description = "" });
542+
543+ parent_flag_from_sub = "" ;
544+ var failed_cmd : ? * const Command = null ;
545+ const args = &[_ ][]const u8 { "--config" , "custom.conf" , "run" , "input.txt" };
546+ try root .execute (args , null , & failed_cmd );
547+
548+ try testing .expect (failed_cmd == null );
549+ // The subcommand's exec must see the root-level --config value, not the default
550+ try testing .expectEqualStrings ("custom.conf" , parent_flag_from_sub );
551+ }
552+
553+ test "command: -- before subcommand stops resolution" {
554+ const allocator = testing .allocator ;
555+ var root = try Command .init (allocator , .{ .name = "app" , .description = "" , .exec = dummyExec });
556+ defer root .deinit ();
557+
558+ exec_called_on = null ;
559+ const sub = try Command .init (allocator , .{ .name = "run" , .description = "" , .exec = trackingExec });
560+ try root .addSubcommand (sub );
561+ try root .addPositional (.{ .name = "arg" , .is_required = true , .description = "" });
562+
563+ var failed_cmd : ? * const Command = null ;
564+ // -- stops subcommand resolution, so "run" becomes a positional for root
565+ const args = &[_ ][]const u8 { "--" , "run" };
566+ try root .execute (args , null , & failed_cmd );
567+
568+ try testing .expect (failed_cmd == null );
569+ try testing .expectEqualStrings ("app" , exec_called_on .? );
570+ }
571+
446572test "command: addSubcommand detects empty alias" {
447573 const allocator = std .testing .allocator ;
448574 var root = try Command .init (allocator , .{ .name = "root" , .description = "" , .exec = dummyExec });
0 commit comments