From 104a111cf60878550d0121cf30d9fdf7f3f8fc78 Mon Sep 17 00:00:00 2001 From: Bruno Romano Date: Wed, 19 Aug 2026 23:15:54 -0300 Subject: [PATCH] find: share output files across actions --- src/find/matchers/mod.rs | 41 +++++++++++++++++++++++++++------------- tests/test_find.rs | 30 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index d8d4c018..26d71823 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -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}, @@ -286,7 +287,8 @@ pub fn build_top_level_matcher( args: &[&str], config: &mut Config, ) -> Result, Box> { - 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() { @@ -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> { +/// 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, +) -> Result> { + 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 @@ -456,6 +467,7 @@ fn get_or_create_file(path: &str) -> Result> { fn build_matcher_tree( args: &[&str], config: &mut Config, + output_files: &mut HashMap, arg_index: usize, mut expecting_bracket: bool, ) -> Result<(usize, Box), Box> { @@ -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" => { @@ -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()) @@ -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()), @@ -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()), @@ -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) } @@ -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()); } diff --git a/tests/test_find.rs b/tests/test_find.rs index fe294bb0..5c7a925a 100644 --- a/tests/test_find.rs +++ b/tests/test_find.rs @@ -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()