Repository: uutils/findutils (Rust)

Issue:
Implement `-fprintf`
refer: https://www.gnu.org/software/findutils/manual/html_node/find_html/Print-File-Information.html

```
Action: -fprintf file format
True; like ‘-printf’ but write to file like ‘-fprint’. 
The output file is always created, even if no output is ever sent to it.
```

`-fprint` see [Print File Name](https://www.gnu.org/software/findutils/manual/html_node/find_html/Print-File-Name.html)


Discussion:
is print in printer.rs?
Oh, I'm sorry for overlooking this. `-printf` has been implemented. thanks :)
Could you point me where because from what I read they seem to have similar implementation 
I would to try and improve cuz I want more rust in Debian 
> Could you point me where because from what I read they seem to have similar implementation

Have a look at `matchers/printf.rs`.
> Could you point me where because from what I read they seem to have similar implementation


Sorry for the late reply.

In simple terms, `-fprintf` is an implementation of `-printf` that directs output to a file and `-printf` is implemented in `printf.rs`(https://github.com/uutils/findutils/blob/main/src/find/matchers/printf.rs).
Here is a detailed description of `-fprintf`: [Print File Name](https://www.gnu.org/software/findutils/manual/html_node/find_html/Print-File-Name.html)

Some information that might be helpful:
- The general structure of `find` starts from `src/find`, with `main.rs` as the entry point, and then to `src/find/matcher/mod.rs` to parse the command line parameters. Then other files in `src/find/matcher` are responsible for processing and determining whether a file meets the requirements of the command line parameters, and finally using the `write!` macro to output.

-  Use the `write!` macro for output.
    ```rust
    let mut out = matcher_io.deps.get_output().borrow_mut();
    write!(out, "content").unwrap();
    ```
the thing im confused about is

should i make a new file for fprintf or just have a function since most the functionaluity is the same
> should i make a new file for fprintf or just have a function since most the functionaluity is the same

I guess yes, making the modification in the `printf.rs` file might use less code. For example, add an `Option<Path>` field to the `Printf`(https://github.com/uutils/findutils/blob/main/src/find/matchers/printf.rs#L600) structure and switch different outputs in write!() depending on whether Option has data.

What are your thoughts on this? :) @cakebaker 



Patch:
diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs
index 82c47ee2..03fbef9c 100644
--- a/src/find/matchers/mod.rs
+++ b/src/find/matchers/mod.rs
@@ -453,7 +453,7 @@ fn build_matcher_tree(
                     return Err(From::from(format!("missing argument to {}", args[i])));
                 }
                 i += 1;
-                Some(Printf::new(args[i])?.into_box())
+                Some(Printf::new(args[i], None)?.into_box())
             }
             "-fprint" => {
                 if i >= args.len() - 1 {
@@ -464,6 +464,19 @@ fn build_matcher_tree(
                 let file = get_or_create_file(args[i])?;
                 Some(Printer::new(PrintDelimiter::Newline, Some(file)).into_box())
             }
+            "-fprintf" => {
+                if i >= args.len() - 2 {
+                    return Err(From::from(format!("missing argument to {}", args[i])));
+                }
+
+                // Action: -fprintf file format
+                // Args + 1: output file path
+                // Args + 2: format string
+                i += 1;
+                let file = get_or_create_file(args[i])?;
+                i += 1;
+                Some(Printf::new(args[i], Some(file))?.into_box())
+            }
             "-fprint0" => {
                 if i >= args.len() - 1 {
                     return Err(From::from(format!("missing argument to {}", args[i])));
diff --git a/src/find/matchers/printf.rs b/src/find/matchers/printf.rs
index 23949d33..451a91d8 100644
--- a/src/find/matchers/printf.rs
+++ b/src/find/matchers/printf.rs
@@ -4,11 +4,11 @@
 // license that can be found in the LICENSE file or at
 // https://opensource.org/licenses/MIT.
 
-use std::borrow::Cow;
 use std::error::Error;
-use std::fs;
+use std::fs::{self, File};
 use std::path::Path;
 use std::time::SystemTime;
+use std::{borrow::Cow, io::Write};
 
 use chrono::{format::StrftimeItems, DateTime, Local};
 
@@ -571,20 +571,18 @@ fn format_directive<'entry>(
 /// find's printf syntax.
 pub struct Printf {
     format: FormatString,
+    output_file: Option<File>,
 }
 
 impl Printf {
-    pub fn new(format: &str) -> Result<Self, Box<dyn Error>> {
+    pub fn new(format: &str, output_file: Option<File>) -> Result<Self, Box<dyn Error>> {
         Ok(Self {
             format: FormatString::parse(format)?,
+            output_file,
         })
     }
-}
-
-impl Matcher for Printf {
-    fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
-        let mut out = matcher_io.deps.get_output().borrow_mut();
 
+    fn print(&self, file_info: &WalkEntry, mut out: impl Write) {
         for component in &self.format.components {
             match component {
                 FormatComponent::Literal(literal) => write!(out, "{literal}").unwrap(),
@@ -619,6 +617,16 @@ impl Matcher for Printf {
                 },
             }
         }
+    }
+}
+
+impl Matcher for Printf {
+    fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
+        if let Some(file) = &self.output_file {
+            self.print(file_info, file);
+        } else {
+            self.print(file_info, &mut *matcher_io.deps.get_output().borrow_mut());
+        }
 
         true
     }
@@ -804,7 +812,7 @@ mod tests {
         let file_info = get_dir_entry_for("test_data/simple", "abbbc");
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%f,%7f,%-7f").unwrap();
+        let matcher = Printf::new("%f,%7f,%-7f", None).unwrap();
         assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
         assert_eq!("abbbc,  abbbc,abbbc  ", deps.get_output_as_string());
     }
@@ -814,7 +822,7 @@ mod tests {
         let file_info = get_dir_entry_for("test_data/simple", "abbbc");
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%h %H %p %P").unwrap();
+        let matcher = Printf::new("%h %H %p %P", None).unwrap();
         assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
         assert_eq!(
             format!(
@@ -833,7 +841,7 @@ mod tests {
         let file_info = get_dir_entry_for("test_data/simple", "subdir/ABBBC");
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%h %H %p %P").unwrap();
+        let matcher = Printf::new("%h %H %p %P", None).unwrap();
         assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
         assert_eq!(
             format!(
@@ -853,7 +861,7 @@ mod tests {
         let file_info_2 = get_dir_entry_for("test_data/depth/1", "2/f2");
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%d.").unwrap();
+        let matcher = Printf::new("%d.", None).unwrap();
         assert!(matcher.matches(&file_info_1, &mut deps.new_matcher_io()));
         assert!(matcher.matches(&file_info_2, &mut deps.new_matcher_io()));
         assert_eq!("1.2.", deps.get_output_as_string());
@@ -865,7 +873,7 @@ mod tests {
         let file_info_d = get_dir_entry_for("test_data/simple", "subdir");
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%y").unwrap();
+        let matcher = Printf::new("%y", None).unwrap();
         assert!(matcher.matches(&file_info_f, &mut deps.new_matcher_io()));
         assert!(matcher.matches(&file_info_d, &mut deps.new_matcher_io()));
         assert_eq!("fd", deps.get_output_as_string());
@@ -893,7 +901,7 @@ mod tests {
         let socket_info = get_dir_entry_for(&temp_dir_path, socket_name);
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%y").unwrap();
+        let matcher = Printf::new("%y", None).unwrap();
         assert!(matcher.matches(&fifo_info, &mut deps.new_matcher_io()));
         assert!(matcher.matches(&socket_info, &mut deps.new_matcher_io()));
         assert_eq!("ps", deps.get_output_as_string());
@@ -904,7 +912,7 @@ mod tests {
         let file_info = get_dir_entry_for("test_data/size", "512bytes");
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%s").unwrap();
+        let matcher = Printf::new("%s", None).unwrap();
         assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
         assert_eq!("512", deps.get_output_as_string());
     }
@@ -986,7 +994,7 @@ mod tests {
 
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%y %Y %l\n").unwrap();
+        let matcher = Printf::new("%y %Y %l\n", None).unwrap();
         assert!(matcher.matches(&regular_file, &mut deps.new_matcher_io()));
         assert!(matcher.matches(&link_f, &mut deps.new_matcher_io()));
         assert!(matcher.matches(&link_d, &mut deps.new_matcher_io()));
@@ -1033,7 +1041,7 @@ mod tests {
         let file_info = get_dir_entry_for(&temp_dir_path, new_file_name);
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%t,%T@,%TF").unwrap();
+        let matcher = Printf::new("%t,%T@,%TF", None).unwrap();
         assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
         assert_eq!(
             format!(
@@ -1061,7 +1069,7 @@ mod tests {
         let file_info = get_dir_entry_for(&temp_dir_path, new_file_name);
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%u %U %g %G").unwrap();
+        let matcher = Printf::new("%u %U %g %G", None).unwrap();
         assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
         assert_eq!(
             format!("{user} {uid} {group} {gid}"),
@@ -1086,7 +1094,7 @@ mod tests {
         let file_info = get_dir_entry_for(&temp_dir_path, new_file_name);
         let deps = FakeDependencies::new();
 
-        let matcher = Printf::new("%m %M").unwrap();
+        let matcher = Printf::new("%m %M", None).unwrap();
         assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
         assert_eq!("755 -rwxr-xr-x", deps.get_output_as_string());
     }
diff --git a/src/find/mod.rs b/src/find/mod.rs
index bb9cb91f..a94d5086 100644
--- a/src/find/mod.rs
+++ b/src/find/mod.rs
@@ -1434,6 +1434,24 @@ mod tests {
         );
     }
 
+    #[test]
+    fn find_fprintf() {
+        let deps = FakeDependencies::new();
+        let rc = find_main(
+            &[
+                "find",
+                "./test_data/simple",
+                "-fprintf",
+                "test_data/find_fprintf",
+                "%h %H %p %P",
+            ],
+            &deps,
+        );
+        assert_eq!(rc, 0);
+
+        let _ = fs::remove_file("test_data/find_fprintf");
+    }
+
     #[test]
     #[cfg(unix)]
     fn test_ls() {


Test patch:
diff --git a/tests/find_cmd_tests.rs b/tests/find_cmd_tests.rs
index 1510260a..ad2a34fb 100644
--- a/tests/find_cmd_tests.rs
+++ b/tests/find_cmd_tests.rs
@@ -992,6 +992,33 @@ fn find_follow() {
         .stderr(predicate::str::is_empty());
 }
 
+#[test]
+#[serial(working_dir)]
+fn find_fprintf() {
+    let _ = fs::remove_file("test_data/find_fprintf");
+
+    Command::cargo_bin("find")
+        .expect("found binary")
+        .args([
+            "test_data/simple",
+            "-fprintf",
+            "test_data/find_fprintf",
+            "%h %H %p %P",
+        ])
+        .assert()
+        .success()
+        .stdout(predicate::str::is_empty())
+        .stderr(predicate::str::is_empty());
+
+    // read test_data/find_fprintf
+    let mut f = File::open("test_data/find_fprintf").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_fprintf");
+}
+
 #[test]
 #[serial(working_dir)]
 fn find_ls() {