From 423453eaf8e29099bd93956c81efc35da2dfd418 Mon Sep 17 00:00:00 2001 From: hanbings Date: Sun, 11 Aug 2024 15:38:05 +0800 Subject: [PATCH] Implement `-fprint` (#421) --- src/find/matchers/mod.rs | 49 ++++++++++++++++++++++-- src/find/matchers/printer.rs | 73 ++++++++++++++++++++++++++++++------ src/find/mod.rs | 17 +++++++++ tests/find_cmd_tests.rs | 26 ++++++++++++- 4 files changed, 149 insertions(+), 16 deletions(-) diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index 4bd39890..108d5fc7 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -32,6 +32,7 @@ mod user; use ::regex::Regex; use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; use fs::FileSystemMatcher; +use std::fs::File; use std::path::Path; use std::time::SystemTime; use std::{error::Error, str::FromStr}; @@ -202,7 +203,7 @@ pub fn build_top_level_matcher( if !top_level_matcher.has_side_effects() { let mut new_and_matcher = AndMatcherBuilder::new(); new_and_matcher.new_and_condition(top_level_matcher); - new_and_matcher.new_and_condition(Printer::new(PrintDelimiter::Newline)); + new_and_matcher.new_and_condition(Printer::new(PrintDelimiter::Newline, None)); return Ok(new_and_matcher.build()); } Ok(top_level_matcher) @@ -339,6 +340,13 @@ 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> { + let file = File::create(path)?; + Ok(file) +} + /// The main "translate command-line args into a matcher" function. Will call /// itself recursively if it encounters an opening bracket. A successful return /// consists of a tuple containing the new index into the args array to use (if @@ -361,8 +369,8 @@ fn build_matcher_tree( let mut invert_next_matcher = false; while i < args.len() { let possible_submatcher = match args[i] { - "-print" => Some(Printer::new(PrintDelimiter::Newline).into_box()), - "-print0" => Some(Printer::new(PrintDelimiter::Null).into_box()), + "-print" => Some(Printer::new(PrintDelimiter::Newline, None).into_box()), + "-print0" => Some(Printer::new(PrintDelimiter::Null, None).into_box()), "-printf" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); @@ -370,6 +378,15 @@ fn build_matcher_tree( i += 1; Some(Printf::new(args[i])?.into_box()) } + "-fprint" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + + let file = get_or_create_file(args[i])?; + Some(Printer::new(PrintDelimiter::Newline, Some(file)).into_box()) + } "-true" => Some(TrueMatcher.into_box()), "-false" => Some(FalseMatcher.into_box()), "-lname" | "-ilname" => { @@ -1518,4 +1535,30 @@ mod tests { .expect("-version should stop parsing"); assert!(config.version_requested); } + + #[test] + fn get_or_create_file_test() { + use std::fs; + + // 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"); + assert!(file.is_ok()); + + let file = get_or_create_file("test_data/get_or_create_file_test"); + assert!(file.is_ok()); + + // test error when file no permission + #[cfg(unix)] + { + let result = get_or_create_file("/etc/shadow"); + assert!(result.is_err()); + } + + let _ = fs::remove_file("test_data/get_or_create_file_test"); + } } diff --git a/src/find/matchers/printer.rs b/src/find/matchers/printer.rs index 039741c7..6e01a84f 100644 --- a/src/find/matchers/printer.rs +++ b/src/find/matchers/printer.rs @@ -4,6 +4,11 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +use std::{ + fs::File, + io::{stderr, Write}, +}; + use walkdir::DirEntry; use super::{Matcher, MatcherIO}; @@ -25,25 +30,53 @@ impl std::fmt::Display for PrintDelimiter { /// This matcher just prints the name of the file to stdout. pub struct Printer { delimiter: PrintDelimiter, + output_file: Option, } impl Printer { - pub fn new(delimiter: PrintDelimiter) -> Self { - Self { delimiter } + pub fn new(delimiter: PrintDelimiter, output_file: Option) -> Self { + Self { + delimiter, + output_file, + } } -} -impl Matcher for Printer { - fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { - let mut out = matcher_io.deps.get_output().borrow_mut(); - write!( + fn print(&self, file_info: &DirEntry, mut out: impl Write, print_error_message: bool) { + match write!( out, "{}{}", file_info.path().to_string_lossy(), self.delimiter - ) - .unwrap(); + ) { + Ok(_) => {} + Err(e) => { + if print_error_message { + writeln!( + &mut stderr(), + "Error writing {:?} for {}", + file_info.path().to_string_lossy(), + e + ) + .unwrap(); + uucore::error::set_exit_code(1); + } + } + } out.flush().unwrap(); + } +} + +impl Matcher for Printer { + fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + if let Some(file) = &self.output_file { + self.print(file_info, file, true); + } else { + self.print( + file_info, + &mut *matcher_io.deps.get_output().borrow_mut(), + false, + ); + } true } @@ -64,7 +97,7 @@ mod tests { fn prints_newline() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); - let matcher = Printer::new(PrintDelimiter::Newline); + let matcher = Printer::new(PrintDelimiter::Newline, None); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!( @@ -77,7 +110,7 @@ mod tests { fn prints_null() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); - let matcher = Printer::new(PrintDelimiter::Null); + let matcher = Printer::new(PrintDelimiter::Null, None); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!( @@ -85,4 +118,22 @@ mod tests { deps.get_output_as_string() ); } + + #[test] + #[cfg(target_os = "linux")] + fn prints_error_message() { + let dev_full = File::open("/dev/full").unwrap(); + let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); + + let matcher = Printer::new(PrintDelimiter::Newline, Some(dev_full)); + let deps = FakeDependencies::new(); + + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + + // Reset the exit code global variable in case we run another test after this one + // See https://github.com/uutils/coreutils/issues/5777 + uucore::error::set_exit_code(0); + + assert!(deps.get_output_as_string().is_empty()); + } } diff --git a/src/find/mod.rs b/src/find/mod.rs index 738562db..54c8f65c 100644 --- a/src/find/mod.rs +++ b/src/find/mod.rs @@ -1300,4 +1300,21 @@ mod tests { assert_eq!(rc, 0); } + + #[test] + fn find_fprint() { + let deps = FakeDependencies::new(); + let rc = find_main( + &[ + "find", + "./test_data/simple", + "-fprint", + "test_data/find_fprint", + ], + &deps, + ); + assert_eq!(rc, 0); + + let _ = fs::remove_file("test_data/find_fprint"); + } } diff --git a/tests/find_cmd_tests.rs b/tests/find_cmd_tests.rs index 20fa1a19..364d484f 100644 --- a/tests/find_cmd_tests.rs +++ b/tests/find_cmd_tests.rs @@ -11,8 +11,8 @@ use assert_cmd::Command; use predicates::prelude::*; use serial_test::serial; -use std::fs::File; -use std::io::Write; +use std::fs::{self, File}; +use std::io::{Read, Write}; use std::{env, io::ErrorKind}; use tempfile::Builder; @@ -946,3 +946,25 @@ fn find_daystart() { .success() .stderr(predicate::str::is_empty()); } + +#[test] +#[serial(working_dir)] +fn find_fprint() { + let _ = fs::remove_file("test_data/find_fprint"); + + Command::cargo_bin("find") + .expect("found binary") + .args(["test_data/simple", "-fprint", "test_data/find_fprint"]) + .assert() + .success() + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::is_empty()); + + // read test_data/find_fprint + let mut f = File::open("test_data/find_fprint").unwrap(); + let mut contents = String::new(); + f.read_to_string(&mut contents).unwrap(); + assert!(contents.contains("test_data/simple")); + + let _ = fs::remove_file("test_data/find_fprint"); +}