Skip to content

Commit 0704f58

Browse files
devnulledkylehgc
authored andcommitted
fix(grep): stop -l/-m/-t shadowing native grep flags
The Grep variant's own short options -l (max_len), -m (max) and -t (file_type) shadowed native grep/rg flags of the same letter. A leading `grep -l PATTERN` rewritten by the hook to `rtk grep -l PATTERN` made clap parse the pattern as a usize and abort (exit 2 -> raw fallback, zero token savings); -m and -t silently applied the wrong semantics. Make these tuning options long-only (--max-len/--max/--file-type) so the native flags route to extra_args and are parsed by grep_cmd.rs, which already handles them. Completes the -v/-n/pattern/path cleanup from 84616d1; complements the rg-only-flag handling tracked in rtk-ai#2224. Add clap-layer tests for the fix plus characterization tests for the previously-untested routing (combined -rn clusters, -v invert, -nE alternation, long-option binding, the trailing_var_arg ordering gotcha, --version passthrough and -- handling). Refs: rtk-ai#2614, rtk-ai#1604, rtk-ai#1436
1 parent 62430db commit 0704f58

1 file changed

Lines changed: 188 additions & 3 deletions

File tree

‎src/main.rs‎

Lines changed: 188 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)