4 changed files with 122 additions and 126 deletions
+32 -110
View File
@@ -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);
}
}
}
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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");