mirror of
https://github.com/uutils/grep.git
synced 2026-06-10 16:15:11 -07:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8b93e94cb | ||
|
|
0326b43cb2 | ||
|
|
e925ff4ac8 | ||
|
|
f2f86ef572 | ||
|
|
265a562c26 | ||
|
|
bc61aece73 | ||
|
|
55a7b0180b | ||
|
|
d556159952 |
+32
-110
@@ -11,12 +11,11 @@ use rand::RngExt;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use rustix::io::dup;
|
||||
use rustix::io::read;
|
||||
use rustix::pipe::pipe;
|
||||
use rustix::stdio::{dup2_stderr, dup2_stdin, dup2_stdout};
|
||||
use std::env::temp_dir;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
use std::io::{Seek, SeekFrom, Write, pipe};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Once, atomic::AtomicBool};
|
||||
@@ -69,58 +68,18 @@ where
|
||||
F: FnOnce(std::vec::IntoIter<OsString>) -> i32 + Send + 'static,
|
||||
{
|
||||
// Duplicate the stdout and stderr file descriptors to restore later
|
||||
let original_stdout_fd_owned = match dup(std::io::stdout()) {
|
||||
Ok(fd) => fd,
|
||||
Err(_) => {
|
||||
return CommandResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "Failed to duplicate STDOUT_FILENO".to_string(),
|
||||
exit_code: -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let original_stderr_fd_owned = match dup(std::io::stderr()) {
|
||||
Ok(fd) => fd,
|
||||
Err(_) => {
|
||||
return CommandResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "Failed to duplicate STDERR_FILENO".to_string(),
|
||||
exit_code: -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
let original_stdout_fd_owned =
|
||||
dup(std::io::stdout()).expect("Failed to duplicate STDOUT_FILENO");
|
||||
let original_stderr_fd_owned =
|
||||
dup(std::io::stderr()).expect("Failed to duplicate STDERR_FILENO");
|
||||
|
||||
println!("Running test {:?}", &args[0..]);
|
||||
let (read_pipe_stdout, write_pipe_stdout) = match pipe() {
|
||||
Ok(fds) => fds,
|
||||
Err(_) => {
|
||||
return CommandResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "Failed to create pipes".to_string(),
|
||||
exit_code: -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
let (read_pipe_stderr, write_pipe_stderr) = match pipe() {
|
||||
Ok(fds) => fds,
|
||||
Err(_) => {
|
||||
return CommandResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "Failed to create pipes".to_string(),
|
||||
exit_code: -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
let (read_pipe_stdout, write_pipe_stdout) = pipe().expect("Failed to create pipes");
|
||||
let (read_pipe_stderr, write_pipe_stderr) = pipe().expect("Failed to create pipes");
|
||||
|
||||
// Redirect stdout and stderr to their respective pipes
|
||||
if dup2_stdout(&write_pipe_stdout).is_err() || dup2_stderr(&write_pipe_stderr).is_err() {
|
||||
return CommandResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "Failed to redirect STDOUT_FILENO or STDERR_FILENO".to_string(),
|
||||
exit_code: -1,
|
||||
};
|
||||
}
|
||||
dup2_stdout(&write_pipe_stdout).expect("Failed to redirect STDOUT_FILENO");
|
||||
dup2_stderr(&write_pipe_stderr).expect("Failed to redirect STDERR_FILENO");
|
||||
|
||||
// Handle stdin redirection if needed
|
||||
let original_stdin_fd_owned = if let Some(input_str) = pipe_input {
|
||||
@@ -130,25 +89,10 @@ where
|
||||
input_file.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
// Redirect stdin to read from the in-memory file
|
||||
let stdin_fd = match dup(std::io::stdin()) {
|
||||
Ok(fd) => fd,
|
||||
Err(_) => {
|
||||
return CommandResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "Failed to duplicate STDIN".to_string(),
|
||||
exit_code: -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
let stdin_fd = dup(std::io::stdin()).expect("Failed to duplicate STDIN");
|
||||
|
||||
// Redirect stdin to read from the in-memory file
|
||||
if dup2_stdin(&input_file).is_err() {
|
||||
return CommandResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "Failed to set up stdin redirection".to_string(),
|
||||
exit_code: -1,
|
||||
};
|
||||
}
|
||||
dup2_stdin(&input_file).expect("Failed to set up stdin redirection");
|
||||
|
||||
Some(stdin_fd)
|
||||
} else {
|
||||
@@ -176,14 +120,8 @@ where
|
||||
});
|
||||
|
||||
// Restore the original stdin if it was modified
|
||||
if let Some(fd) = original_stdin_fd_owned
|
||||
&& dup2_stdin(&fd).is_err()
|
||||
{
|
||||
return CommandResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "Failed to restore the original STDIN".to_string(),
|
||||
exit_code: -1,
|
||||
};
|
||||
if let Some(fd) = original_stdin_fd_owned {
|
||||
dup2_stdin(&fd).expect("Failed to restore the original STDIN");
|
||||
}
|
||||
|
||||
CommandResult {
|
||||
@@ -226,18 +164,14 @@ pub fn run_gnu_cmd(
|
||||
check_gnu: bool,
|
||||
pipe_input: Option<&str>,
|
||||
) -> Result<CommandResult, CommandResult> {
|
||||
if check_gnu {
|
||||
match is_gnu_cmd(cmd_path) {
|
||||
Ok(_) => {} // if the check passes, do nothing
|
||||
Err(e) => {
|
||||
// Convert the io::Error into the function's error type
|
||||
return Err(CommandResult {
|
||||
stdout: String::new(),
|
||||
stderr: e.to_string(),
|
||||
exit_code: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
// if the check passes, do nothing
|
||||
if check_gnu && let Err(e) = is_gnu_cmd(cmd_path) {
|
||||
// Convert the io::Error into the function's error type
|
||||
return Err(CommandResult {
|
||||
stdout: String::new(),
|
||||
stderr: e.to_string(),
|
||||
exit_code: -1,
|
||||
});
|
||||
}
|
||||
|
||||
let mut command = Command::new(cmd_path);
|
||||
@@ -505,12 +439,9 @@ mod tests {
|
||||
let result = run_gnu_cmd("echo", &args, false, None);
|
||||
|
||||
// Should succeed (echo --version might not be standard but echo should exist)
|
||||
match result {
|
||||
Ok(_) => {} // Command succeeded
|
||||
Err(err_result) => {
|
||||
// Command failed but at least ran
|
||||
assert_ne!(err_result.exit_code, -1); // -1 would indicate the command couldn't be found
|
||||
}
|
||||
if let Err(e) = result {
|
||||
// Command failed but at least ran
|
||||
assert_ne!(e.exit_code, -1); // -1 would indicate the command couldn't be found
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,29 +450,20 @@ mod tests {
|
||||
let args: Vec<OsString> = vec![];
|
||||
let pipe_input = "hello world";
|
||||
let result = run_gnu_cmd("cat", &args, false, Some(pipe_input));
|
||||
|
||||
match result {
|
||||
Ok(cmd_result) => {
|
||||
assert_eq!(cmd_result.stdout.trim(), "hello world");
|
||||
}
|
||||
Err(_) => {
|
||||
// cat might not be available in test environment, that's ok
|
||||
}
|
||||
// cat might not be available in test environment, that's ok
|
||||
if let Ok(cmd_result) = result {
|
||||
assert_eq!(cmd_result.stdout.trim(), "hello world");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_random_file() {
|
||||
let result = generate_random_file();
|
||||
match result {
|
||||
Ok(file_path) => {
|
||||
assert!(!file_path.is_empty());
|
||||
// Clean up - try to remove the file
|
||||
let _ = std::fs::remove_file(&file_path);
|
||||
}
|
||||
Err(_) => {
|
||||
// File creation might fail due to permissions, that's acceptable for this test
|
||||
}
|
||||
// File creation might fail due to permissions, that's acceptable for this test
|
||||
if let Ok(path) = result {
|
||||
assert!(!path.is_empty());
|
||||
// Clean up - try to remove the file
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,10 @@ impl CompiledPattern {
|
||||
// GNU grep supports `{,n}` as an alias for `{0,n}`.
|
||||
syntax.enable_behavior(SyntaxBehavior::SYNTAX_BEHAVIOR_ALLOW_INTERVAL_LOW_ABBREV);
|
||||
}
|
||||
if matches!(config.regex_mode, RegexMode::Basic | RegexMode::Extended) {
|
||||
// GNU grep supports \` and \' as buffer anchors in BRE and ERE.
|
||||
syntax.enable_operators(SyntaxOperator::SYNTAX_OPERATOR_ESC_GNU_BUF_ANCHOR);
|
||||
}
|
||||
if config.regex_mode == RegexMode::Perl {
|
||||
// GNU grep supports `(?P<name>...)`.
|
||||
// Unfortunately, the onig crate defines the OP2 flag without the
|
||||
@@ -285,6 +289,12 @@ impl CompiledPattern {
|
||||
if config.ignore_case {
|
||||
options |= RegexOptions::REGEX_OPTION_IGNORECASE;
|
||||
}
|
||||
// In GNU grep's Basic/Extended modes, `-z` makes newline ordinary data
|
||||
// for `.`, but PCRE keeps its existing non-DOTALL behavior. The GNU
|
||||
// `pcre-context` test documents this as current behavior until PCRE2.
|
||||
if config.null_data && matches!(config.regex_mode, RegexMode::Basic | RegexMode::Extended) {
|
||||
options |= RegexOptions::REGEX_OPTION_MULTILINE;
|
||||
}
|
||||
|
||||
fn compile_with(pattern: &str, syntax: &Syntax, options: RegexOptions) -> UResult<Regex> {
|
||||
Regex::with_options_and_encoding(pattern, options, syntax).map_err(|err| {
|
||||
|
||||
+13
-9
@@ -643,15 +643,19 @@ impl<'a> Searcher<'a> {
|
||||
|
||||
/// End-of-file bookkeeping: count / `-L` / binary notice.
|
||||
fn session_finalize(&mut self, path: &Path) -> io::Result<bool> {
|
||||
if self.config.count && !self.config.files_with_matches && !self.config.files_without_match
|
||||
{
|
||||
self.writer.write_count(self.session_match_count, path)?;
|
||||
}
|
||||
if self.config.files_without_match && !self.session_any_match() {
|
||||
self.writer.write_filename(path)?;
|
||||
}
|
||||
if self.session_should_emit_binary_notice() {
|
||||
self.writer.report_binary_match(path);
|
||||
if !self.config.quiet {
|
||||
if self.config.count
|
||||
&& !self.config.files_with_matches
|
||||
&& !self.config.files_without_match
|
||||
{
|
||||
self.writer.write_count(self.session_match_count, path)?;
|
||||
}
|
||||
if self.config.files_without_match && !self.session_any_match() {
|
||||
self.writer.write_filename(path)?;
|
||||
}
|
||||
if self.session_should_emit_binary_notice() {
|
||||
self.writer.report_binary_match(path);
|
||||
}
|
||||
}
|
||||
Ok(self.session_any_match())
|
||||
}
|
||||
|
||||
+67
-7
@@ -85,6 +85,21 @@ fn bre_gnu_extensions() {
|
||||
.stdout_only("*foo\n**foo\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gnu_buffer_anchors() {
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&[r"\`c\|r\'"])
|
||||
.pipe_in("cat\nscat\ntar\ndog\n")
|
||||
.succeeds()
|
||||
.stdout_only("cat\ntar\n");
|
||||
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-E", r"\`c|r\'"])
|
||||
.pipe_in("cat\nscat\ntar\ndog\n")
|
||||
.succeeds()
|
||||
.stdout_only("cat\ntar\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ere_metacharacters() {
|
||||
let cases: &[(&[&str], &str, &str)] = &[
|
||||
@@ -587,6 +602,23 @@ fn only_matching() {
|
||||
.succeeds()
|
||||
.stdout_only("Hello\nhELLO\nHELLO\n");
|
||||
|
||||
// Zero-width matches do not print in -o mode, but still mark the line as
|
||||
// matched. This matters for -v because matched lines must not be selected.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-o", "$"]).pipe_in("\n").succeeds().no_output();
|
||||
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-o", "-v", "$"])
|
||||
.pipe_in("a\n\nb\n")
|
||||
.fails_with_code(1)
|
||||
.no_output();
|
||||
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-o", "-v", "x*"])
|
||||
.pipe_in("a\n\nb\n")
|
||||
.fails_with_code(1)
|
||||
.no_output();
|
||||
|
||||
// After a match ends, ^ must not re-match at that position.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-o", "^hello*"])
|
||||
@@ -607,6 +639,20 @@ fn quiet_modes() {
|
||||
.pipe_in("a\n")
|
||||
.fails_with_code(1)
|
||||
.no_output();
|
||||
|
||||
// Quiet mode suppresses EOF bookkeeping output from -c and -L even when
|
||||
// the no-match path reaches finalization.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-q", "-c", "z"])
|
||||
.pipe_in("a\n")
|
||||
.fails_with_code(1)
|
||||
.no_output();
|
||||
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-q", "-L", "z"])
|
||||
.pipe_in("a\n")
|
||||
.fails_with_code(1)
|
||||
.no_output();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1264,6 +1310,27 @@ fn null_data_mode_records() {
|
||||
.succeeds()
|
||||
.stdout_is_bytes(b"hello\0");
|
||||
|
||||
// With NUL-delimited records, newline is ordinary data and `.` matches it.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-z", "-o", "."])
|
||||
.pipe_in(&b"a\nb"[..])
|
||||
.succeeds()
|
||||
.stdout_is_bytes(b"a\0\n\0b\0");
|
||||
|
||||
// GNU grep's PCRE path currently does not let `.*` consume the extra
|
||||
// newline here under -z; this mirrors the GNU pcre-context test.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-P", "-z", "-o", r"(?<=\n\n\n).*"])
|
||||
.pipe_in(
|
||||
&b"NUL preceded by 0 empty lines.\0\
|
||||
\nNUL preceded by 1 empty line.\0\
|
||||
\n\nNUL preceded by 2 empty lines.\0\
|
||||
\n\n\nNUL preceded by 3 empty lines.\0\
|
||||
\n\n\n\nNUL preceded by 4 empty lines.\0\n"[..],
|
||||
)
|
||||
.succeeds()
|
||||
.stdout_is_bytes(b"NUL preceded by 3 empty lines.\0NUL preceded by 4 empty lines.\0");
|
||||
|
||||
// Counting works under -z.
|
||||
let (_s, mut c) = ucmd();
|
||||
c.args(&["-z", "-c", "hello"])
|
||||
@@ -1478,13 +1545,6 @@ fn slow_path_binary_handling() {
|
||||
.succeeds()
|
||||
.stdout_is_bytes(b"hit\0\n");
|
||||
|
||||
// --binary-files=without-match bails out on an invalid-UTF-8 match.
|
||||
scene
|
||||
.cmd(env!("CARGO_BIN_EXE_grep"))
|
||||
.args(&["--binary-files=without-match", "[a]", "bad"])
|
||||
.fails_with_code(1)
|
||||
.no_output();
|
||||
|
||||
// A NUL after the matched line means binariness is discovered at EOF, so
|
||||
// the line is printed first and the notice is emitted during finalization.
|
||||
scene.fixtures.write_bytes("late", b"hit\nno\0\n");
|
||||
|
||||
Reference in New Issue
Block a user