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 febcb63..b477329 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -153,6 +153,46 @@ 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". 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(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(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(MISSING); +} + #[test] fn misspelled_character_class_is_error() { // `[:digit:]` is almost certainly a typo for `[[:digit:]]`; GNU rejects it