diff --git a/crates/nu-command/src/commands/path/expand.rs b/crates/nu-command/src/commands/path/expand.rs index b637ae5894..41d22c02b2 100644 --- a/crates/nu-command/src/commands/path/expand.rs +++ b/crates/nu-command/src/commands/path/expand.rs @@ -1,13 +1,18 @@ use super::{operate, PathSubcommandArguments}; use crate::prelude::*; +#[cfg(feature = "dirs")] +use nu_engine::filesystem::path::expand_tilde; +use nu_engine::filesystem::path::resolve_dots; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use std::path::{Path, PathBuf}; +use nu_source::Span; +use std::path::Path; pub struct PathExpand; struct PathExpandArguments { + strict: bool, rest: Vec, } @@ -24,17 +29,23 @@ impl WholeStreamCommand for PathExpand { fn signature(&self) -> Signature { Signature::build("path expand") + .switch( + "strict", + "Throw an error if the path could not be expanded", + Some('s'), + ) .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "Expand a path to its absolute form" + "Try to expand a path to its absolute form" } fn run(&self, args: CommandArgs) -> Result { let tag = args.call_info.name_tag.clone(); let args = args.evaluate_once()?; let cmd_args = Arc::new(PathExpandArguments { + strict: args.has_flag("strict"), rest: args.rest(0)?, }); @@ -43,32 +54,65 @@ impl WholeStreamCommand for PathExpand { #[cfg(windows)] fn examples(&self) -> Vec { - vec![Example { - description: "Expand relative directories", - example: "echo 'C:\\Users\\joe\\foo\\..\\bar' | path expand", - result: None, - // fails to canonicalize into Some(vec![Value::from("C:\\Users\\joe\\bar")]) due to non-existing path - }] + vec![ + Example { + description: "Expand an absolute path", + example: r"'C:\Users\joe\foo\..\bar' | path expand", + result: Some(vec![ + UntaggedValue::filepath(r"C:\Users\joe\bar").into_value(Span::new(0, 25)) + ]), + }, + Example { + description: "Expand a relative path", + example: r"'foo\..\bar' | path expand", + result: Some(vec![ + UntaggedValue::filepath("bar").into_value(Span::new(0, 12)) + ]), + }, + ] } #[cfg(not(windows))] fn examples(&self) -> Vec { - vec![Example { - description: "Expand relative directories", - example: "echo '/home/joe/foo/../bar' | path expand", - result: None, - // fails to canonicalize into Some(vec![Value::from("/home/joe/bar")]) due to non-existing path - }] + vec![ + Example { + description: "Expand an absolute path", + example: "'/home/joe/foo/../bar' | path expand", + result: Some(vec![ + UntaggedValue::filepath("/home/joe/bar").into_value(Span::new(0, 22)) + ]), + }, + Example { + description: "Expand a relative path", + example: "'foo/../bar' | path expand", + result: Some(vec![ + UntaggedValue::filepath("bar").into_value(Span::new(0, 12)) + ]), + }, + ] } } -fn action(path: &Path, tag: Tag, _args: &PathExpandArguments) -> Value { - let ps = path.to_string_lossy(); - let expanded = shellexpand::tilde(&ps); - let path: &Path = expanded.as_ref().as_ref(); +fn action(path: &Path, tag: Tag, args: &PathExpandArguments) -> Value { + if let Ok(p) = dunce::canonicalize(path) { + UntaggedValue::filepath(p).into_value(tag) + } else if args.strict { + Value::error(ShellError::labeled_error( + "Could not expand path", + "could not be expanded (path might not exist, non-final \ + component is not a directory, or other cause)", + tag.span, + )) + } else { + // "best effort" mode, just expand tilde and resolve single/double dots + #[cfg(feature = "dirs")] + let path = match expand_tilde(path) { + Some(expanded) => expanded, + None => path.into(), + }; - UntaggedValue::filepath(dunce::canonicalize(path).unwrap_or_else(|_| PathBuf::from(path))) - .into_value(tag) + UntaggedValue::filepath(resolve_dots(&path)).into_value(tag) + } } #[cfg(test)] diff --git a/crates/nu-engine/src/filesystem/path.rs b/crates/nu-engine/src/filesystem/path.rs index 5b59ae5d18..7535be4159 100644 --- a/crates/nu-engine/src/filesystem/path.rs +++ b/crates/nu-engine/src/filesystem/path.rs @@ -1,6 +1,25 @@ 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, @@ -67,7 +86,7 @@ where // borrowed from here https://stackoverflow.com/questions/54267608/expand-tilde-in-rust-path-idiomatically #[cfg(feature = "dirs")] -fn expand_tilde>(path_user_input: P) -> Option { +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());