diff --git a/README.md b/README.md index dfdc899400..10e4985925 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ Nu adheres closely to a set of goals that make up its design philosophy. As feat | cd path | Change to a new path | | cp source path | Copy files | | ls (path) | View the contents of the current or given path | +| mkdir path | Make directories, (to create intermediary directories append '--p') | | date (--utc) | Get the current datetime | | ps | View current processes | | sys | View information about the current system | diff --git a/src/commands/cp.rs b/src/commands/cp.rs index 00183647bc..fb3b056af8 100644 --- a/src/commands/cp.rs +++ b/src/commands/cp.rs @@ -31,10 +31,79 @@ impl Command for Copycp { } } +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Res { + pub loc: PathBuf, + pub at: usize, +} + +impl Res {} + +pub struct FileStructure { + root: PathBuf, + resources: Vec, +} + +impl FileStructure { + pub fn new() -> FileStructure { + FileStructure { + root: PathBuf::new(), + resources: Vec::::new(), + } + } + + pub fn set_root(&mut self, path: &Path) { + self.root = path.to_path_buf(); + } + + pub fn translate(&mut self, to: F) -> Vec<(PathBuf, PathBuf)> + where + F: Fn((PathBuf, usize)) -> (PathBuf, PathBuf), + { + self.resources + .iter() + .map(|f| (PathBuf::from(&f.loc), f.at)) + .map(|f| to(f)) + .collect() + } + + pub fn walk_decorate(&mut self, start_path: &Path) { + self.set_root(&dunce::canonicalize(start_path).unwrap()); + self.resources = Vec::::new(); + self.build(start_path, 0); + self.resources.sort(); + } + + fn build(&mut self, src: &'a Path, lvl: usize) { + let source = dunce::canonicalize(src).unwrap(); + + if source.is_dir() { + for entry in std::fs::read_dir(&source).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + + if path.is_dir() { + self.build(&path, lvl + 1); + } + + self.resources.push(Res { + loc: path.to_path_buf(), + at: lvl, + }); + } + } else { + self.resources.push(Res { + loc: source, + at: lvl, + }); + } + } +} + pub fn cp(args: CommandArgs) -> Result { let mut source = PathBuf::from(args.shell_manager.path()); let mut destination = PathBuf::from(args.shell_manager.path()); - + let name_span = args.call_info.name_span; match args .nth(0) @@ -58,124 +127,273 @@ pub fn cp(args: CommandArgs) -> Result { } } - let (sources, destinations) = ( - glob::glob(&source.to_string_lossy()), - glob::glob(&destination.to_string_lossy()), - ); + let sources = glob::glob(&source.to_string_lossy()); - if sources.is_err() || destinations.is_err() { - return Err(ShellError::string("Invalid pattern.")); + if sources.is_err() { + return Err(ShellError::labeled_error( + "Invalid pattern.", + "Invalid pattern.", + args.nth(0).unwrap().span(), + )); } - let (sources, destinations): (Vec<_>, Vec<_>) = - (sources.unwrap().collect(), destinations.unwrap().collect()); + let sources: Vec<_> = sources.unwrap().collect(); if sources.len() == 1 { - if let Ok(entry) = &sources[0] { - if entry.is_file() { - if destinations.len() == 1 { - if let Ok(dst) = &destinations[0] { - if dst.is_file() { - std::fs::copy(entry, dst); - } + if let Ok(val) = &sources[0] { + if val.is_dir() && !args.has("recursive") { + return Err(ShellError::labeled_error( + "is a directory (not copied). Try using \"--recursive\".", + "is a directory (not copied). Try using \"--recursive\".", + args.nth(0).unwrap().span(), + )); + } - if dst.is_dir() { - destination.push(entry.file_name().unwrap()); - std::fs::copy(entry, destination); - } - } - } else if destinations.is_empty() { - if destination.is_dir() { - destination.push(entry.file_name().unwrap()); - std::fs::copy(entry, destination); + let mut sources: FileStructure = FileStructure::new(); + + sources.walk_decorate(&val); + + if val.is_file() { + for (ref src, ref dst) in sources.translate(|(src, _)| { + if destination.exists() { + let mut dst = dunce::canonicalize(destination.clone()).unwrap(); + dst.push(val.file_name().unwrap()); + (src, dst) } else { - std::fs::copy(entry, destination); + (src, destination.clone()) + } + }) { + if src.is_file() { + match std::fs::copy(src, dst) { + Err(e) => { + return Err(ShellError::labeled_error( + e.to_string(), + e.to_string(), + name_span, + )); + } + Ok(o) => o, + }; } } } - if entry.is_dir() { - if destinations.len() == 1 { - if let Ok(dst) = &destinations[0] { - if dst.is_dir() && !args.has("recursive") { - return Err(ShellError::string(&format!( - "{:?} is a directory (not copied)", - entry.to_string_lossy() - ))); + if val.is_dir() { + if !destination.exists() { + match std::fs::create_dir_all(&destination) { + Err(e) => { + return Err(ShellError::labeled_error( + e.to_string(), + e.to_string(), + name_span, + )); + } + Ok(o) => o, + }; + + for (ref src, ref dst) in sources.translate(|(src, loc)| { + let mut final_path = destination.clone(); + let path = dunce::canonicalize(&src).unwrap(); + + let mut comps: Vec<_> = path + .components() + .map(|fragment| fragment.as_os_str()) + .rev() + .take(1 + loc) + .collect(); + + comps.reverse(); + + for fragment in comps.iter() { + final_path.push(fragment); } - - if dst.is_dir() && args.has("recursive") { - let entries = std::fs::read_dir(&entry); - - let entries = match entries { - Err(e) => { - if let Some(s) = args.nth(0) { + (PathBuf::from(&src), PathBuf::from(final_path)) + }) { + if src.is_dir() { + if !dst.exists() { + match std::fs::create_dir_all(dst) { + Err(e) => { return Err(ShellError::labeled_error( e.to_string(), e.to_string(), - s.span(), - )); - } else { - return Err(ShellError::labeled_error( - e.to_string(), - e.to_string(), - args.call_info.name_span, + name_span, )); } + Ok(o) => o, + }; + } + } + + if src.is_file() { + match std::fs::copy(src, dst) { + Err(e) => { + return Err(ShellError::labeled_error( + e.to_string(), + e.to_string(), + name_span, + )); } Ok(o) => o, }; + } + } + } else { + destination.push(val.file_name().unwrap()); - let mut x = dst.clone(); + match std::fs::create_dir_all(&destination) { + Err(e) => { + return Err(ShellError::labeled_error( + e.to_string(), + e.to_string(), + name_span, + )); + } + Ok(o) => o, + }; - //x.pop(); - x.push(entry.file_name().unwrap()); + for (ref src, ref dst) in sources.translate(|(src, loc)| { + let mut final_path = dunce::canonicalize(&destination).unwrap(); + let path = dunce::canonicalize(&src).unwrap(); + let mut comps: Vec<_> = path + .components() + .map(|fragment| fragment.as_os_str()) + .rev() + .take(1 + loc) + .collect(); - std::fs::create_dir(&x).expect("can not create directory"); + comps.reverse(); - for entry in entries { - let entry = entry?; - let file_path = entry.path(); - let file_name = file_path.file_name().unwrap(); + for fragment in comps.iter() { + final_path.push(fragment); + } - let mut d = PathBuf::new(); - d.push(&x); - d.push(file_name); - - std::fs::copy(entry.path(), d); + (PathBuf::from(&src), PathBuf::from(final_path)) + }) { + if src.is_dir() { + if !dst.exists() { + match std::fs::create_dir_all(dst) { + Err(e) => { + return Err(ShellError::labeled_error( + e.to_string(), + e.to_string(), + name_span, + )); + } + Ok(o) => o, + }; } } + + if src.is_file() { + match std::fs::copy(src, dst) { + Err(e) => { + return Err(ShellError::labeled_error( + e.to_string(), + e.to_string(), + name_span, + )); + } + Ok(o) => o, + }; + } } } } } - } - /* - if destination.is_dir() { - if source.is_file() { - let file_name = source.file_name().expect(""); - let file_name = file_name.to_str().expect(""); - destination.push(Path::new(file_name)); - - match std::fs::copy(source, destination) { - Err(_error) => return Err(ShellError::string("can not copy file")), - Ok(_) => return Ok(OutputStream::empty()), + } else { + if destination.exists() { + if !sources.iter().all(|x| (x.as_ref().unwrap()).is_file()) && !args.has("recursive") { + return Err(ShellError::labeled_error( + "Copy aborted (directories found). Try using \"--recursive\".", + "Copy aborted (directories found). Try using \"--recursive\".", + args.nth(0).unwrap().span(), + )); } - } else if source.is_dir() { - return Err(ShellError::string(&format!( - "{:?} is a directory (not copied)", - source.to_string_lossy() - ))); + for entry in sources { + if let Ok(entry) = entry { + let mut to = PathBuf::from(&destination); + to.push(&entry.file_name().unwrap()); + match std::fs::copy(&entry, &to) { + Err(e) => { + return Err(ShellError::labeled_error( + e.to_string(), + e.to_string(), + name_span, + )); + } + Ok(o) => o, + }; + } + } + } else { + return Err(ShellError::labeled_error( + format!( + "Copy aborted. (Does {:?} exist?)", + &destination.file_name().unwrap() + ), + format!( + "Copy aborted. (Does {:?} exist?)", + &destination.file_name().unwrap() + ), + args.nth(1).unwrap().span(), + )); } } - match std::fs::copy(source, destination) { - Err(_error) => Err(ShellError::string("can not copy file")), - Ok(_) => Ok(OutputStream::empty()), - }*/ Ok(OutputStream::empty()) } + +#[cfg(test)] +mod tests { + + use super::{FileStructure, Res}; + use std::path::PathBuf; + + fn fixtures() -> PathBuf { + let mut sdx = PathBuf::new(); + sdx.push("tests"); + sdx.push("fixtures"); + sdx.push("formats"); + dunce::canonicalize(sdx).unwrap() + } + + #[test] + fn prepares_and_decorates_source_files_for_copying() { + let mut res = FileStructure::new(); + res.walk_decorate(fixtures().as_path()); + + assert_eq!( + res.resources, + vec![ + Res { + loc: fixtures().join("appveyor.yml"), + at: 0 + }, + Res { + loc: fixtures().join("caco3_plastics.csv"), + at: 0 + }, + Res { + loc: fixtures().join("cargo_sample.toml"), + at: 0 + }, + Res { + loc: fixtures().join("jonathan.xml"), + at: 0 + }, + Res { + loc: fixtures().join("sample.ini"), + at: 0 + }, + Res { + loc: fixtures().join("sgml_description.json"), + at: 0 + } + ] + ); + } +} diff --git a/src/commands/mkdir.rs b/src/commands/mkdir.rs index efa732827a..13c8e2ef43 100644 --- a/src/commands/mkdir.rs +++ b/src/commands/mkdir.rs @@ -34,7 +34,6 @@ impl Command for Mkdir { pub fn mkdir(args: CommandArgs) -> Result { let env = args.env.lock().unwrap(); let path = env.path.to_path_buf(); - let cwd = path.clone(); let mut full_path = PathBuf::from(path); match &args.nth(0) { diff --git a/src/commands/rm.rs b/src/commands/rm.rs index aa0d10c43f..c57446bcf1 100644 --- a/src/commands/rm.rs +++ b/src/commands/rm.rs @@ -61,7 +61,7 @@ pub fn rm(args: CommandArgs) -> Result { if !args.has("recursive") { return Err(ShellError::labeled_error( "is a directory", - "", + "is a directory", args.call_info.name_span, )); } diff --git a/tests/command_clip_tests.rs b/tests/command_clip_tests.rs deleted file mode 100644 index 5d418c9a60..0000000000 --- a/tests/command_clip_tests.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod helpers; - -use helpers::in_directory as cwd; - -use clipboard::{ClipboardProvider, ClipboardContext}; - -#[test] -fn clip() { - - let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); - - nu!( - _output, - cwd("tests/fixtures/formats"), - "open caco3_plastics.csv --raw | lines | clip" - ); - - - assert!(ctx.get_contents().is_ok()); -} \ No newline at end of file diff --git a/tests/command_cp_tests.rs b/tests/command_cp_tests.rs index 59d145b911..ef2a68c364 100644 --- a/tests/command_cp_tests.rs +++ b/tests/command_cp_tests.rs @@ -6,7 +6,7 @@ use helpers as h; use std::path::{Path, PathBuf}; #[test] -fn cp_copies_a_file() { +fn copies_a_file() { let sandbox = Playground::setup_for("cp_test").test_dir_name(); let full_path = format!("{}/{}", Playground::root(), sandbox); @@ -22,7 +22,7 @@ fn cp_copies_a_file() { } #[test] -fn cp_copies_the_file_inside_directory_if_path_to_copy_is_directory() { +fn copies_the_file_inside_directory_if_path_to_copy_is_directory() { let sandbox = Playground::setup_for("cp_test_2").test_dir_name(); let full_path = format!("{}/{}", Playground::root(), sandbox); @@ -38,7 +38,7 @@ 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() { +fn error_if_attempting_to_copy_a_directory_to_another_directory() { Playground::setup_for("cp_test_3"); nu_error!(output, cwd(&Playground::root()), "cp ../formats cp_test_3"); @@ -48,7 +48,7 @@ fn cp_error_if_attempting_to_copy_a_directory_to_another_directory() { } #[test] -fn cp_copies_the_directory_inside_directory_if_path_to_copy_is_directory_and_with_recursive_flag() { +fn copies_the_directory_inside_directory_if_path_to_copy_is_directory_and_with_recursive_flag() { let sandbox = Playground::setup_for("cp_test_4") .within("originals") .with_files(vec![ @@ -69,5 +69,101 @@ fn cp_copies_the_directory_inside_directory_if_path_to_copy_is_directory_and_wit ); assert!(h::dir_exists_at(PathBuf::from(&expected_dir))); - assert!(h::files_exist_at(vec![Path::new("yehuda.txt"), Path::new("jonathan.txt"), Path::new("andres.txt")], PathBuf::from(&expected_dir))); -} \ No newline at end of file + assert!(h::files_exist_at( + vec![ + Path::new("yehuda.txt"), + Path::new("jonathan.txt"), + Path::new("andres.txt") + ], + PathBuf::from(&expected_dir) + )); +} + +#[test] +fn deep_copies_with_recursive_flag() { + r#" + Given these files and directories + originals + originals/manifest.txt + originals/contributors + originals/contributors/yehuda.txt + originals/contributors/jonathan.txt + originals/contributors/andres.txt + originals/contributors/jonathan + originals/contributors/jonathan/errors.txt + originals/contributors/jonathan/multishells.txt + originals/contributors/andres + originals/contributors/andres/coverage.txt + originals/contributors/andres/commands.txt + originals/contributors/yehuda + originals/contributors/yehuda/defer-evaluation.txt + "#; + + let sandbox = Playground::setup_for("cp_test_5") + .within("originals") + .with_files(vec![EmptyFile("manifest.txt")]) + .within("originals/contributors") + .with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]) + .within("originals/contributors/jonathan") + .with_files(vec![EmptyFile("errors.txt"), EmptyFile("multishells.txt")]) + .within("originals/contributors/andres") + .with_files(vec![EmptyFile("coverage.txt"), EmptyFile("commands.txt")]) + .within("originals/contributors/yehuda") + .with_files(vec![EmptyFile("defer-evaluation.txt")]) + .within("copies_expected") + .test_dir_name(); + + let full_path = format!("{}/{}", Playground::root(), sandbox); + let expected_dir = format!("{}/{}", full_path, "copies_expected/originals"); + + let jonathans_expected_copied_dir = format!("{}/contributors/jonathan", expected_dir); + let andres_expected_copied_dir = format!("{}/contributors/andres", expected_dir); + let yehudas_expected_copied_dir = format!("{}/contributors/yehuda", expected_dir); + + nu!( + _output, + cwd(&full_path), + "cp originals copies_expected --recursive" + ); + + assert!(h::dir_exists_at(PathBuf::from(&expected_dir))); + assert!(h::files_exist_at( + vec![Path::new("errors.txt"), Path::new("multishells.txt")], + PathBuf::from(&jonathans_expected_copied_dir) + )); + assert!(h::files_exist_at( + vec![Path::new("coverage.txt"), Path::new("commands.txt")], + PathBuf::from(&andres_expected_copied_dir) + )); + assert!(h::files_exist_at( + vec![Path::new("defer-evaluation.txt")], + PathBuf::from(&yehudas_expected_copied_dir) + )); +} + +#[test] +fn copies_using_globs() { + let sandbox = Playground::setup_for("cp_test_6").test_dir_name(); + let expected_copies_path = format!("{}/{}", Playground::root(), sandbox); + + nu!( + _output, + cwd(&Playground::root()), + "cp ../formats/* cp_test_6" + ); + + assert!(h::files_exist_at( + vec![ + Path::new("caco3_plastics.csv"), + Path::new("cargo_sample.toml"), + Path::new("jonathan.xml"), + Path::new("sample.ini"), + Path::new("sgml_description.json") + ], + PathBuf::from(&expected_copies_path) + )); +}