Path expand fixes (#3505)

* Throw an error if path failed to expand

Previously, it just repeated the non-expanded path.

* Allow expanding non-existent paths

This commit has a strange error in examples.

* Specify span manually in examples; Add an example

* Expand relative path without requiring cwd

* Remove redundant tilde expansion

This makes the tilde expansion in relative paths dependant on "dirs"
feature.

* Add missing example result

* Adjust path expand description

* Fix import error with missing feature
This commit is contained in:
Jakub Žádník 2021-06-06 20:28:55 +03:00 committed by GitHub
parent 57a009b8e6
commit 82d69305b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 84 additions and 21 deletions

View File

@ -1,13 +1,18 @@
use super::{operate, PathSubcommandArguments}; use super::{operate, PathSubcommandArguments};
use crate::prelude::*; 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_engine::WholeStreamCommand;
use nu_errors::ShellError; use nu_errors::ShellError;
use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value};
use std::path::{Path, PathBuf}; use nu_source::Span;
use std::path::Path;
pub struct PathExpand; pub struct PathExpand;
struct PathExpandArguments { struct PathExpandArguments {
strict: bool,
rest: Vec<ColumnPath>, rest: Vec<ColumnPath>,
} }
@ -24,17 +29,23 @@ impl WholeStreamCommand for PathExpand {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("path expand") 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") .rest(SyntaxShape::ColumnPath, "Optionally operate by column path")
} }
fn usage(&self) -> &str { 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<OutputStream, ShellError> { fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.call_info.name_tag.clone(); let tag = args.call_info.name_tag.clone();
let args = args.evaluate_once()?; let args = args.evaluate_once()?;
let cmd_args = Arc::new(PathExpandArguments { let cmd_args = Arc::new(PathExpandArguments {
strict: args.has_flag("strict"),
rest: args.rest(0)?, rest: args.rest(0)?,
}); });
@ -43,32 +54,65 @@ impl WholeStreamCommand for PathExpand {
#[cfg(windows)] #[cfg(windows)]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![Example { vec![
description: "Expand relative directories", Example {
example: "echo 'C:\\Users\\joe\\foo\\..\\bar' | path expand", description: "Expand an absolute path",
result: None, example: r"'C:\Users\joe\foo\..\bar' | path expand",
// fails to canonicalize into Some(vec![Value::from("C:\\Users\\joe\\bar")]) due to non-existing path 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))] #[cfg(not(windows))]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![Example { vec![
description: "Expand relative directories", Example {
example: "echo '/home/joe/foo/../bar' | path expand", description: "Expand an absolute path",
result: None, example: "'/home/joe/foo/../bar' | path expand",
// fails to canonicalize into Some(vec![Value::from("/home/joe/bar")]) due to non-existing path 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 { fn action(path: &Path, tag: Tag, args: &PathExpandArguments) -> Value {
let ps = path.to_string_lossy(); if let Ok(p) = dunce::canonicalize(path) {
let expanded = shellexpand::tilde(&ps); UntaggedValue::filepath(p).into_value(tag)
let path: &Path = expanded.as_ref().as_ref(); } 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))) UntaggedValue::filepath(resolve_dots(&path)).into_value(tag)
.into_value(tag) }
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,6 +1,25 @@
use std::io; use std::io;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
pub fn resolve_dots<P>(path: P) -> PathBuf
where
P: AsRef<Path>,
{
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<P, Q>(relative_to: P, path: Q) -> PathBuf pub fn absolutize<P, Q>(relative_to: P, path: Q) -> PathBuf
where where
P: AsRef<Path>, P: AsRef<Path>,
@ -67,7 +86,7 @@ where
// borrowed from here https://stackoverflow.com/questions/54267608/expand-tilde-in-rust-path-idiomatically // borrowed from here https://stackoverflow.com/questions/54267608/expand-tilde-in-rust-path-idiomatically
#[cfg(feature = "dirs")] #[cfg(feature = "dirs")]
fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Option<PathBuf> { pub fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Option<PathBuf> {
let p = path_user_input.as_ref(); let p = path_user_input.as_ref();
if !p.starts_with("~") { if !p.starts_with("~") {
return Some(p.to_path_buf()); return Some(p.to_path_buf());