diff --git a/build.rs b/build.rs index 48feb7e..58743cd 100644 --- a/build.rs +++ b/build.rs @@ -25,6 +25,12 @@ fn create_man_page() { .long("--preview") .help("Emit the replacement to STDOUT"), ) + .flag( + Flag::new() + .short("-d") + .long("--delete") + .help("Delete files."), + ) .flag( Flag::new() .short("-s") diff --git a/src/cli.rs b/src/cli.rs index 068e3ff..6148afc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -16,6 +16,10 @@ environment variable REN_PAGER can be used to override the pager. */ pub write: bool, + #[structopt(short = "d", long = "delete")] + /// Delete files + pub delete: bool, + #[structopt(short = "s", long = "string-mode")] /// Treat expressions as non-regex strings pub literal_mode: bool, diff --git a/src/input.rs b/src/input.rs index a4b164d..53219cd 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,17 +1,18 @@ use crate::{edit::Edit, output::OutputType, writer::Writer, Replacer, Result}; use std::io::prelude::*; use std::path::PathBuf; +use indexmap::IndexMap; pub(crate) struct App { - replacer: Replacer, + replacer: Option } impl App { - pub(crate) fn new(replacer: Replacer) -> Self { + pub(crate) fn new(replacer: Option) -> Self { Self { replacer } } - pub(crate) fn run(&self, preview: bool, color: bool, pager: Option) -> Result<()> { + pub(crate) fn run(&self, preview: bool, delete: bool, color: bool, pager: Option) -> Result<()> { { let stdin = std::io::stdin(); let handle = stdin.lock(); @@ -43,24 +44,24 @@ impl App { sorted_paths.push(key); } sorted_paths.sort_by(|a, b| b.to_str().unwrap().len().cmp(&a.to_str().unwrap().len())); - let edit = Edit::new(&self.replacer); - match edit.parse(&sorted_paths) { - Ok(src_to_dst) => { - if preview { - let writer = Writer::new(sorted_paths, src_to_dst); - let text = match writer.patch_preview(color) { - Ok(text) => text, - Err(_) => return Ok(()), // FIXME: - }; - write!(write, "{}", text)?; - } else { - let writer = Writer::new(sorted_paths, src_to_dst); - if let Err(_) = writer.write_file() { - return Ok(()); // FIXME: - } - } - } - Err(_) => { + let mut src_to_dst: Option> = None; + if let Some(replacer) = &self.replacer { + let edit = Edit::new(&replacer); + src_to_dst = match edit.parse(&sorted_paths) { + Ok(src_to_dst) => Some(src_to_dst), + Err(_) => return Ok(()), // FIXME: + }; + } + if preview { + let writer = Writer::new(sorted_paths, src_to_dst); + let text = match writer.patch_preview(color, delete) { + Ok(text) => text, + Err(_) => return Ok(()), // FIXME: + }; + write!(write, "{}", text)?; + } else { + let writer = Writer::new(sorted_paths, src_to_dst); + if let Err(_) = writer.write_file(delete) { return Ok(()); // FIXME: } } diff --git a/src/main.rs b/src/main.rs index 1a0886f..0140602 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,14 +38,19 @@ fn main() -> Result<()> { let pager = env::var("REN_PAGER").ok(); if let (Some(find), Some(replace_with)) = (options.find, options.replace_with) { - App::new(Replacer::new( - find, - replace_with, - options.literal_mode, - options.flags, - options.replacements, - )?) - .run(!options.write, color, pager)?; + App::new( + Some(Replacer::new( + find, + replace_with, + options.literal_mode, + options.flags, + options.replacements, + )?) + ) + .run(!options.write, options.delete, color, pager)?; + } else if options.delete { + App::new(None) + .run(!options.write, options.delete, color, pager)?; } process::exit(0); } diff --git a/src/writer.rs b/src/writer.rs index 2b521d4..54ec1c9 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -6,7 +6,7 @@ pub type Result = std::result::Result; pub(crate) struct Writer { paths: Vec, - src_to_dst: IndexMap, + src_to_dst: Option>, } #[derive(Debug, thiserror::Error)] @@ -20,27 +20,14 @@ pub enum Error { } impl Writer { - pub(crate) fn new(paths: Vec, src_to_dst: IndexMap) -> Self { + pub(crate) fn new(paths: Vec, src_to_dst: Option>) -> Self { Self { paths, src_to_dst } } - pub(crate) fn patch_preview(&self, color: bool) -> Result { + pub(crate) fn patch_preview(&self, color: bool, delete: bool) -> Result { let mut modified_paths: Vec = Vec::new(); let mut print_diff = false; - for path in &self.paths { - let dst = &self.src_to_dst[path]; - if path == dst || (path != dst && !Self::check(&path.to_path_buf(), &dst)) { - let path_string = path.to_string_lossy(); - modified_paths.push(path_string.to_string()); - continue; - } - print_diff = true; - modified_paths.push(dst.to_string_lossy().to_string()); - } - if !print_diff { - return Ok("".to_string()); - } - let modified = modified_paths.join("\n"); + let mut modified = "".to_string(); let original: String = self .paths .clone() @@ -48,6 +35,26 @@ impl Writer { .map(|p| p.to_string_lossy().to_string()) .collect::>() .join("\n"); + if !delete { + let src_to_dst = match &self.src_to_dst { + Some(src_to_dst) => src_to_dst, + None => panic!("Missing source to destination"), + }; + for path in &self.paths { + let dst = &src_to_dst[path]; + if path == dst || (path != dst && !Self::check(&path.to_path_buf(), &dst)) { + let path_string = path.to_string_lossy(); + modified_paths.push(path_string.to_string()); + continue; + } + print_diff = true; + modified_paths.push(dst.to_string_lossy().to_string()); + } + if !print_diff { + return Ok("".to_string()); + } + modified = modified_paths.join("\n").to_string(); + } let patch = create_patch(&original, &modified); let f = match color { true => PatchFormatter::new().with_color(), @@ -56,19 +63,33 @@ impl Writer { return Ok(f.fmt_patch(&patch).to_string()); } - pub(crate) fn write_file(&self) -> Result<()> { + pub(crate) fn write_file(&self, delete: bool) -> Result<()> { for path in &self.paths { - let dst = &self.src_to_dst[path]; - if path == dst || !Self::check(&path.to_path_buf(), &dst) { - continue; - } - if let Err(err) = fs::rename(path, &dst) { - eprintln!( - "Error: failed to move '{}' to '{}', underlying error: {}", - path.display(), - &dst.display(), - err - ); + if delete { + if let Err(err) = fs::remove_file(path) { + eprintln!( + "Error: failed to remove file '{}': {}", + path.display(), + err + ); + } + } else { + let src_to_dst = match &self.src_to_dst { + Some(src_to_dst) => src_to_dst, + None => panic!("Missing source to destination"), + }; + let dst = &src_to_dst[path]; + if path == dst || !Self::check(&path.to_path_buf(), &dst) { + continue; + } + if let Err(err) = fs::rename(path, &dst) { + eprintln!( + "Error: failed to move '{}' to '{}', underlying error: {}", + path.display(), + &dst.display(), + err + ); + } } } Ok(()) diff --git a/tests/cli.rs b/tests/cli.rs index 422e83e..8415e4e 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -114,23 +114,87 @@ mod cli { let file_path_dst = tmp_dir_path.join(file_path_component); let prefix = file_path_dst.parent().unwrap(); std::fs::create_dir_all(prefix).unwrap(); - assert!(Path::exists(&file_path)); fs::copy(file_path, &file_path_dst).expect("Error copying file"); - assert!(Path::exists(&file_path_dst)); - let command = ren() + ren() .current_dir(tmp_dir_path) .write_stdin(input) .args(&["changes", "altered", "-w"]) .assert() .success(); - let output = command.get_output(); - println!("stdout = {:?}", String::from_utf8_lossy(&output.stdout)); - println!("stderr = {:?}", String::from_utf8_lossy(&output.stderr)); assert!(!Path::exists(&file_path_dst)); let file_path_component_moved = "altered dir with spaces/stays dir with spaces two/altered file with spaces"; let file_path_moved = tmp_dir_path.join(file_path_component_moved); - println!("file_path_moved = {:?}", file_path_moved); assert!(Path::exists(&file_path_moved)); Ok(()) } + + #[test] + fn simple_delete_preview() -> Result<()> { + let input = fs::read_to_string("tests/data/simple/find.txt").expect("Error reading input"); + let result = fs::read_to_string("tests/data/simple/delete.patch").expect("Error reading input"); + ren() + .current_dir("tests/data/simple") + .write_stdin(input) + .args(&["-d"]) + .assert() + .success() + .stdout(result); + Ok(()) + } + + #[test] + fn nested_delete_preview() -> Result<()> { + let input = fs::read_to_string("tests/data/nested/find.txt").expect("Error reading input"); + let result = fs::read_to_string("tests/data/nested/delete.patch").expect("Error reading input"); + ren() + .current_dir("tests/data/nested") + .write_stdin(input) + .args(&["-d"]) + .assert() + .success() + .stdout(result); + Ok(()) + } + + #[test] + fn simple_delete() -> Result<()> { + let input = fs::read_to_string("tests/data/simple/find.txt").expect("Error reading input"); + let file_path_component = "changes"; + let file_path = Path::new("tests/data/simple").join(file_path_component); + let tmp_dir = tempfile::tempdir()?; + let tmp_dir_path = tmp_dir.path(); + let file_path_dst = tmp_dir_path.join(file_path_component); + let prefix = file_path_dst.parent().unwrap(); + std::fs::create_dir_all(prefix).unwrap(); + fs::copy(file_path, &file_path_dst).expect("Error copying file"); + ren() + .current_dir(tmp_dir_path) + .write_stdin(input) + .args(&["-d", "-w"]) + .assert() + .success(); + assert!(!Path::exists(&file_path_dst)); + Ok(()) + } + + #[test] + fn nested_delete() -> Result<()> { + let input = fs::read_to_string("tests/data/nested/find.txt").expect("Error reading input"); + let file_path_component = "changes dir with spaces/stays dir with spaces two/changes file with spaces"; + let file_path = Path::new("tests/data/nested").join(file_path_component); + let tmp_dir = tempfile::tempdir()?; + let tmp_dir_path = tmp_dir.path(); + let file_path_dst = tmp_dir_path.join(file_path_component); + let prefix = file_path_dst.parent().unwrap(); + std::fs::create_dir_all(prefix).unwrap(); + fs::copy(file_path, &file_path_dst).expect("Error copying file"); + ren() + .current_dir(tmp_dir_path) + .write_stdin(input) + .args(&["-d", "-w"]) + .assert() + .success(); + assert!(!Path::exists(&file_path_dst)); + Ok(()) + } } diff --git a/tests/data/nested/delete.patch b/tests/data/nested/delete.patch new file mode 100644 index 0000000..6a99f39 --- /dev/null +++ b/tests/data/nested/delete.patch @@ -0,0 +1,6 @@ +--- original ++++ modified +@@ -1,2 +0,0 @@ +-changes dir with spaces/stays dir with spaces two/changes file with spaces +-changes dir with spaces +\ No newline at end of file diff --git a/tests/data/nested/generate.sh b/tests/data/nested/generate.sh index e5bb763..59fc05b 100755 --- a/tests/data/nested/generate.sh +++ b/tests/data/nested/generate.sh @@ -21,3 +21,16 @@ line_fix='$a\ sed -i.bak "${line_fix}" patch.patch rm patch.patch.bak + +# Delete + +diff --unified <(echo "$sorted") <(printf "") > delete.patch || true + +sed -i.bak '1s/.*/--- original/' delete.patch +sed -i.bak '2s/.*/+++ modified/' delete.patch +line_fix='$a\ +\\ No newline at end of file +' +sed -i.bak "${line_fix}" delete.patch + +rm delete.patch.bak diff --git a/tests/data/simple/delete.patch b/tests/data/simple/delete.patch new file mode 100644 index 0000000..4312ab2 --- /dev/null +++ b/tests/data/simple/delete.patch @@ -0,0 +1,5 @@ +--- original ++++ modified +@@ -1 +0,0 @@ +-changes +\ No newline at end of file diff --git a/tests/data/simple/generate.sh b/tests/data/simple/generate.sh index e2da7b2..f58ea7a 100755 --- a/tests/data/simple/generate.sh +++ b/tests/data/simple/generate.sh @@ -18,5 +18,17 @@ line_fix='$a\ \\ No newline at end of file ' sed -i.bak "${line_fix}" patch.patch - rm patch.patch.bak + +# Delete + +diff --unified find.txt <(printf "") > delete.patch || true + +sed -i.bak '1s/.*/--- original/' delete.patch +sed -i.bak '2s/.*/+++ modified/' delete.patch +line_fix='$a\ +\\ No newline at end of file +' +sed -i.bak "${line_fix}" delete.patch + +rm delete.patch.bak