diff --git a/Cargo.lock b/Cargo.lock index 9831ee895..1a20ba9a9 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 = "console" version = "0.15.0" @@ -255,13 +270,25 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9dd058f8b65922819fabb4a41e7d1964e56344042c26efbccd465202c23fa0c" +dependencies = [ + "console 0.14.1", + "lazy_static", + "tempfile", + "zeroize", +] + [[package]] name = "dialoguer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb" dependencies = [ - "console", + "console 0.15.0", "fuzzy-matcher", "lazy_static", "tempfile", @@ -331,7 +358,7 @@ version = "0.1.0" dependencies = [ "assert_cmd", "crossterm", - "dialoguer", + "dialoguer 0.9.0", "miette", "nu-cli", "nu-command", @@ -585,6 +612,7 @@ version = "0.1.0" dependencies = [ "bytesize", "chrono", + "dialoguer 0.8.0", "glob", "lscolors", "nu-engine", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 5b24859f3..8053070e9 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 b042d50f1..8c6ea0655 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 000000000..939caa565 --- /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 db5bc4bea..964985994 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 98dd8a321..fe9b91943 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 e10206a1c..48d912670 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-command/src/filters/lines.rs b/crates/nu-command/src/filters/lines.rs index 87721e8df..a6dd017a8 100644 --- a/crates/nu-command/src/filters/lines.rs +++ b/crates/nu-command/src/filters/lines.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EvaluationContext}; -use nu_protocol::{ShellError, Signature, Span, Value, ValueStream}; +use nu_protocol::{ShellError, Signature, Value, ValueStream}; pub struct Lines; diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 1329205c4..88a1880dd 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 {