diff --git a/Cargo.lock b/Cargo.lock index 9763ec548..21a24b46e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,9 +115,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" [[package]] name = "cfg-if" @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "miette" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4786c5b04c6f73e96d88444e7f37e241d99479ea5dd88a4887363ab2e03b4e53" +checksum = "ec47e61dc212c43f44dcd1f2841ccba79c6ec10da357cab7a7859b5f87bd27a9" dependencies = [ "atty", "backtrace", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "miette-derive" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee63a981bc9cde5f26665ffd756b624963bf0b5956e0df51e52ef8f6b5466d6" +checksum = "2c0f0b6f999b9a9f7e86322125583a437cf015054b7aaa9926dff0ff13005b7e" dependencies = [ "proc-macro2", "quote", @@ -546,6 +546,7 @@ dependencies = [ "sysinfo", "terminal_size", "thiserror", + "trash", ] [[package]] @@ -715,9 +716,9 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "predicates" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c143348f141cc87aab5b950021bac6145d0e5ae754b0591de23244cee42c9308" +checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474" dependencies = [ "difflib", "itertools", @@ -732,12 +733,12 @@ checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" [[package]] name = "predicates-tree" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dd0fd014130206c9352efbdc92be592751b2b9274dff685348341082c6ea3d" +checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7" dependencies = [ "predicates-core", - "treeline", + "termtree", ] [[package]] @@ -763,9 +764,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -857,7 +858,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.2.0" -source = "git+https://github.com/nushell/reedline?branch=main#88bded3417e7f6c1242b444f403448de583357f0" +source = "git+https://github.com/nushell/reedline?branch=main#6fedafffb7a783949b5e9a86149286014eddba15" dependencies = [ "chrono", "crossterm", @@ -1038,9 +1039,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" dependencies = [ "proc-macro2", "quote", @@ -1096,6 +1097,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "termtree" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78fbf2dd23e79c28ccfa2472d3e6b3b189866ffef1aeb91f17c2d968b6586378" + [[package]] name = "textwrap" version = "0.14.2" @@ -1109,18 +1116,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -1139,10 +1146,13 @@ dependencies = [ ] [[package]] -name = "treeline" -version = "0.1.0" +name = "trash" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" +checksum = "90df96afb154814e214f37eac04920c66886fd95962f22febb4d537b0dacd512" +dependencies = [ + "winapi", +] [[package]] name = "unicode-linebreak" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 554cd5ddc..1eed40b67 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -13,6 +13,7 @@ nu-protocol = { path = "../nu-protocol" } nu-table = { path = "../nu-table" } nu-term-grid = { path = "../nu-term-grid" } nu-parser = { path = "../nu-parser" } +trash = { version = "1.3.0", optional = true } # Potential dependencies for extras glob = "0.3.0" @@ -21,3 +22,6 @@ sysinfo = "0.20.4" chrono = { version = "0.4.19", features = ["serde"] } terminal_size = "0.1.17" lscolors = { version = "0.8.0", features = ["crossterm"] } + +[features] +trash-support = ["trash"] diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 8b28172a0..52c18e61e 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -13,51 +13,25 @@ pub fn create_default_context() -> Rc> { let engine_state = engine_state.borrow(); let mut working_set = StateWorkingSet::new(&*engine_state); - working_set.add_decl(Box::new(Alias)); - working_set.add_decl(Box::new(Benchmark)); - working_set.add_decl(Box::new(BuildString)); - working_set.add_decl(Box::new(Cd)); - working_set.add_decl(Box::new(Cp)); - working_set.add_decl(Box::new(Def)); - working_set.add_decl(Box::new(Do)); - working_set.add_decl(Box::new(Each)); - working_set.add_decl(Box::new(ExportDef)); - working_set.add_decl(Box::new(External)); - working_set.add_decl(Box::new(For)); - working_set.add_decl(Box::new(From)); - working_set.add_decl(Box::new(FromJson)); - working_set.add_decl(Box::new(Get)); - working_set.add_decl(Box::new(Griddle)); - working_set.add_decl(Box::new(Help)); - working_set.add_decl(Box::new(Hide)); - working_set.add_decl(Box::new(If)); - working_set.add_decl(Box::new(Length)); - working_set.add_decl(Box::new(Let)); - working_set.add_decl(Box::new(LetEnv)); - working_set.add_decl(Box::new(Lines)); - working_set.add_decl(Box::new(Ls)); - working_set.add_decl(Box::new(Mkdir)); - working_set.add_decl(Box::new(Module)); - working_set.add_decl(Box::new(Mv)); - working_set.add_decl(Box::new(Ps)); - working_set.add_decl(Box::new(Select)); - working_set.add_decl(Box::new(Split)); - working_set.add_decl(Box::new(SplitChars)); - working_set.add_decl(Box::new(SplitColumn)); - working_set.add_decl(Box::new(SplitRow)); - working_set.add_decl(Box::new(Sys)); - working_set.add_decl(Box::new(Table)); - working_set.add_decl(Box::new(Touch)); - working_set.add_decl(Box::new(Use)); - working_set.add_decl(Box::new(Where)); - working_set.add_decl(Box::new(Wrap)); + macro_rules! bind_command { + ( $command:expr ) => { + working_set.add_decl(Box::new($command)); + }; + ( $( $command:expr ),* ) => { + $( working_set.add_decl(Box::new($command)); )* + }; + } + + // TODO: sort items categorically + #[rustfmt::skip] bind_command!( Alias, Benchmark, BuildString, + Cd, Cp, Def, Do, Each, ExportDef, External, For, From, + FromJson, Get, Griddle, Help, Hide, If, Length, Let, LetEnv, + Lines, Ls, Mkdir, Module, Mv, Open, Ps, Rm, Save, Select, + Split, SplitChars, SplitColumn, SplitRow, Sys, Table, Touch, + Use, Where, Wrap ); // This is a WIP proof of concept - working_set.add_decl(Box::new(ListGitBranches)); - working_set.add_decl(Box::new(Git)); - working_set.add_decl(Box::new(GitCheckout)); - - working_set.add_decl(Box::new(Source)); + bind_command!(ListGitBranches, Git, GitCheckout, Source); let sig = Signature::build("exit"); working_set.add_decl(sig.predeclare()); diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index afbbac525..241e2c81a 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -3,6 +3,9 @@ mod cp; mod ls; mod mkdir; mod mv; +mod open; +mod rm; +mod save; mod touch; mod util; @@ -11,4 +14,7 @@ pub use cp::Cp; pub use ls::Ls; pub use mkdir::Mkdir; pub use mv::Mv; +pub use open::Open; +pub use rm::Rm; +pub use save::Save; pub use touch::Touch; diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs new file mode 100644 index 000000000..8d1632cc5 --- /dev/null +++ b/crates/nu-command/src/filesystem/open.rs @@ -0,0 +1,53 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value}; + +pub struct Open; + +impl Command for Open { + fn name(&self) -> &str { + "open" + } + + fn usage(&self) -> &str { + "Load a file into a cell, convert to table if possible (avoid by appending '--raw')." + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "path", + SyntaxShape::Filepath, + "the file path to load values from", + ) + .switch( + "raw", + "load content as a string instead of a table", + Some('r'), + ) + .named( + "encoding", + SyntaxShape::String, + "encoding to use to open file", + Some('e'), + ) + } + + fn extra_usage(&self) -> &str { + r#"Multiple encodings are supported for reading text files by using +the '--encoding ' parameter. Here is an example of a few: +big5, euc-jp, euc-kr, gbk, iso-8859-1, utf-16, cp1252, latin5 + +For a more complete list of encodings please refer to the encoding_rs +documentation link at https://docs.rs/encoding_rs/0.8.28/encoding_rs/#statics"# + } + + fn run( + &self, + _context: &EvaluationContext, + _call: &Call, + _input: Value, + ) -> Result { + unimplemented!(); + } +} diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs new file mode 100644 index 000000000..899ce6c76 --- /dev/null +++ b/crates/nu-command/src/filesystem/rm.rs @@ -0,0 +1,252 @@ +use std::collections::HashMap; +use std::env::current_dir; +use std::os::unix::prelude::FileTypeExt; +use std::path::{Path, PathBuf}; + +use glob::Paths; +use nu_engine::CallExt; +use nu_protocol::ast::{Call, Expression}; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value, ValueStream}; + +pub struct Rm; + +// Where self.0 is the unexpanded target's positional index (i.e. call.positional[self.0].span) +struct Target(usize, PathBuf); + +struct RmArgs { + targets: Vec, + recursive: bool, + trash: bool, + permanent: bool, + force: bool, +} + +impl Command for Rm { + fn name(&self) -> &str { + "rm" + } + + fn usage(&self) -> &str { + "Remove file(s)." + } + + fn signature(&self) -> Signature { + Signature::build("rm") + .switch( + "trash", + "use the platform's recycle bin instead of permanently deleting", + Some('t'), + ) + .switch( + "permanent", + "don't use recycle bin, delete permanently", + Some('p'), + ) + .switch("recursive", "delete subdirectories recursively", Some('r')) + .switch("force", "suppress error when no file", Some('f')) + .rest( + "rest", + SyntaxShape::GlobPattern, + "the file path(s) to remove", + ) + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + rm(context, call) + } +} + +fn rm(context: &EvaluationContext, call: &Call) -> Result { + let trash = call.has_flag("trash"); + let permanent = call.has_flag("permanent"); + + if trash && permanent { + return Err(ShellError::IncompatibleParametersSingle( + "Can't use \"--trash\" with \"--permanent\"".to_string(), + call.head, + )); + + // let trash_span = call.get_flag_expr("trash").unwrap().span; + // let perm_span = call.get_flag_expr("permanent").unwrap().span; + + // let left_message = "cannot use".to_string(); + // let right_message = "with".to_string(); + // let (left_span, right_span) = match trash_span.start < perm_span.start { + // true => (trash_span, perm_span), + // false => (perm_span, trash_span), + // }; + + // return Err(ShellError::IncompatibleParameters { + // left_message, + // left_span, + // right_message, + // right_span, + // }); + } + + let current_path = current_dir()?; + let mut paths = call + .rest::(context, 0)? + .into_iter() + .map(|path| current_path.join(path)) + .peekable(); + + if paths.peek().is_none() { + return Err(ShellError::FileNotFound(call.positional[0].span)); + } + + // Expand and flatten files + let resolve_path = |i: usize, path: PathBuf| { + glob::glob(&path.to_string_lossy()).map_or_else( + |_| Vec::new(), + |path_iter| path_iter.flatten().map(|f| Target(i, f)).collect(), + ) + }; + + let mut targets: Vec = vec![]; + for (i, path) in paths.enumerate() { + let mut paths: Vec = resolve_path(i, path); + + if paths.is_empty() { + return Err(ShellError::FileNotFound(call.positional[i].span)); + } + + targets.append(paths.as_mut()); + } + + let recursive = call.has_flag("recursive"); + let force = call.has_flag("force"); + + let args = RmArgs { + targets, + recursive, + trash, + permanent, + force, + }; + let response = rm_helper(call, args); + + // let temp = rm_helper(call, args).flatten(); + // let temp = input.flatten(call.head, move |_| rm_helper(call, args)); + + Ok(Value::Stream { + stream: ValueStream::from_stream(response.into_iter()), + span: call.head, + }) + + // Ok(Value::Nothing { span }) +} + +fn rm_helper(call: &Call, args: RmArgs) -> Vec { + let (targets, recursive, trash, permanent, force) = ( + args.targets, + args.recursive, + args.trash, + args.permanent, + args.force, + ); + + #[cfg(not(feature = "trash-support"))] + { + if trash { + return vec![Value::Error { + error: ShellError::FeatureNotEnabled(call.get_flag_expr("trash").unwrap().span), + }]; + } + } + + if targets.is_empty() && !force { + return vec![Value::Error { + error: ShellError::FileNotFound(call.head), + }]; + } + + targets + .into_iter() + .map(move |target| { + let (i, f) = (target.0, target.1); + + let is_empty = || match f.read_dir() { + Ok(mut p) => p.next().is_none(), + Err(_) => false, + }; + + if let Ok(metadata) = f.symlink_metadata() { + #[cfg(unix)] + let is_socket = metadata.file_type().is_socket(); + #[cfg(unix)] + let is_fifo = metadata.file_type().is_fifo(); + + #[cfg(not(unix))] + let is_socket = false; + #[cfg(not(unix))] + let is_fifo = false; + + if metadata.is_file() + || metadata.file_type().is_symlink() + || recursive + || is_socket + || is_fifo + || is_empty() + { + let result; + #[cfg(feature = "trash-support")] + { + use std::io::Error; + result = if trash { + trash::delete(&f).map_err(|e: trash::Error| { + Error::new(ErrorKind::Other, format!("{:?}", e)) + }) + } else if metadata.is_file() { + std::fs::remove_file(&f) + } else { + std::fs::remove_dir_all(&f) + }; + } + #[cfg(not(feature = "trash-support"))] + { + result = if metadata.is_file() || is_socket || is_fifo { + std::fs::remove_file(&f) + } else { + std::fs::remove_dir_all(&f) + }; + } + + if let Err(e) = result { + return Value::Error { + error: ShellError::RemoveNotPossible( + format!("Could not delete because: {:}\nTry '--trash' flag", e), + call.head, + ), + }; + } else { + return Value::String { + val: format!("deleted {:}", f.to_string_lossy()).into(), + span: call.positional[i].span, + }; + } + } else { + return Value::Error { + error: ShellError::RemoveNotPossible( + "Cannot remove. try --recursive".to_string(), + call.positional[i].span, + ), + }; + } + } else { + return Value::Error { + error: ShellError::RemoveNotPossible( + "no such file or directory".to_string(), + call.positional[i].span, + ), + }; + } + }) + .collect() +} diff --git a/crates/nu-command/src/filesystem/save.rs b/crates/nu-command/src/filesystem/save.rs new file mode 100644 index 000000000..4ccff3b31 --- /dev/null +++ b/crates/nu-command/src/filesystem/save.rs @@ -0,0 +1,39 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value}; + +pub struct Save; + +impl Command for Save { + fn name(&self) -> &str { + "save" + } + + fn signature(&self) -> Signature { + Signature::build("save") + .optional( + "path", + SyntaxShape::Filepath, + "the path to save contents to", + ) + .switch( + "raw", + "treat values as-is rather than auto-converting based on file extension", + Some('r'), + ) + .switch("append", "append values rather than overriding", Some('a')) + } + + fn usage(&self) -> &str { + "Save the contents of the pipeline to a file." + } + + fn run( + &self, + _context: &EvaluationContext, + _call: &Call, + _input: Value, + ) -> Result { + unimplemented!(); + } +} diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs index 382b50ee9..9724558b3 100644 --- a/crates/nu-command/src/strings/split/chars.rs +++ b/crates/nu-command/src/strings/split/chars.rs @@ -64,7 +64,8 @@ impl Command for SubCommand { fn split_chars(call: &Call, input: Value) -> Result { let span = call.head; - Ok(input.flat_map(span, move |x| split_chars_helper(&x, span))) + let temp = input.flat_map(span, move |x| split_chars_helper(&x, span)); + Ok(temp) } fn split_chars_helper(v: &Value, name: Span) -> Vec { diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 87f15fc3b..405f0cc6d 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -41,6 +41,26 @@ pub enum ShellError { #[diagnostic(code(nu::shell::missing_parameter), url(docsrs))] MissingParameter(String, #[label = "missing parameter: {0}"] Span), + // Be cautious, as flags can share the same span, resulting in a panic (ex: `rm -pt`) + #[error("Incompatible parameters.")] + #[diagnostic(code(nu::shell::incompatible_parameters), url(docsrs))] + IncompatibleParameters { + left_message: String, + #[label("{left_message}")] + left_span: Span, + right_message: String, + #[label("{right_message}")] + right_span: Span, + }, + + #[error("Incompatible parameters.")] + #[diagnostic(code(nu::shell::incompatible_parameters), url(docsrs))] + IncompatibleParametersSingle(String, #[label = "{0}"] Span), + + #[error("Feature not enabled.")] + #[diagnostic(code(nu::shell::feature_not_enabled), url(docsrs))] + FeatureNotEnabled(#[label = "feature not enabled"] Span), + #[error("External commands not yet supported")] #[diagnostic(code(nu::shell::external_commands), url(docsrs))] ExternalNotSupported(#[label = "external not supported"] Span), @@ -131,6 +151,10 @@ pub enum ShellError { #[error("Create not possible")] #[diagnostic(code(nu::shell::create_not_possible), url(docsrs))] CreateNotPossible(String, #[label("{0}")] Span), + + #[error("Remove not possible")] + #[diagnostic(code(nu::shell::remove_not_possible), url(docsrs))] + RemoveNotPossible(String, #[label("{0}")] Span), } impl From for ShellError {