Fix run_external::expand_glob() to return paths that are PWD-relative but reflect the original intent (#13028)

# Description

Fix #13021

This changes the `expand_glob()` function to use
`nu_engine::glob_from()` so that absolute paths are actually preserved,
rather than being made relative to the provided parent. This preserves
the intent of whoever wrote the original path/glob, and also makes it so
that tilde always produces absolute paths.

I also made `expand_glob()` handle Ctrl-C so that it can be interrupted.

cc @YizhePKU

# Tests + Formatting
No additional tests here... but that might be a good idea.
This commit is contained in:
Devyn Cairns 2024-06-03 00:38:55 -07:00 committed by GitHub
parent b50903cf58
commit be8c1dc006
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -4,7 +4,7 @@ use nu_protocol::{
ast::{Expr, Expression}, ast::{Expr, Expression},
did_you_mean, did_you_mean,
process::ChildProcess, process::ChildProcess,
ByteStream, OutDest, ByteStream, NuGlob, OutDest,
}; };
use nu_system::ForegroundChild; use nu_system::ForegroundChild;
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
@ -13,7 +13,7 @@ use std::{
io::Write, io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Stdio, process::Stdio,
sync::Arc, sync::{atomic::AtomicBool, Arc},
thread, thread,
}; };
@ -155,6 +155,9 @@ impl Command for External {
} }
}; };
// Log the command we're about to run in case it's useful for debugging purposes.
log::trace!("run-external spawning: {command:?}");
// Spawn the child process. On Unix, also put the child process to // Spawn the child process. On Unix, also put the child process to
// foreground if we're in an interactive session. // foreground if we're in an interactive session.
#[cfg(windows)] #[cfg(windows)]
@ -232,6 +235,7 @@ pub fn eval_arguments_from_call(
stack: &mut Stack, stack: &mut Stack,
call: &Call, call: &Call,
) -> Result<Vec<Spanned<String>>, ShellError> { ) -> Result<Vec<Spanned<String>>, ShellError> {
let ctrlc = &engine_state.ctrlc;
let cwd = engine_state.cwd(Some(stack))?; let cwd = engine_state.cwd(Some(stack))?;
let mut args: Vec<Spanned<String>> = vec![]; let mut args: Vec<Spanned<String>> = vec![];
for (expr, spread) in call.rest_iter(1) { for (expr, spread) in call.rest_iter(1) {
@ -240,7 +244,7 @@ pub fn eval_arguments_from_call(
// glob-expansion, and inner-quotes-removal, in that order. // glob-expansion, and inner-quotes-removal, in that order.
for arg in eval_argument(engine_state, stack, expr, spread)? { for arg in eval_argument(engine_state, stack, expr, spread)? {
let tilde_expanded = expand_tilde(&arg); let tilde_expanded = expand_tilde(&arg);
for glob_expanded in expand_glob(&tilde_expanded, &cwd, expr.span)? { for glob_expanded in expand_glob(&tilde_expanded, &cwd, expr.span, ctrlc)? {
let inner_quotes_removed = remove_inner_quotes(&glob_expanded); let inner_quotes_removed = remove_inner_quotes(&glob_expanded);
args.push(inner_quotes_removed.into_owned().into_spanned(expr.span)); args.push(inner_quotes_removed.into_owned().into_spanned(expr.span));
} }
@ -321,37 +325,75 @@ fn expand_tilde(arg: &str) -> String {
/// ///
/// Note: This matches the default behavior of Bash, but is known to be /// Note: This matches the default behavior of Bash, but is known to be
/// error-prone. We might want to change this behavior in the future. /// error-prone. We might want to change this behavior in the future.
fn expand_glob(arg: &str, cwd: &Path, span: Span) -> Result<Vec<String>, ShellError> { fn expand_glob(
let Ok(paths) = nu_glob::glob_with_parent(arg, nu_glob::MatchOptions::default(), cwd) else { arg: &str,
cwd: &Path,
span: Span,
interrupt: &Option<Arc<AtomicBool>>,
) -> Result<Vec<String>, ShellError> {
const GLOB_CHARS: &[char] = &['*', '?', '['];
// Don't expand something that doesn't include the GLOB_CHARS
if !arg.contains(GLOB_CHARS) {
return Ok(vec![arg.into()]);
}
// We must use `nu_engine::glob_from` here, in order to ensure we get paths from the correct
// dir
let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span);
let Ok((_prefix, paths)) = nu_engine::glob_from(&glob, cwd, span, None) else {
// If an error occurred, return the original input
return Ok(vec![arg.into()]); return Ok(vec![arg.into()]);
}; };
let mut result = vec![]; // If the first component of the original `arg` string path was '.', that should be preserved
for path in paths { let relative_to_dot = Path::new(arg).starts_with(".");
let path = path.map_err(|err| ShellError::IOErrorSpanned {
msg: format!("{}: {:?}", err.path().display(), err.error()),
span,
})?;
// Strip PWD from the resulting paths if possible.
let path_stripped = if let Ok(remainder) = path.strip_prefix(cwd) {
// If stripping PWD results in an empty path, return `.` instead.
if remainder.components().next().is_none() {
Path::new(".")
} else {
remainder
}
} else {
&path
};
let path_string = path_stripped.to_string_lossy().to_string();
result.push(path_string);
}
if result.is_empty() { let paths = paths
result.push(arg.to_string()); // Skip over glob failures. These are usually just inaccessible paths.
.flat_map(|path_result| match path_result {
Ok(path) => Some(path),
Err(err) => {
// But internally log them just in case we need to debug this.
log::warn!("Error in run_external::expand_glob(): {}", err);
None
} }
})
// Make the paths relative to the cwd
.map(|path| {
path.strip_prefix(cwd)
.map(|path| path.to_owned())
.unwrap_or(path)
})
// Add './' to relative paths if the original pattern had it
.map(|path| {
if relative_to_dot && path.is_relative() {
Path::new(".").join(path)
} else {
path
}
})
// Convert the paths returned to UTF-8 strings.
//
// FIXME: this fails to return the correct results for non-UTF-8 paths, but we don't support
// those in Nushell yet.
.map(|path| path.to_string_lossy().into_owned())
// Abandon if ctrl-c is pressed
.map(|path| {
if !nu_utils::ctrl_c::was_pressed(interrupt) {
Ok(path)
} else {
Err(ShellError::InterruptedByUser { span: Some(span) })
}
})
.collect::<Result<Vec<String>, ShellError>>()?;
Ok(result) if !paths.is_empty() {
Ok(paths)
} else {
// If we failed to match, return the original input
Ok(vec![arg.into()])
}
} }
/// Transforms `--option="value"` into `--option=value`. `value` can be quoted /// Transforms `--option="value"` into `--option=value`. `value` can be quoted
@ -607,6 +649,7 @@ fn escape_cmd_argument(arg: &Spanned<String>) -> Result<Cow<'_, str>, ShellError
mod test { mod test {
use super::*; use super::*;
use nu_protocol::ast::ListItem; use nu_protocol::ast::ListItem;
use nu_test_support::{fs::Stub, playground::Playground};
#[test] #[test]
fn test_remove_quotes() { fn test_remove_quotes() {
@ -669,26 +712,38 @@ mod test {
#[test] #[test]
fn test_expand_glob() { fn test_expand_glob() {
let tempdir = tempfile::tempdir().unwrap(); Playground::setup("test_expand_glob", |dirs, play| {
let cwd = tempdir.path(); play.with_files(&[Stub::EmptyFile("a.txt"), Stub::EmptyFile("b.txt")]);
std::fs::File::create(cwd.join("a.txt")).unwrap();
std::fs::File::create(cwd.join("b.txt")).unwrap();
let actual = expand_glob("*.txt", cwd, Span::unknown()).unwrap(); let cwd = dirs.test();
let actual = expand_glob("*.txt", cwd, Span::unknown(), &None).unwrap();
let expected = &["a.txt", "b.txt"]; let expected = &["a.txt", "b.txt"];
assert_eq!(actual, expected); assert_eq!(actual, expected);
let actual = expand_glob("'*.txt'", cwd, Span::unknown()).unwrap(); let actual = expand_glob("./*.txt", cwd, Span::unknown(), &None).unwrap();
let expected = vec![
Path::new(".").join("a.txt").to_string_lossy().into_owned(),
Path::new(".").join("b.txt").to_string_lossy().into_owned(),
];
assert_eq!(actual, expected);
let actual = expand_glob("'*.txt'", cwd, Span::unknown(), &None).unwrap();
let expected = &["'*.txt'"]; let expected = &["'*.txt'"];
assert_eq!(actual, expected); assert_eq!(actual, expected);
let actual = expand_glob(cwd.to_str().unwrap(), cwd, Span::unknown()).unwrap(); let actual = expand_glob(".", cwd, Span::unknown(), &None).unwrap();
let expected = &["."]; let expected = &["."];
assert_eq!(actual, expected); assert_eq!(actual, expected);
let actual = expand_glob("[*.txt", cwd, Span::unknown()).unwrap(); let actual = expand_glob("./a.txt", cwd, Span::unknown(), &None).unwrap();
let expected = &["./a.txt"];
assert_eq!(actual, expected);
let actual = expand_glob("[*.txt", cwd, Span::unknown(), &None).unwrap();
let expected = &["[*.txt"]; let expected = &["[*.txt"];
assert_eq!(actual, expected); assert_eq!(actual, expected);
})
} }
#[test] #[test]