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.
This commit is contained in:
Sylvestre Ledru
2026-05-29 22:21:27 +02:00
committed by GitHub
parent f7813500c9
commit c50c0458cb
2 changed files with 39 additions and 0 deletions
+4
View File
@@ -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 \
+35
View File
@@ -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");
}