Repository: pkolaczk/fclones (language: rust)

Issue:
Write report to a file
Add an `-o <file>` option. This would allow for more flexibility when building pipelines, e.g. wrapping `fclones` with `time`.


Discussion / hints:

Fix patch:
diff --git a/src/config.rs b/src/config.rs
index a23e797..67bec6c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,5 @@
 use std::io::{stdin, BufRead, BufReader};
+use std::path::PathBuf;
 use std::process::exit;
 
 use clap::arg_enum;
@@ -10,7 +11,6 @@ use crate::log::Log;
 use crate::path::Path;
 use crate::pattern::{Pattern, PatternOpts};
 use crate::selector::PathSelector;
-use std::path::PathBuf;
 
 arg_enum! {
     #[derive(Debug, StructOpt)]
@@ -33,8 +33,12 @@ impl Default for OutputFormat {
     setting(AppSettings::DeriveDisplayOrder)
 )]
 pub struct Config {
+    /// Writes the report to a file instead of the standard output
+    #[structopt(short = "o", long, value_name("path"))]
+    pub output: Option<PathBuf>,
+
     /// Sets output file format
-    #[structopt(short="f", long, possible_values = &OutputFormat::variants(),
+    #[structopt(short = "f", long, possible_values = &OutputFormat::variants(),
     case_insensitive = true, default_value="Text")]
     pub format: OutputFormat,
 
diff --git a/src/main.rs b/src/main.rs
index 5f55ccf..28fed4e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,11 @@
 use std::cell::RefCell;
 use std::cmp::Reverse;
 use std::env::current_dir;
-use std::io::BufWriter;
+use std::fs::File;
+use std::io::{BufWriter, Write};
+use std::path::PathBuf;
+use std::process::exit;
+use std::sync::Arc;
 
 use console::{style, Term};
 use itertools::Itertools;
@@ -16,6 +20,7 @@ use fclones::group::*;
 use fclones::log::Log;
 use fclones::path::Path;
 use fclones::pattern::ESCAPE_CHAR;
+use fclones::progress::FastProgressBar;
 use fclones::report::Reporter;
 use fclones::walk::Walk;
 use indoc::indoc;
@@ -307,19 +312,18 @@ fn group_by_contents(ctx: &mut AppCtx, groups: Vec<FileGroup<Path>>) -> Vec<File
     groups
 }
 
