diff --git a/Cargo.lock b/Cargo.lock index ef2ad6f6d532c..691aa4be0e286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2884,6 +2884,7 @@ dependencies = [ "ureq", "url", "uu_cp", + "uu_mkdir", "uu_whoami", "uuid", "wax", @@ -5621,6 +5622,16 @@ dependencies = [ "xattr", ] +[[package]] +name = "uu_mkdir" +version = "0.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4776960a036a4ec375f0701004a41013d66d2e3e46a19e9216fd18d4d92f88f3" +dependencies = [ + "clap", + "uucore", +] + [[package]] name = "uu_whoami" version = "0.0.22" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 226639369ede2..3387e28908056 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -89,6 +89,7 @@ ureq = { version = "2.8", default-features = false, features = ["charset", "gzip url = "2.2" uu_cp = "0.0.22" uu_whoami = "0.0.22" +uu_mkdir = "0.0.22" uuid = { version = "1.5", features = ["v4"] } wax = { version = "0.6" } which = { version = "5.0", optional = true } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 5a2c9f89a33a1..0c50edda7bbef 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -204,6 +204,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { Cd, Ls, Mkdir, + UMkdir, Mv, Cp, UCp, diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index 1cdad31c239ce..caaca5bc86b1e 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -10,6 +10,7 @@ mod save; mod start; mod touch; mod ucp; +mod umkdir; mod util; mod watch; @@ -25,4 +26,5 @@ pub use save::Save; pub use start::Start; pub use touch::Touch; pub use ucp::UCp; +pub use umkdir::UMkdir; pub use watch::Watch; diff --git a/crates/nu-command/src/filesystem/umkdir.rs b/crates/nu-command/src/filesystem/umkdir.rs new file mode 100644 index 0000000000000..cb89c230a8746 --- /dev/null +++ b/crates/nu-command/src/filesystem/umkdir.rs @@ -0,0 +1,98 @@ +use nu_engine::env::current_dir; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type}; + +use uu_mkdir::mkdir; + +#[derive(Clone)] +pub struct UMkdir; + +const IS_RECURSIVE: bool = true; +// This is the same default as Rust's std uses: +// https://doc.rust-lang.org/nightly/std/os/unix/fs/trait.DirBuilderExt.html#tymethod.mode +const DEFAULT_MODE: u32 = 0o777; + +impl Command for UMkdir { + fn name(&self) -> &str { + "umkdir" + } + + fn usage(&self) -> &str { + "Create directories, with intermediary directories if required using uutils/coreutils mkdir." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["directory", "folder", "create", "make_dirs", "coreutils"] + } + + fn signature(&self) -> Signature { + Signature::build("umkdir") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .rest( + "rest", + SyntaxShape::Directory, + "the name(s) of the path(s) to create", + ) + .switch( + "verbose", + "print a message for each created directory.", + Some('v'), + ) + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let path = current_dir(engine_state, stack)?; + let mut directories = call + .rest::(engine_state, stack, 0)? + .into_iter() + .map(|dir| path.join(dir)) + .peekable(); + + let is_verbose = call.has_flag("verbose"); + + if directories.peek().is_none() { + return Err(ShellError::MissingParameter { + param_name: "requires directory paths".to_string(), + span: call.head, + }); + } + + for dir in directories { + if let Err(error) = mkdir(&dir, IS_RECURSIVE, DEFAULT_MODE, is_verbose) { + return Err(ShellError::GenericError( + format!("{}", error), + format!("{}", error), + None, + None, + Vec::new(), + )); + } + } + + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Make a directory named foo", + example: "umkdir foo", + result: None, + }, + Example { + description: "Make multiple directories and show the paths created", + example: "umkdir -v foo/bar foo2", + result: None, + }, + ] + } +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 38b7c2bf5a5dc..7e75c920de83f 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -105,6 +105,7 @@ mod touch; mod transpose; mod try_; mod ucp; +mod umkdir; mod uniq; mod uniq_by; mod update; diff --git a/crates/nu-command/tests/commands/umkdir.rs b/crates/nu-command/tests/commands/umkdir.rs new file mode 100644 index 0000000000000..8d4cf78c79018 --- /dev/null +++ b/crates/nu-command/tests/commands/umkdir.rs @@ -0,0 +1,124 @@ +use nu_test_support::fs::files_exist_at; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; +use std::path::Path; + +#[test] +fn creates_directory() { + Playground::setup("umkdir_test_1", |dirs, _| { + nu!( + cwd: dirs.test(), + "umkdir my_new_directory" + ); + + let expected = dirs.test().join("my_new_directory"); + + assert!(expected.exists()); + }) +} + +#[test] +fn accepts_and_creates_directories() { + Playground::setup("umkdir_test_2", |dirs, _| { + nu!( + cwd: dirs.test(), + "umkdir dir_1 dir_2 dir_3" + ); + + assert!(files_exist_at( + vec![Path::new("dir_1"), Path::new("dir_2"), Path::new("dir_3")], + dirs.test() + )); + }) +} + +#[test] +fn creates_intermediary_directories() { + Playground::setup("umkdir_test_3", |dirs, _| { + nu!( + cwd: dirs.test(), + "umkdir some_folder/another/deeper_one" + ); + + let expected = dirs.test().join("some_folder/another/deeper_one"); + + assert!(expected.exists()); + }) +} + +#[test] +fn create_directory_two_parents_up_using_multiple_dots() { + Playground::setup("umkdir_test_4", |dirs, sandbox| { + sandbox.within("foo").mkdir("bar"); + + nu!( + cwd: dirs.test().join("foo/bar"), + "umkdir .../boo" + ); + + let expected = dirs.test().join("boo"); + + assert!(expected.exists()); + }) +} + +#[test] +fn print_created_paths() { + Playground::setup("umkdir_test_2", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + pipeline("umkdir -v dir_1 dir_2 dir_3") + ); + + assert!(files_exist_at( + vec![Path::new("dir_1"), Path::new("dir_2"), Path::new("dir_3")], + dirs.test() + )); + + assert!(actual.out.contains("dir_1")); + assert!(actual.out.contains("dir_2")); + assert!(actual.out.contains("dir_3")); + }) +} + +#[test] +fn creates_directory_three_dots() { + Playground::setup("umkdir_test_1", |dirs, _| { + nu!( + cwd: dirs.test(), + "umkdir test..." + ); + + let expected = dirs.test().join("test..."); + + assert!(expected.exists()); + }) +} + +#[test] +fn creates_directory_four_dots() { + Playground::setup("umkdir_test_1", |dirs, _| { + nu!( + cwd: dirs.test(), + "umkdir test...." + ); + + let expected = dirs.test().join("test...."); + + assert!(expected.exists()); + }) +} + +#[test] +fn creates_directory_three_dots_quotation_marks() { + Playground::setup("umkdir_test_1", |dirs, _| { + nu!( + cwd: dirs.test(), + "umkdir 'test...'" + ); + + let expected = dirs.test().join("test..."); + + assert!(expected.exists()); + }) +}