diff --git a/src/app.rs b/src/app.rs index 4515d2a..e27e757 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,7 @@ use std::io::prelude::*; use std::path::{Path, PathBuf}; use std::process; -use crate::{replace, DirectoryPatcher, Query, Settings, Stats}; +use crate::{console::Verbosity, replace, Console, DirectoryPatcher, Query, Settings}; #[derive(Debug)] enum ColorWhen { @@ -52,6 +52,12 @@ struct Options { #[clap(long = "go", help = "Write the changes to the filesystem")] go: bool, + #[clap( + long = "quiet", + help = "Don't show any output (except in case of errors)" + )] + quiet: bool, + #[clap(help = "The pattern to search for")] pattern: String, @@ -148,15 +154,6 @@ fn configure_color(when: &ColorWhen) { } } -fn print_stats(stats: &Stats, dry_run: bool) { - if dry_run { - print!("Would perform ") - } else { - print!("Performed ") - } - println!("{}", stats) -} - fn on_type_list() { println!("Known file types:"); let mut types_builder = ignore::types::TypesBuilder::new(); @@ -174,6 +171,7 @@ pub fn run() -> Result<()> { color_when, file_type_list, go, + quiet, hidden, ignored, ignored_file_types, @@ -192,6 +190,12 @@ pub fn run() -> Result<()> { } let dry_run = !go; + let verbosity = if quiet { + Verbosity::Quiet + } else { + Verbosity::Normal + }; + let console = Console::with_verbosity(verbosity); let color_when = &color_when.unwrap_or(ColorWhen::Auto); configure_color(color_when); @@ -205,6 +209,7 @@ pub fn run() -> Result<()> { }; let settings = Settings { + verbosity, dry_run, hidden, ignored, @@ -216,7 +221,7 @@ pub fn run() -> Result<()> { if path == PathBuf::from("-") { run_on_stdin(query) } else { - run_on_directory(path, settings, query) + run_on_directory(console, path, settings, query) } } @@ -234,21 +239,36 @@ fn run_on_stdin(query: Query) -> Result<()> { Ok(()) } -fn run_on_directory(path: PathBuf, settings: Settings, query: Query) -> Result<()> { +fn run_on_directory( + console: Console, + path: PathBuf, + settings: Settings, + query: Query, +) -> Result<()> { let dry_run = settings.dry_run; - let mut directory_patcher = DirectoryPatcher::new(&path, &settings); + let mut directory_patcher = DirectoryPatcher::new(&console, &path, &settings); directory_patcher.run(&query)?; let stats = directory_patcher.stats(); if stats.total_replacements() == 0 { - #[allow(clippy::print_literal)] - { - eprintln!("{}: {}", "Error".bold().red(), "nothing found to replace"); - } + console.print_error(&format!( + "{}: {}", + "Error".bold().red(), + "nothing found to replace" + )); process::exit(2); } - print_stats(&stats, dry_run); + + let stats = &stats; + let message = if dry_run { + "Would perform " + } else { + "Performed " + }; + console.print_message(message); + console.print_message(&format!("{stats}\n")); + if dry_run { - println!("Re-run ruplacer with --go to write these changes to the filesystem"); + console.print_message("Re-run ruplacer with --go to write these changes to the filesystem"); } Ok(()) } diff --git a/src/console.rs b/src/console.rs new file mode 100644 index 0000000..a2aaf86 --- /dev/null +++ b/src/console.rs @@ -0,0 +1,37 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Verbosity { + Quiet, + Normal, +} + +impl Default for Verbosity { + fn default() -> Self { + Verbosity::Normal + } +} + +#[derive(Debug, Default)] +pub struct Console { + verbosity: Verbosity, +} + +impl Console { + pub fn with_verbosity(verbosity: Verbosity) -> Self { + Self { verbosity } + } + + pub fn new() -> Self { + Default::default() + } + + pub fn print_message(&self, message: &str) { + if matches!(self.verbosity, Verbosity::Quiet) { + return; + } + print!("{message}"); + } + + pub fn print_error(&self, error: &str) { + eprintln!("{error}"); + } +} diff --git a/src/directory_patcher.rs b/src/directory_patcher.rs index 8334586..f88b1d5 100644 --- a/src/directory_patcher.rs +++ b/src/directory_patcher.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use std::path::Path; +use crate::console::Console; use crate::file_patcher::FilePatcher; use crate::query::Query; use crate::settings::Settings; @@ -9,7 +10,7 @@ use crate::stats::Stats; #[derive(Debug)] /// Used to run replacement query on every text file present in a given path /// ```rust -/// use ruplacer::{DirectoryPatcher, Query, Settings, Stats}; +/// use ruplacer::{Console, DirectoryPatcher, Query, Settings, Stats}; /// use std::path::PathBuf; /// /// let settings = Settings{ @@ -17,7 +18,8 @@ use crate::stats::Stats; /// .. Default::default() /// }; /// let path = PathBuf::from("tests/data"); -/// let mut directory_patcher = DirectoryPatcher::new(&path, &settings); +/// let console = Console::new(); +/// let mut directory_patcher = DirectoryPatcher::new(&console, &path, &settings); /// /// let query = Query::substring("old", "new"); /// directory_patcher.run(&query).unwrap(); @@ -29,13 +31,19 @@ use crate::stats::Stats; pub struct DirectoryPatcher<'a> { path: &'a Path, settings: &'a Settings, + console: &'a Console, stats: Stats, } impl<'a> DirectoryPatcher<'a> { - pub fn new(path: &'a Path, settings: &'a Settings) -> DirectoryPatcher<'a> { + pub fn new( + console: &'a Console, + path: &'a Path, + settings: &'a Settings, + ) -> DirectoryPatcher<'a> { let stats = Stats::default(); DirectoryPatcher { + console, path, settings, stats, @@ -61,14 +69,14 @@ impl<'a> DirectoryPatcher<'a> { } pub(crate) fn patch_file(&mut self, entry: &Path, query: &Query) -> Result<()> { - let file_patcher = FilePatcher::new(entry, query)?; + let file_patcher = FilePatcher::new(self.console, entry, query)?; let file_patcher = match file_patcher { None => return Ok(()), Some(f) => f, }; let num_replacements = file_patcher.num_replacements(); if num_replacements != 0 { - println!(); + self.console.print_message("\n"); } let num_lines = file_patcher.num_lines(); self.stats.update(num_lines, num_replacements); diff --git a/src/file_patcher.rs b/src/file_patcher.rs index b5c4ec4..53a209f 100644 --- a/src/file_patcher.rs +++ b/src/file_patcher.rs @@ -5,18 +5,20 @@ use std::path::{Path, PathBuf}; use crate::query::Query; use crate::replace; +use crate::Console; /// Run replacement query on a given file /// /// Example, assuming the `data.txt` file contains 'This is my old car' /// ```rust -/// use ruplacer::{FilePatcher, Query}; +/// use ruplacer::{Console, FilePatcher, Query}; /// use std::path::PathBuf; /// /// # std::fs::write("data.txt", "This is my old car.").unwrap(); /// let file = PathBuf::from("data.txt"); /// let query = Query::substring("old", "new"); -/// let file_patcher = FilePatcher::new(&file, &query).unwrap(); +/// let console = Console::new(); +/// let file_patcher = FilePatcher::new(&console, &file, &query).unwrap(); /// file_patcher.unwrap().run().unwrap(); /// /// let new_contents = std::fs::read_to_string("data.txt").unwrap(); @@ -30,7 +32,7 @@ pub struct FilePatcher { } impl FilePatcher { - pub fn new(path: &Path, query: &Query) -> Result> { + pub fn new(console: &Console, path: &Path, query: &Query) -> Result> { let mut num_replacements = 0; let mut num_lines = 0; let file = @@ -55,7 +57,7 @@ impl FilePatcher { let lineno = num + 1; let prefix = format!("{}:{} ", path.display(), lineno); let new_line = replacement.output(); - replacement.print_self(&prefix); + replacement.print_self(console, &prefix); new_contents.push_str(new_line); } } @@ -131,7 +133,8 @@ mod tests { let file_path = temp_dir.path().join("without-trailing-newline.txt"); fs::write(&file_path, "first line\nI say: old is nice\nlast line").unwrap(); let query = Query::substring("old", "new"); - let file_patcher = FilePatcher::new(&file_path, &query).unwrap(); + let console = Console::new(); + let file_patcher = FilePatcher::new(&console, &file_path, &query).unwrap(); file_patcher.unwrap().run().unwrap(); let actual = fs::read_to_string(&file_path).unwrap(); let expected = "first line\nI say: new is nice\nlast line"; @@ -140,7 +143,7 @@ mod tests { let file_path = temp_dir.path().join("with-trailing-newline.txt"); fs::write(&file_path, "first line\nI say: old is nice\nlast line\n").unwrap(); let query = Query::substring("old", "new"); - let file_patcher = FilePatcher::new(&file_path, &query).unwrap(); + let file_patcher = FilePatcher::new(&console, &file_path, &query).unwrap(); file_patcher.unwrap().run().unwrap(); let actual = fs::read_to_string(&file_path).unwrap(); let expected = "first line\nI say: new is nice\nlast line\n"; diff --git a/src/lib.rs b/src/lib.rs index 57c035f..b6b478e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,17 @@ +mod app; +mod console; mod directory_patcher; mod file_patcher; mod query; mod replacer; mod settings; -pub use settings::Settings; mod stats; -pub use crate::replacer::{replace, Replacement}; + +pub use app::run; +pub use console::{Console, Verbosity}; pub use directory_patcher::DirectoryPatcher; pub use file_patcher::FilePatcher; pub use query::Query; +pub use replacer::{replace, Replacement}; +pub use settings::Settings; pub use stats::Stats; -mod app; -pub use app::run; diff --git a/src/replacer.rs b/src/replacer.rs index a8cf449..9728f72 100644 --- a/src/replacer.rs +++ b/src/replacer.rs @@ -1,4 +1,5 @@ use crate::query::Query; +use crate::Console; use colored::*; use regex::Regex; @@ -59,25 +60,33 @@ impl<'a> Replacement<'a> { /// Print the replacement as two lines (red then green) /// ``` - /// use ruplacer::{Query, replace}; + /// use ruplacer::{Console, Query, replace}; /// let input = "let foo_bar = FooBar::new();"; /// let query = Query::subvert("foo_bar", "spam_eggs"); + /// let console = Console::new(); /// let replacement = replace(input, &query).unwrap(); - /// replacement.print_self("foo.rs:3"); + /// replacement.print_self(&console, "foo.rs:3"); /// // outputs: /// // foo.rs:3 let foo_bar = FooBar::new() /// // foo.rs:3 let spam_eggs = SpamEggs::new() /// ``` - pub fn print_self(&self, prefix: &str) { + pub fn print_self(&self, console: &Console, prefix: &str) { let red_underline = { |x: &str| x.red().underline() }; let input_fragments = self.fragments.into_iter().map(|x| &x.0); let red_prefix = format!("{}{}", prefix, "- ".red()); - Self::print_fragments(&red_prefix, red_underline, self.input, input_fragments); + Self::print_fragments( + console, + &red_prefix, + red_underline, + self.input, + input_fragments, + ); let green_underline = { |x: &str| x.green().underline() }; let green_prefix = format!("{}{}", prefix, "+ ".green()); let output_fragments = self.fragments.into_iter().map(|x| &x.1); Self::print_fragments( + console, &green_prefix, green_underline, &self.output, @@ -86,6 +95,7 @@ impl<'a> Replacement<'a> { } fn print_fragments<'f, C>( + console: &Console, prefix: &str, color: C, line: &str, @@ -93,22 +103,22 @@ impl<'a> Replacement<'a> { ) where C: Fn(&str) -> ColoredString, { - print!("{}", prefix); + console.print_message(prefix); let mut current_index = 0; for (i, fragment) in fragments.enumerate() { let Fragment { index, text } = fragment; // Whitespace between prefix and the first fragment does not matter if i == 0 { - print!("{}", &line[current_index..*index].trim_start()); + console.print_message((&line[current_index..*index].trim_start()).as_ref()); } else { - print!("{}", &line[current_index..*index]); + console.print_message(&line[current_index..*index]); } - print!("{}", color(text)); + console.print_message(&format!("{}", color(text))); current_index = index + text.len(); } - print!("{}", &line[current_index..]); + console.print_message(&line[current_index..]); if !line.ends_with('\n') { - println!() + console.print_message("\n"); } } } @@ -370,7 +380,8 @@ mod tests { let replacement = "new"; let query = Query::substring(pattern, replacement); let replacement = replace(input, &query).unwrap(); - replacement.print_self("foo.txt:3 "); + let console = Console::new(); + replacement.print_self(&console, "foo.txt:3 "); } #[test] diff --git a/src/settings.rs b/src/settings.rs index dcb243a..36ace1d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,6 +1,10 @@ +use crate::console::Verbosity; + #[derive(Debug, Default)] /// Settings applied for a DirectoryPatcher run pub struct Settings { + /// Control verbosity of ruplacer's console output + pub verbosity: Verbosity, /// If true, do not write changes to the file system (default: false) pub dry_run: bool, /// If true, also patch hidden files (default: false) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 42e0689..8027675 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -5,6 +5,7 @@ use std::process::Command; use anyhow::Result; use tempfile::TempDir; +use ruplacer::Console; use ruplacer::Query; use ruplacer::Settings; use ruplacer::{DirectoryPatcher, Stats}; @@ -45,7 +46,8 @@ fn assert_not_replaced(path: &Path) { } fn run_ruplacer(data_path: &Path, settings: Settings) -> Result { - let mut directory_patcher = DirectoryPatcher::new(data_path, &settings); + let console = Console::new(); + let mut directory_patcher = DirectoryPatcher::new(&console, data_path, &settings); directory_patcher.run(&Query::substring("old", "new"))?; Ok(directory_patcher.stats()) }