diff --git a/Cargo.lock b/Cargo.lock index 65709e251e..4cee1d4209 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,21 @@ dependencies = [ "chrono", ] +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "core-foundation-sys" version = "0.8.2" @@ -240,6 +255,18 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9dd058f8b65922819fabb4a41e7d1964e56344042c26efbccd465202c23fa0c" +dependencies = [ + "console", + "lazy_static", + "tempfile", + "zeroize", +] + [[package]] name = "diff" version = "0.1.12" @@ -291,6 +318,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "engine-q" version = "0.1.0" @@ -541,6 +574,7 @@ version = "0.1.0" dependencies = [ "bytesize", "chrono", + "dialoguer", "glob", "lscolors", "nu-engine", @@ -1258,3 +1292,9 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zeroize" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 5b24859f37..8053070e90 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -25,6 +25,7 @@ chrono = { version = "0.4.19", features = ["serde"] } terminal_size = "0.1.17" lscolors = { version = "0.8.0", features = ["crossterm"] } bytesize = "1.1.0" +dialoguer = "0.8.0" [features] trash-support = ["trash"] diff --git a/crates/nu-command/src/filesystem/cp.rs b/crates/nu-command/src/filesystem/cp.rs index b042d50f11..8c6ea0655a 100644 --- a/crates/nu-command/src/filesystem/cp.rs +++ b/crates/nu-command/src/filesystem/cp.rs @@ -1,6 +1,7 @@ use std::env::current_dir; use std::path::PathBuf; +use super::interactive_helper::get_confirmation; use nu_engine::CallExt; use nu_path::canonicalize_with; use nu_protocol::ast::Call; @@ -11,6 +12,7 @@ use crate::filesystem::util::FileStructure; pub struct Cp; +#[allow(unused_must_use)] impl Command for Cp { fn name(&self) -> &str { "cp" @@ -29,6 +31,8 @@ impl Command for Cp { "copy recursively through subdirectories", Some('r'), ) + .switch("force", "suppress error when no file", Some('f')) + .switch("interactive", "ask user to confirm action", Some('i')) } fn run( @@ -39,12 +43,14 @@ impl Command for Cp { ) -> Result { let source: String = call.req(context, 0)?; let destination: String = call.req(context, 1)?; + let interactive = call.has_flag("interactive"); + let force = call.has_flag("force"); let path: PathBuf = current_dir().unwrap(); let source = path.join(source.as_str()); let destination = path.join(destination.as_str()); - let sources = + let mut sources = glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect); if sources.is_empty() { return Err(ShellError::FileNotFound(call.positional[0].span)); @@ -68,6 +74,38 @@ impl Command for Cp { )); } + if interactive && !force { + let mut remove: Vec = vec![]; + for (index, file) in sources.iter().enumerate() { + let prompt = format!( + "Are you shure that you want to copy {} to {}?", + file.as_ref() + .unwrap() + .file_name() + .unwrap() + .to_str() + .unwrap(), + destination.file_name().unwrap().to_str().unwrap() + ); + + let input = get_confirmation(prompt)?; + + if !input { + remove.push(index); + } + } + + remove.reverse(); + + for index in remove { + sources.remove(index); + } + + if sources.is_empty() { + return Err(ShellError::NoFileToBeCopied()); + } + } + for entry in sources.into_iter().flatten() { let mut sources = FileStructure::new(); sources.walk_decorate(&entry)?; diff --git a/crates/nu-command/src/filesystem/interactive_helper.rs b/crates/nu-command/src/filesystem/interactive_helper.rs new file mode 100644 index 0000000000..939caa5658 --- /dev/null +++ b/crates/nu-command/src/filesystem/interactive_helper.rs @@ -0,0 +1,26 @@ +use dialoguer::Input; +use std::error::Error; + +pub fn get_confirmation(prompt: String) -> Result> { + let input = Input::new() + .with_prompt(prompt) + .validate_with(|c_input: &String| -> Result<(), String> { + if c_input.len() == 1 + && (c_input == "y" || c_input == "Y" || c_input == "n" || c_input == "N") + { + Ok(()) + } else if c_input.len() > 1 { + Err("Enter only one letter (Y/N)".to_string()) + } else { + Err("Input not valid".to_string()) + } + }) + .default("Y/N".into()) + .interact_text()?; + + if input == "y" || input == "Y" { + Ok(true) + } else { + Ok(false) + } +} diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index db5bc4bea6..9649859945 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -1,5 +1,6 @@ mod cd; mod cp; +mod interactive_helper; mod ls; mod mkdir; mod mv; diff --git a/crates/nu-command/src/filesystem/mv.rs b/crates/nu-command/src/filesystem/mv.rs index 98dd8a3217..fe9b919432 100644 --- a/crates/nu-command/src/filesystem/mv.rs +++ b/crates/nu-command/src/filesystem/mv.rs @@ -1,6 +1,7 @@ use std::env::current_dir; use std::path::{Path, PathBuf}; +use super::interactive_helper::get_confirmation; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EvaluationContext}; @@ -8,6 +9,7 @@ use nu_protocol::{ShellError, Signature, SyntaxShape, Value}; pub struct Mv; +#[allow(unused_must_use)] impl Command for Mv { fn name(&self) -> &str { "mv" @@ -29,6 +31,8 @@ impl Command for Mv { SyntaxShape::Filepath, "the location to move files/directories to", ) + .switch("interactive", "ask user to confirm action", Some('i')) + .switch("force", "suppress error when no file", Some('f')) } fn run( @@ -40,6 +44,8 @@ impl Command for Mv { // TODO: handle invalid directory or insufficient permissions when moving let source: String = call.req(context, 0)?; let destination: String = call.req(context, 1)?; + let interactive = call.has_flag("interactive"); + let force = call.has_flag("force"); let path: PathBuf = current_dir().unwrap(); let source = path.join(source.as_str()); @@ -54,6 +60,38 @@ impl Command for Mv { )); } + if interactive && !force { + let mut remove: Vec = vec![]; + for (index, file) in sources.iter().enumerate() { + let prompt = format!( + "Are you shure that you want to move {} to {}?", + file.as_ref() + .unwrap() + .file_name() + .unwrap() + .to_str() + .unwrap(), + destination.file_name().unwrap().to_str().unwrap() + ); + + let input = get_confirmation(prompt)?; + + if !input { + remove.push(index); + } + } + + remove.reverse(); + + for index in remove { + sources.remove(index); + } + + if sources.is_empty() { + return Err(ShellError::NoFileToBeMoved()); + } + } + if (destination.exists() && !destination.is_dir() && sources.len() > 1) || (!destination.exists() && sources.len() > 1) { diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs index e10206a1c6..48d9126700 100644 --- a/crates/nu-command/src/filesystem/rm.rs +++ b/crates/nu-command/src/filesystem/rm.rs @@ -3,6 +3,8 @@ use std::env::current_dir; use std::os::unix::prelude::FileTypeExt; use std::path::PathBuf; +use super::interactive_helper::get_confirmation; + use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EvaluationContext}; @@ -44,6 +46,7 @@ impl Command for Rm { ) .switch("recursive", "delete subdirectories recursively", Some('r')) .switch("force", "suppress error when no file", Some('f')) + .switch("interactive", "ask user to confirm action", Some('i')) .rest( "rest", SyntaxShape::GlobPattern, @@ -64,6 +67,7 @@ impl Command for Rm { fn rm(context: &EvaluationContext, call: &Call) -> Result { let trash = call.has_flag("trash"); let permanent = call.has_flag("permanent"); + let interactive = call.has_flag("interactive"); if trash && permanent { return Err(ShellError::IncompatibleParametersSingle( @@ -122,6 +126,32 @@ fn rm(context: &EvaluationContext, call: &Call) -> Result { let recursive = call.has_flag("recursive"); let force = call.has_flag("force"); + if interactive && !force { + let mut remove: Vec = vec![]; + for (index, file) in targets.iter().enumerate() { + let prompt: String = format!( + "Are you sure that you what to delete {}?", + file.1.file_name().unwrap().to_str().unwrap() + ); + + let input = get_confirmation(prompt)?; + + if !input { + remove.push(index); + } + } + + remove.reverse(); + + for index in remove { + targets.remove(index); + } + + if targets.is_empty() { + return Err(ShellError::NoFileToBeRemoved()); + } + } + let args = RmArgs { targets, recursive, diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 1329205c49..88a1880ddf 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -158,6 +158,13 @@ pub enum ShellError { #[error("Remove not possible")] #[diagnostic(code(nu::shell::remove_not_possible), url(docsrs))] RemoveNotPossible(String, #[label("{0}")] Span), + + #[error("No file to be removed")] + NoFileToBeRemoved(), + #[error("No file to be moved")] + NoFileToBeMoved(), + #[error("No file to be copied")] + NoFileToBeCopied(), } impl From for ShellError {