diff --git a/Cargo.lock b/Cargo.lock index dbdb6ffa56..4d787cd238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1821,6 +1821,7 @@ dependencies = [ "futures_codec 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "getset 0.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "heim 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 408c51f237..10e49ec045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ adhoc_derive = "0.1.2" lazy_static = "1.3.0" git2 = "0.9.2" dirs = "2.0.2" +glob = "0.3.0" ctrlc = "3.1.3" ptree = "0.2" clipboard = "0.5" diff --git a/src/commands/rm.rs b/src/commands/rm.rs index f3dc55b9a9..51a08ec6e2 100644 --- a/src/commands/rm.rs +++ b/src/commands/rm.rs @@ -2,6 +2,8 @@ use crate::errors::ShellError; use crate::parser::hir::SyntaxType; use crate::parser::registry::{CommandConfig, NamedType, PositionalType}; use crate::prelude::*; + +use glob::glob; use indexmap::IndexMap; pub struct Remove; @@ -43,17 +45,24 @@ pub fn rm(args: CommandArgs) -> Result { file => full_path.push(file), } - if full_path.is_dir() { - if !args.has("recursive") { - return Err(ShellError::labeled_error( - "is a directory", - "", - args.call_info.name_span.unwrap(), - )); + for entry in glob(&full_path.to_string_lossy()).expect("Failed to read glob pattern") { + match entry { + Ok(path) => { + if path.is_dir() { + if !args.has("recursive") { + return Err(ShellError::labeled_error( + "is a directory", + "", + args.call_info.name_span.unwrap(), + )); + } + std::fs::remove_dir_all(&path).expect("can not remove directory"); + } else if path.is_file() { + std::fs::remove_file(&path).expect("can not remove file"); + } + } + Err(e) => return Err(ShellError::string(&format!("{:?}", e))), } - std::fs::remove_dir_all(&full_path).expect("can not remove directory"); - } else if full_path.is_file() { - std::fs::remove_file(&full_path).expect("can not remove file"); } Ok(OutputStream::empty()) diff --git a/tests/commands_test.rs b/tests/commands_test.rs index 4e3b95ed67..606cd49039 100644 --- a/tests/commands_test.rs +++ b/tests/commands_test.rs @@ -1,7 +1,8 @@ mod helpers; -use h::in_directory as cwd; +use h::{in_directory as cwd, Playground, Stub::*}; use helpers as h; +use std::path::PathBuf; #[test] fn lines() { @@ -83,17 +84,28 @@ fn open_error_if_file_not_found() { #[test] fn save_figures_out_intelligently_where_to_write_out_with_metadata() { - let (playground_path, tests_dir) = h::setup_playground_for("save_smart_test"); + let sandbox = Playground::setup_for("save_smart_test") + .with_files(vec![FileWithContent( + "cargo_sample.toml", + r#" + [package] + name = "nu" + version = "0.1.1" + authors = ["Yehuda Katz "] + description = "A shell for the GitHub era" + license = "ISC" + edition = "2018" + "#, + )]) + .test_dir_name(); - let full_path = format!("{}/{}", playground_path, tests_dir); - let subject_file = format!("{}/{}", full_path , "cargo_sample.toml"); - - h::copy_file_to("tests/fixtures/formats/cargo_sample.toml", &subject_file); + let full_path = format!("{}/{}", Playground::root(), sandbox); + let subject_file = format!("{}/{}", full_path, "cargo_sample.toml"); nu!( _output, - cwd("tests/fixtures"), - "open nuplayground/save_smart_test/cargo_sample.toml | inc package.version --minor | save" + cwd(&Playground::root()), + "open save_smart_test/cargo_sample.toml | inc package.version --minor | save" ); let actual = h::file_contents(&subject_file); @@ -102,14 +114,14 @@ fn save_figures_out_intelligently_where_to_write_out_with_metadata() { #[test] fn save_can_write_out_csv() { - let (playground_path, tests_dir) = h::setup_playground_for("save_test"); + let sandbox = Playground::setup_for("save_test").test_dir_name(); - let full_path = format!("{}/{}", playground_path, tests_dir); - let expected_file = format!("{}/{}", full_path , "cargo_sample.csv"); + let full_path = format!("{}/{}", Playground::root(), sandbox); + let expected_file = format!("{}/{}", full_path, "cargo_sample.csv"); nu!( _output, - cwd(&playground_path), + cwd(&Playground::root()), "open ../formats/cargo_sample.toml | inc package.version --minor | get package | save save_test/cargo_sample.csv" ); @@ -119,14 +131,14 @@ fn save_can_write_out_csv() { #[test] fn cp_can_copy_a_file() { - let (playground_path, tests_dir) = h::setup_playground_for("cp_test"); + let sandbox = Playground::setup_for("cp_test").test_dir_name(); - let full_path = format!("{}/{}", playground_path, tests_dir); - let expected_file = format!("{}/{}", full_path , "sample.ini"); + let full_path = format!("{}/{}", Playground::root(), sandbox); + let expected_file = format!("{}/{}", full_path, "sample.ini"); nu!( _output, - cwd(&playground_path), + cwd(&Playground::root()), "cp ../formats/sample.ini cp_test/sample.ini" ); @@ -135,14 +147,14 @@ fn cp_can_copy_a_file() { #[test] fn cp_copies_the_file_inside_directory_if_path_to_copy_is_directory() { - let (playground_path, tests_dir) = h::setup_playground_for("cp_test_2"); + let sandbox = Playground::setup_for("cp_test_2").test_dir_name(); - let full_path = format!("{}/{}", playground_path, tests_dir); - let expected_file = format!("{}/{}", full_path , "sample.ini"); + let full_path = format!("{}/{}", Playground::root(), sandbox); + let expected_file = format!("{}/{}", full_path, "sample.ini"); nu!( _output, - cwd(&playground_path), + cwd(&Playground::root()), "cp ../formats/sample.ini cp_test_2" ); @@ -151,86 +163,149 @@ fn cp_copies_the_file_inside_directory_if_path_to_copy_is_directory() { #[test] fn cp_error_if_attempting_to_copy_a_directory_to_another_directory() { - let (playground_path, _) = h::setup_playground_for("cp_test_3"); + Playground::setup_for("cp_test_3"); - nu_error!( - output, - cwd(&playground_path), - "cp ../formats cp_test_3" - ); + nu_error!(output, cwd(&Playground::root()), "cp ../formats cp_test_3"); assert!(output.contains("../formats")); assert!(output.contains("is a directory (not copied)")); } #[test] -fn rm_can_remove_a_file() { - let directory = "tests/fixtures/nuplayground"; - let file = format!("{}/rm_test.txt", directory); - - h::create_file_at(&file); +fn rm_removes_a_file() { + let sandbox = Playground::setup_for("rm_test") + .with_files(vec![EmptyFile("i_will_be_deleted.txt")]) + .test_dir_name(); nu!( _output, - cwd(directory), - "rm rm_test.txt" + cwd(&Playground::root()), + "rm rm_test/i_will_be_deleted.txt" ); - assert!(!h::file_exists_at(&file)); + assert!(!h::file_exists_at(&format!( + "{}/{}/{}", + Playground::root(), + sandbox, + "i_will_be_deleted.txt" + ))); } #[test] -fn rm_can_remove_directory_contents_with_recursive_flag() { - let (playground_path, tests_dir) = h::setup_playground_for("rm_test"); +fn rm_removes_files_with_wildcard() { + r#" + Given these files and directories + src + src/cli.rs + src/lib.rs + src/prelude.rs + src/parser + src/parser/parse.rs + src/parser/parser.rs + src/parser/parse + src/parser/hir + src/parser/parse/token_tree.rs + src/parser/hir/baseline_parse.rs + src/parser/hir/baseline_parse_tokens.rs + "#; - for f in ["yehuda.txt", "jonathan.txt", "andres.txt"].iter() { - h::create_file_at(&format!("{}/{}/{}", playground_path, tests_dir, f)); - } + let sandbox = Playground::setup_for("rm_test_wildcard") + .within("src") + .with_files(vec![ + EmptyFile("cli.rs"), + EmptyFile("lib.rs"), + EmptyFile("prelude.rs"), + ]) + .within("src/parser") + .with_files(vec![EmptyFile("parse.rs"), EmptyFile("parser.rs")]) + .within("src/parser/parse") + .with_files(vec![EmptyFile("token_tree.rs")]) + .within("src/parser/hir") + .with_files(vec![ + EmptyFile("baseline_parse.rs"), + EmptyFile("baseline_parse_tokens.rs"), + ]) + .test_dir_name(); + + let full_path = format!("{}/{}", Playground::root(), sandbox); + + r#" The pattern + src/*/*/*.rs + matches + src/parser/parse/token_tree.rs + src/parser/hir/baseline_parse.rs + src/parser/hir/baseline_parse_tokens.rs + "#; + + nu!( + _output, + cwd("tests/fixtures/nuplayground/rm_test_wildcard"), + "rm \"src/*/*/*.rs\"" + ); + + assert!(!h::file_exists_at(&format!( + "{}/src/parser/parse/token_tree.rs", + full_path + ))); + assert!(!h::file_exists_at(&format!( + "{}/src/parser/hir/baseline_parse.rs", + full_path + ))); + assert!(!h::file_exists_at(&format!( + "{}/src/parser/hir/baseline_parse_tokens.rs", + full_path + ))); + + assert_eq!( + Playground::glob_vec(&format!("{}/src/*/*/*.rs", full_path)), + Vec::::new() + ); +} + +#[test] +fn rm_removes_directory_contents_with_recursive_flag() { + let sandbox = Playground::setup_for("rm_test_recursive") + .with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]) + .test_dir_name(); nu!( _output, cwd("tests/fixtures/nuplayground"), - "rm rm_test --recursive" + "rm rm_test_recursive --recursive" ); - assert!(!h::file_exists_at(&format!("{}/{}", playground_path, tests_dir))); + assert!(!h::file_exists_at(&format!( + "{}/{}", + Playground::root(), + sandbox + ))); } #[test] -fn rm_error_if_attempting_to_delete_a_directory_without_recursive_flag() { - let (playground_path, tests_dir) = h::setup_playground_for("rm_test_2"); - let full_path = format!("{}/{}", playground_path, tests_dir); +fn rm_errors_if_attempting_to_delete_a_directory_without_recursive_flag() { + let sandbox = Playground::setup_for("rm_test_2").test_dir_name(); + let full_path = format!("{}/{}", Playground::root(), sandbox); - nu_error!( - output, - cwd("tests/fixtures/nuplayground"), - "rm rm_test_2" - ); + nu_error!(output, cwd(&Playground::root()), "rm rm_test_2"); assert!(h::file_exists_at(&full_path)); assert!(output.contains("is a directory")); } #[test] -fn rm_error_if_attempting_to_delete_single_dot_as_argument() { - - nu_error!( - output, - cwd("tests/fixtures/nuplayground"), - "rm ." - ); +fn rm_errors_if_attempting_to_delete_single_dot_as_argument() { + nu_error!(output, cwd(&Playground::root()), "rm ."); assert!(output.contains("may not be removed")); } #[test] -fn rm_error_if_attempting_to_delete_two_dot_as_argument() { - - nu_error!( - output, - cwd("tests/fixtures/nuplayground"), - "rm .." - ); +fn rm_errors_if_attempting_to_delete_two_dot_as_argument() { + nu_error!(output, cwd(&Playground::root()), "rm .."); assert!(output.contains("may not be removed")); -} \ No newline at end of file +} diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs index 38aac3144b..87b5d96005 100644 --- a/tests/helpers/mod.rs +++ b/tests/helpers/mod.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] +use glob::glob; +pub use std::path::Path; pub use std::path::PathBuf; use std::io::Read; @@ -79,17 +81,80 @@ macro_rules! nu_error { }; } -pub fn setup_playground_for(topic: &str) -> (String, String) { - let home = "tests/fixtures/nuplayground"; - let full_path = format!("{}/{}", home, topic); +pub enum Stub<'a> { + FileWithContent(&'a str, &'a str), + EmptyFile(&'a str), +} - if file_exists_at(&full_path) { - delete_directory_at(&full_path); +pub struct Playground { + tests: String, + cwd: PathBuf, +} + +impl Playground { + pub fn root() -> String { + String::from("tests/fixtures/nuplayground") } - create_directory_at(&full_path); + pub fn test_dir_name(&self) -> String { + self.tests.clone() + } - (home.to_string(), topic.to_string()) + pub fn back_to_playground(&mut self) -> &mut Self { + self.cwd = PathBuf::from([Playground::root(), self.tests.clone()].join("/")); + self + } + + pub fn setup_for(topic: &str) -> Playground { + let nuplay_dir = format!("{}/{}", Playground::root(), topic); + + if PathBuf::from(&nuplay_dir).exists() { + std::fs::remove_dir_all(PathBuf::from(&nuplay_dir)).expect("can not remove directory"); + } + + std::fs::create_dir(PathBuf::from(&nuplay_dir)).expect("can not create directory"); + + Playground { + tests: topic.to_string(), + cwd: PathBuf::from([Playground::root(), topic.to_string()].join("/")), + } + } + + pub fn cd(&mut self, path: &str) -> &mut Self { + self.cwd.push(path); + self + } + + pub fn with_files(&mut self, files: Vec) -> &mut Self { + files + .iter() + .map(|f| { + let mut path = PathBuf::from(&self.cwd); + + let (file_name, contents) = match *f { + Stub::EmptyFile(name) => (name, "fake data"), + Stub::FileWithContent(name, content) => (name, content), + }; + + path.push(file_name); + + std::fs::write(PathBuf::from(path), contents.as_bytes()) + .expect("can not create file"); + }) + .for_each(drop); + self.back_to_playground(); + self + } + + pub fn within(&mut self, directory: &str) -> &mut Self { + self.cwd.push(directory); + std::fs::create_dir(&self.cwd).expect("can not create directory"); + self + } + + pub fn glob_vec(pattern: &str) -> Vec { + glob(pattern).unwrap().map(|r| r.unwrap()).collect() + } } pub fn file_contents(full_path: &str) -> String { @@ -116,10 +181,6 @@ pub fn delete_directory_at(full_path: &str) { std::fs::remove_dir_all(PathBuf::from(full_path)).expect("can not remove directory"); } -pub fn create_directory_at(full_path: &str) { - std::fs::create_dir(PathBuf::from(full_path)).expect("can not create directory"); -} - pub fn executable_path() -> PathBuf { let mut buf = PathBuf::new(); buf.push("target");