From 87c5f6e45533af47ab0f83ce10ab78812f817c1d Mon Sep 17 00:00:00 2001 From: Wind Date: Mon, 25 Mar 2024 10:08:38 +0800 Subject: [PATCH] ls, rm, cp, open, touch, mkdir: Don't expand tilde if input path is quoted string or a variable. (#12232) # Description Fixes: #11887 Fixes: #11626 This pr unify the tilde expand behavior over several filesystem relative commands. It follows the same rule with glob expansion: | command | result | | ----------- | ------ | | ls ~/aaa | expand tilde | ls "~/aaa" | don't expand tilde | let f = "~/aaa"; ls $f | don't expand tilde, if you want to: use `ls ($f \| path expand)` | let f: glob = "~/aaa"; ls $f | expand tilde, they don't expand on `mkdir`, `touch` comamnd. Actually I'm not sure for 4th item, currently it's expanding is just because it followes the same rule with glob expansion. ### About the change It changes `expand_path_with` to accept a new argument called `expand_tilde`, if it's true, expand it, if not, just keep it as `~` itself. # User-Facing Changes After this change, `ls "~/aaa"` won't expand tilde. # Tests + Formatting Done --- crates/nu-cli/src/repl.rs | 2 +- crates/nu-command/src/env/load_env.rs | 2 +- crates/nu-command/src/filesystem/ls.rs | 22 ++++++--- crates/nu-command/src/filesystem/open.rs | 4 +- crates/nu-command/src/filesystem/rm.rs | 14 ++++-- crates/nu-command/src/filesystem/save.rs | 4 +- crates/nu-command/src/filesystem/ucp.rs | 15 ++++--- crates/nu-command/src/filesystem/umkdir.rs | 5 +-- crates/nu-command/src/filesystem/umv.rs | 28 +++++++----- crates/nu-command/src/path/exists.rs | 2 +- crates/nu-command/src/path/expand.rs | 15 +++++-- crates/nu-command/tests/commands/ls.rs | 22 +++++++++ crates/nu-command/tests/commands/move_/umv.rs | 31 +++++++++++++ crates/nu-command/tests/commands/rm.rs | 31 +++++++++++++ crates/nu-command/tests/commands/touch.rs | 17 ++++++- crates/nu-command/tests/commands/ucp.rs | 36 +++++++++++++++ crates/nu-command/tests/commands/umkdir.rs | 14 ++++++ crates/nu-engine/src/eval.rs | 6 +-- crates/nu-engine/src/glob_from.rs | 4 +- crates/nu-path/src/expansions.rs | 21 +++++---- crates/nu-protocol/src/value/glob.rs | 4 ++ tests/path/expand_path.rs | 45 +++++++++++-------- 22 files changed, 272 insertions(+), 72 deletions(-) diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index b5d7170ea29fd..8fc00cc887cdd 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -734,7 +734,7 @@ fn parse_operation( orig = trim_quotes_str(&orig).to_string() } - let path = nu_path::expand_path_with(&orig, &cwd); + let path = nu_path::expand_path_with(&orig, &cwd, true); if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 { Ok(ReplOperation::AutoCd { cwd, diff --git a/crates/nu-command/src/env/load_env.rs b/crates/nu-command/src/env/load_env.rs index 66f995e9efbc3..59214c18dfa92 100644 --- a/crates/nu-command/src/env/load_env.rs +++ b/crates/nu-command/src/env/load_env.rs @@ -70,7 +70,7 @@ impl Command for LoadEnv { if env_var == "PWD" { let cwd = current_dir(engine_state, stack)?; let rhs = rhs.coerce_into_string()?; - let rhs = nu_path::expand_path_with(rhs, cwd); + let rhs = nu_path::expand_path_with(rhs, cwd, true); stack.add_env_var( env_var, Value::string(rhs.to_string_lossy(), call.head), diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index c28af6ba5e652..b7f0133cf202e 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -118,12 +118,14 @@ impl Command for Ls { let (path, p_tag, absolute_path, quoted) = match pattern_arg { Some(pat) => { let p_tag = pat.span; - let p = expand_to_real_path(pat.item.as_ref()); - - let expanded = nu_path::expand_path_with(&p, &cwd); + let expanded = nu_path::expand_path_with( + pat.item.as_ref(), + &cwd, + matches!(pat.item, NuGlob::Expand(..)), + ); // Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true if !directory && expanded.is_dir() { - if permission_denied(&p) { + if permission_denied(&expanded) { #[cfg(unix)] let error_msg = format!( "The permissions of {:o} do not allow access for this user", @@ -151,9 +153,17 @@ impl Command for Ls { } extra_star_under_given_directory = true; } - let absolute_path = p.is_absolute(); + + // it's absolute path if: + // 1. pattern is absolute. + // 2. pattern can be expanded, and after expands to real_path, it's absolute. + // here `expand_to_real_path` call is required, because `~/aaa` should be absolute + // path. + let absolute_path = Path::new(pat.item.as_ref()).is_absolute() + || (pat.item.is_expand() + && expand_to_real_path(pat.item.as_ref()).is_absolute()); ( - p, + expanded, p_tag, absolute_path, matches!(pat.item, NuGlob::DoNotExpand(_)), diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index a766e11c7059f..dec947c84b9ab 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -1,6 +1,5 @@ use super::util::get_rest_for_glob_pattern; use nu_engine::{current_dir, get_eval_block, CallExt}; -use nu_path::expand_to_real_path; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::util::BufferedReader; @@ -153,7 +152,6 @@ impl Command for Open { }; let buf_reader = BufReader::new(file); - let real_path = expand_to_real_path(path); let file_contents = PipelineData::ExternalStream { stdout: Some(RawStream::new( @@ -166,7 +164,7 @@ impl Command for Open { exit_code: None, span: call_span, metadata: Some(PipelineMetadata { - data_source: DataSource::FilePath(real_path), + data_source: DataSource::FilePath(path.to_path_buf()), }), trim_end_newline: false, }; diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs index 8fdf569cc15fa..52e47aa18ae19 100644 --- a/crates/nu-command/src/filesystem/rm.rs +++ b/crates/nu-command/src/filesystem/rm.rs @@ -157,7 +157,7 @@ fn rm( for (idx, path) in paths.clone().into_iter().enumerate() { if let Some(ref home) = home { - if expand_path_with(path.item.as_ref(), ¤tdir_path) + if expand_path_with(path.item.as_ref(), ¤tdir_path, path.item.is_expand()) .to_string_lossy() .as_ref() == home.as_str() @@ -242,7 +242,11 @@ fn rm( let mut all_targets: HashMap = HashMap::new(); for target in paths { - let path = expand_path_with(target.item.as_ref(), ¤tdir_path); + let path = expand_path_with( + target.item.as_ref(), + ¤tdir_path, + target.item.is_expand(), + ); if currentdir_path.to_string_lossy() == path.to_string_lossy() || currentdir_path.starts_with(format!("{}{}", target.item, std::path::MAIN_SEPARATOR)) { @@ -281,7 +285,11 @@ fn rm( } all_targets - .entry(nu_path::expand_path_with(f, ¤tdir_path)) + .entry(nu_path::expand_path_with( + f, + ¤tdir_path, + target.item.is_expand(), + )) .or_insert_with(|| target.span); } Err(e) => { diff --git a/crates/nu-command/src/filesystem/save.rs b/crates/nu-command/src/filesystem/save.rs index 73ddacb4c9eae..0b6e7b5f700db 100644 --- a/crates/nu-command/src/filesystem/save.rs +++ b/crates/nu-command/src/filesystem/save.rs @@ -92,14 +92,14 @@ impl Command for Save { let path_arg = call.req::>(engine_state, stack, 0)?; let path = Spanned { - item: expand_path_with(path_arg.item, &cwd), + item: expand_path_with(path_arg.item, &cwd, true), span: path_arg.span, }; let stderr_path = call .get_flag::>(engine_state, stack, "stderr")? .map(|arg| Spanned { - item: expand_path_with(arg.item, cwd), + item: expand_path_with(arg.item, cwd, true), span: arg.span, }); diff --git a/crates/nu-command/src/filesystem/ucp.rs b/crates/nu-command/src/filesystem/ucp.rs index f2435d1a42c88..1ad98f28fc2d1 100644 --- a/crates/nu-command/src/filesystem/ucp.rs +++ b/crates/nu-command/src/filesystem/ucp.rs @@ -183,7 +183,7 @@ impl Command for UCp { target.item.to_string(), )); let cwd = current_dir(engine_state, stack)?; - let target_path = nu_path::expand_path_with(target_path, &cwd); + let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand()); if target.item.as_ref().ends_with(PATH_SEPARATOR) && !target_path.is_dir() { return Err(ShellError::GenericError { error: "is not a directory".into(), @@ -196,7 +196,7 @@ impl Command for UCp { // paths now contains the sources - let mut sources: Vec = Vec::new(); + let mut sources: Vec<(Vec, bool)> = Vec::new(); for mut p in paths { p.item = p.item.strip_ansi_string_unlikely(); @@ -230,16 +230,19 @@ impl Command for UCp { Err(e) => return Err(e), } } - sources.append(&mut app_vals); + sources.push((app_vals, p.item.is_expand())); } // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not // supported in Nushell - for src in sources.iter_mut() { - if !src.is_absolute() { - *src = nu_path::expand_path_with(&src, &cwd); + for (sources, need_expand_tilde) in sources.iter_mut() { + for src in sources.iter_mut() { + if !src.is_absolute() { + *src = nu_path::expand_path_with(&src, &cwd, *need_expand_tilde); + } } } + let sources: Vec = sources.into_iter().flat_map(|x| x.0).collect(); let attributes = make_attributes(preserve)?; diff --git a/crates/nu-command/src/filesystem/umkdir.rs b/crates/nu-command/src/filesystem/umkdir.rs index daeb4d39ee198..1fc1602575743 100644 --- a/crates/nu-command/src/filesystem/umkdir.rs +++ b/crates/nu-command/src/filesystem/umkdir.rs @@ -1,8 +1,8 @@ -use nu_engine::env::current_dir; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type}; +use std::path::PathBuf; use uu_mkdir::mkdir; #[cfg(not(windows))] @@ -60,11 +60,10 @@ impl Command for UMkdir { call: &Call, _input: PipelineData, ) -> Result { - let cwd = current_dir(engine_state, stack)?; let mut directories = call .rest::(engine_state, stack, 0)? .into_iter() - .map(|dir| nu_path::expand_path_with(dir, &cwd)) + .map(PathBuf::from) .peekable(); let is_verbose = call.has_flag(engine_state, stack, "verbose")?; diff --git a/crates/nu-command/src/filesystem/umv.rs b/crates/nu-command/src/filesystem/umv.rs index 73079f80dbe81..bf86fb575d961 100644 --- a/crates/nu-command/src/filesystem/umv.rs +++ b/crates/nu-command/src/filesystem/umv.rs @@ -1,9 +1,10 @@ use super::util::get_rest_for_glob_pattern; use nu_engine::current_dir; use nu_engine::CallExt; -use nu_path::{expand_path_with, expand_to_real_path}; +use nu_path::expand_path_with; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::NuGlob; use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type}; use std::ffi::OsString; use std::path::PathBuf; @@ -98,7 +99,8 @@ impl Command for UMv { error: "Missing destination path".into(), msg: format!( "Missing destination path operand after {}", - expand_path_with(paths[0].item.as_ref(), cwd).to_string_lossy() + expand_path_with(paths[0].item.as_ref(), cwd, paths[0].item.is_expand()) + .to_string_lossy() ), span: Some(paths[0].span), help: None, @@ -112,7 +114,7 @@ impl Command for UMv { label: "Missing file operand".into(), span: call.head, })?; - let mut files: Vec = Vec::new(); + let mut files: Vec<(Vec, bool)> = Vec::new(); for mut p in paths { p.item = p.item.strip_ansi_string_unlikely(); let exp_files: Vec> = @@ -134,22 +136,26 @@ impl Command for UMv { Err(e) => return Err(e), } } - files.append(&mut app_vals); + files.push((app_vals, p.item.is_expand())); } // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not // supported in Nushell - for src in files.iter_mut() { - if !src.is_absolute() { - *src = nu_path::expand_path_with(&src, &cwd); + for (files, need_expand_tilde) in files.iter_mut() { + for src in files.iter_mut() { + if !src.is_absolute() { + *src = nu_path::expand_path_with(&src, &cwd, *need_expand_tilde); + } } } + let mut files: Vec = files.into_iter().flat_map(|x| x.0).collect(); // Add back the target after globbing - let expanded_target = expand_to_real_path(nu_utils::strip_ansi_string_unlikely( - spanned_target.item.to_string(), - )); - let abs_target_path = expand_path_with(expanded_target, &cwd); + let abs_target_path = expand_path_with( + nu_utils::strip_ansi_string_unlikely(spanned_target.item.to_string()), + &cwd, + matches!(spanned_target.item, NuGlob::Expand(..)), + ); files.push(abs_target_path.clone()); let files = files .into_iter() diff --git a/crates/nu-command/src/path/exists.rs b/crates/nu-command/src/path/exists.rs index 04819dfa61cfd..b6764db141adf 100644 --- a/crates/nu-command/src/path/exists.rs +++ b/crates/nu-command/src/path/exists.rs @@ -137,7 +137,7 @@ fn exists(path: &Path, span: Span, args: &Arguments) -> Value { if path.as_os_str().is_empty() { return Value::bool(false, span); } - let path = expand_path_with(path, &args.pwd); + let path = expand_path_with(path, &args.pwd, true); let exists = if args.not_follow_symlink { // symlink_metadata returns true if the file/folder exists // whether it is a symbolic link or not. Sorry, but returns Err diff --git a/crates/nu-command/src/path/expand.rs b/crates/nu-command/src/path/expand.rs index d2c1b7f6cc8ea..9a6fc1145b94a 100644 --- a/crates/nu-command/src/path/expand.rs +++ b/crates/nu-command/src/path/expand.rs @@ -152,7 +152,10 @@ fn expand(path: &Path, span: Span, args: &Arguments) -> Value { match canonicalize_with(path, &args.cwd) { Ok(p) => { if args.not_follow_symlink { - Value::string(expand_path_with(path, &args.cwd).to_string_lossy(), span) + Value::string( + expand_path_with(path, &args.cwd, true).to_string_lossy(), + span, + ) } else { Value::string(p.to_string_lossy(), span) } @@ -171,12 +174,18 @@ fn expand(path: &Path, span: Span, args: &Arguments) -> Value { ), } } else if args.not_follow_symlink { - Value::string(expand_path_with(path, &args.cwd).to_string_lossy(), span) + Value::string( + expand_path_with(path, &args.cwd, true).to_string_lossy(), + span, + ) } else { canonicalize_with(path, &args.cwd) .map(|p| Value::string(p.to_string_lossy(), span)) .unwrap_or_else(|_| { - Value::string(expand_path_with(path, &args.cwd).to_string_lossy(), span) + Value::string( + expand_path_with(path, &args.cwd, true).to_string_lossy(), + span, + ) }) } } diff --git a/crates/nu-command/tests/commands/ls.rs b/crates/nu-command/tests/commands/ls.rs index 14fd2d69ef4a6..4c0ba59d19e21 100644 --- a/crates/nu-command/tests/commands/ls.rs +++ b/crates/nu-command/tests/commands/ls.rs @@ -711,3 +711,25 @@ fn list_empty_string() { assert!(actual.err.contains("does not exist")); }) } + +#[test] +fn list_with_tilde() { + Playground::setup("ls_tilde", |dirs, sandbox| { + sandbox + .within("~tilde") + .with_files(vec![EmptyFile("f1.txt"), EmptyFile("f2.txt")]); + + let actual = nu!(cwd: dirs.test(), "ls '~tilde'"); + assert!(actual.out.contains("f1.txt")); + assert!(actual.out.contains("f2.txt")); + assert!(actual.out.contains("~tilde")); + let actual = nu!(cwd: dirs.test(), "ls ~tilde"); + assert!(actual.err.contains("does not exist")); + + // pass variable + let actual = nu!(cwd: dirs.test(), "let f = '~tilde'; ls $f"); + assert!(actual.out.contains("f1.txt")); + assert!(actual.out.contains("f2.txt")); + assert!(actual.out.contains("~tilde")); + }) +} diff --git a/crates/nu-command/tests/commands/move_/umv.rs b/crates/nu-command/tests/commands/move_/umv.rs index 92ea760ba96ca..0d47fe3146f22 100644 --- a/crates/nu-command/tests/commands/move_/umv.rs +++ b/crates/nu-command/tests/commands/move_/umv.rs @@ -2,6 +2,7 @@ use nu_test_support::fs::{files_exist_at, Stub::EmptyFile, Stub::FileWithContent use nu_test_support::nu; use nu_test_support::playground::Playground; use rstest::rstest; +use std::path::Path; #[test] fn moves_a_file() { @@ -656,3 +657,33 @@ fn test_cp_inside_glob_metachars_dir() { assert!(files_exist_at(vec!["test_file.txt"], dirs.test())); }); } + +#[test] +fn mv_with_tilde() { + Playground::setup("mv_tilde", |dirs, sandbox| { + sandbox.within("~tilde").with_files(vec![ + EmptyFile("f1.txt"), + EmptyFile("f2.txt"), + EmptyFile("f3.txt"), + ]); + sandbox.within("~tilde2"); + + // mv file + let actual = nu!(cwd: dirs.test(), "mv '~tilde/f1.txt' ./"); + assert!(actual.err.is_empty()); + assert!(!files_exist_at( + vec![Path::new("f1.txt")], + dirs.test().join("~tilde") + )); + assert!(files_exist_at(vec![Path::new("f1.txt")], dirs.test())); + + // pass variable + let actual = nu!(cwd: dirs.test(), "let f = '~tilde/f2.txt'; mv $f ./"); + assert!(actual.err.is_empty()); + assert!(!files_exist_at( + vec![Path::new("f2.txt")], + dirs.test().join("~tilde") + )); + assert!(files_exist_at(vec![Path::new("f1.txt")], dirs.test())); + }) +} diff --git a/crates/nu-command/tests/commands/rm.rs b/crates/nu-command/tests/commands/rm.rs index 62cf638dd09eb..caeefc6152f11 100644 --- a/crates/nu-command/tests/commands/rm.rs +++ b/crates/nu-command/tests/commands/rm.rs @@ -538,3 +538,34 @@ fn force_rm_suppress_error() { assert!(actual.err.is_empty()); }); } + +#[test] +fn rm_with_tilde() { + Playground::setup("rm_tilde", |dirs, sandbox| { + sandbox.within("~tilde").with_files(vec![ + EmptyFile("f1.txt"), + EmptyFile("f2.txt"), + EmptyFile("f3.txt"), + ]); + + let actual = nu!(cwd: dirs.test(), "rm '~tilde/f1.txt'"); + assert!(actual.err.is_empty()); + assert!(!files_exist_at( + vec![Path::new("f1.txt")], + dirs.test().join("~tilde") + )); + + // pass variable + let actual = nu!(cwd: dirs.test(), "let f = '~tilde/f2.txt'; rm $f"); + assert!(actual.err.is_empty()); + assert!(!files_exist_at( + vec![Path::new("f2.txt")], + dirs.test().join("~tilde") + )); + + // remove directory + let actual = nu!(cwd: dirs.test(), "let f = '~tilde'; rm -r $f"); + assert!(actual.err.is_empty()); + assert!(!files_exist_at(vec![Path::new("~tilde")], dirs.test())); + }) +} diff --git a/crates/nu-command/tests/commands/touch.rs b/crates/nu-command/tests/commands/touch.rs index 1b7c9df7835e8..f5b3d6f611f30 100644 --- a/crates/nu-command/tests/commands/touch.rs +++ b/crates/nu-command/tests/commands/touch.rs @@ -1,7 +1,8 @@ use chrono::{DateTime, Local}; -use nu_test_support::fs::Stub; +use nu_test_support::fs::{files_exist_at, Stub}; use nu_test_support::nu; use nu_test_support::playground::Playground; +use std::path::Path; // Use 1 instead of 0 because 0 has a special meaning in Windows const TIME_ONE: filetime::FileTime = filetime::FileTime::from_unix_time(1, 0); @@ -487,3 +488,17 @@ fn change_dir_atime_to_reference() { ); }) } + +#[test] +fn create_a_file_with_tilde() { + Playground::setup("touch with tilde", |dirs, _| { + let actual = nu!(cwd: dirs.test(), "touch '~tilde'"); + assert!(actual.err.is_empty()); + assert!(files_exist_at(vec![Path::new("~tilde")], dirs.test())); + + // pass variable + let actual = nu!(cwd: dirs.test(), "let f = '~tilde2'; touch $f"); + assert!(actual.err.is_empty()); + assert!(files_exist_at(vec![Path::new("~tilde2")], dirs.test())); + }) +} diff --git a/crates/nu-command/tests/commands/ucp.rs b/crates/nu-command/tests/commands/ucp.rs index afcac908c197d..d434d77824c97 100644 --- a/crates/nu-command/tests/commands/ucp.rs +++ b/crates/nu-command/tests/commands/ucp.rs @@ -1174,6 +1174,42 @@ fn test_cp_to_customized_home_directory() { }) } +#[test] +fn cp_with_tilde() { + Playground::setup("cp_tilde", |dirs, sandbox| { + sandbox.within("~tilde").with_files(vec![ + EmptyFile("f1.txt"), + EmptyFile("f2.txt"), + EmptyFile("f3.txt"), + ]); + sandbox.within("~tilde2"); + // cp directory + let actual = nu!( + cwd: dirs.test(), + "let f = '~tilde'; cp -r $f '~tilde2'; ls '~tilde2/~tilde' | length" + ); + assert_eq!(actual.out, "3"); + + // cp file + let actual = nu!(cwd: dirs.test(), "cp '~tilde/f1.txt' ./"); + assert!(actual.err.is_empty()); + assert!(files_exist_at( + vec![Path::new("f1.txt")], + dirs.test().join("~tilde") + )); + assert!(files_exist_at(vec![Path::new("f1.txt")], dirs.test())); + + // pass variable + let actual = nu!(cwd: dirs.test(), "let f = '~tilde/f2.txt'; cp $f ./"); + assert!(actual.err.is_empty()); + assert!(files_exist_at( + vec![Path::new("f2.txt")], + dirs.test().join("~tilde") + )); + assert!(files_exist_at(vec![Path::new("f1.txt")], dirs.test())); + }) +} + #[test] fn copy_file_with_update_flag() { copy_file_with_update_flag_impl(false); diff --git a/crates/nu-command/tests/commands/umkdir.rs b/crates/nu-command/tests/commands/umkdir.rs index 0cead39339949..5aa4b0f40bda1 100644 --- a/crates/nu-command/tests/commands/umkdir.rs +++ b/crates/nu-command/tests/commands/umkdir.rs @@ -145,3 +145,17 @@ fn mkdir_umask_permission() { ); }) } + +#[test] +fn mkdir_with_tilde() { + Playground::setup("mkdir with tilde", |dirs, _| { + let actual = nu!(cwd: dirs.test(), "mkdir '~tilde'"); + assert!(actual.err.is_empty()); + assert!(files_exist_at(vec![Path::new("~tilde")], dirs.test())); + + // pass variable + let actual = nu!(cwd: dirs.test(), "let f = '~tilde2'; mkdir $f"); + assert!(actual.err.is_empty()); + assert!(files_exist_at(vec![Path::new("~tilde2")], dirs.test())); + }) +} diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 03e62c85ca623..fb17815466758 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -328,7 +328,7 @@ fn eval_redirection( let cwd = current_dir(engine_state, stack)?; let value = eval_expression::(engine_state, stack, expr)?; let path = Spanned::::from_value(value)?.item; - let path = expand_path_with(path, cwd); + let path = expand_path_with(path, cwd, true); let mut options = OpenOptions::new(); if *append { @@ -634,7 +634,7 @@ impl Eval for EvalRuntime { Ok(Value::string(path, span)) } else { let cwd = current_dir_str(engine_state, stack)?; - let path = expand_path_with(path, cwd); + let path = expand_path_with(path, cwd, true); Ok(Value::string(path.to_string_lossy(), span)) } @@ -653,7 +653,7 @@ impl Eval for EvalRuntime { Ok(Value::string(path, span)) } else { let cwd = current_dir_str(engine_state, stack)?; - let path = expand_path_with(path, cwd); + let path = expand_path_with(path, cwd, true); Ok(Value::string(path.to_string_lossy(), span)) } diff --git a/crates/nu-engine/src/glob_from.rs b/crates/nu-engine/src/glob_from.rs index 08cd4e3fb6d5f..d195ba34334a7 100644 --- a/crates/nu-engine/src/glob_from.rs +++ b/crates/nu-engine/src/glob_from.rs @@ -58,13 +58,13 @@ pub fn glob_from( } // Now expand `p` to get full prefix - let path = expand_path_with(p, cwd); + let path = expand_path_with(p, cwd, pattern.item.is_expand()); let escaped_prefix = PathBuf::from(nu_glob::Pattern::escape(&path.to_string_lossy())); (Some(path), escaped_prefix.join(just_pattern)) } else { let path = PathBuf::from(&pattern.item.as_ref()); - let path = expand_path_with(path, cwd); + let path = expand_path_with(path, cwd, pattern.item.is_expand()); let is_symlink = match fs::symlink_metadata(&path) { Ok(attr) => attr.file_type().is_symlink(), Err(_) => false, diff --git a/crates/nu-path/src/expansions.rs b/crates/nu-path/src/expansions.rs index e044254dc51f1..f20b149fb8bc7 100644 --- a/crates/nu-path/src/expansions.rs +++ b/crates/nu-path/src/expansions.rs @@ -6,7 +6,7 @@ use super::helpers; use super::tilde::expand_tilde; // Join a path relative to another path. Paths starting with tilde are considered as absolute. -fn join_path_relative(path: P, relative_to: Q) -> PathBuf +fn join_path_relative(path: P, relative_to: Q, expand_tilde: bool) -> PathBuf where P: AsRef, Q: AsRef, @@ -19,7 +19,7 @@ where // more ugly - so we don't do anything, which should result in an equal // path on all supported systems. relative_to.into() - } else if path.to_string_lossy().as_ref().starts_with('~') { + } else if path.to_string_lossy().as_ref().starts_with('~') && expand_tilde { // do not end up with "/some/path/~" or "/some/path/~user" path.into() } else { @@ -45,13 +45,18 @@ where P: AsRef, Q: AsRef, { - let path = join_path_relative(path, relative_to); + let path = join_path_relative(path, relative_to, true); canonicalize(path) } -fn expand_path(path: impl AsRef) -> PathBuf { - let path = expand_to_real_path(path); +fn expand_path(path: impl AsRef, need_expand_tilde: bool) -> PathBuf { + let path = if need_expand_tilde { + expand_tilde(path) + } else { + PathBuf::from(path.as_ref()) + }; + let path = expand_ndots(path); expand_dots(path) } @@ -64,14 +69,14 @@ fn expand_path(path: impl AsRef) -> PathBuf { /// /// Does not convert to absolute form nor does it resolve symlinks. /// The input path is specified relative to another path -pub fn expand_path_with(path: P, relative_to: Q) -> PathBuf +pub fn expand_path_with(path: P, relative_to: Q, expand_tilde: bool) -> PathBuf where P: AsRef, Q: AsRef, { - let path = join_path_relative(path, relative_to); + let path = join_path_relative(path, relative_to, expand_tilde); - expand_path(path) + expand_path(path, expand_tilde) } /// Resolve to a path that is accepted by the system and no further - tilde is expanded, and ndot path components are expanded. diff --git a/crates/nu-protocol/src/value/glob.rs b/crates/nu-protocol/src/value/glob.rs index e254ab2b703be..b158691e5e5d5 100644 --- a/crates/nu-protocol/src/value/glob.rs +++ b/crates/nu-protocol/src/value/glob.rs @@ -20,6 +20,10 @@ impl NuGlob { NuGlob::Expand(s) => NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(s)), } } + + pub fn is_expand(&self) -> bool { + matches!(self, NuGlob::Expand(..)) + } } impl AsRef for NuGlob { diff --git a/tests/path/expand_path.rs b/tests/path/expand_path.rs index 85162fd7e7077..26ce10b3c345d 100644 --- a/tests/path/expand_path.rs +++ b/tests/path/expand_path.rs @@ -12,8 +12,8 @@ fn expand_path_with_and_without_relative() { let cwd = std::env::current_dir().expect("Could not get current directory"); assert_eq!( - expand_path_with(full_path, cwd), - expand_path_with(path, relative_to), + expand_path_with(full_path, cwd, true), + expand_path_with(path, relative_to, true), ); } @@ -22,7 +22,10 @@ fn expand_path_with_relative() { let relative_to = "/foo/bar"; let path = "../.."; - assert_eq!(PathBuf::from("/"), expand_path_with(path, relative_to),); + assert_eq!( + PathBuf::from("/"), + expand_path_with(path, relative_to, true), + ); } #[cfg(not(windows))] @@ -31,7 +34,7 @@ fn expand_path_no_change() { let path = "/foo/bar"; let cwd = std::env::current_dir().expect("Could not get current directory"); - let actual = expand_path_with(path, cwd); + let actual = expand_path_with(path, cwd, true); assert_eq!(actual, PathBuf::from(path)); } @@ -43,7 +46,7 @@ fn expand_unicode_path_no_change() { spam.push("πŸš’.txt"); let cwd = std::env::current_dir().expect("Could not get current directory"); - let actual = expand_path_with(spam, cwd); + let actual = expand_path_with(spam, cwd, true); let mut expected = dirs.test().to_owned(); expected.push("πŸš’.txt"); @@ -60,7 +63,7 @@ fn expand_non_utf8_path() { #[test] fn expand_path_relative_to() { Playground::setup("nu_path_test_1", |dirs, _| { - let actual = expand_path_with("spam.txt", dirs.test()); + let actual = expand_path_with("spam.txt", dirs.test(), true); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -74,7 +77,7 @@ fn expand_unicode_path_relative_to_unicode_path_with_spaces() { let mut relative_to = dirs.test().to_owned(); relative_to.push("e-$ Γ¨Ρ€Ρ‚πŸš’β™žδΈ­η‰‡-j"); - let actual = expand_path_with("πŸš’.txt", relative_to); + let actual = expand_path_with("πŸš’.txt", relative_to, true); let mut expected = dirs.test().to_owned(); expected.push("e-$ Γ¨Ρ€Ρ‚πŸš’β™žδΈ­η‰‡-j/πŸš’.txt"); @@ -94,7 +97,7 @@ fn expand_absolute_path_relative_to() { let mut absolute_path = dirs.test().to_owned(); absolute_path.push("spam.txt"); - let actual = expand_path_with(&absolute_path, "non/existent/directory"); + let actual = expand_path_with(&absolute_path, "non/existent/directory", true); let expected = absolute_path; assert_eq!(actual, expected); @@ -104,7 +107,7 @@ fn expand_absolute_path_relative_to() { #[test] fn expand_path_with_dot_relative_to() { Playground::setup("nu_path_test_1", |dirs, _| { - let actual = expand_path_with("./spam.txt", dirs.test()); + let actual = expand_path_with("./spam.txt", dirs.test(), true); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -115,7 +118,7 @@ fn expand_path_with_dot_relative_to() { #[test] fn expand_path_with_many_dots_relative_to() { Playground::setup("nu_path_test_1", |dirs, _| { - let actual = expand_path_with("././/.//////./././//.////spam.txt", dirs.test()); + let actual = expand_path_with("././/.//////./././//.////spam.txt", dirs.test(), true); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -126,7 +129,7 @@ fn expand_path_with_many_dots_relative_to() { #[test] fn expand_path_with_double_dot_relative_to() { Playground::setup("nu_path_test_1", |dirs, _| { - let actual = expand_path_with("foo/../spam.txt", dirs.test()); + let actual = expand_path_with("foo/../spam.txt", dirs.test(), true); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -137,7 +140,7 @@ fn expand_path_with_double_dot_relative_to() { #[test] fn expand_path_with_many_double_dots_relative_to() { Playground::setup("nu_path_test_1", |dirs, _| { - let actual = expand_path_with("foo/bar/baz/../../../spam.txt", dirs.test()); + let actual = expand_path_with("foo/bar/baz/../../../spam.txt", dirs.test(), true); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -148,7 +151,7 @@ fn expand_path_with_many_double_dots_relative_to() { #[test] fn expand_path_with_3_ndots_relative_to() { Playground::setup("nu_path_test_1", |dirs, _| { - let actual = expand_path_with("foo/bar/.../spam.txt", dirs.test()); + let actual = expand_path_with("foo/bar/.../spam.txt", dirs.test(), true); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -162,6 +165,7 @@ fn expand_path_with_many_3_ndots_relative_to() { let actual = expand_path_with( "foo/bar/baz/eggs/sausage/bacon/.../.../.../spam.txt", dirs.test(), + true, ); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -173,7 +177,7 @@ fn expand_path_with_many_3_ndots_relative_to() { #[test] fn expand_path_with_4_ndots_relative_to() { Playground::setup("nu_path_test_1", |dirs, _| { - let actual = expand_path_with("foo/bar/baz/..../spam.txt", dirs.test()); + let actual = expand_path_with("foo/bar/baz/..../spam.txt", dirs.test(), true); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -187,6 +191,7 @@ fn expand_path_with_many_4_ndots_relative_to() { let actual = expand_path_with( "foo/bar/baz/eggs/sausage/bacon/..../..../spam.txt", dirs.test(), + true, ); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -201,7 +206,11 @@ fn expand_path_with_way_too_many_dots_relative_to() { let mut relative_to = dirs.test().to_owned(); relative_to.push("foo/bar/baz/eggs/sausage/bacon/vikings"); - let actual = expand_path_with("././..////././...///././.....///spam.txt", relative_to); + let actual = expand_path_with( + "././..////././...///././.....///spam.txt", + relative_to, + true, + ); let mut expected = dirs.test().to_owned(); expected.push("spam.txt"); @@ -215,7 +224,7 @@ fn expand_unicode_path_with_way_too_many_dots_relative_to_unicode_path_with_spac let mut relative_to = dirs.test().to_owned(); relative_to.push("foo/Ñčěéí +Ε‘Ε™=Γ©/baz/eggs/e-$ Γ¨Ρ€Ρ‚πŸš’β™žδΈ­η‰‡-j/bacon/âÀâÀ âÀâÀ"); - let actual = expand_path_with("././..////././...///././.....///πŸš’.txt", relative_to); + let actual = expand_path_with("././..////././...///././.....///πŸš’.txt", relative_to, true); let mut expected = dirs.test().to_owned(); expected.push("πŸš’.txt"); @@ -228,7 +237,7 @@ fn expand_path_tilde() { let tilde_path = "~"; let cwd = std::env::current_dir().expect("Could not get current directory"); - let actual = expand_path_with(tilde_path, cwd); + let actual = expand_path_with(tilde_path, cwd, true); assert!(actual.is_absolute()); assert!(!actual.starts_with("~")); @@ -238,7 +247,7 @@ fn expand_path_tilde() { fn expand_path_tilde_relative_to() { let tilde_path = "~"; - let actual = expand_path_with(tilde_path, "non/existent/path"); + let actual = expand_path_with(tilde_path, "non/existent/path", true); assert!(actual.is_absolute()); assert!(!actual.starts_with("~"));