From c50c0458cb3cfc6c5661c8f348c8a0fb474af6a3 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 29 May 2026 22:21:27 +0200 Subject: [PATCH] fix: accept repeated options like GNU grep (#2) GNU grep tolerates options given more than once (boolean flags are idempotent, value options take the last occurrence), but clap rejected them with "cannot be used multiple times" (exit 2). Set args_override_self(true) so clap replaces rather than errors; args with ArgAction::Append (-e/-f/--include/--exclude) still accumulate. Found by the differential fuzzer (e.g. `grep -e .* -E -n -o -n -x`). Add a regression test covering repeated booleans, repeated value options (last wins), and that -e still accumulates patterns. --- src/lib.rs | 4 ++++ tests/test_grep.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b2b75bd..b915ae0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -421,6 +421,10 @@ pub fn uu_app() -> Command { .about("Search for PATTERNS in each FILE.") .disable_help_flag(true) .disable_version_flag(true) + // GNU grep accepts repeated options (booleans are idempotent, value + // options take the last); make clap replace rather than error. Args + // with ArgAction::Append (e.g. -e/-f/--include) still accumulate. + .args_override_self(true) .after_help( "When FILE is '-', read standard input. If no FILE is given, read standard \ input, but with -r, recursively search the working directory instead. With \ diff --git a/tests/test_grep.rs b/tests/test_grep.rs index 573ca03..df629f2 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -1138,3 +1138,38 @@ fn help_and_version() { .succeeds() .stdout_contains(env!("CARGO_PKG_VERSION")); } + +#[test] +fn repeated_options_are_accepted() { + // GNU grep tolerates options given more than once: boolean flags are + // idempotent and value options take the last occurrence. clap would + // otherwise error with "cannot be used multiple times". + + // Repeated boolean flags are a no-op (not an error). + let (_s, mut c) = ucmd(); + c.args(&["-n", "-n", "a"]) + .pipe_in("abc\n") + .succeeds() + .stdout_only("1:abc\n"); + + // Mixed repeated booleans behave like a single occurrence. + let (_s, mut c) = ucmd(); + c.args(&["-i", "-i", "abc"]) + .pipe_in("ABC\n") + .succeeds() + .stdout_only("ABC\n"); + + // Repeated value options take the last value (here: -m 1 wins). + let (_s, mut c) = ucmd(); + c.args(&["-m", "5", "-m", "1", "x"]) + .pipe_in("x\nx\nx\n") + .succeeds() + .stdout_only("x\n"); + + // -e (ArgAction::Append) must still accumulate every pattern. + let (_s, mut c) = ucmd(); + c.args(&["-e", "a", "-e", "b"]) + .pipe_in("a\nb\nc\n") + .succeeds() + .stdout_only("a\nb\n"); +}