Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions src/find/matchers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
use fs::FileSystemMatcher;
use ls::Ls;
use std::{
collections::HashMap,
error::Error,
fs::{File, Metadata},
path::{Path, PathBuf},
Expand Down Expand Up @@ -286,7 +287,8 @@ pub fn build_top_level_matcher(
args: &[&str],
config: &mut Config,
) -> Result<Box<dyn Matcher>, Box<dyn Error>> {
let (_, top_level_matcher) = (build_matcher_tree(args, config, 0, false))?;
let mut output_files = HashMap::new();
let (_, top_level_matcher) = (build_matcher_tree(args, config, &mut output_files, 0, false))?;

// if the matcher doesn't have any side-effects, then we default to printing
if !top_level_matcher.has_side_effects() {
Expand Down Expand Up @@ -442,11 +444,20 @@ fn parse_str_to_newer_args(input: &str) -> Option<(String, String)> {
}
}

/// Creates a file if it doesn't exist.
/// If it does exist, it will be overwritten.
fn get_or_create_file(path: &str) -> Result<File, Box<dyn Error>> {
/// Opens an output file once, truncating any existing contents, and returns a
/// handle that shares its cursor with the other actions targeting that path.
fn get_or_create_file(
path: &str,
output_files: &mut HashMap<PathBuf, File>,
) -> Result<File, Box<dyn Error>> {
if let Some(file) = output_files.get(Path::new(path)) {
return Ok(file.try_clone()?);
}

let file = File::create(path)?;
Ok(file)
let action_file = file.try_clone()?;
output_files.insert(PathBuf::from(path), file);
Ok(action_file)
}

/// The main "translate command-line args into a matcher" function. Will call
Expand All @@ -456,6 +467,7 @@ fn get_or_create_file(path: &str) -> Result<File, Box<dyn Error>> {
fn build_matcher_tree(
args: &[&str],
config: &mut Config,
output_files: &mut HashMap<PathBuf, File>,
arg_index: usize,
mut expecting_bracket: bool,
) -> Result<(usize, Box<dyn Matcher>), Box<dyn Error>> {
Expand Down Expand Up @@ -486,7 +498,7 @@ fn build_matcher_tree(
}
i += 1;

let file = get_or_create_file(args[i])?;
let file = get_or_create_file(args[i], output_files)?;
Some(Printer::new(PrintDelimiter::Newline, Some(file)).into_box())
}
"-fprintf" => {
Expand All @@ -498,7 +510,7 @@ fn build_matcher_tree(
// Args + 1: output file path
// Args + 2: format string
i += 1;
let file = get_or_create_file(args[i])?;
let file = get_or_create_file(args[i], output_files)?;
let output_path = PathBuf::from(args[i]);
i += 1;
Some(Printf::new(args[i], Some((file, output_path)))?.into_box())
Expand All @@ -509,7 +521,7 @@ fn build_matcher_tree(
}
i += 1;

let file = get_or_create_file(args[i])?;
let file = get_or_create_file(args[i], output_files)?;
Some(Printer::new(PrintDelimiter::Null, Some(file)).into_box())
}
"-ls" => Some(Ls::new(None).into_box()),
Expand All @@ -519,7 +531,7 @@ fn build_matcher_tree(
}
i += 1;

let file = get_or_create_file(args[i])?;
let file = get_or_create_file(args[i], output_files)?;
Some(Ls::new(Some(file)).into_box())
}
"-true" => Some(TrueMatcher.into_box()),
Expand Down Expand Up @@ -872,7 +884,8 @@ fn build_matcher_tree(
None
}
"(" => {
let (new_arg_index, sub_matcher) = build_matcher_tree(args, config, i + 1, true)?;
let (new_arg_index, sub_matcher) =
build_matcher_tree(args, config, output_files, i + 1, true)?;
i = new_arg_index;
Some(sub_matcher)
}
Expand Down Expand Up @@ -1910,22 +1923,24 @@ mod tests {
fn get_or_create_file_test() {
use std::fs;

let mut output_files = HashMap::new();

// remove file if hard link file exist.
// But you can't delete a file that doesn't exist,
// so ignore the error returned here.
let _ = fs::remove_file("test_data/get_or_create_file_test");

// test create file
let file = get_or_create_file("test_data/get_or_create_file_test");
let file = get_or_create_file("test_data/get_or_create_file_test", &mut output_files);
assert!(file.is_ok());

let file = get_or_create_file("test_data/get_or_create_file_test");
let file = get_or_create_file("test_data/get_or_create_file_test", &mut output_files);
assert!(file.is_ok());

// test error when file no permission
#[cfg(unix)]
{
let result = get_or_create_file("/etc/shadow");
let result = get_or_create_file("/etc/shadow", &mut output_files);
assert!(result.is_err());
}

Expand Down
30 changes: 30 additions & 0 deletions tests/test_find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,36 @@ fn find_fprinter() {
}
}

#[test]
fn find_output_actions_share_same_file() {
let temp_dir = Builder::new()
.prefix("find_shared_output_")
.tempdir()
.unwrap();
let search_path = temp_dir.path().join("search");
fs::create_dir(&search_path).unwrap();
let out_file = temp_dir.path().join("output");
let search_path_str = search_path.to_str().unwrap();
let out_file_str = out_file.to_str().unwrap();

ucmd()
.args(&[
search_path_str,
"-maxdepth",
"0",
"-fprintf",
out_file_str,
"%p\n",
"-fprint0",
out_file_str,
])
.succeeds()
.no_output();

let expected = format!("{search_path_str}\n{search_path_str}\0").into_bytes();
assert_eq!(fs::read(out_file).unwrap(), expected);
}

#[test]
fn find_follow() {
ucmd()
Expand Down
Loading