From d7681f5558904ef13988096aa9a391697f1d82d0 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 8 Aug 2026 23:44:58 +0200 Subject: [PATCH 1/2] grep: exit 2 when a -f or --exclude-from file cannot be read --- src/lib.rs | 15 +++++++++++---- tests/test_grep.rs | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fbf25c3..2d05fda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::{OsStr, OsString}; use std::io::{IsTerminal as _, Read}; use std::path::Path; -use uucore::error::{ExitCode, FromIo, UResult, USimpleError}; +use uucore::error::{ExitCode, UError, UResult, USimpleError, strip_errno}; use uucore::show_warning; #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -128,6 +128,13 @@ pub struct Config<'a> { pub color_config: ColorConfig<'a>, } +/// A file named by `-f` or `--exclude-from` that cannot be read is a usage +/// error for GNU grep: it exits 2, unlike the exit 1 of a search that found +/// nothing. +fn read_error(label: impl std::fmt::Display, err: &std::io::Error) -> Box { + USimpleError::new(2, format!("{label}: {}", strip_errno(err))) +} + #[uucore::main(no_signals)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = expand_num_shorthand(args); @@ -236,10 +243,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut buf = String::new(); std::io::stdin() .read_to_string(&mut buf) - .map_err_context(|| "(standard input)".to_string())?; + .map_err(|err| read_error("(standard input)", &err))?; buf } else { - std::fs::read_to_string(path).map_err_context(|| path.to_string())? + std::fs::read_to_string(path).map_err(|err| read_error(path, &err))? }; pattern_strings.push(contents); } @@ -342,7 +349,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { patterns.add(pattern)?; } for path in exclude_from { - let contents = std::fs::read_to_string(path).map_err_context(|| path.to_string())?; + let contents = std::fs::read_to_string(path).map_err(|err| read_error(path, &err))?; for line in contents.lines() { let trimmed = line.trim(); if !trimmed.is_empty() { diff --git a/tests/test_grep.rs b/tests/test_grep.rs index a5a2e22..95b244d 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -153,6 +153,30 @@ fn reversed_range_endpoints_match_gnu_message() { } } +#[test] +fn unreadable_pattern_file_is_error() { + // An unreadable -f / --exclude-from file is a usage error (exit 2), not the + // exit 1 that means "no match". + let (scenario, mut c) = ucmd(); + scenario.fixtures.mkdir("adir"); + c.args(&["-f", "adir"]) + .pipe_in("hello\n") + .fails_with_code(2) + .stderr_contains("adir: Is a directory"); + + let (_s, mut c) = ucmd(); + c.args(&["-f", "no-such-pattern-file"]) + .pipe_in("hello\n") + .fails_with_code(2) + .stderr_contains("no-such-pattern-file: No such file or directory"); + + let (_s, mut c) = ucmd(); + c.args(&["--exclude-from=no-such-pattern-file", "hello"]) + .pipe_in("hello\n") + .fails_with_code(2) + .stderr_contains("no-such-pattern-file: No such file or directory"); +} + #[test] fn quiet_match_overrides_file_error() { // With -q, a match makes grep exit 0 even if an earlier file could not be From 51b233d8ada61043706c1fe12c7e5ceb20af2234 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 9 Aug 2026 00:11:09 +0200 Subject: [PATCH 2/2] grep: make the unreadable pattern file test reliable and portable --- tests/test_grep.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_grep.rs b/tests/test_grep.rs index 95b244d..5e51448 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -156,25 +156,41 @@ fn reversed_range_endpoints_match_gnu_message() { #[test] fn unreadable_pattern_file_is_error() { // An unreadable -f / --exclude-from file is a usage error (exit 2), not the - // exit 1 that means "no match". + // exit 1 that means "no match". Only the label is ours: the text after it + // is the operating system's, so match it on Unix alone (Windows says + // "Access is denied." for a directory). + #[cfg(unix)] + const DIRECTORY: &str = "adir: Is a directory"; + #[cfg(not(unix))] + const DIRECTORY: &str = "adir: "; + #[cfg(unix)] + const MISSING: &str = "no-such-pattern-file: No such file or directory"; + #[cfg(not(unix))] + const MISSING: &str = "no-such-pattern-file: "; + + // The pattern file is read before any input, so grep exits without draining + // stdin and the harness's write can lose the race with EPIPE. let (scenario, mut c) = ucmd(); scenario.fixtures.mkdir("adir"); c.args(&["-f", "adir"]) .pipe_in("hello\n") + .ignore_stdin_write_error() .fails_with_code(2) - .stderr_contains("adir: Is a directory"); + .stderr_contains(DIRECTORY); let (_s, mut c) = ucmd(); c.args(&["-f", "no-such-pattern-file"]) .pipe_in("hello\n") + .ignore_stdin_write_error() .fails_with_code(2) - .stderr_contains("no-such-pattern-file: No such file or directory"); + .stderr_contains(MISSING); let (_s, mut c) = ucmd(); c.args(&["--exclude-from=no-such-pattern-file", "hello"]) .pipe_in("hello\n") + .ignore_stdin_write_error() .fails_with_code(2) - .stderr_contains("no-such-pattern-file: No such file or directory"); + .stderr_contains(MISSING); } #[test]