Merge pull request #235 from GabrielBG0/interactive-flag

cp, mv, and rm commands need to support -i flag
This commit is contained in:
JT 2021-10-16 07:17:03 +13:00 committed by GitHub
commit bd5009a865
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 182 additions and 1 deletions

40
Cargo.lock generated
View file

@ -154,6 +154,21 @@ dependencies = [
"chrono", "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]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.2" version = "0.8.2"
@ -240,6 +255,18 @@ dependencies = [
"syn", "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]] [[package]]
name = "diff" name = "diff"
version = "0.1.12" version = "0.1.12"
@ -291,6 +318,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "engine-q" name = "engine-q"
version = "0.1.0" version = "0.1.0"
@ -541,6 +574,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bytesize", "bytesize",
"chrono", "chrono",
"dialoguer",
"glob", "glob",
"lscolors", "lscolors",
"nu-engine", "nu-engine",
@ -1258,3 +1292,9 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "zeroize"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970"

View file

@ -25,6 +25,7 @@ chrono = { version = "0.4.19", features = ["serde"] }
terminal_size = "0.1.17" terminal_size = "0.1.17"
lscolors = { version = "0.8.0", features = ["crossterm"] } lscolors = { version = "0.8.0", features = ["crossterm"] }
bytesize = "1.1.0" bytesize = "1.1.0"
dialoguer = "0.8.0"
[features] [features]
trash-support = ["trash"] trash-support = ["trash"]

View file

@ -1,6 +1,7 @@
use std::env::current_dir; use std::env::current_dir;
use std::path::PathBuf; use std::path::PathBuf;
use super::interactive_helper::get_confirmation;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
@ -11,6 +12,7 @@ use crate::filesystem::util::FileStructure;
pub struct Cp; pub struct Cp;
#[allow(unused_must_use)]
impl Command for Cp { impl Command for Cp {
fn name(&self) -> &str { fn name(&self) -> &str {
"cp" "cp"
@ -29,6 +31,8 @@ impl Command for Cp {
"copy recursively through subdirectories", "copy recursively through subdirectories",
Some('r'), Some('r'),
) )
.switch("force", "suppress error when no file", Some('f'))
.switch("interactive", "ask user to confirm action", Some('i'))
} }
fn run( fn run(
@ -39,12 +43,14 @@ impl Command for Cp {
) -> Result<Value, ShellError> { ) -> Result<Value, ShellError> {
let source: String = call.req(context, 0)?; let source: String = call.req(context, 0)?;
let destination: String = call.req(context, 1)?; 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 path: PathBuf = current_dir().unwrap();
let source = path.join(source.as_str()); let source = path.join(source.as_str());
let destination = path.join(destination.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); glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect);
if sources.is_empty() { if sources.is_empty() {
return Err(ShellError::FileNotFound(call.positional[0].span)); return Err(ShellError::FileNotFound(call.positional[0].span));
@ -68,6 +74,38 @@ impl Command for Cp {
)); ));
} }
if interactive && !force {
let mut remove: Vec<usize> = 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() { for entry in sources.into_iter().flatten() {
let mut sources = FileStructure::new(); let mut sources = FileStructure::new();
sources.walk_decorate(&entry)?; sources.walk_decorate(&entry)?;

View file

@ -0,0 +1,26 @@
use dialoguer::Input;
use std::error::Error;
pub fn get_confirmation(prompt: String) -> Result<bool, Box<dyn Error>> {
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)
}
}

View file

@ -1,5 +1,6 @@
mod cd; mod cd;
mod cp; mod cp;
mod interactive_helper;
mod ls; mod ls;
mod mkdir; mod mkdir;
mod mv; mod mv;

View file

@ -1,6 +1,7 @@
use std::env::current_dir; use std::env::current_dir;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::interactive_helper::get_confirmation;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EvaluationContext}; use nu_protocol::engine::{Command, EvaluationContext};
@ -8,6 +9,7 @@ use nu_protocol::{ShellError, Signature, SyntaxShape, Value};
pub struct Mv; pub struct Mv;
#[allow(unused_must_use)]
impl Command for Mv { impl Command for Mv {
fn name(&self) -> &str { fn name(&self) -> &str {
"mv" "mv"
@ -29,6 +31,8 @@ impl Command for Mv {
SyntaxShape::Filepath, SyntaxShape::Filepath,
"the location to move files/directories to", "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( fn run(
@ -40,6 +44,8 @@ impl Command for Mv {
// TODO: handle invalid directory or insufficient permissions when moving // TODO: handle invalid directory or insufficient permissions when moving
let source: String = call.req(context, 0)?; let source: String = call.req(context, 0)?;
let destination: String = call.req(context, 1)?; 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 path: PathBuf = current_dir().unwrap();
let source = path.join(source.as_str()); let source = path.join(source.as_str());
@ -54,6 +60,38 @@ impl Command for Mv {
)); ));
} }
if interactive && !force {
let mut remove: Vec<usize> = 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) if (destination.exists() && !destination.is_dir() && sources.len() > 1)
|| (!destination.exists() && sources.len() > 1) || (!destination.exists() && sources.len() > 1)
{ {

View file

@ -3,6 +3,8 @@ use std::env::current_dir;
use std::os::unix::prelude::FileTypeExt; use std::os::unix::prelude::FileTypeExt;
use std::path::PathBuf; use std::path::PathBuf;
use super::interactive_helper::get_confirmation;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EvaluationContext}; use nu_protocol::engine::{Command, EvaluationContext};
@ -44,6 +46,7 @@ impl Command for Rm {
) )
.switch("recursive", "delete subdirectories recursively", Some('r')) .switch("recursive", "delete subdirectories recursively", Some('r'))
.switch("force", "suppress error when no file", Some('f')) .switch("force", "suppress error when no file", Some('f'))
.switch("interactive", "ask user to confirm action", Some('i'))
.rest( .rest(
"rest", "rest",
SyntaxShape::GlobPattern, SyntaxShape::GlobPattern,
@ -64,6 +67,7 @@ impl Command for Rm {
fn rm(context: &EvaluationContext, call: &Call) -> Result<Value, ShellError> { fn rm(context: &EvaluationContext, call: &Call) -> Result<Value, ShellError> {
let trash = call.has_flag("trash"); let trash = call.has_flag("trash");
let permanent = call.has_flag("permanent"); let permanent = call.has_flag("permanent");
let interactive = call.has_flag("interactive");
if trash && permanent { if trash && permanent {
return Err(ShellError::IncompatibleParametersSingle( return Err(ShellError::IncompatibleParametersSingle(
@ -122,6 +126,32 @@ fn rm(context: &EvaluationContext, call: &Call) -> Result<Value, ShellError> {
let recursive = call.has_flag("recursive"); let recursive = call.has_flag("recursive");
let force = call.has_flag("force"); let force = call.has_flag("force");
if interactive && !force {
let mut remove: Vec<usize> = 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 { let args = RmArgs {
targets, targets,
recursive, recursive,

View file

@ -158,6 +158,13 @@ pub enum ShellError {
#[error("Remove not possible")] #[error("Remove not possible")]
#[diagnostic(code(nu::shell::remove_not_possible), url(docsrs))] #[diagnostic(code(nu::shell::remove_not_possible), url(docsrs))]
RemoveNotPossible(String, #[label("{0}")] Span), 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<std::io::Error> for ShellError { impl From<std::io::Error> for ShellError {