@@ -311,17 +311,25 @@ enum Commands {
311311
312312 /// Compact grep - strips whitespace, truncates, groups by file
313313 Grep {
314+ // NOTE: these rtk-tuning options are intentionally long-only. Their
315+ // natural short forms (`-l`, `-m`, `-t`) collide with native grep/rg
316+ // flags of the same letter (files-with-matches, max-count, type) which
317+ // users routinely pass. A short option here shadows the native flag and
318+ // either crashes clap (`-l <pattern>` is not a `usize`) or silently
319+ // applies the wrong semantics, defeating the `extra_args` passthrough.
320+ // Keep these long-only so native flags reach grep_cmd.rs. (Completes the
321+ // `-v`/`-n`/pattern/path cleanup from 84616d1.)
314322 /// Max line length
315- #[ arg( short = 'l' , long, default_value = "80" ) ]
323+ #[ arg( long, default_value = "80" ) ]
316324 max_len : usize ,
317325 /// Max results to show
318- #[ arg( short , long, default_value = "200" ) ]
326+ #[ arg( long, default_value = "200" ) ]
319327 max : usize ,
320328 /// Show only match context (not full line)
321329 #[ arg( long) ]
322330 context_only : bool ,
323331 /// Filter by file type (e.g., ts, py, rust)
324- #[ arg( short = 't' , long) ]
332+ #[ arg( long) ]
325333 file_type : Option < String > ,
326334 /// Pattern, path, and any grep/rg flags (e.g. -v, -i, -A 3, --glob, --version)
327335 #[ arg( trailing_var_arg = true , allow_hyphen_values = true ) ]
@@ -3671,4 +3679,181 @@ mod tests {
36713679 _ => panic ! ( "Expected Init command" ) ,
36723680 }
36733681 }
3682+
3683+ // --- grep argument routing (clap layer) ---
3684+ //
3685+ // The `Grep` variant funnels the pattern, path, and every native grep/rg
3686+ // flag into a single `trailing_var_arg` slot so `src/cmds/system/grep_cmd.rs`
3687+ // can parse them with full grep/rg semantics. Commit 84616d1
3688+ // ("fix(grep): stabilize argument parsing") moved to this design but left
3689+ // the struct's own short options `-l`/`-m`/`-t` in place, where they still
3690+ // shadow native flags of the same letter. These tests pin the routing.
3691+
3692+ /// Parse `rtk grep …` and return the captured `extra_args`, or `None` if
3693+ /// clap rejects the invocation (e.g. a colliding short option mis-binds).
3694+ fn grep_extra_args ( args : & [ & str ] ) -> Option < Vec < String > > {
3695+ match Cli :: try_parse_from ( args) . ok ( ) ?. command {
3696+ Commands :: Grep { extra_args, .. } => Some ( extra_args) ,
3697+ _ => None ,
3698+ }
3699+ }
3700+
3701+ // Characterization: behavior that must be PRESERVED (passes today, untested
3702+ // until now). Guards the 84616d1 design against regression.
3703+
3704+ #[ test]
3705+ fn test_grep_parse_simple_pattern_path ( ) {
3706+ assert_eq ! (
3707+ grep_extra_args( & [ "rtk" , "grep" , "FOO" , "src/" ] ) . unwrap( ) ,
3708+ vec![ "FOO" , "src/" ]
3709+ ) ;
3710+ }
3711+
3712+ #[ test]
3713+ fn test_grep_parse_combined_short_cluster ( ) {
3714+ // `-rn` is a native grep cluster (recursive + line-numbers); it must
3715+ // reach grep_cmd.rs intact, not be intercepted by clap.
3716+ assert_eq ! (
3717+ grep_extra_args( & [ "rtk" , "grep" , "-rn" , "FOO" , "src/" ] ) . unwrap( ) ,
3718+ vec![ "-rn" , "FOO" , "src/" ]
3719+ ) ;
3720+ }
3721+
3722+ #[ test]
3723+ fn test_grep_parse_value_flag_after_context ( ) {
3724+ // `-A 3` (after-context) — the value must not be stolen by clap.
3725+ assert_eq ! (
3726+ grep_extra_args( & [ "rtk" , "grep" , "-A" , "3" , "FOO" , "file" ] ) . unwrap( ) ,
3727+ vec![ "-A" , "3" , "FOO" , "file" ]
3728+ ) ;
3729+ }
3730+
3731+ #[ test]
3732+ fn test_grep_parse_invert_match_v_not_shadowed ( ) {
3733+ // Regression guard for 84616d1: `-v` (invert-match) must reach grep,
3734+ // not be captured as rtk's top-level verbose flag.
3735+ assert_eq ! (
3736+ grep_extra_args( & [ "rtk" , "grep" , "-v" , "FOO" , "file" ] ) . unwrap( ) ,
3737+ vec![ "-v" , "FOO" , "file" ]
3738+ ) ;
3739+ }
3740+
3741+ #[ test]
3742+ fn test_grep_parse_alternation_pattern_intact ( ) {
3743+ // Extended-regex alternation must pass through untouched (#1436 lineage).
3744+ assert_eq ! (
3745+ grep_extra_args( & [ "rtk" , "grep" , "-nE" , "a|b" , "file" ] ) . unwrap( ) ,
3746+ vec![ "-nE" , "a|b" , "file" ]
3747+ ) ;
3748+ }
3749+
3750+ // The fix: native `-l`/`-m`/`-t` must route to grep_cmd.rs, not be captured
3751+ // by the struct's own short options (max_len / max / file_type). These FAIL
3752+ // before the fix — `-l` mis-binds to `max_len: usize` and clap rejects the
3753+ // pattern; `-m`/`-t` silently swallow their value with the wrong semantics.
3754+
3755+ #[ test]
3756+ fn test_grep_parse_files_with_matches_l ( ) {
3757+ // Native grep `-l` (files-with-matches). Must parse and pass `-l` through.
3758+ assert_eq ! (
3759+ grep_extra_args( & [ "rtk" , "grep" , "-l" , "FOO" , "src/" ] ) . unwrap( ) ,
3760+ vec![ "-l" , "FOO" , "src/" ]
3761+ ) ;
3762+ }
3763+
3764+ #[ test]
3765+ fn test_grep_parse_max_count_m_forwarded ( ) {
3766+ // Native grep `-m N` (max-count). The value `5` must reach grep_cmd.rs,
3767+ // not be eaten as rtk's `--max`.
3768+ assert_eq ! (
3769+ grep_extra_args( & [ "rtk" , "grep" , "-m" , "5" , "FOO" , "file" ] ) . unwrap( ) ,
3770+ vec![ "-m" , "5" , "FOO" , "file" ]
3771+ ) ;
3772+ }
3773+
3774+ #[ test]
3775+ fn test_grep_parse_type_t_forwarded ( ) {
3776+ // ripgrep `-t TYPE` (type filter). Must pass through to grep_cmd.rs,
3777+ // which already handles it, instead of binding to rtk's `--file-type`.
3778+ assert_eq ! (
3779+ grep_extra_args( & [ "rtk" , "grep" , "-t" , "rust" , "FOO" , "src/" ] ) . unwrap( ) ,
3780+ vec![ "-t" , "rust" , "FOO" , "src/" ]
3781+ ) ;
3782+ }
3783+
3784+ // --- additional grep characterization (preserved behavior, was untested) ---
3785+
3786+ /// Parse `rtk grep …` into the full `Grep` field set for inspection.
3787+ #[ allow( clippy:: type_complexity) ]
3788+ fn parse_grep (
3789+ args : & [ & str ] ,
3790+ ) -> Result < ( usize , usize , bool , Option < String > , Vec < String > ) , clap:: Error > {
3791+ match Cli :: try_parse_from ( args) ?. command {
3792+ Commands :: Grep {
3793+ max_len,
3794+ max,
3795+ context_only,
3796+ file_type,
3797+ extra_args,
3798+ } => Ok ( ( max_len, max, context_only, file_type, extra_args) ) ,
3799+ _ => unreachable ! ( "parsed a grep command" ) ,
3800+ }
3801+ }
3802+
3803+ #[ test]
3804+ fn test_grep_long_options_bind_before_pattern ( ) {
3805+ // rtk's tuning knobs are long-only now (the short forms were removed to
3806+ // stop shadowing native grep flags). They must still bind when placed
3807+ // before the pattern, and only the pattern/path land in extra_args.
3808+ let ( max_len, max, context_only, file_type, extra_args) = parse_grep ( & [
3809+ "rtk" ,
3810+ "grep" ,
3811+ "--file-type" ,
3812+ "rust" ,
3813+ "--max-len" ,
3814+ "40" ,
3815+ "--max" ,
3816+ "5" ,
3817+ "--context-only" ,
3818+ "FOO" ,
3819+ "path" ,
3820+ ] )
3821+ . unwrap ( ) ;
3822+ assert_eq ! ( max_len, 40 ) ;
3823+ assert_eq ! ( max, 5 ) ;
3824+ assert ! ( context_only) ;
3825+ assert_eq ! ( file_type, Some ( "rust" . to_string( ) ) ) ;
3826+ assert_eq ! ( extra_args, vec![ "FOO" , "path" ] ) ;
3827+ }
3828+
3829+ #[ test]
3830+ fn test_grep_options_after_pattern_stay_in_extra_args ( ) {
3831+ // trailing_var_arg gotcha: once the pattern starts the trailing slot,
3832+ // later tokens — even rtk's own `--file-type` — are NOT parsed as
3833+ // options; they pass through verbatim. (Native grep ordering: flags
3834+ // come before the pattern.)
3835+ let ( _, _, _, file_type, extra_args) =
3836+ parse_grep ( & [ "rtk" , "grep" , "FOO" , "--file-type" , "rust" ] ) . unwrap ( ) ;
3837+ assert_eq ! ( file_type, None , "option after pattern must NOT bind" ) ;
3838+ assert_eq ! ( extra_args, vec![ "FOO" , "--file-type" , "rust" ] ) ;
3839+ }
3840+
3841+ #[ test]
3842+ fn test_grep_version_routes_to_extra_args ( ) {
3843+ // `--version` is not a subcommand flag (version isn't propagated), so it
3844+ // lands in extra_args and grep_cmd::run forwards it to rg --version.
3845+ assert_eq ! (
3846+ grep_extra_args( & [ "rtk" , "grep" , "--version" ] ) . unwrap( ) ,
3847+ vec![ "--version" ]
3848+ ) ;
3849+ }
3850+
3851+ #[ test]
3852+ fn test_grep_double_dash_consumed_by_clap ( ) {
3853+ // clap strips the `--` separator before populating trailing_var_arg.
3854+ // core::args_utils::restore_double_dash re-inserts it at runtime — this
3855+ // test pins the premise that helper depends on.
3856+ let ( _, _, _, _, extra_args) = parse_grep ( & [ "rtk" , "grep" , "--" , "-v" , "file" ] ) . unwrap ( ) ;
3857+ assert_eq ! ( extra_args, vec![ "-v" , "file" ] , "clap must consume the --" ) ;
3858+ }
36743859}
0 commit comments