diff --git a/Cargo.lock b/Cargo.lock index a677cb4f35..cf02dbe5c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3254,6 +3254,7 @@ dependencies = [ "nu-engine", "nu-errors", "nu-parser", + "nu-path", "nu-plugin", "nu-protocol", "nu-source", @@ -3373,7 +3374,6 @@ dependencies = [ "serde_yaml", "sha2 0.9.5", "shadow-rs", - "shellexpand", "strip-ansi-escapes", "sxd-document", "sxd-xpath", @@ -3438,6 +3438,7 @@ dependencies = [ "nu-errors", "nu-json", "nu-parser", + "nu-path", "nu-plugin", "nu-pretty-hex", "nu-protocol", @@ -3472,7 +3473,6 @@ dependencies = [ "serde_yaml", "sha2 0.9.5", "shadow-rs", - "shellexpand", "strip-ansi-escapes", "sxd-document", "sxd-xpath", @@ -3501,6 +3501,7 @@ dependencies = [ "nu-data", "nu-errors", "nu-parser", + "nu-path", "nu-protocol", "nu-source", "nu-test-support", @@ -3571,6 +3572,7 @@ dependencies = [ "nu-data", "nu-errors", "nu-parser", + "nu-path", "nu-plugin", "nu-protocol", "nu-source", @@ -3639,16 +3641,24 @@ dependencies = [ "itertools", "log 0.4.14", "nu-errors", + "nu-path", "nu-protocol", "nu-source", "nu-test-support", "num-bigint 0.3.2", "num-traits 0.2.14", "serde 1.0.126", - "shellexpand", "smart-default", ] +[[package]] +name = "nu-path" +version = "0.32.1" +dependencies = [ + "dirs-next", + "dunce", +] + [[package]] name = "nu-plugin" version = "0.32.1" @@ -5812,15 +5822,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" -[[package]] -name = "shellexpand" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829" -dependencies = [ - "dirs-next", -] - [[package]] name = "signal-hook" version = "0.1.17" diff --git a/Cargo.toml b/Cargo.toml index 172435ff4e..f611f8882c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ nu-data = { version = "0.32.1", path = "./crates/nu-data" } nu-engine = { version = "0.32.1", path = "./crates/nu-engine" } nu-errors = { version = "0.32.1", path = "./crates/nu-errors" } nu-parser = { version = "0.32.1", path = "./crates/nu-parser" } +nu-path = { version = "0.32.1", path = "./crates/nu-path" } nu-plugin = { version = "0.32.1", path = "./crates/nu-plugin" } nu-protocol = { version = "0.32.1", path = "./crates/nu-protocol" } nu-source = { version = "0.32.1", path = "./crates/nu-source" } diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 178a003a99..37eb3f321c 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -83,7 +83,6 @@ serde_json = "1.0.61" serde_urlencoded = "0.7.0" serde_yaml = "0.8.16" sha2 = "0.9.3" -shellexpand = "2.1.0" strip-ansi-escapes = "0.1.0" sxd-document = "0.3.2" sxd-xpath = "0.4.2" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 8da475f894..52fb906f4a 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -15,6 +15,7 @@ nu-data = { version = "0.32.1", path = "../nu-data" } nu-engine = { version = "0.32.1", path = "../nu-engine" } nu-errors = { version = "0.32.1", path = "../nu-errors" } nu-json = { version = "0.32.1", path = "../nu-json" } +nu-path = { version = "0.32.1", path = "../nu-path" } nu-parser = { version = "0.32.1", path = "../nu-parser" } nu-plugin = { version = "0.32.1", path = "../nu-plugin" } nu-protocol = { version = "0.32.1", path = "../nu-protocol" } @@ -81,7 +82,6 @@ serde_json = "1.0.61" serde_urlencoded = "0.7.0" serde_yaml = "0.8.16" sha2 = "0.9.3" -shellexpand = "2.1.0" strip-ansi-escapes = "0.1.0" sxd-document = "0.3.2" sxd-xpath = "0.4.2" diff --git a/crates/nu-command/src/classified/external.rs b/crates/nu-command/src/classified/external.rs index f155de44de..d8d816cf62 100644 --- a/crates/nu-command/src/classified/external.rs +++ b/crates/nu-command/src/classified/external.rs @@ -5,7 +5,6 @@ use nu_test_support::NATIVE_PATH_ENV_VAR; use parking_lot::Mutex; use std::io::Write; -use std::ops::Deref; use std::process::{Command, Stdio}; use std::sync::mpsc; use std::{borrow::Cow, io::BufReader}; @@ -105,7 +104,7 @@ fn run_with_stdin( let process_args = command_args .iter() .map(|(arg, _is_literal)| { - let arg = expand_tilde(arg.deref(), dirs_next::home_dir); + let arg = nu_path::expand_tilde_string(Cow::Borrowed(arg)); #[cfg(not(windows))] { @@ -126,7 +125,7 @@ fn run_with_stdin( if let Some(unquoted) = remove_quotes(&arg) { unquoted.to_string() } else { - arg.as_ref().to_string() + arg.to_string() } } }) @@ -486,15 +485,6 @@ impl Iterator for ChannelReceiver { } } -fn expand_tilde(input: &SI, home_dir: HD) -> std::borrow::Cow -where - SI: AsRef, - P: AsRef, - HD: FnOnce() -> Option

, -{ - shellexpand::tilde_with_context(input, home_dir) -} - fn argument_is_quoted(argument: &str) -> bool { if argument.len() < 2 { return false; @@ -543,9 +533,7 @@ fn shell_os_paths() -> Vec { #[cfg(test)] mod tests { - use super::{ - add_double_quotes, argument_is_quoted, escape_double_quotes, expand_tilde, remove_quotes, - }; + use super::{add_double_quotes, argument_is_quoted, escape_double_quotes, remove_quotes}; #[cfg(feature = "which")] use super::{run_external_command, InputStream}; @@ -667,20 +655,4 @@ mod tests { assert_eq!(remove_quotes("'andrés'"), Some("andrés")); assert_eq!(remove_quotes(r#""andrés""#), Some("andrés")); } - - #[test] - fn expands_tilde_if_starts_with_tilde_character() { - assert_eq!( - expand_tilde("~", || Some(std::path::Path::new("the_path_to_nu_light"))), - "the_path_to_nu_light" - ); - } - - #[test] - fn does_not_expand_tilde_if_tilde_is_not_first_character() { - assert_eq!( - expand_tilde("1~1", || Some(std::path::Path::new("the_path_to_nu_light"))), - "1~1" - ); - } } diff --git a/crates/nu-command/src/commands/core_commands/nu_plugin.rs b/crates/nu-command/src/commands/core_commands/nu_plugin.rs index 35bee3b377..1dff1d0886 100644 --- a/crates/nu-command/src/commands/core_commands/nu_plugin.rs +++ b/crates/nu-command/src/commands/core_commands/nu_plugin.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; use crate::prelude::*; -use nu_engine::filesystem::path::canonicalize; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; +use nu_path::canonicalize; use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; use nu_source::Tagged; diff --git a/crates/nu-command/src/commands/core_commands/source.rs b/crates/nu-command/src/commands/core_commands/source.rs index 653019658b..ee8317713f 100644 --- a/crates/nu-command/src/commands/core_commands/source.rs +++ b/crates/nu-command/src/commands/core_commands/source.rs @@ -2,10 +2,12 @@ use crate::prelude::*; use nu_engine::{script, WholeStreamCommand}; use nu_errors::ShellError; -use nu_parser::expand_path; +use nu_path::expand_path; use nu_protocol::{Signature, SyntaxShape}; use nu_source::Tagged; +use std::{borrow::Cow, path::Path}; + pub struct Source; #[derive(Deserialize)] @@ -46,7 +48,7 @@ pub fn source(args: CommandArgs) -> Result { // Note: this is a special case for setting the context from a command // In this case, if we don't set it now, we'll lose the scope that this // variable should be set into. - let contents = std::fs::read_to_string(expand_path(&filename.item).into_owned()); + let contents = std::fs::read_to_string(&expand_path(Cow::Borrowed(Path::new(&filename.item)))); match contents { Ok(contents) => { let result = script::run_script_standalone(contents, true, &ctx, false); diff --git a/crates/nu-command/src/commands/path/expand.rs b/crates/nu-command/src/commands/path/expand.rs index efc4874d3b..6a63bf8cf4 100644 --- a/crates/nu-command/src/commands/path/expand.rs +++ b/crates/nu-command/src/commands/path/expand.rs @@ -1,12 +1,11 @@ use super::{operate, PathSubcommandArguments}; use crate::prelude::*; -use nu_engine::filesystem::path::expand_tilde; -use nu_engine::filesystem::path::resolve_dots; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; +use nu_path::expand_path; use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; use nu_source::Span; -use std::path::Path; +use std::{borrow::Cow, path::Path}; pub struct PathExpand; @@ -102,13 +101,7 @@ fn action(path: &Path, tag: Tag, args: &PathExpandArguments) -> Value { tag.span, )) } else { - // "best effort" mode, just expand tilde and resolve single/double dots - let path = match expand_tilde(path) { - Some(expanded) => expanded, - None => path.into(), - }; - - UntaggedValue::filepath(resolve_dots(&path)).into_value(tag) + UntaggedValue::filepath(expand_path(Cow::Borrowed(path))).into_value(tag) } } diff --git a/crates/nu-command/tests/commands/path/expand.rs b/crates/nu-command/tests/commands/path/expand.rs index 9ff7b57117..08ca9dc587 100644 --- a/crates/nu-command/tests/commands/path/expand.rs +++ b/crates/nu-command/tests/commands/path/expand.rs @@ -43,3 +43,36 @@ fn expands_path_with_double_dot() { assert_eq!(PathBuf::from(actual.out), expected); }) } + +#[cfg(windows)] +mod windows { + use super::*; + + #[test] + fn expands_path_with_tilde_backward_slash() { + Playground::setup("path_expand_2", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "~\tmp.txt" | path expand + "# + )); + + assert!(!PathBuf::from(actual.out).starts_with("~")); + }) + } + + #[test] + fn win_expands_path_with_tilde_forward_slash() { + Playground::setup("path_expand_2", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "~/tmp.txt" | path expand + "# + )); + + assert!(!PathBuf::from(actual.out).starts_with("~")); + }) + } +} diff --git a/crates/nu-completion/Cargo.toml b/crates/nu-completion/Cargo.toml index 356cbf9bfc..6f218b8bc5 100644 --- a/crates/nu-completion/Cargo.toml +++ b/crates/nu-completion/Cargo.toml @@ -13,6 +13,7 @@ doctest = false nu-data = { version = "0.32.1", path = "../nu-data" } nu-errors = { version = "0.32.1", path = "../nu-errors" } nu-parser = { version = "0.32.1", path = "../nu-parser" } +nu-path = { version = "0.32.1", path = "../nu-path" } nu-protocol = { version = "0.32.1", path = "../nu-protocol" } nu-source = { version = "0.32.1", path = "../nu-source" } nu-test-support = { version = "0.32.1", path = "../nu-test-support" } diff --git a/crates/nu-completion/src/path.rs b/crates/nu-completion/src/path.rs index ea39cf3670..79c32d3433 100644 --- a/crates/nu-completion/src/path.rs +++ b/crates/nu-completion/src/path.rs @@ -1,4 +1,5 @@ -use std::path::PathBuf; +use std::borrow::Cow; +use std::path::{is_separator, Path, PathBuf}; use super::matchers::Matcher; use crate::{Completer, CompletionContext, Suggestion}; @@ -7,6 +8,7 @@ const SEP: char = std::path::MAIN_SEPARATOR; pub struct PathCompleter; +#[derive(Debug)] pub struct PathSuggestion { pub(crate) path: PathBuf, pub(crate) suggestion: Suggestion, @@ -14,27 +16,23 @@ pub struct PathSuggestion { impl PathCompleter { pub fn path_suggestions(&self, partial: &str, matcher: &dyn Matcher) -> Vec { - let expanded = nu_parser::expand_ndots(partial); - let expanded = expanded.replace(std::path::is_separator, &SEP.to_string()); - let expanded: &str = expanded.as_ref(); + 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()); - let (base_dir_name, partial) = match expanded.rfind(SEP) { - Some(pos) => expanded.split_at(pos + SEP.len_utf8()), - None => ("", expanded), + // rsplit_once removes the separator + base.push(SEP); + (base, rest) }; - let base_dir = if base_dir_name.is_empty() { - PathBuf::from(".") - } else { - let home_prefix = format!("~{}", SEP); - if base_dir_name.starts_with(&home_prefix) { - let mut home_dir = dirs_next::home_dir().unwrap_or_else(|| PathBuf::from("~")); - home_dir.push(&base_dir_name[2..]); - home_dir - } else { - PathBuf::from(base_dir_name) - } - }; + let base_dir = nu_path::expand_path(Cow::Borrowed(Path::new(&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 @@ -42,10 +40,10 @@ impl PathCompleter { entry.ok().and_then(|entry| { let mut file_name = entry.file_name().to_string_lossy().into_owned(); if matcher.matches(partial, file_name.as_str()) { - let mut path = format!("{}{}", base_dir_name, file_name); + let mut path = format!("{}{}", &base_dir_name, file_name); if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { - path.push(std::path::MAIN_SEPARATOR); - file_name.push(std::path::MAIN_SEPARATOR); + path.push(SEP); + file_name.push(SEP); } Some(PathSuggestion { diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index d453309cc6..a711d3ae5f 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -17,6 +17,7 @@ nu-stream = { version="0.32.1", path="../nu-stream" } nu-value-ext = { version="0.32.1", path="../nu-value-ext" } nu-ansi-term = { version="0.32.1", path="../nu-ansi-term" } nu-test-support = { version="0.32.1", path="../nu-test-support" } +nu-path = { version = "0.32.1", path = "../nu-path" } trash = { version="1.3.0", optional=true } which = { version="4.0.2", optional=true } diff --git a/crates/nu-engine/src/filesystem/filesystem_shell.rs b/crates/nu-engine/src/filesystem/filesystem_shell.rs index 0595824dfb..d4c49dd104 100644 --- a/crates/nu-engine/src/filesystem/filesystem_shell.rs +++ b/crates/nu-engine/src/filesystem/filesystem_shell.rs @@ -1,4 +1,3 @@ -use crate::filesystem::path::canonicalize; use crate::filesystem::utils::FileStructure; use crate::maybe_text_codec::{MaybeTextCodec, StringOrBinary}; use crate::shell::shell_args::{CdArgs, CopyArgs, LsArgs, MkdirArgs, MvArgs, RemoveArgs}; @@ -10,6 +9,7 @@ use crate::{ }; use encoding_rs::Encoding; use nu_data::config::LocalConfigDiff; +use nu_path::canonicalize; use nu_protocol::{CommandAction, ConfigPath, TaggedDictBuilder, Value}; use nu_source::{Span, Tag}; use nu_stream::{ActionStream, Interruptible, IntoActionStream, OutputStream}; diff --git a/crates/nu-engine/src/filesystem/mod.rs b/crates/nu-engine/src/filesystem/mod.rs index 105462f27f..8c9b64452d 100644 --- a/crates/nu-engine/src/filesystem/mod.rs +++ b/crates/nu-engine/src/filesystem/mod.rs @@ -1,4 +1,3 @@ pub(crate) mod dir_info; pub mod filesystem_shell; -pub mod path; pub(crate) mod utils; diff --git a/crates/nu-engine/src/filesystem/path.rs b/crates/nu-engine/src/filesystem/path.rs deleted file mode 100644 index b0c152b574..0000000000 --- a/crates/nu-engine/src/filesystem/path.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::io; -use std::path::{Component, Path, PathBuf}; - -pub fn resolve_dots

(path: P) -> PathBuf -where - P: AsRef, -{ - let mut result = PathBuf::new(); - - path.as_ref() - .components() - .for_each(|component| match component { - Component::ParentDir => { - result.pop(); - } - Component::CurDir => {} - _ => result.push(component), - }); - - dunce::simplified(&result).to_path_buf() -} - -pub fn absolutize(relative_to: P, path: Q) -> PathBuf -where - P: AsRef, - Q: AsRef, -{ - let path = if path.as_ref() == Path::new(".") { - // Joining a Path with '.' appends a '.' at the end, making the prompt - // more ugly - so we don't do anything, which should result in an equal - // path on all supported systems. - relative_to.as_ref().to_owned() - } else if path.as_ref().starts_with("~") { - let expanded_path = expand_tilde(path.as_ref()); - match expanded_path { - Some(p) => p, - _ => path.as_ref().to_owned(), - } - } else { - relative_to.as_ref().join(path) - }; - - let (relative_to, path) = { - let components: Vec<_> = path.components().collect(); - let separator = components - .iter() - .enumerate() - .find(|(_, c)| c == &&Component::CurDir || c == &&Component::ParentDir); - - if let Some((index, _)) = separator { - let (absolute, relative) = components.split_at(index); - let absolute: PathBuf = absolute.iter().collect(); - let relative: PathBuf = relative.iter().collect(); - - (absolute, relative) - } else { - ( - relative_to.as_ref().to_path_buf(), - components.iter().collect::(), - ) - } - }; - - let path = if path.is_relative() { - let mut result = relative_to; - path.components().for_each(|component| match component { - Component::ParentDir => { - result.pop(); - } - Component::Normal(normal) => result.push(normal), - _ => {} - }); - - result - } else { - path - }; - - dunce::simplified(&path).to_path_buf() -} - -// borrowed from here https://stackoverflow.com/questions/54267608/expand-tilde-in-rust-path-idiomatically -pub fn expand_tilde>(path_user_input: P) -> Option { - let p = path_user_input.as_ref(); - if !p.starts_with("~") { - return Some(p.to_path_buf()); - } - - if p == Path::new("~") { - return dirs_next::home_dir(); - } - - dirs_next::home_dir().map(|mut h| { - if h == Path::new("/") { - // Corner case: `h` root directory; - // don't prepend extra `/`, just drop the tilde. - p.strip_prefix("~") - .expect("cannot strip ~ prefix") - .to_path_buf() - } else { - h.push(p.strip_prefix("~/").expect("cannot strip ~/ prefix")); - h - } - }) -} - -pub fn canonicalize(relative_to: P, path: Q) -> io::Result -where - P: AsRef, - Q: AsRef, -{ - let absolutized = absolutize(&relative_to, path); - let path = match std::fs::read_link(&absolutized) { - Ok(resolved) => { - let parent = absolutized.parent().unwrap_or(&absolutized); - absolutize(parent, resolved) - } - - Err(e) => { - if absolutized.exists() { - absolutized - } else { - return Err(e); - } - } - }; - - Ok(dunce::simplified(&path).to_path_buf()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io; - - #[test] - fn absolutize_two_dots() { - let relative_to = Path::new("/foo/bar"); - let path = Path::new(".."); - - assert_eq!( - PathBuf::from("/foo"), // missing path - absolutize(relative_to, path) - ); - } - - #[test] - fn absolutize_with_curdir() { - let relative_to = Path::new("/foo"); - let path = Path::new("./bar/./baz"); - - assert!(!absolutize(relative_to, path) - .to_str() - .unwrap() - .contains('.')); - } - - #[test] - fn canonicalize_should_succeed() -> io::Result<()> { - let relative_to = Path::new("/foo/bar"); - let path = Path::new("../.."); - - assert_eq!( - PathBuf::from("/"), // existing path - canonicalize(relative_to, path)?, - ); - - Ok(()) - } - - #[test] - fn canonicalize_should_fail() { - let relative_to = Path::new("/foo/bar/baz"); // '/foo' is missing - let path = Path::new("../.."); - - assert!(canonicalize(relative_to, path).is_err()); - } -} diff --git a/crates/nu-engine/src/filesystem/utils.rs b/crates/nu-engine/src/filesystem/utils.rs index 5b833876a9..24bde5342e 100644 --- a/crates/nu-engine/src/filesystem/utils.rs +++ b/crates/nu-engine/src/filesystem/utils.rs @@ -1,5 +1,5 @@ -use crate::filesystem::path::canonicalize; use nu_errors::ShellError; +use nu_path::canonicalize; use std::path::{Path, PathBuf}; #[derive(Default)] diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index fb3c90cf99..45452b0821 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -29,7 +29,6 @@ pub use crate::evaluation_context::EvaluationContext; pub use crate::example::Example; pub use crate::filesystem::dir_info::{DirBuilder, DirInfo, FileInfo}; pub use crate::filesystem::filesystem_shell::FilesystemShell; -pub use crate::filesystem::path; pub use crate::from_value::FromValue; pub use crate::maybe_text_codec::{BufCodecReader, MaybeTextCodec, StringOrBinary}; pub use crate::print::maybe_print_errors; diff --git a/crates/nu-engine/src/script.rs b/crates/nu-engine/src/script.rs index b56d37b4bc..b0840bd39c 100644 --- a/crates/nu-engine/src/script.rs +++ b/crates/nu-engine/src/script.rs @@ -1,9 +1,7 @@ -use crate::{ - evaluate::internal::InternalIterator, maybe_print_errors, path::canonicalize, run_block, - shell::CdArgs, -}; +use crate::{evaluate::internal::InternalIterator, maybe_print_errors, run_block, shell::CdArgs}; use crate::{BufCodecReader, MaybeTextCodec, StringOrBinary}; use nu_errors::ShellError; +use nu_path::canonicalize; use nu_protocol::hir::{ Call, ClassifiedCommand, Expression, ExternalRedirection, InternalCommand, Literal, NamedArguments, SpannedExpression, diff --git a/crates/nu-parser/Cargo.toml b/crates/nu-parser/Cargo.toml index b09860f2b4..480cdaa85a 100644 --- a/crates/nu-parser/Cargo.toml +++ b/crates/nu-parser/Cargo.toml @@ -6,8 +6,6 @@ license = "MIT" name = "nu-parser" version = "0.32.1" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] bigdecimal = { version = "0.2.0", features = ["serde"] } codespan-reporting = "0.11.0" @@ -18,12 +16,12 @@ log = "0.4" num-bigint = { version = "0.3.1", features = ["serde"] } num-traits = "0.2.14" serde = "1.0" -shellexpand = "2.1.0" itertools = "0.10.0" smart-default = "0.6.0" dunce = "1.0.1" nu-errors = { version = "0.32.1", path = "../nu-errors" } +nu-path = { version = "0.32.1", path = "../nu-path" } nu-protocol = { version = "0.32.1", path = "../nu-protocol" } nu-source = { version = "0.32.1", path = "../nu-source" } nu-test-support = { version = "0.32.1", path = "../nu-test-support" } diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index 9ad9768598..4b8abb8b7b 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -7,7 +7,6 @@ mod errors; mod flag; mod lex; mod parse; -mod path; mod scope; mod shapes; mod signature; @@ -15,8 +14,6 @@ mod signature; pub use lex::lexer::{lex, parse_block}; pub use lex::tokens::{LiteBlock, LiteCommand, LiteGroup, LitePipeline}; pub use parse::{classify_block, garbage, parse, parse_full_column_path, parse_math_expression}; -pub use path::expand_ndots; -pub use path::expand_path; pub use scope::ParserScope; pub use shapes::shapes; pub use signature::{Signature, SignatureRegistry}; diff --git a/crates/nu-parser/src/parse.rs b/crates/nu-parser/src/parse.rs index 60bdbefbd0..f215e71505 100644 --- a/crates/nu-parser/src/parse.rs +++ b/crates/nu-parser/src/parse.rs @@ -1,9 +1,14 @@ -use std::{path::Path, sync::Arc}; +use std::borrow::Cow; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use bigdecimal::BigDecimal; use indexmap::IndexMap; use log::trace; use nu_errors::{ArgumentError, ParseError}; +use nu_path::{expand_path, expand_path_string}; use nu_protocol::hir::{ self, Binary, Block, ClassifiedCommand, Expression, ExternalRedirection, Flag, FlagKind, Group, InternalCommand, Member, NamedArguments, Operator, Pipeline, RangeOperator, SpannedExpression, @@ -13,6 +18,7 @@ use nu_protocol::{NamedType, PositionalType, Signature, SyntaxShape, UnspannedPa use nu_source::{HasSpan, Span, Spanned, SpannedItem}; use num_bigint::BigInt; +use crate::parse::def::parse_parameter; use crate::{ lex::lexer::{lex, parse_block}, ParserScope, @@ -24,7 +30,6 @@ use crate::{ }, parse::def::lex_split_baseline_tokens_on, }; -use crate::{parse::def::parse_parameter, path::expand_path}; use self::{ def::{parse_definition, parse_definition_prototype}, @@ -67,7 +72,7 @@ pub fn parse_simple_column_path( output.push(Member::Int(row_number, part_span)); } else { let trimmed = trim_quotes(¤t_part); - output.push(Member::Bare(trimmed.clone().spanned(part_span))); + output.push(Member::Bare(trimmed.spanned(part_span))); } current_part.clear(); // Note: I believe this is safe because of the delimiter we're using, @@ -944,8 +949,8 @@ fn parse_arg( ) } SyntaxShape::GlobPattern => { - let trimmed = trim_quotes(&lite_arg.item); - let expanded = expand_path(&trimmed).to_string(); + let trimmed = Cow::Owned(trim_quotes(&lite_arg.item)); + let expanded = expand_path_string(trimmed).to_string(); ( SpannedExpression::new(Expression::glob_pattern(expanded), lite_arg.span), None, @@ -961,10 +966,10 @@ fn parse_arg( SyntaxShape::Duration => parse_duration(lite_arg), SyntaxShape::FilePath => { let trimmed = trim_quotes(&lite_arg.item); - let expanded = expand_path(&trimmed).to_string(); - let path = Path::new(&expanded); + let path = PathBuf::from(trimmed); + let expanded = expand_path(Cow::Owned(path)).to_path_buf(); ( - SpannedExpression::new(Expression::FilePath(path.to_path_buf()), lite_arg.span), + SpannedExpression::new(Expression::FilePath(expanded), lite_arg.span), None, ) } @@ -1647,8 +1652,8 @@ fn parse_external_call( ) -> (Option, Option) { let mut error = None; let name = lite_cmd.parts[0].clone().map(|v| { - let trimmed = trim_quotes(&v); - expand_path(&trimmed).to_string() + let trimmed = Cow::Owned(trim_quotes(&v)); + expand_path_string(trimmed).to_string() }); let mut args = vec![]; @@ -1877,9 +1882,9 @@ fn parse_call( )), ); } - if let Ok(contents) = - std::fs::read_to_string(expand_path(&lite_cmd.parts[1].item).into_owned()) - { + if let Ok(contents) = std::fs::read_to_string(&expand_path(Cow::Borrowed(Path::new( + &lite_cmd.parts[1].item, + )))) { let _ = parse(&contents, 0, scope); } else { return ( diff --git a/crates/nu-parser/src/path.rs b/crates/nu-parser/src/path.rs deleted file mode 100644 index 379886a89e..0000000000 --- a/crates/nu-parser/src/path.rs +++ /dev/null @@ -1,180 +0,0 @@ -use std::borrow::Cow; - -const EXPAND_STR: &str = if cfg!(windows) { r"..\" } else { "../" }; - -fn handle_dots_push(string: &mut String, count: u8) { - if count < 1 { - return; - } - - if count == 1 { - string.push('.'); - return; - } - - for _ in 0..(count - 1) { - string.push_str(EXPAND_STR); - } - - string.pop(); // remove last '/' -} - -pub fn expand_ndots(path: &str) -> Cow<'_, str> { - // helpers - #[cfg(windows)] - fn is_separator(c: char) -> bool { - // AFAIK, Windows can have both \ and / as path components separators - (c == '/') || (c == '\\') - } - - #[cfg(not(windows))] - fn is_separator(c: char) -> bool { - c == '/' - } - - // find if we need to expand any >2 dot paths and early exit if not - let mut dots_count = 0u8; - let ndots_present = { - for chr in path.chars() { - if chr == '.' { - dots_count += 1; - } else { - if is_separator(chr) && (dots_count > 2) { - // this path component had >2 dots - break; - } - - dots_count = 0; - } - } - - dots_count > 2 - }; - - if !ndots_present { - return path.into(); - } - - let mut dots_count = 0u8; - let mut expanded = String::new(); - for chr in path.chars() { - if chr == '.' { - dots_count += 1; - } else { - if is_separator(chr) { - // check for dots expansion only at path component boundaries - handle_dots_push(&mut expanded, dots_count); - dots_count = 0; - } else { - // got non-dot within path component => do not expand any dots - while dots_count > 0 { - expanded.push('.'); - dots_count -= 1; - } - } - expanded.push(chr); - } - } - - handle_dots_push(&mut expanded, dots_count); - - expanded.into() -} - -pub fn expand_path<'a>(path: &'a str) -> Cow<'a, str> { - let tilde_expansion: Cow<'a, str> = shellexpand::tilde(path); - let ndots_expansion: Cow<'a, str> = match tilde_expansion { - Cow::Borrowed(b) => expand_ndots(b), - Cow::Owned(o) => expand_ndots(&o).to_string().into(), - }; - - ndots_expansion -} - -#[cfg(test)] -mod tests { - use super::*; - - // common tests - #[test] - fn string_without_ndots() { - assert_eq!("../hola", &expand_ndots("../hola").to_string()); - } - - #[test] - fn string_with_three_ndots_and_chars() { - assert_eq!("a...b", &expand_ndots("a...b").to_string()); - } - - #[test] - fn string_with_two_ndots_and_chars() { - assert_eq!("a..b", &expand_ndots("a..b").to_string()); - } - - #[test] - fn string_with_one_dot_and_chars() { - assert_eq!("a.b", &expand_ndots("a.b").to_string()); - } - - // Windows tests - #[cfg(windows)] - #[test] - fn string_with_three_ndots() { - assert_eq!(r"..\..", &expand_ndots("...").to_string()); - } - - #[cfg(windows)] - #[test] - fn string_with_mixed_ndots_and_chars() { - assert_eq!( - r"a...b/./c..d/../e.f/..\..\..//.", - &expand_ndots("a...b/./c..d/../e.f/....//.").to_string() - ); - } - - #[cfg(windows)] - #[test] - fn string_with_three_ndots_and_final_slash() { - assert_eq!(r"..\../", &expand_ndots(".../").to_string()); - } - - #[cfg(windows)] - #[test] - fn string_with_three_ndots_and_garbage() { - assert_eq!( - r"ls ..\../ garbage.*[", - &expand_ndots("ls .../ garbage.*[").to_string(), - ); - } - - // non-Windows tests - #[cfg(not(windows))] - #[test] - fn string_with_three_ndots() { - assert_eq!(r"../..", &expand_ndots("...").to_string()); - } - - #[cfg(not(windows))] - #[test] - fn string_with_mixed_ndots_and_chars() { - assert_eq!( - "a...b/./c..d/../e.f/../../..//.", - &expand_ndots("a...b/./c..d/../e.f/....//.").to_string() - ); - } - - #[cfg(not(windows))] - #[test] - fn string_with_three_ndots_and_final_slash() { - assert_eq!("../../", &expand_ndots(".../").to_string()); - } - - #[cfg(not(windows))] - #[test] - fn string_with_three_ndots_and_garbage() { - assert_eq!( - "ls ../../ garbage.*[", - &expand_ndots("ls .../ garbage.*[").to_string(), - ); - } -} diff --git a/crates/nu-path/Cargo.toml b/crates/nu-path/Cargo.toml new file mode 100644 index 0000000000..5c34fc34d4 --- /dev/null +++ b/crates/nu-path/Cargo.toml @@ -0,0 +1,11 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Nushell parser" +edition = "2018" +license = "MIT" +name = "nu-path" +version = "0.32.1" + +[dependencies] +dirs-next = "2.0.0" +dunce = "1.0.1" diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs new file mode 100644 index 0000000000..037484f897 --- /dev/null +++ b/crates/nu-path/src/lib.rs @@ -0,0 +1,530 @@ +use std::borrow::Cow; +use std::io; +use std::path::{Component, Path, PathBuf}; + +// Utility for applying a function that can only be called on the borrowed type of the Cow +// and also returns a ref. If the Cow is a borrow, we can return the same borrow but an +// owned value needs extra handling because the returned valued has to be owned as well +pub fn cow_map_by_ref(c: Cow<'_, B>, f: F) -> Cow<'_, B> +where + B: ToOwned + ?Sized, + O: AsRef, + F: FnOnce(&B) -> &B, +{ + match c { + Cow::Borrowed(b) => Cow::Borrowed(f(b)), + Cow::Owned(o) => Cow::Owned(f(o.as_ref()).to_owned()), + } +} + +// Utility for applying a function over Cow<'a, Path> over a Cow<'a, str> while avoiding unnecessary conversions +fn cow_map_str_path<'a, F>(c: Cow<'a, str>, f: F) -> Cow<'a, str> +where + F: FnOnce(Cow<'a, Path>) -> Cow<'a, Path>, +{ + let ret = match c { + Cow::Borrowed(b) => f(Cow::Borrowed(Path::new(b))), + Cow::Owned(o) => f(Cow::Owned(PathBuf::from(o))), + }; + + match ret { + Cow::Borrowed(expanded) => expanded.to_string_lossy(), + Cow::Owned(expanded) => Cow::Owned(expanded.to_string_lossy().to_string()), + } +} + +// Utility for applying a function over Cow<'a, str> over a Cow<'a, Path> while avoiding unnecessary conversions +fn cow_map_path_str<'a, F>(c: Cow<'a, Path>, f: F) -> Cow<'a, Path> +where + F: FnOnce(Cow<'a, str>) -> Cow<'a, str>, +{ + let ret = match c { + Cow::Borrowed(path) => f(path.to_string_lossy()), + Cow::Owned(buf) => f(Cow::Owned(buf.to_string_lossy().to_string())), + }; + + match ret { + Cow::Borrowed(expanded) => Cow::Borrowed(Path::new(expanded)), + Cow::Owned(expanded) => Cow::Owned(PathBuf::from(expanded)), + } +} + +const EXPAND_STR: &str = if cfg!(windows) { r"..\" } else { "../" }; +fn handle_dots_push(string: &mut String, count: u8) { + if count < 1 { + return; + } + + if count == 1 { + string.push('.'); + return; + } + + for _ in 0..(count - 1) { + string.push_str(EXPAND_STR); + } + + string.pop(); // remove last '/' +} + +// Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g. +// ... into ../.. +// .... into ../../../ +fn expand_ndots_string(path: Cow<'_, str>) -> Cow<'_, str> { + use std::path::is_separator; + // find if we need to expand any >2 dot paths and early exit if not + let mut dots_count = 0u8; + let ndots_present = { + for chr in path.chars() { + if chr == '.' { + dots_count += 1; + } else { + if is_separator(chr) && (dots_count > 2) { + // this path component had >2 dots + break; + } + + dots_count = 0; + } + } + + dots_count > 2 + }; + + if !ndots_present { + return path; + } + + let mut dots_count = 0u8; + let mut expanded = String::new(); + for chr in path.chars() { + if chr == '.' { + dots_count += 1; + } else { + if is_separator(chr) { + // check for dots expansion only at path component boundaries + handle_dots_push(&mut expanded, dots_count); + dots_count = 0; + } else { + // got non-dot within path component => do not expand any dots + while dots_count > 0 { + expanded.push('.'); + dots_count -= 1; + } + } + expanded.push(chr); + } + } + + handle_dots_push(&mut expanded, dots_count); + + expanded.into() +} + +// Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g. +// ... into ../.. +// .... into ../../../ +fn expand_ndots(path: Cow<'_, Path>) -> Cow<'_, Path> { + cow_map_path_str(path, expand_ndots_string) +} + +pub fn absolutize(relative_to: P, path: Q) -> PathBuf +where + P: AsRef, + Q: AsRef, +{ + let path = if path.as_ref() == Path::new(".") { + // Joining a Path with '.' appends a '.' at the end, making the prompt + // more ugly - so we don't do anything, which should result in an equal + // path on all supported systems. + relative_to.as_ref().to_owned() + } else if path.as_ref().starts_with("~") { + expand_tilde(Cow::Borrowed(path.as_ref())).to_path_buf() + } else { + relative_to.as_ref().join(path) + }; + + let (relative_to, path) = { + let components: Vec<_> = path.components().collect(); + let separator = components + .iter() + .enumerate() + .find(|(_, c)| c == &&Component::CurDir || c == &&Component::ParentDir); + + if let Some((index, _)) = separator { + let (absolute, relative) = components.split_at(index); + let absolute: PathBuf = absolute.iter().collect(); + let relative: PathBuf = relative.iter().collect(); + + (absolute, relative) + } else { + ( + relative_to.as_ref().to_path_buf(), + components.iter().collect::(), + ) + } + }; + + let path = if path.is_relative() { + let mut result = relative_to; + path.components().for_each(|component| match component { + Component::ParentDir => { + result.pop(); + } + Component::Normal(normal) => result.push(normal), + _ => {} + }); + + result + } else { + path + }; + + dunce::simplified(&path).to_path_buf() +} + +pub fn canonicalize(relative_to: P, path: Q) -> io::Result +where + P: AsRef, + Q: AsRef, +{ + let absolutized = absolutize(&relative_to, path); + let path = match std::fs::read_link(&absolutized) { + Ok(resolved) => { + let parent = absolutized.parent().unwrap_or(&absolutized); + absolutize(parent, resolved) + } + + Err(e) => { + if absolutized.exists() { + absolutized + } else { + return Err(e); + } + } + }; + + Ok(dunce::simplified(&path).to_path_buf()) +} + +// Expansion logic lives here to enable testing without depending on dirs-next +fn expand_tilde_with(path: Cow<'_, Path>, home: Option) -> Cow<'_, Path> { + if !path.starts_with("~") { + return path; + } + + match home { + None => path, + Some(mut h) => { + if h == Path::new("/") { + // Corner case: `h` root directory; + // don't prepend extra `/`, just drop the tilde. + cow_map_by_ref(path, |p: &Path| { + p.strip_prefix("~").expect("cannot strip ~ prefix") + }) + } else { + h.push(path.strip_prefix("~/").expect("cannot strip ~/ prefix")); + Cow::Owned(h) + } + } + } +} + +pub fn expand_tilde(path: Cow<'_, Path>) -> Cow<'_, Path> { + expand_tilde_with(path, dirs_next::home_dir()) +} + +pub fn expand_tilde_string(path: Cow<'_, str>) -> Cow<'_, str> { + cow_map_str_path(path, expand_tilde) +} + +// Remove "." and ".." in a path. Prefix ".." are not removed as we don't have access to the +// current dir. This is merely 'string manipulation'. Does not handle "...+", see expand_ndots for that +pub fn resolve_dots(path: Cow<'_, Path>) -> Cow<'_, Path> { + debug_assert!(!path.components().any(|c| std::matches!(c, Component::Normal(os_str) if os_str.to_string_lossy().starts_with("..."))), "Unexpected ndots!"); + if !path + .components() + .any(|c| std::matches!(c, Component::CurDir | Component::ParentDir)) + { + return path; + } + + let mut result = PathBuf::with_capacity(path.as_os_str().len()); + + // Only pop/skip path elements if the previous one was an actual path element + let prev_is_normal = |p: &Path| -> bool { + p.components() + .next_back() + .map(|c| std::matches!(c, Component::Normal(_))) + .unwrap_or(false) + }; + path.as_ref() + .components() + .for_each(|component| match component { + Component::ParentDir if prev_is_normal(&result) => { + result.pop(); + } + Component::CurDir if prev_is_normal(&result) => {} + _ => result.push(component), + }); + + Cow::Owned(dunce::simplified(&result).to_path_buf()) +} + +// Expands ~ to home and shortens paths by removing unecessary ".." and "." +// where possible. Also expands "...+" appropriately. +pub fn expand_path(path: Cow<'_, Path>) -> Cow<'_, Path> { + let path = expand_tilde(path); + let path = expand_ndots(path); + resolve_dots(path) +} + +pub fn expand_path_string(path: Cow<'_, str>) -> Cow<'_, str> { + cow_map_str_path(path, expand_path) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io; + + #[test] + fn absolutize_two_dots() { + let relative_to = Path::new("/foo/bar"); + let path = Path::new(".."); + + assert_eq!( + PathBuf::from("/foo"), // missing path + absolutize(relative_to, path) + ); + } + + #[test] + fn absolutize_with_curdir() { + let relative_to = Path::new("/foo"); + let path = Path::new("./bar/./baz"); + + assert!(!absolutize(relative_to, path) + .to_str() + .unwrap() + .contains('.')); + } + + #[test] + fn canonicalize_should_succeed() -> io::Result<()> { + let relative_to = Path::new("/foo/bar"); + let path = Path::new("../.."); + + assert_eq!( + PathBuf::from("/"), // existing path + canonicalize(relative_to, path)?, + ); + + Ok(()) + } + + #[test] + fn canonicalize_should_fail() { + let relative_to = Path::new("/foo/bar/baz"); // '/foo' is missing + let path = Path::new("../.."); + + assert!(canonicalize(relative_to, path).is_err()); + } + + fn check_ndots_expansion(expected: &str, s: &str) { + let expanded = expand_ndots(Cow::Borrowed(Path::new(s))); + // If we don't expect expansion, verify that we get a borrow back and no PathBuf creation has been made + if expected == s { + assert!( + std::matches!(expanded, Cow::Borrowed(_)), + "No PathBuf should be needed here (unnecessary allocation)" + ); + } + assert_eq!(Path::new(expected), &expanded); + } + + // common tests + #[test] + fn string_without_ndots() { + check_ndots_expansion("../hola", "../hola"); + } + + #[test] + fn string_with_three_ndots_and_chars() { + check_ndots_expansion("a...b", "a...b"); + } + + #[test] + fn string_with_two_ndots_and_chars() { + check_ndots_expansion("a..b", "a..b"); + } + + #[test] + fn string_with_one_dot_and_chars() { + check_ndots_expansion("a.b", "a.b"); + } + + #[test] + fn resolve_dots_double_dots_no_change() { + // Can't resolve this as we don't know our parent dir + assert_eq!(Path::new(".."), resolve_dots(Path::new("..").into())); + } + + #[test] + fn resolve_dots_single_dot_no_change() { + // Can't resolve this as we don't know our current dir + assert_eq!(Path::new("."), resolve_dots(Path::new(".").into())); + } + + #[test] + fn resolve_dots_multi_single_dots_no_change() { + assert_eq!(Path::new("././."), resolve_dots(Path::new("././.").into())); + } + + #[test] + fn resolve_multi_double_dots_no_change() { + assert_eq!( + Path::new("../../../"), + resolve_dots(Path::new("../../../").into()) + ); + } + + #[test] + fn resolve_dots_no_change_with_dirs() { + // Can't resolve this as we don't know our parent dir + assert_eq!( + Path::new("../../../dir1/dir2/"), + resolve_dots(Path::new("../../../dir1/dir2").into()) + ); + } + + #[test] + fn resolve_dots_simple() { + assert_eq!( + Path::new("/foo"), + resolve_dots(Path::new("/foo/bar/..").into()) + ); + } + + #[test] + fn resolve_dots_complex() { + assert_eq!( + Path::new("/test"), + resolve_dots(Path::new("/foo/./bar/../../test/././test2/../").into()) + ); + } + + // Windows tests + #[cfg(windows)] + mod windows { + use super::*; + + #[test] + fn string_with_three_ndots() { + check_ndots_expansion(r"..\..", "..."); + } + + #[test] + fn string_with_mixed_ndots_and_chars() { + check_ndots_expansion( + r"a...b/./c..d/../e.f/..\..\..//.", + "a...b/./c..d/../e.f/....//.", + ); + } + + #[test] + fn string_with_three_ndots_and_final_slash() { + check_ndots_expansion(r"..\../", ".../"); + } + + #[test] + fn string_with_three_ndots_and_garbage() { + check_ndots_expansion(r"ls ..\../ garbage.*[", "ls .../ garbage.*["); + } + } + + // non-Windows tests + #[cfg(not(windows))] + mod non_windows { + use super::*; + #[test] + fn string_with_three_ndots() { + check_ndots_expansion(r"../..", "..."); + } + + #[test] + fn string_with_mixed_ndots_and_chars() { + check_ndots_expansion( + "a...b/./c..d/../e.f/../../..//.", + "a...b/./c..d/../e.f/....//.", + ); + } + + #[test] + fn string_with_three_ndots_and_final_slash() { + check_ndots_expansion("../../", ".../"); + } + + #[test] + fn string_with_three_ndots_and_garbage() { + check_ndots_expansion("ls ../../ garbage.*[", "ls .../ garbage.*["); + } + } + + mod tilde { + use super::*; + + fn check_expanded(s: &str) { + let home = Path::new("/home"); + let buf = Some(PathBuf::from(home)); + assert!(expand_tilde_with(Cow::Borrowed(Path::new(s)), buf).starts_with(&home)); + + // Tests the special case in expand_tilde for "/" as home + let home = Path::new("/"); + let buf = Some(PathBuf::from(home)); + assert!(!expand_tilde_with(Cow::Borrowed(Path::new(s)), buf).starts_with("//")); + } + + fn check_not_expanded(s: &str) { + let home = PathBuf::from("/home"); + let expanded = expand_tilde_with(Cow::Borrowed(Path::new(s)), Some(home)); + assert!( + std::matches!(expanded, Cow::Borrowed(_)), + "No PathBuf should be needed here (unecessary allocation)" + ); + assert!(&expanded == Path::new(s)); + } + + #[test] + fn string_with_tilde() { + check_expanded("~"); + } + + #[test] + fn string_with_tilde_forward_slash() { + check_expanded("~/test/"); + } + + #[test] + fn string_with_tilde_double_forward_slash() { + check_expanded("~//test/"); + } + + #[test] + fn does_not_expand_tilde_if_tilde_is_not_first_character() { + check_not_expanded("1~1"); + } + + #[cfg(windows)] + #[test] + fn string_with_tilde_backslash() { + check_expanded("~\\test/test2/test3"); + } + + #[cfg(windows)] + #[test] + fn string_with_double_tilde_backslash() { + check_expanded("~\\\\test\\test2/test3"); + } + } +}