-fn write_report(ctx: &mut AppCtx, groups: &mut Vec<FileGroup<Path>>) {
-    let stdout = Term::stdout();
+fn write_report(ctx: &mut AppCtx, groups: &[FileGroup<Path>]) {
     let remaining_files = groups.total_count();
     let progress = ctx
         .log
         .progress_bar("[7/7] Writing report", remaining_files as u64);
 
     // No progress bar when we write to terminal
-    if stdout.is_term() {
-        progress.finish_and_clear()
-    }
-    let out = BufWriter::new(stdout);
-    let mut reporter = Reporter::new(out, progress);
+    let mut reporter = match &ctx.config.output {
+        Some(path) => file_reporter(ctx, path, progress),
+        None => stdout_reporter(progress),
+    };
+
     let result = match &ctx.config.format {
         OutputFormat::Text => reporter.write_as_text(groups),
         OutputFormat::Csv => reporter.write_as_csv(groups),
@@ -331,6 +335,37 @@ fn write_report(ctx: &mut AppCtx, groups: &mut Vec<FileGroup<Path>>) {
     }
 }
 
+/// Creates a reporter that writes to the standard output
+fn stdout_reporter(progress: Arc<FastProgressBar>) -> Reporter<Box<dyn Write>> {
+    let stdout = Term::stdout();
+    let is_term = stdout.is_term();
+    if is_term {
+        progress.finish_and_clear()
+    }
+    let out: Box<dyn Write> = Box::new(BufWriter::new(stdout));
+    Reporter::new(out, is_term, progress)
+}
+
+/// Creates a reporter that writes to the given file.
+/// Writes and error message and exists the application if the file cannot be open.
+fn file_reporter(
+    ctx: &AppCtx,
+    path: &PathBuf,
+    progress: Arc<FastProgressBar>,
+) -> Reporter<Box<dyn Write>> {
+    match File::create(path) {
+        Ok(file) => {
+            let out: Box<dyn Write> = Box::new(BufWriter::new(file));
+            Reporter::new(out, false, progress)
+        }
+        Err(e) => {
+            ctx.log
+                .eprintln(format!("Could not create {}: {}", path.display(), e));
+            exit(1)
+        }
+    }
+}
+
 fn paint_help(s: &str) -> String {
     let escape_regex = "{escape}";
     let code_regex = Regex::new(r"`([^`]+)`").unwrap();
@@ -372,8 +407,8 @@ fn main() {
     let log = Log::new();
     let mut ctx = AppCtx { log, config };
 
-    let mut contents_groups = process(&mut ctx);
-    write_report(&mut ctx, &mut contents_groups)
+    let results = process(&mut ctx);
+    write_report(&mut ctx, &results)
 }
 
 // Extracted for testing
@@ -396,14 +431,14 @@ fn process(mut ctx: &mut AppCtx) -> Vec<FileGroup<Path>> {
 
 #[cfg(test)]
 mod test {
-    use std::io::Write;
+    use std::fs::{hard_link, OpenOptions};
+    use std::io::{Read, Write};
     use std::path::PathBuf;
 
     use fclones::path::Path;
     use fclones::util::test::*;
 
     use super::*;
-    use std::fs::{hard_link, OpenOptions};
 
     #[test]
     fn identical_small_files() {
@@ -596,6 +631,31 @@ mod test {
         );
     }
 
+    #[test]
+    fn report() {
+        with_dir("target/test/main/report", |root| {
+            let file = root.join("file1");
+            write_test_file(&file, b"foo", b"", b"");
+
+            let report_file = root.join("report.txt");
+            let mut ctx = test_ctx();
+            ctx.config.paths = vec![file.clone()];
+            ctx.config.unique = true;
+            ctx.config.output = Some(report_file.clone());
+
+            let results = process(&mut ctx);
+            write_report(&mut ctx, &results);
+
+            assert!(report_file.exists());
+            let mut report = String::new();
+            File::open(report_file)
+                .unwrap()
+                .read_to_string(&mut report)
+                .unwrap();
+            assert!(report.contains("file1"))
+        });
+    }
+
     fn write_test_file(path: &PathBuf, prefix: &[u8], mid: &[u8], suffix: &[u8]) {
         let mut file = OpenOptions::new()
             .write(true)
diff --git a/src/report.rs b/src/report.rs
index 749df09..7a0679f 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -3,20 +3,25 @@ use std::io::Write;
 use std::sync::Arc;
 
 use console::style;
+use serde::{Serialize, Serializer};
 
 use crate::group::FileGroup;
 use crate::path::Path;
 use crate::progress::FastProgressBar;
-use serde::{Serialize, Serializer};
 
 pub struct Reporter<W: Write> {
     out: W,
+    color: bool,
     progress: Arc<FastProgressBar>,
 }
 
 impl<W: Write> Reporter<W> {
-    pub fn new(out: W, progress: Arc<FastProgressBar>) -> Reporter<W> {
-        Reporter { out, progress }
+    pub fn new(out: W, color: bool, progress: Arc<FastProgressBar>) -> Reporter<W> {
+        Reporter {
+            out,
+            color,
+            progress,
+        }
     }
 
     /// Writes report in human-readable text format.
@@ -38,7 +43,13 @@ impl<W: Write> Reporter<W> {
                 None => style("-".repeat(32)).white().dim(),
                 Some(hash) => style(format!("{}", hash)).blue().bold().bright(),
             };
-            writeln!(self.out, "{} {}:", len, hash.for_stdout())?;
+
+            writeln!(
+                self.out,
+                "{} {}:",
+                len.force_styling(self.color),
+                hash.force_styling(self.color)
+            )?;
             for f in g.files.iter() {
                 self.progress.tick();
                 writeln!(self.out, "    {}", f.display())?;


Test patch: