diff --git a/Cargo.lock b/Cargo.lock index 1c6fd045cd..5ff65ec7db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,15 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "byte-unit" +version = "4.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063197e6eb4b775b64160dedde7a0986bb2836cce140e9492e9e96f28e18bcd8" +dependencies = [ + "utf8-width", +] + [[package]] name = "cc" version = "1.0.70" @@ -119,10 +128,20 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi", ] +[[package]] +name = "chrono-humanize" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eddc119501d583fd930cb92144e605f44e0252c38dd89d9247fffa1993375cb" +dependencies = [ + "chrono", +] + [[package]] name = "core-foundation-sys" version = "0.8.2" @@ -487,6 +506,7 @@ dependencies = [ "nu-ansi-term", "nu-engine", "nu-parser", + "nu-path", "nu-protocol", "reedline", "thiserror", @@ -496,6 +516,7 @@ dependencies = [ name = "nu-command" version = "0.1.0" dependencies = [ + "chrono", "glob", "nu-engine", "nu-json", @@ -549,6 +570,9 @@ dependencies = [ name = "nu-protocol" version = "0.1.0" dependencies = [ + "byte-unit", + "chrono", + "chrono-humanize", "miette", "serde", "thiserror", @@ -803,7 +827,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.2.0" -source = "git+https://github.com/jntrnr/reedline?branch=main#bfddc5870ca2d8301694b4211bdcdb29e647c6f3" +source = "git+https://github.com/nushell/reedline?branch=main#88bded3417e7f6c1242b444f403448de583357f0" dependencies = [ "chrono", "crossterm", @@ -947,9 +971,9 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "supports-color" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3cef55878ee693bb9f6765515f52910ec20b776d222fce5d11fbb9f5368028" +checksum = "4872ced36b91d47bae8a214a683fe54e7078875b399dfa251df346c9b547d1f9" dependencies = [ "atty", "is_ci", @@ -975,9 +999,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.77" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" +checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0" dependencies = [ "proc-macro2", "quote", @@ -1098,6 +1122,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "utf8-width" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 826e3d542e..01c94b401a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" members = ["crates/nu-cli", "crates/nu-engine", "crates/nu-parser", "crates/nu-command", "crates/nu-protocol"] [dependencies] -reedline = { git = "https://github.com/jntrnr/reedline", branch = "main" } +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } crossterm = "0.21.*" nu-cli = { path="./crates/nu-cli" } nu-command = { path="./crates/nu-command" } diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 117fe77d98..3f2f90c7cc 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -5,9 +5,11 @@ edition = "2018" [dependencies] nu-engine = { path = "../nu-engine" } +nu-path = { path = "../nu-path" } nu-parser = { path = "../nu-parser" } nu-protocol = { path = "../nu-protocol" } + miette = { version = "3.0.0", features = ["fancy"] } thiserror = "1.0.29" nu-ansi-term = "0.36.0" -reedline = { git = "https://github.com/jntrnr/reedline", branch = "main" } +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs index 2140ca0da9..57298d0936 100644 --- a/crates/nu-cli/src/completions.rs +++ b/crates/nu-cli/src/completions.rs @@ -8,6 +8,8 @@ use nu_protocol::{ }; use reedline::Completer; +const SEP: char = std::path::MAIN_SEPARATOR; + pub struct NuCompleter { engine_state: Rc>, } @@ -30,6 +32,40 @@ impl Completer for NuCompleter { for flat in flattened { if pos >= flat.0.start && pos <= flat.0.end { + let prefix = working_set.get_span_contents(flat.0); + if prefix.starts_with(b"$") { + let mut output = vec![]; + + for scope in &working_set.delta.scope { + for v in &scope.vars { + if v.0.starts_with(prefix) { + output.push(( + reedline::Span { + start: flat.0.start - offset, + end: flat.0.end - offset, + }, + String::from_utf8_lossy(v.0).to_string(), + )); + } + } + } + for scope in &engine_state.scope { + for v in &scope.vars { + if v.0.starts_with(prefix) { + output.push(( + reedline::Span { + start: flat.0.start - offset, + end: flat.0.end - offset, + }, + String::from_utf8_lossy(v.0).to_string(), + )); + } + } + } + + return output; + } + match &flat.1 { nu_parser::FlatShape::Custom(custom_completion) => { let prefix = working_set.get_span_contents(flat.0).to_vec(); @@ -80,6 +116,27 @@ impl Completer for NuCompleter { }) .collect(); } + nu_parser::FlatShape::Filepath + | nu_parser::FlatShape::GlobPattern + | nu_parser::FlatShape::ExternalArg => { + let prefix = working_set.get_span_contents(flat.0); + let prefix = String::from_utf8_lossy(prefix).to_string(); + + let results = file_path_completion(flat.0, &prefix); + + return results + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, + x.1, + ) + }) + .collect(); + } _ => {} } } @@ -88,3 +145,56 @@ impl Completer for NuCompleter { vec![] } } + +fn file_path_completion( + span: nu_protocol::Span, + partial: &str, +) -> Vec<(nu_protocol::Span, String)> { + use std::path::{is_separator, Path}; + + let (base_dir_name, partial) = { + // If partial is only a word we want to search in the current dir + let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", partial)); + // On windows, this standardizes paths to use \ + let mut base = base.replace(is_separator, &SEP.to_string()); + + // rsplit_once removes the separator + base.push(SEP); + (base, rest) + }; + + let base_dir = nu_path::expand_path(&base_dir_name); + // This check is here as base_dir.read_dir() with base_dir == "" will open the current dir + // which we don't want in this case (if we did, base_dir would already be ".") + if base_dir == Path::new("") { + return Vec::new(); + } + + if let Ok(result) = base_dir.read_dir() { + result + .filter_map(|entry| { + entry.ok().and_then(|entry| { + let mut file_name = entry.file_name().to_string_lossy().into_owned(); + if matches(partial, &file_name) { + let mut path = format!("{}{}", base_dir_name, file_name); + if entry.path().is_dir() { + path.push(SEP); + file_name.push(SEP); + } + + Some((span, path)) + } else { + None + } + }) + }) + .collect() + } else { + Vec::new() + } +} + +fn matches(partial: &str, from: &str) -> bool { + from.to_ascii_lowercase() + .starts_with(&partial.to_ascii_lowercase()) +} diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 67198959ba..fccc0e9544 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -83,6 +83,14 @@ impl Highlighter for NuHighlighter { Style::new().fg(nu_ansi_term::Color::Yellow).bold(), next_token, )), + FlatShape::Filepath => output.push(( + Style::new().fg(nu_ansi_term::Color::Yellow).bold(), + next_token, + )), + FlatShape::GlobPattern => output.push(( + Style::new().fg(nu_ansi_term::Color::Yellow).bold(), + next_token, + )), FlatShape::Variable => output.push(( Style::new().fg(nu_ansi_term::Color::Blue).bold(), next_token, diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 213365d4c1..d840fb7d33 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -16,3 +16,4 @@ nu-table = { path = "../nu-table" } glob = "0.3.0" thiserror = "1.0.29" sysinfo = "0.20.4" +chrono = { version="0.4.19", features=["serde"] } diff --git a/crates/nu-command/src/core_commands/source.rs b/crates/nu-command/src/core_commands/source.rs index c154566c22..73f5425d27 100644 --- a/crates/nu-command/src/core_commands/source.rs +++ b/crates/nu-command/src/core_commands/source.rs @@ -17,7 +17,7 @@ impl Command for Source { fn signature(&self) -> Signature { Signature::build("source").required( "filename", - SyntaxShape::FilePath, + SyntaxShape::Filepath, "the filepath to the script file to source", ) } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 29bb4dd480..fd2ea37555 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -17,6 +17,7 @@ pub fn create_default_context() -> Rc> { 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)); @@ -35,6 +36,7 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Lines)); working_set.add_decl(Box::new(Ls)); 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(Sys)); diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index 94282c75e2..919e11e021 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -15,7 +15,7 @@ impl Command for Cd { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("cd").optional("path", SyntaxShape::FilePath, "the path to change to") + Signature::build("cd").optional("path", SyntaxShape::Filepath, "the path to change to") } fn run( @@ -28,7 +28,7 @@ impl Command for Cd { let path = match path { Some(path) => { - let path = nu_path::expand_tilde(path); + let path = nu_path::expand_path(path); path.to_string_lossy().to_string() } None => { diff --git a/crates/nu-command/src/filesystem/cp.rs b/crates/nu-command/src/filesystem/cp.rs new file mode 100644 index 0000000000..79751f2c93 --- /dev/null +++ b/crates/nu-command/src/filesystem/cp.rs @@ -0,0 +1,169 @@ +use std::env::current_dir; +use std::path::PathBuf; + +use nu_engine::CallExt; +use nu_path::canonicalize_with; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value}; + +use crate::filesystem::util::FileStructure; + +pub struct Cp; + +impl Command for Cp { + fn name(&self) -> &str { + "cp" + } + + fn usage(&self) -> &str { + "Copy files." + } + + fn signature(&self) -> Signature { + Signature::build("cp") + .required("source", SyntaxShape::GlobPattern, "the place to copy from") + .required("destination", SyntaxShape::Filepath, "the place to copy to") + .switch( + "recursive", + "copy recursively through subdirectories", + Some('r'), + ) + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + let source: String = call.req(context, 0)?; + let destination: String = call.req(context, 1)?; + + let path: PathBuf = current_dir().unwrap(); + let source = path.join(source.as_str()); + let destination = path.join(destination.as_str()); + + let 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)); + } + + if sources.len() > 1 && !destination.is_dir() { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move many files".to_string(), + source_span: call.positional[0].span, + destination_message: "into single file".to_string(), + destination_span: call.positional[1].span, + }); + } + + let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir())); + let recursive = call.named.iter().any(|p| &p.0 == "recursive"); + if any_source_is_dir && !recursive { + return Err(ShellError::MoveNotPossibleSingle( + "Directories must be copied using \"--recursive\"".to_string(), + call.positional[0].span, + )); + } + + for entry in sources.into_iter().flatten() { + let mut sources = FileStructure::new(); + sources.walk_decorate(&entry)?; + + if entry.is_file() { + let sources = sources.paths_applying_with(|(source_file, _depth_level)| { + if destination.is_dir() { + let mut dest = canonicalize_with(&destination, &path)?; + if let Some(name) = entry.file_name() { + dest.push(name); + } + Ok((source_file, dest)) + } else { + Ok((source_file, destination.clone())) + } + })?; + + for (src, dst) in sources { + if src.is_file() { + std::fs::copy(&src, dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to move containing file \"{}\": {}", + src.to_string_lossy(), + e + ), + call.positional[0].span, + ) + })?; + } + } + } else if entry.is_dir() { + let destination = if !destination.exists() { + destination.clone() + } else { + match entry.file_name() { + Some(name) => destination.join(name), + None => { + return Err(ShellError::FileNotFoundCustom( + format!("containing \"{:?}\" is not a valid path", entry), + call.positional[0].span, + )) + } + } + }; + + std::fs::create_dir_all(&destination).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!("failed to recursively fill destination: {}", e), + call.positional[1].span, + ) + })?; + + let sources = sources.paths_applying_with(|(source_file, depth_level)| { + let mut dest = destination.clone(); + let path = canonicalize_with(&source_file, &path)?; + let components = path + .components() + .map(|fragment| fragment.as_os_str()) + .rev() + .take(1 + depth_level); + + components.for_each(|fragment| dest.push(fragment)); + Ok((PathBuf::from(&source_file), dest)) + })?; + + for (src, dst) in sources { + if src.is_dir() && !dst.exists() { + std::fs::create_dir_all(&dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to create containing directory \"{}\": {}", + dst.to_string_lossy(), + e + ), + call.positional[1].span, + ) + })?; + } + + if src.is_file() { + std::fs::copy(&src, &dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to move containing file \"{}\": {}", + src.to_string_lossy(), + e + ), + call.positional[0].span, + ) + })?; + } + } + } + } + + Ok(Value::Nothing { span: call.head }) + } +} diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index 2e243e05f4..36fbaff74a 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use nu_engine::eval_expression; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EvaluationContext}; @@ -31,7 +32,17 @@ impl Command for Ls { ) -> Result { let pattern = if let Some(expr) = call.positional.get(0) { let result = eval_expression(context, expr)?; - result.as_string()? + let mut result = result.as_string()?; + + let path = std::path::Path::new(&result); + if path.is_dir() { + if !result.ends_with(std::path::MAIN_SEPARATOR) { + result.push(std::path::MAIN_SEPARATOR); + } + result.push('*'); + } + + result } else { "*".into() }; @@ -49,25 +60,39 @@ impl Command for Ls { let is_dir = metadata.is_dir(); let filesize = metadata.len(); + let mut cols = vec!["name".into(), "type".into(), "size".into()]; + + let mut vals = vec![ + Value::String { + val: path.to_string_lossy().to_string(), + span: call_span, + }, + if is_file { + Value::string("File", call_span) + } else if is_dir { + Value::string("Dir", call_span) + } else { + Value::Nothing { span: call_span } + }, + Value::Filesize { + val: filesize as i64, + span: call_span, + }, + ]; + + if let Ok(date) = metadata.modified() { + let utc: DateTime = date.into(); + + cols.push("modified".into()); + vals.push(Value::Date { + val: utc.into(), + span: call_span, + }); + } + Value::Record { - cols: vec!["name".into(), "type".into(), "size".into()], - vals: vec![ - Value::String { - val: path.to_string_lossy().to_string(), - span: call_span, - }, - if is_file { - Value::string("file", call_span) - } else if is_dir { - Value::string("dir", call_span) - } else { - Value::Nothing { span: call_span } - }, - Value::Filesize { - val: filesize, - span: call_span, - }, - ], + cols, + vals, span: call_span, } } diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index 13148b5358..2d2212766a 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -1,5 +1,10 @@ mod cd; +mod cp; mod ls; +mod mv; +mod util; pub use cd::Cd; +pub use cp::Cp; pub use ls::Ls; +pub use mv::Mv; diff --git a/crates/nu-command/src/filesystem/mv.rs b/crates/nu-command/src/filesystem/mv.rs new file mode 100644 index 0000000000..19cdf0d896 --- /dev/null +++ b/crates/nu-command/src/filesystem/mv.rs @@ -0,0 +1,139 @@ +use std::env::current_dir; +use std::path::{Path, PathBuf}; + +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value}; + +pub struct Mv; + +impl Command for Mv { + fn name(&self) -> &str { + "mv" + } + + fn usage(&self) -> &str { + "Move files or directories." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("mv") + .required( + "source", + SyntaxShape::GlobPattern, + "the location to move files/directories from", + ) + .required( + "destination", + SyntaxShape::Filepath, + "the location to move files/directories to", + ) + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + // TODO: handle invalid directory or insufficient permissions + let source: String = call.req(context, 0)?; + let destination: String = call.req(context, 1)?; + + let path: PathBuf = current_dir().unwrap(); + let source = path.join(source.as_str()); + let destination = path.join(destination.as_str()); + + 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.first().unwrap().span, + )); + } + + if (destination.exists() && !destination.is_dir() && sources.len() > 1) + || (!destination.exists() && sources.len() > 1) + { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move many files".to_string(), + source_span: call.positional[0].span, + destination_message: "into single file".to_string(), + destination_span: call.positional[1].span, + }); + } + + let some_if_source_is_destination = sources + .iter() + .find(|f| matches!(f, Ok(f) if destination.starts_with(f))); + if destination.exists() && destination.is_dir() && sources.len() == 1 { + if let Some(Ok(_filename)) = some_if_source_is_destination { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move directory".to_string(), + source_span: call.positional[0].span, + destination_message: "into itself".to_string(), + destination_span: call.positional[1].span, + }); + } + } + + if let Some(Ok(_filename)) = some_if_source_is_destination { + sources = sources + .into_iter() + .filter(|f| matches!(f, Ok(f) if !destination.starts_with(f))) + .collect(); + } + + for entry in sources.into_iter().flatten() { + move_file(call, &entry, &destination)? + } + + Ok(Value::Nothing { span: call.head }) + } +} + +fn move_file(call: &Call, from: &Path, to: &Path) -> Result<(), ShellError> { + if to.exists() && from.is_dir() && to.is_file() { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move a directory".to_string(), + source_span: call.positional[0].span, + destination_message: "to a file".to_string(), + destination_span: call.positional[1].span, + }); + } + + let destination_dir_exists = if to.is_dir() { + true + } else { + to.parent().map(Path::exists).unwrap_or(true) + }; + + if !destination_dir_exists { + return Err(ShellError::DirectoryNotFound(call.positional[1].span)); + } + + let mut to = to.to_path_buf(); + if to.is_dir() { + let from_file_name = match from.file_name() { + Some(name) => name, + None => return Err(ShellError::DirectoryNotFound(call.positional[1].span)), + }; + + to.push(from_file_name); + } + + move_item(call, from, &to) +} + +fn move_item(call: &Call, from: &Path, to: &Path) -> Result<(), ShellError> { + // We first try a rename, which is a quick operation. If that doesn't work, we'll try a copy + // and remove the old file/folder. This is necessary if we're moving across filesystems or devices. + std::fs::rename(&from, &to).map_err(|_| ShellError::MoveNotPossible { + source_message: "failed to move".to_string(), + source_span: call.positional[0].span, + destination_message: "into".to_string(), + destination_span: call.positional[1].span, + }) +} diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs new file mode 100644 index 0000000000..a2d217cf05 --- /dev/null +++ b/crates/nu-command/src/filesystem/util.rs @@ -0,0 +1,81 @@ +use std::path::{Path, PathBuf}; + +use nu_path::canonicalize_with; +use nu_protocol::ShellError; + +#[derive(Default)] +pub struct FileStructure { + pub resources: Vec, +} + +#[allow(dead_code)] +impl FileStructure { + pub fn new() -> FileStructure { + FileStructure { resources: vec![] } + } + + pub fn contains_more_than_one_file(&self) -> bool { + self.resources.len() > 1 + } + + pub fn contains_files(&self) -> bool { + !self.resources.is_empty() + } + + pub fn paths_applying_with( + &mut self, + to: F, + ) -> Result, Box> + where + F: Fn((PathBuf, usize)) -> Result<(PathBuf, PathBuf), Box>, + { + self.resources + .iter() + .map(|f| (PathBuf::from(&f.location), f.at)) + .map(to) + .collect() + } + + pub fn walk_decorate(&mut self, start_path: &Path) -> Result<(), ShellError> { + self.resources = Vec::::new(); + self.build(start_path, 0)?; + self.resources.sort(); + + Ok(()) + } + + fn build(&mut self, src: &Path, lvl: usize) -> Result<(), ShellError> { + let source = canonicalize_with(src, std::env::current_dir()?)?; + + if source.is_dir() { + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + self.build(&path, lvl + 1)?; + } + + self.resources.push(Resource { + location: path.to_path_buf(), + at: lvl, + }); + } + } else { + self.resources.push(Resource { + location: source, + at: lvl, + }); + } + + Ok(()) + } +} + +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Resource { + pub at: usize, + pub location: PathBuf, +} + +impl Resource {} diff --git a/crates/nu-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs index c06badbb26..7ad1c12823 100644 --- a/crates/nu-command/src/filters/get.rs +++ b/crates/nu-command/src/filters/get.rs @@ -15,7 +15,7 @@ impl Command for Get { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("wrap").required( + Signature::build("get").required( "cell_path", SyntaxShape::CellPath, "the cell path to the data", diff --git a/crates/nu-command/src/system/ps.rs b/crates/nu-command/src/system/ps.rs index b51310a387..68f9409a89 100644 --- a/crates/nu-command/src/system/ps.rs +++ b/crates/nu-command/src/system/ps.rs @@ -86,13 +86,13 @@ fn run_ps(call: &Call) -> Result { cols.push("mem".into()); vals.push(Value::Filesize { - val: result.memory() * 1000, + val: result.memory() as i64 * 1000, span, }); cols.push("virtual".into()); vals.push(Value::Filesize { - val: result.virtual_memory() * 1000, + val: result.virtual_memory() as i64 * 1000, span, }); diff --git a/crates/nu-command/src/system/sys.rs b/crates/nu-command/src/system/sys.rs index c3bf8591dd..b0323ffdf6 100644 --- a/crates/nu-command/src/system/sys.rs +++ b/crates/nu-command/src/system/sys.rs @@ -112,13 +112,13 @@ pub fn disks(sys: &mut System, span: Span) -> Option { cols.push("total".into()); vals.push(Value::Filesize { - val: disk.total_space(), + val: disk.total_space() as i64, span, }); cols.push("free".into()); vals.push(Value::Filesize { - val: disk.available_space(), + val: disk.available_space() as i64, span, }); @@ -148,13 +148,13 @@ pub fn net(sys: &mut System, span: Span) -> Option { cols.push("sent".into()); vals.push(Value::Filesize { - val: data.total_transmitted(), + val: data.total_transmitted() as i64, span, }); cols.push("recv".into()); vals.push(Value::Filesize { - val: data.total_received(), + val: data.total_received() as i64, span, }); @@ -215,25 +215,25 @@ pub fn mem(sys: &mut System, span: Span) -> Option { cols.push("total".into()); vals.push(Value::Filesize { - val: total_mem * 1000, + val: total_mem as i64 * 1000, span, }); cols.push("free".into()); vals.push(Value::Filesize { - val: free_mem * 1000, + val: free_mem as i64 * 1000, span, }); cols.push("swap total".into()); vals.push(Value::Filesize { - val: total_swap * 1000, + val: total_swap as i64 * 1000, span, }); cols.push("swap free".into()); vals.push(Value::Filesize { - val: free_swap * 1000, + val: free_swap as i64 * 1000, span, }); @@ -276,7 +276,7 @@ pub fn host(sys: &mut System, span: Span) -> Option { } cols.push("uptime".into()); vals.push(Value::Duration { - val: 1000000000 * sys.uptime() as u64, + val: 1000000000 * sys.uptime() as i64, span, }); diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 6fba899933..7408cf2cc9 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -1,7 +1,7 @@ use nu_parser::parse; use nu_protocol::ast::{Block, Call, Expr, Expression, Operator, Statement}; use nu_protocol::engine::EvaluationContext; -use nu_protocol::{Range, ShellError, Span, Type, Value}; +use nu_protocol::{Range, ShellError, Span, Type, Unit, Value}; pub fn eval_operator(op: &Expression) -> Result { match op { @@ -124,6 +124,10 @@ pub fn eval_expression( val: *f, span: expr.span, }), + Expr::ValueWithUnit(e, unit) => match eval_expression(context, e)? { + Value::Int { val, .. } => Ok(compute(val, unit.item, unit.span)), + _ => Err(ShellError::CantConvert("unit value".into(), e.span)), + }, Expr::Range(from, next, to, operator) => { let from = if let Some(f) = from { eval_expression(context, f)? @@ -241,6 +245,14 @@ pub fn eval_expression( val: s.clone(), span: expr.span, }), + Expr::Filepath(s) => Ok(Value::String { + val: s.clone(), + span: expr.span, + }), + Expr::GlobPattern(s) => Ok(Value::String { + val: s.clone(), + span: expr.span, + }), Expr::Signature(_) => Ok(Value::Nothing { span: expr.span }), Expr::Garbage => Ok(Value::Nothing { span: expr.span }), } @@ -285,14 +297,79 @@ pub fn eval_block( Ok(input) } -// pub fn eval(context: &EvaluationContext, script: &str) -> Result { -// let engine_state = EngineState::new(); -// let mut working_set = StateWorkingSet::new(&engine_state); +pub fn compute(size: i64, unit: Unit, span: Span) -> Value { + match unit { + Unit::Byte => Value::Filesize { val: size, span }, + Unit::Kilobyte => Value::Filesize { + val: size * 1000, + span, + }, + Unit::Megabyte => Value::Filesize { + val: size * 1000 * 1000, + span, + }, + Unit::Gigabyte => Value::Filesize { + val: size * 1000 * 1000 * 1000, + span, + }, + Unit::Terabyte => Value::Filesize { + val: size * 1000 * 1000 * 1000 * 1000, + span, + }, + Unit::Petabyte => Value::Filesize { + val: size * 1000 * 1000 * 1000 * 1000 * 1000, + span, + }, -// let (block, err) = parse(&mut working_set, None, b"3", true); -// if let Some(e) = err { -// Err(e) -// } else { -// eval_block(context, &block, Value::nothing()) -// } -// } + Unit::Kibibyte => Value::Filesize { + val: size * 1024, + span, + }, + Unit::Mebibyte => Value::Filesize { + val: size * 1024 * 1024, + span, + }, + Unit::Gibibyte => Value::Filesize { + val: size * 1024 * 1024 * 1024, + span, + }, + Unit::Tebibyte => Value::Filesize { + val: size * 1024 * 1024 * 1024 * 1024, + span, + }, + Unit::Pebibyte => Value::Filesize { + val: size * 1024 * 1024 * 1024 * 1024 * 1024, + span, + }, + + Unit::Nanosecond => Value::Duration { val: size, span }, + Unit::Microsecond => Value::Duration { + val: size * 1000, + span, + }, + Unit::Millisecond => Value::Duration { + val: size * 1000 * 1000, + span, + }, + Unit::Second => Value::Duration { + val: size * 1000 * 1000 * 1000, + span, + }, + Unit::Minute => Value::Duration { + val: size * 1000 * 1000 * 1000 * 60, + span, + }, + Unit::Hour => Value::Duration { + val: size * 1000 * 1000 * 1000 * 60 * 60, + span, + }, + Unit::Day => Value::Duration { + val: size * 1000 * 1000 * 1000 * 60 * 60 * 24, + span, + }, + Unit::Week => Value::Duration { + val: size * 1000 * 1000 * 1000 * 60 * 60 * 24 * 7, + span, + }, + } +} diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index fffb5626ec..90cfc5092d 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -15,6 +15,8 @@ pub enum FlatShape { Operator, Signature, String, + Filepath, + GlobPattern, Variable, Custom(String), } @@ -79,6 +81,12 @@ pub fn flatten_expression( Expr::Float(_) => { vec![(expr.span, FlatShape::Float)] } + Expr::ValueWithUnit(x, unit) => { + let mut output = flatten_expression(working_set, x); + output.push((unit.span, FlatShape::String)); + + output + } Expr::CellPath(cell_path) => { let mut output = vec![]; for path_element in &cell_path.members { @@ -118,7 +126,12 @@ pub fn flatten_expression( Expr::Bool(_) => { vec![(expr.span, FlatShape::Bool)] } - + Expr::Filepath(_) => { + vec![(expr.span, FlatShape::Filepath)] + } + Expr::GlobPattern(_) => { + vec![(expr.span, FlatShape::GlobPattern)] + } Expr::List(list) => { let mut output = vec![]; for l in list { diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 1069c3c706..c1510edbb7 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -601,12 +601,90 @@ pub fn parse_hide( let (name_expr, err) = parse_string(working_set, spans[1]); error = error.or(err); - let name_bytes: Vec = working_set.get_span_contents(spans[1]).into(); + let (import_pattern, err) = parse_import_pattern(working_set, spans[1]); + error = error.or(err); - // TODO: Do the import pattern stuff for bulk-hiding + let exported_names: Vec> = + if let Some(block_id) = working_set.find_module(&import_pattern.head) { + working_set + .get_block(block_id) + .exports + .iter() + .map(|(name, _)| name.clone()) + .collect() + } else if import_pattern.members.is_empty() { + // The pattern head can be e.g. a function name, not just a module + vec![import_pattern.head.clone()] + } else { + return ( + garbage_statement(spans), + Some(ParseError::ModuleNotFound(spans[1])), + ); + }; - if working_set.hide_decl(&name_bytes).is_none() { - error = error.or_else(|| Some(ParseError::UnknownCommand(spans[1]))); + // This kind of inverts the import pattern matching found in parse_use() + let names_to_hide = if import_pattern.members.is_empty() { + exported_names + } else { + match &import_pattern.members[0] { + ImportPatternMember::Glob { .. } => exported_names + .into_iter() + .map(|name| { + let mut new_name = import_pattern.head.to_vec(); + new_name.push(b'.'); + new_name.extend(&name); + new_name + }) + .collect(), + ImportPatternMember::Name { name, span } => { + let new_exports: Vec> = exported_names + .into_iter() + .filter(|n| n == name) + .map(|n| { + let mut new_name = import_pattern.head.to_vec(); + new_name.push(b'.'); + new_name.extend(&n); + new_name + }) + .collect(); + + if new_exports.is_empty() { + error = error.or(Some(ParseError::ExportNotFound(*span))) + } + + new_exports + } + ImportPatternMember::List { names } => { + let mut output = vec![]; + + for (name, span) in names { + let mut new_exports: Vec> = exported_names + .iter() + .filter_map(|n| if n == name { Some(n.clone()) } else { None }) + .map(|n| { + let mut new_name = import_pattern.head.to_vec(); + new_name.push(b'.'); + new_name.extend(n); + new_name + }) + .collect(); + + if new_exports.is_empty() { + error = error.or(Some(ParseError::ExportNotFound(*span))) + } else { + output.append(&mut new_exports) + } + } + + output + } + } + }; + + for name in names_to_hide { + if working_set.hide_decl(&name).is_none() { + error = error.or_else(|| Some(ParseError::UnknownCommand(spans[1]))); + } } // Create the Hide command call diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index d0cabbd2a5..8cf6351c57 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -11,7 +11,7 @@ use nu_protocol::{ Operator, PathMember, Pipeline, RangeInclusion, RangeOperator, Statement, }, engine::StateWorkingSet, - span, Flag, PositionalArg, Signature, Span, SyntaxShape, Type, VarId, + span, Flag, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, Unit, VarId, }; use crate::parse_keywords::{ @@ -1321,6 +1321,255 @@ pub fn parse_full_cell_path( } } +pub fn parse_filepath( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + let bytes = if (bytes.starts_with(b"\"") && bytes.ends_with(b"\"") && bytes.len() > 1) + || (bytes.starts_with(b"\'") && bytes.ends_with(b"\'") && bytes.len() > 1) + { + &bytes[1..(bytes.len() - 1)] + } else { + bytes + }; + + if let Ok(token) = String::from_utf8(bytes.into()) { + ( + Expression { + expr: Expr::Filepath(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("string".into(), span)), + ) + } +} + +/// Parse a duration type, eg '10day' +pub fn parse_duration( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + fn parse_decimal_str_to_number(decimal: &str) -> Option { + let string_to_parse = format!("0.{}", decimal); + if let Ok(x) = string_to_parse.parse::() { + return Some((1_f64 / x) as i64); + } + None + } + + let bytes = working_set.get_span_contents(span); + let token = String::from_utf8_lossy(bytes).to_string(); + + let unit_groups = [ + (Unit::Nanosecond, "NS", None), + (Unit::Microsecond, "US", Some((Unit::Nanosecond, 1000))), + (Unit::Millisecond, "MS", Some((Unit::Microsecond, 1000))), + (Unit::Second, "SEC", Some((Unit::Millisecond, 1000))), + (Unit::Minute, "MIN", Some((Unit::Second, 60))), + (Unit::Hour, "HR", Some((Unit::Minute, 60))), + (Unit::Day, "DAY", Some((Unit::Minute, 1440))), + (Unit::Week, "WK", Some((Unit::Day, 7))), + ]; + if let Some(unit) = unit_groups + .iter() + .find(|&x| token.to_uppercase().ends_with(x.1)) + { + let mut lhs = token.clone(); + for _ in 0..unit.1.len() { + lhs.pop(); + } + + let input: Vec<&str> = lhs.split('.').collect(); + let (value, unit_to_use) = match &input[..] { + [number_str] => (number_str.parse::().ok(), unit.0), + [number_str, decimal_part_str] => match unit.2 { + Some(unit_to_convert_to) => match ( + number_str.parse::(), + parse_decimal_str_to_number(decimal_part_str), + ) { + (Ok(number), Some(decimal_part)) => ( + Some( + (number * unit_to_convert_to.1) + (unit_to_convert_to.1 / decimal_part), + ), + unit_to_convert_to.0, + ), + _ => (None, unit.0), + }, + None => (None, unit.0), + }, + _ => (None, unit.0), + }; + + if let Some(x) = value { + let lhs_span = Span::new(span.start, span.start + lhs.len()); + let unit_span = Span::new(span.start + lhs.len(), span.end); + return ( + Expression { + expr: Expr::ValueWithUnit( + Box::new(Expression { + expr: Expr::Int(x), + span: lhs_span, + ty: Type::Number, + custom_completion: None, + }), + Spanned { + item: unit_to_use, + span: unit_span, + }, + ), + span, + ty: Type::Duration, + custom_completion: None, + }, + None, + ); + } + } + + ( + garbage(span), + Some(ParseError::Mismatch( + "duration".into(), + "non-duration unit".into(), + span, + )), + ) +} + +/// Parse a unit type, eg '10kb' +pub fn parse_filesize( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + fn parse_decimal_str_to_number(decimal: &str) -> Option { + let string_to_parse = format!("0.{}", decimal); + if let Ok(x) = string_to_parse.parse::() { + return Some((1_f64 / x) as i64); + } + None + } + + let bytes = working_set.get_span_contents(span); + let token = String::from_utf8_lossy(bytes).to_string(); + + let unit_groups = [ + (Unit::Kilobyte, "KB", Some((Unit::Byte, 1000))), + (Unit::Megabyte, "MB", Some((Unit::Kilobyte, 1000))), + (Unit::Gigabyte, "GB", Some((Unit::Megabyte, 1000))), + (Unit::Terabyte, "TB", Some((Unit::Gigabyte, 1000))), + (Unit::Petabyte, "PB", Some((Unit::Terabyte, 1000))), + (Unit::Kibibyte, "KIB", Some((Unit::Byte, 1024))), + (Unit::Mebibyte, "MIB", Some((Unit::Kibibyte, 1024))), + (Unit::Gibibyte, "GIB", Some((Unit::Mebibyte, 1024))), + (Unit::Tebibyte, "TIB", Some((Unit::Gibibyte, 1024))), + (Unit::Pebibyte, "PIB", Some((Unit::Tebibyte, 1024))), + (Unit::Byte, "B", None), + ]; + if let Some(unit) = unit_groups + .iter() + .find(|&x| token.to_uppercase().ends_with(x.1)) + { + let mut lhs = token.clone(); + for _ in 0..unit.1.len() { + lhs.pop(); + } + + let input: Vec<&str> = lhs.split('.').collect(); + let (value, unit_to_use) = match &input[..] { + [number_str] => (number_str.parse::().ok(), unit.0), + [number_str, decimal_part_str] => match unit.2 { + Some(unit_to_convert_to) => match ( + number_str.parse::(), + parse_decimal_str_to_number(decimal_part_str), + ) { + (Ok(number), Some(decimal_part)) => ( + Some( + (number * unit_to_convert_to.1) + (unit_to_convert_to.1 / decimal_part), + ), + unit_to_convert_to.0, + ), + _ => (None, unit.0), + }, + None => (None, unit.0), + }, + _ => (None, unit.0), + }; + + if let Some(x) = value { + let lhs_span = Span::new(span.start, span.start + lhs.len()); + let unit_span = Span::new(span.start + lhs.len(), span.end); + return ( + Expression { + expr: Expr::ValueWithUnit( + Box::new(Expression { + expr: Expr::Int(x), + span: lhs_span, + ty: Type::Number, + custom_completion: None, + }), + Spanned { + item: unit_to_use, + span: unit_span, + }, + ), + span, + ty: Type::Filesize, + custom_completion: None, + }, + None, + ); + } + } + + ( + garbage(span), + Some(ParseError::Mismatch( + "filesize".into(), + "non-filesize unit".into(), + span, + )), + ) +} + +pub fn parse_glob_pattern( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + let bytes = if (bytes.starts_with(b"\"") && bytes.ends_with(b"\"") && bytes.len() > 1) + || (bytes.starts_with(b"\'") && bytes.ends_with(b"\'") && bytes.len() > 1) + { + &bytes[1..(bytes.len() - 1)] + } else { + bytes + }; + + if let Ok(token) = String::from_utf8(bytes.into()) { + ( + Expression { + expr: Expr::GlobPattern(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("string".into(), span)), + ) + } +} + pub fn parse_string( working_set: &mut StateWorkingSet, span: Span, @@ -1365,7 +1614,7 @@ pub fn parse_shape_name( b"number" => SyntaxShape::Number, b"range" => SyntaxShape::Range, b"int" => SyntaxShape::Int, - b"path" => SyntaxShape::FilePath, + b"path" => SyntaxShape::Filepath, b"glob" => SyntaxShape::GlobPattern, b"block" => SyntaxShape::Block(None), //FIXME b"cond" => SyntaxShape::RowCondition, @@ -2320,10 +2569,12 @@ pub fn parse_value( } SyntaxShape::Number => parse_number(bytes, span), SyntaxShape::Int => parse_int(bytes, span), + SyntaxShape::Duration => parse_duration(working_set, span), + SyntaxShape::Filesize => parse_filesize(working_set, span), SyntaxShape::Range => parse_range(working_set, span), - SyntaxShape::String | SyntaxShape::GlobPattern | SyntaxShape::FilePath => { - parse_string(working_set, span) - } + SyntaxShape::Filepath => parse_filepath(working_set, span), + SyntaxShape::GlobPattern => parse_glob_pattern(working_set, span), + SyntaxShape::String => parse_string(working_set, span), SyntaxShape::Block(_) => { if bytes.starts_with(b"{") { parse_block_expression(working_set, shape, span) diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index fa07f755da..03ce14c6e6 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -29,6 +29,9 @@ pub fn math_result_type( (Type::Int, Type::Float) => (Type::Float, None), (Type::Float, Type::Float) => (Type::Float, None), (Type::String, Type::String) => (Type::String, None), + (Type::Duration, Type::Duration) => (Type::Duration, None), + (Type::Filesize, Type::Filesize) => (Type::Filesize, None), + (Type::Unknown, _) => (Type::Unknown, None), (_, Type::Unknown) => (Type::Unknown, None), (Type::Int, _) => { @@ -64,6 +67,9 @@ pub fn math_result_type( (Type::Float, Type::Int) => (Type::Float, None), (Type::Int, Type::Float) => (Type::Float, None), (Type::Float, Type::Float) => (Type::Float, None), + (Type::Duration, Type::Duration) => (Type::Duration, None), + (Type::Filesize, Type::Filesize) => (Type::Filesize, None), + (Type::Unknown, _) => (Type::Unknown, None), (_, Type::Unknown) => (Type::Unknown, None), _ => { @@ -85,6 +91,7 @@ pub fn math_result_type( (Type::Float, Type::Int) => (Type::Float, None), (Type::Int, Type::Float) => (Type::Float, None), (Type::Float, Type::Float) => (Type::Float, None), + (Type::Unknown, _) => (Type::Unknown, None), (_, Type::Unknown) => (Type::Unknown, None), _ => { @@ -106,6 +113,7 @@ pub fn math_result_type( (Type::Float, Type::Int) => (Type::Float, None), (Type::Int, Type::Float) => (Type::Float, None), (Type::Float, Type::Float) => (Type::Float, None), + (Type::Unknown, _) => (Type::Unknown, None), (_, Type::Unknown) => (Type::Unknown, None), _ => { @@ -127,6 +135,9 @@ pub fn math_result_type( (Type::Float, Type::Int) => (Type::Bool, None), (Type::Int, Type::Float) => (Type::Bool, None), (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + (Type::Unknown, _) => (Type::Bool, None), (_, Type::Unknown) => (Type::Bool, None), _ => { @@ -148,6 +159,9 @@ pub fn math_result_type( (Type::Float, Type::Int) => (Type::Bool, None), (Type::Int, Type::Float) => (Type::Bool, None), (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + (Type::Unknown, _) => (Type::Bool, None), (_, Type::Unknown) => (Type::Bool, None), _ => { @@ -169,6 +183,9 @@ pub fn math_result_type( (Type::Float, Type::Int) => (Type::Bool, None), (Type::Int, Type::Float) => (Type::Bool, None), (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + (Type::Unknown, _) => (Type::Bool, None), (_, Type::Unknown) => (Type::Bool, None), _ => { @@ -190,6 +207,9 @@ pub fn math_result_type( (Type::Float, Type::Int) => (Type::Bool, None), (Type::Int, Type::Float) => (Type::Bool, None), (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + (Type::Unknown, _) => (Type::Bool, None), (_, Type::Unknown) => (Type::Bool, None), _ => { @@ -209,6 +229,9 @@ pub fn math_result_type( Operator::Equal => match (&lhs.ty, &rhs.ty) { (Type::Float, Type::Int) => (Type::Bool, None), (Type::Int, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + (x, y) if x == y => (Type::Bool, None), (Type::Unknown, _) => (Type::Bool, None), (_, Type::Unknown) => (Type::Bool, None), @@ -231,6 +254,9 @@ pub fn math_result_type( (Type::Float, Type::Int) => (Type::Bool, None), (Type::Int, Type::Float) => (Type::Bool, None), (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + (Type::Unknown, _) => (Type::Bool, None), (_, Type::Unknown) => (Type::Bool, None), _ => { diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index a4fbcd6bf8..c44b03cf08 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -9,3 +9,6 @@ edition = "2018" thiserror = "1.0.29" miette = "3.0.0" serde = {version = "1.0.130", features = ["derive"]} +chrono = { version="0.4.19", features=["serde"] } +chrono-humanize = "0.2.1" +byte-unit = "4.0.9" \ No newline at end of file diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 449f4687df..4a9b42527d 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -1,5 +1,5 @@ use super::{Call, CellPath, Expression, FullCellPath, Operator, RangeOperator}; -use crate::{BlockId, Signature, Span, VarId}; +use crate::{BlockId, Signature, Span, Spanned, Unit, VarId}; #[derive(Debug, Clone)] pub enum Expr { @@ -23,6 +23,9 @@ pub enum Expr { List(Vec), Table(Vec, Vec>), Keyword(Vec, Span, Box), + ValueWithUnit(Box, Spanned), + Filepath(String), + GlobPattern(String), String(String), // FIXME: improve this in the future? CellPath(CellPath), FullCellPath(Box), diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index bbe6bdb1c1..3d8f2c8229 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -12,12 +12,12 @@ pub struct EngineState { vars: Vec, decls: Vec>, blocks: Vec, - scope: Vec, + pub scope: Vec, } #[derive(Debug)] pub struct ScopeFrame { - vars: HashMap, VarId>, + pub vars: HashMap, VarId>, decls: HashMap, DeclId>, aliases: HashMap, Vec>, modules: HashMap, BlockId>, @@ -375,7 +375,7 @@ impl<'a> StateWorkingSet<'a> { if let Some(decl_id) = scope.decls.get(name) { if !hiding.contains(decl_id) { - // Do not hide already hidden decl + // Hide decl only if it's not already hidden last_scope_frame.hiding.insert(*decl_id); return Some(*decl_id); } @@ -409,8 +409,6 @@ impl<'a> StateWorkingSet<'a> { } pub fn activate_overlay(&mut self, overlay: Vec<(Vec, DeclId)>) { - // TODO: This will overwrite all existing definitions in a scope. When we add deactivate, - // we need to re-think how make it recoverable. let scope_frame = self .delta .scope diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 8307e99617..6a7464508e 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -78,4 +78,53 @@ pub enum ShellError { #[error("Flag not found")] #[diagnostic(code(nu::shell::flag_not_found), url(docsrs))] FlagNotFound(String, #[label("{0} not found")] Span), + + #[error("File not found")] + #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] + FileNotFound(#[label("file not found")] Span), + + #[error("File not found")] + #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] + FileNotFoundCustom(String, #[label("{0}")] Span), + + #[error("Directory not found")] + #[diagnostic(code(nu::shell::directory_not_found), url(docsrs))] + DirectoryNotFound(#[label("directory not found")] Span), + + #[error("File not found")] + #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] + DirectoryNotFoundCustom(String, #[label("{0}")] Span), + + #[error("Move not possible")] + #[diagnostic(code(nu::shell::move_not_possible), url(docsrs))] + MoveNotPossible { + source_message: String, + #[label("{source_message}")] + source_span: Span, + destination_message: String, + #[label("{destination_message}")] + destination_span: Span, + }, + + #[error("Move not possible")] + #[diagnostic(code(nu::shell::move_not_possible_single), url(docsrs))] + MoveNotPossibleSingle(String, #[label("{0}")] Span), +} + +impl From for ShellError { + fn from(input: std::io::Error) -> ShellError { + ShellError::InternalError(format!("{:?}", input)) + } +} + +impl std::convert::From> for ShellError { + fn from(input: Box) -> ShellError { + ShellError::InternalError(input.to_string()) + } +} + +impl From> for ShellError { + fn from(input: Box) -> ShellError { + ShellError::InternalError(format!("{:?}", input)) + } } diff --git a/crates/nu-protocol/src/span.rs b/crates/nu-protocol/src/span.rs index 1dd0b38982..744915783a 100644 --- a/crates/nu-protocol/src/span.rs +++ b/crates/nu-protocol/src/span.rs @@ -1,7 +1,11 @@ use miette::SourceSpan; use serde::{Deserialize, Serialize}; -pub struct Spanned { +#[derive(Clone, Debug)] +pub struct Spanned +where + T: Clone + std::fmt::Debug, +{ pub item: T, pub span: Span, } diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index 8b752461e7..d1202528ac 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -28,7 +28,7 @@ pub enum SyntaxShape { Int, /// A filepath is allowed - FilePath, + Filepath, /// A glob pattern is allowed, eg `foo*` GlobPattern, @@ -86,7 +86,7 @@ impl SyntaxShape { SyntaxShape::Custom(custom, _) => custom.to_type(), SyntaxShape::Duration => Type::Duration, SyntaxShape::Expression => Type::Unknown, - SyntaxShape::FilePath => Type::String, + SyntaxShape::Filepath => Type::String, SyntaxShape::Filesize => Type::Filesize, SyntaxShape::FullCellPath => Type::Unknown, SyntaxShape::GlobPattern => Type::String, diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index c11bafaca8..9c70210cfe 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -12,7 +12,7 @@ pub enum Type { Block, CellPath, Duration, - FilePath, + Date, Filesize, List(Box), Number, @@ -31,8 +31,8 @@ impl Display for Type { Type::Block => write!(f, "block"), Type::Bool => write!(f, "bool"), Type::CellPath => write!(f, "cell path"), + Type::Date => write!(f, "date"), Type::Duration => write!(f, "duration"), - Type::FilePath => write!(f, "filepath"), Type::Filesize => write!(f, "filesize"), Type::Float => write!(f, "float"), Type::Int => write!(f, "int"), diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 3259e7496c..afc1f90161 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -1,11 +1,15 @@ mod range; mod row; mod stream; +mod unit; +use chrono::{DateTime, FixedOffset}; +use chrono_humanize::HumanTime; pub use range::*; pub use row::*; use serde::{Deserialize, Serialize}; pub use stream::*; +pub use unit::*; use std::fmt::Debug; @@ -26,11 +30,15 @@ pub enum Value { span: Span, }, Filesize { - val: u64, + val: i64, span: Span, }, Duration { - val: u64, + val: i64, + span: Span, + }, + Date { + val: DateTime, span: Span, }, Range { @@ -95,6 +103,7 @@ impl Value { Value::Float { span, .. } => *span, Value::Filesize { span, .. } => *span, Value::Duration { span, .. } => *span, + Value::Date { span, .. } => *span, Value::Range { span, .. } => *span, Value::String { span, .. } => *span, Value::Record { span, .. } => *span, @@ -115,6 +124,7 @@ impl Value { Value::Float { span, .. } => *span = new_span, Value::Filesize { span, .. } => *span = new_span, Value::Duration { span, .. } => *span = new_span, + Value::Date { span, .. } => *span = new_span, Value::Range { span, .. } => *span = new_span, Value::String { span, .. } => *span = new_span, Value::Record { span, .. } => *span = new_span, @@ -138,6 +148,7 @@ impl Value { Value::Float { .. } => Type::Float, Value::Filesize { .. } => Type::Filesize, Value::Duration { .. } => Type::Duration, + Value::Date { .. } => Type::Date, Value::Range { .. } => Type::Range, Value::String { .. } => Type::String, Value::Record { cols, vals, .. } => { @@ -159,8 +170,9 @@ impl Value { Value::Bool { val, .. } => val.to_string(), Value::Int { val, .. } => val.to_string(), Value::Float { val, .. } => val.to_string(), - Value::Filesize { val, .. } => format!("{} bytes", val), - Value::Duration { val, .. } => format!("{} ns", val), + Value::Filesize { val, .. } => format_filesize(val), + Value::Duration { val, .. } => format_duration(val), + Value::Date { val, .. } => HumanTime::from(val).to_string(), Value::Range { val, .. } => { format!( "range: [{}]", @@ -202,6 +214,7 @@ impl Value { Value::Float { val, .. } => val.to_string(), Value::Filesize { val, .. } => format!("{} bytes", val), Value::Duration { val, .. } => format!("{} ns", val), + Value::Date { val, .. } => format!("{:?}", val), Value::Range { val, .. } => val .into_iter() .map(|x| x.into_string()) @@ -391,6 +404,18 @@ impl Value { val: lhs.to_string() + rhs, span, }), + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Duration { + val: *lhs + *rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Filesize { + val: *lhs + *rhs, + span, + }) + } _ => Err(ShellError::OperatorMismatch { op_span: op, @@ -421,6 +446,18 @@ impl Value { val: lhs - rhs, span, }), + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Duration { + val: *lhs - *rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Filesize { + val: *lhs - *rhs, + span, + }) + } _ => Err(ShellError::OperatorMismatch { op_span: op, @@ -542,6 +579,19 @@ impl Value { val: lhs < rhs, span, }), + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs < rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs < rhs, + span, + }) + } + _ => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), @@ -571,6 +621,18 @@ impl Value { val: lhs <= rhs, span, }), + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs <= rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs <= rhs, + span, + }) + } _ => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), @@ -600,6 +662,18 @@ impl Value { val: lhs > rhs, span, }), + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs > rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs > rhs, + span, + }) + } _ => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), @@ -629,6 +703,18 @@ impl Value { val: lhs >= rhs, span, }), + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs >= rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs >= rhs, + span, + }) + } _ => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), @@ -665,6 +751,18 @@ impl Value { val: lhs == rhs, span, }), + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs == rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs == rhs, + span, + }) + } (Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => Ok(Value::Bool { val: lhs == rhs, span, @@ -720,6 +818,18 @@ impl Value { val: lhs != rhs, span, }), + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs != rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Bool { + val: lhs != rhs, + span, + }) + } (Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => Ok(Value::Bool { val: lhs != rhs, span, @@ -750,3 +860,69 @@ impl Value { } } } + +/// Format a duration in nanoseconds into a string +pub fn format_duration(duration: i64) -> String { + let (sign, duration) = if duration >= 0 { + (1, duration) + } else { + (-1, -duration) + }; + let (micros, nanos): (i64, i64) = (duration / 1000, duration % 1000); + let (millis, micros): (i64, i64) = (micros / 1000, micros % 1000); + let (secs, millis): (i64, i64) = (millis / 1000, millis % 1000); + let (mins, secs): (i64, i64) = (secs / 60, secs % 60); + let (hours, mins): (i64, i64) = (mins / 60, mins % 60); + let (days, hours): (i64, i64) = (hours / 24, hours % 24); + + let mut output_prep = vec![]; + + if days != 0 { + output_prep.push(format!("{}day", days)); + } + + if hours != 0 { + output_prep.push(format!("{}hr", hours)); + } + + if mins != 0 { + output_prep.push(format!("{}min", mins)); + } + // output 0sec for zero duration + if duration == 0 || secs != 0 { + output_prep.push(format!("{}sec", secs)); + } + + if millis != 0 { + output_prep.push(format!("{}ms", millis)); + } + + if micros != 0 { + output_prep.push(format!("{}us", micros)); + } + + if nanos != 0 { + output_prep.push(format!("{}ns", nanos)); + } + + format!( + "{}{}", + if sign == -1 { "-" } else { "" }, + output_prep.join(" ") + ) +} + +fn format_filesize(num_bytes: i64) -> String { + let byte = byte_unit::Byte::from_bytes(num_bytes as u128); + + if byte.get_bytes() == 0u128 { + return "—".to_string(); + } + + let byte = byte.get_appropriate_unit(false); + + match byte.get_unit() { + byte_unit::ByteUnit::B => format!("{} B ", byte.get_value()), + _ => byte.format(1), + } +} diff --git a/crates/nu-protocol/src/value/unit.rs b/crates/nu-protocol/src/value/unit.rs new file mode 100644 index 0000000000..27fd893014 --- /dev/null +++ b/crates/nu-protocol/src/value/unit.rs @@ -0,0 +1,27 @@ +#[derive(Debug, Clone, Copy)] +pub enum Unit { + // Filesize units: metric + Byte, + Kilobyte, + Megabyte, + Gigabyte, + Terabyte, + Petabyte, + + // Filesize units: ISO/IEC 80000 + Kibibyte, + Mebibyte, + Gibibyte, + Tebibyte, + Pebibyte, + + // Duration units + Nanosecond, + Microsecond, + Millisecond, + Second, + Minute, + Hour, + Day, + Week, +} diff --git a/src/main.rs b/src/main.rs index c821e30e8c..f767a89536 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,7 +66,7 @@ fn main() -> Result<()> { Ok(()) } else { - use reedline::{DefaultCompletionActionHandler, FileBackedHistory, Reedline, Signal}; + use reedline::{FileBackedHistory, ListCompletionHandler, Reedline, Signal}; let completer = NuCompleter::new(engine_state.clone()); let mut entry_num = 0; @@ -81,7 +81,7 @@ fn main() -> Result<()> { engine_state: engine_state.clone(), })) .with_completion_action_handler(Box::new( - DefaultCompletionActionHandler::default().with_completer(Box::new(completer)), + ListCompletionHandler::default().with_completer(Box::new(completer)), )) .with_validator(Box::new(NuValidator { engine_state: engine_state.clone(), diff --git a/src/tests.rs b/src/tests.rs index 7416aecb6f..0efd0b513a 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -441,6 +441,46 @@ fn hide_twice_not_allowed() -> TestResult { ) } +#[test] +fn hides_import_1() -> TestResult { + fail_test( + r#"module spam { export def foo [] { "foo" } }; use spam; hide spam.foo; foo"#, + "not found", + ) +} + +#[test] +fn hides_import_2() -> TestResult { + fail_test( + r#"module spam { export def foo [] { "foo" } }; use spam; hide spam.*; foo"#, + "not found", + ) +} + +#[test] +fn hides_import_3() -> TestResult { + fail_test( + r#"module spam { export def foo [] { "foo" } }; use spam; hide spam.[foo]; foo"#, + "not found", + ) +} + +#[test] +fn hides_import_4() -> TestResult { + fail_test( + r#"module spam { export def foo [] { "foo" } }; use spam.foo; hide foo; foo"#, + "not found", + ) +} + +#[test] +fn hides_import_5() -> TestResult { + fail_test( + r#"module spam { export def foo [] { "foo" } }; use spam.*; hide foo; foo"#, + "not found", + ) +} + #[test] fn def_twice_should_fail() -> TestResult { fail_test( @@ -449,6 +489,15 @@ fn def_twice_should_fail() -> TestResult { ) } +// TODO: This test fails if executed each command on a separate line in REPL +#[test] +fn use_import_after_hide() -> TestResult { + run_test( + r#"module spam { export def foo [] { "foo" } }; use spam.foo; hide foo; use spam.foo; foo"#, + "foo", + ) +} + #[test] fn from_json_1() -> TestResult { run_test(r#"('{"name": "Fred"}' | from json).name"#, "Fred")