From f7d647ac3cad6d0884448fbf4cc60bbf35c4a236 Mon Sep 17 00:00:00 2001 From: Wind Date: Fri, 23 Feb 2024 09:17:09 +0800 Subject: [PATCH] `open`, `rm`, `umv`, `cp`, `rm` and `du`: Don't globs if inputs are variables or string interpolation (#11886) # Description This is a follow up to https://github.com/nushell/nushell/pull/11621#issuecomment-1937484322 Also Fixes: #11838 ## About the code change It applys the same logic when we pass variables to external commands: https://github.com/nushell/nushell/blob/0487e9ffcbc57c2d5feca606e10c3f8221ff5e00/crates/nu-command/src/system/run_external.rs#L162-L170 That is: if user input dynamic things(like variables, sub-expression, or string interpolation), it returns a quoted `NuPath`, then user input won't be globbed # User-Facing Changes Given two input files: `a*c.txt`, `abc.txt` * `let f = "a*c.txt"; rm $f` will remove one file: `a*c.txt`. ~* `let f = "a*c.txt"; rm --glob $f` will remove `a*c.txt` and `abc.txt`~ * `let f: glob = "a*c.txt"; rm $f` will remove `a*c.txt` and `abc.txt` ## Rules about globbing with *variable* Given two files: `a*c.txt`, `abc.txt` | Cmd Type | example | Result | | ----- | ------------------ | ------ | | builtin | let f = "a*c.txt"; rm $f | remove `a*c.txt` | | builtin | let f: glob = "a*c.txt"; rm $f | remove `a*c.txt` and `abc.txt` | builtin | let f = "a*c.txt"; rm ($f \| into glob) | remove `a*c.txt` and `abc.txt` | custom | def crm [f: glob] { rm $f }; let f = "a*c.txt"; crm $f | remove `a*c.txt` and `abc.txt` | custom | def crm [f: glob] { rm ($f \| into string) }; let f = "a*c.txt"; crm $f | remove `a*c.txt` | custom | def crm [f: string] { rm $f }; let f = "a*c.txt"; crm $f | remove `a*c.txt` | custom | def crm [f: string] { rm $f }; let f = "a*c.txt"; crm ($f \| into glob) | remove `a*c.txt` and `abc.txt` In general, if a variable is annotated with `glob` type, nushell will expand glob pattern. Or else, we need to use `into | glob` to expand glob pattern # Tests + Formatting Done # After Submitting I think `str glob-escape` command will be no-longer required. We can remove it. --- .../nu-cmd-lang/src/core_commands/describe.rs | 2 +- crates/nu-cmd-lang/src/core_commands/let_.rs | 22 ++- crates/nu-cmd-lang/src/example_support.rs | 2 +- crates/nu-color-config/src/style_computer.rs | 2 +- .../nu-command/src/conversions/into/glob.rs | 133 ++++++++++++++++++ crates/nu-command/src/conversions/into/mod.rs | 2 + .../nu-command/src/conversions/into/string.rs | 2 + .../src/database/commands/into_sqlite.rs | 1 + crates/nu-command/src/debug/explain.rs | 2 +- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/filesystem/du.rs | 11 +- crates/nu-command/src/filesystem/ls.rs | 20 +-- crates/nu-command/src/filesystem/mv.rs | 4 +- crates/nu-command/src/filesystem/open.rs | 7 +- crates/nu-command/src/filesystem/rm.rs | 11 +- crates/nu-command/src/filesystem/ucp.rs | 6 +- crates/nu-command/src/filesystem/umv.rs | 7 +- crates/nu-command/src/filesystem/util.rs | 79 +++++++++++ crates/nu-command/src/filters/find.rs | 2 +- crates/nu-command/src/formats/to/json.rs | 2 +- crates/nu-command/src/formats/to/nuon.rs | 2 +- crates/nu-command/src/formats/to/text.rs | 2 +- crates/nu-command/src/formats/to/toml.rs | 4 +- crates/nu-command/src/formats/to/yaml.rs | 2 +- crates/nu-command/src/system/run_external.rs | 6 +- crates/nu-command/tests/commands/du.rs | 9 ++ crates/nu-command/tests/commands/ls.rs | 2 +- crates/nu-command/tests/commands/move_/umv.rs | 27 ++++ crates/nu-command/tests/commands/open.rs | 9 ++ crates/nu-command/tests/commands/rm.rs | 43 ++++++ crates/nu-command/tests/commands/ucp.rs | 35 +++++ crates/nu-engine/src/glob_from.rs | 6 +- crates/nu-parser/src/type_check.rs | 2 + crates/nu-protocol/src/eval_base.rs | 6 +- crates/nu-protocol/src/syntax_shape.rs | 2 +- crates/nu-protocol/src/ty.rs | 6 + crates/nu-protocol/src/value/from_value.rs | 38 +++-- crates/nu-protocol/src/value/glob.rs | 37 +++++ crates/nu-protocol/src/value/mod.rs | 63 +++++---- crates/nu-protocol/src/value/path.rs | 22 ++- crates/nu-std/testing.nu | 2 +- 41 files changed, 534 insertions(+), 109 deletions(-) create mode 100644 crates/nu-command/src/conversions/into/glob.rs create mode 100644 crates/nu-protocol/src/value/glob.rs diff --git a/crates/nu-cmd-lang/src/core_commands/describe.rs b/crates/nu-cmd-lang/src/core_commands/describe.rs index cd52084f49..3589c27f8a 100644 --- a/crates/nu-cmd-lang/src/core_commands/describe.rs +++ b/crates/nu-cmd-lang/src/core_commands/describe.rs @@ -305,7 +305,7 @@ fn describe_value( | Value::Date { .. } | Value::Range { .. } | Value::String { .. } - | Value::QuotedString { .. } + | Value::Glob { .. } | Value::Nothing { .. } => Value::record( record!( "type" => Value::string(value.get_type().to_string(), head), diff --git a/crates/nu-cmd-lang/src/core_commands/let_.rs b/crates/nu-cmd-lang/src/core_commands/let_.rs index 42f90d0d28..0d9a489aff 100644 --- a/crates/nu-cmd-lang/src/core_commands/let_.rs +++ b/crates/nu-cmd-lang/src/core_commands/let_.rs @@ -1,7 +1,9 @@ use nu_engine::eval_block; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, +}; #[derive(Clone)] pub struct Let; @@ -61,8 +63,24 @@ impl Command for Let { .expect("internal error: missing right hand side"); let block = engine_state.get_block(block_id); + let pipeline_data = eval_block(engine_state, stack, block, input, true, false)?; - stack.add_var(var_id, pipeline_data.into_value(call.head)); + let mut value = pipeline_data.into_value(call.head); + + // if given variable type is Glob, and our result is string + // then nushell need to convert from Value::String to Value::Glob + // it's assigned by demand, then it's not quoted, and it's required to expand + // if we pass it to other commands. + let var_type = &engine_state.get_var(var_id).ty; + let val_span = value.span(); + match value { + Value::String { val, .. } if var_type == &Type::Glob => { + value = Value::glob(val, false, val_span); + } + _ => {} + } + + stack.add_var(var_id, value); Ok(PipelineData::empty()) } diff --git a/crates/nu-cmd-lang/src/example_support.rs b/crates/nu-cmd-lang/src/example_support.rs index 9dd73ff7d4..9b86d75cf8 100644 --- a/crates/nu-cmd-lang/src/example_support.rs +++ b/crates/nu-cmd-lang/src/example_support.rs @@ -228,7 +228,7 @@ impl<'a> std::fmt::Debug for DebuggableValue<'a> { val.from, val.to, val.incr ), }, - Value::String { val, .. } | Value::QuotedString { val, .. } => { + Value::String { val, .. } | Value::Glob { val, .. } => { write!(f, "{:?}", val) } Value::Record { val, .. } => { diff --git a/crates/nu-color-config/src/style_computer.rs b/crates/nu-color-config/src/style_computer.rs index 97bf2af115..173c30c3be 100644 --- a/crates/nu-color-config/src/style_computer.rs +++ b/crates/nu-color-config/src/style_computer.rs @@ -122,7 +122,7 @@ impl<'a> StyleComputer<'a> { Value::Range { .. } => TextStyle::with_style(Left, s), Value::Float { .. } => TextStyle::with_style(Right, s), Value::String { .. } => TextStyle::with_style(Left, s), - Value::QuotedString { .. } => TextStyle::with_style(Left, s), + Value::Glob { .. } => TextStyle::with_style(Left, s), Value::Nothing { .. } => TextStyle::with_style(Left, s), Value::Binary { .. } => TextStyle::with_style(Left, s), Value::CellPath { .. } => TextStyle::with_style(Left, s), diff --git a/crates/nu-command/src/conversions/into/glob.rs b/crates/nu-command/src/conversions/into/glob.rs new file mode 100644 index 0000000000..e3d791b504 --- /dev/null +++ b/crates/nu-command/src/conversions/into/glob.rs @@ -0,0 +1,133 @@ +use nu_cmd_base::input_handler::{operate, CmdArgument}; +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Type, Value, +}; + +struct Arguments { + cell_paths: Option>, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + self.cell_paths.take() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into glob" + } + + fn signature(&self) -> Signature { + Signature::build("into glob") + .input_output_types(vec![ + (Type::String, Type::Glob), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::Glob)), + ), + (Type::Table(vec![]), Type::Table(vec![])), + (Type::Record(vec![]), Type::Record(vec![])), + ]) + .allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032 + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, convert data at the given cell paths.", + ) + .category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Convert value to glob." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "text"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + glob_helper(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert string to glob", + example: "'1234' | into glob", + result: Some(Value::test_string("1234")), + }, + Example { + description: "convert filepath to string", + example: "ls Cargo.toml | get name | into glob", + result: None, + }, + ] + } +} + +fn glob_helper( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let cell_paths = call.rest(engine_state, stack, 0)?; + let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); + let args = Arguments { cell_paths }; + match input { + PipelineData::ExternalStream { stdout: None, .. } => { + Ok(Value::glob(String::new(), false, head).into_pipeline_data()) + } + PipelineData::ExternalStream { + stdout: Some(stream), + .. + } => { + // TODO: in the future, we may want this to stream out, converting each to bytes + let output = stream.into_string()?; + Ok(Value::glob(output.item, false, head).into_pipeline_data()) + } + _ => operate(action, args, input, head, engine_state.ctrlc.clone()), + } +} + +fn action(input: &Value, _args: &Arguments, span: Span) -> Value { + match input { + Value::String { val, .. } => Value::glob(val.to_string(), false, span), + x => Value::error( + ShellError::CantConvert { + to_type: String::from("glob"), + from_type: x.get_type().to_string(), + span, + help: None, + }, + span, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/conversions/into/mod.rs b/crates/nu-command/src/conversions/into/mod.rs index 5e4e602b40..50258c338a 100644 --- a/crates/nu-command/src/conversions/into/mod.rs +++ b/crates/nu-command/src/conversions/into/mod.rs @@ -6,6 +6,7 @@ mod datetime; mod duration; mod filesize; mod float; +mod glob; mod int; mod record; mod string; @@ -19,6 +20,7 @@ pub use command::Into; pub use datetime::SubCommand as IntoDatetime; pub use duration::SubCommand as IntoDuration; pub use float::SubCommand as IntoFloat; +pub use glob::SubCommand as IntoGlob; pub use int::SubCommand as IntoInt; pub use record::SubCommand as IntoRecord; pub use string::SubCommand as IntoString; diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs index 0fc5d06adb..f034bee4e7 100644 --- a/crates/nu-command/src/conversions/into/string.rs +++ b/crates/nu-command/src/conversions/into/string.rs @@ -36,6 +36,7 @@ impl Command for SubCommand { (Type::Int, Type::String), (Type::Number, Type::String), (Type::String, Type::String), + (Type::Glob, Type::String), (Type::Bool, Type::String), (Type::Filesize, Type::String), (Type::Date, Type::String), @@ -202,6 +203,7 @@ fn action(input: &Value, args: &Arguments, span: Span) -> Value { Value::Bool { val, .. } => Value::string(val.to_string(), span), Value::Date { val, .. } => Value::string(val.format("%c").to_string(), span), Value::String { val, .. } => Value::string(val.to_string(), span), + Value::Glob { val, .. } => Value::string(val.to_string(), span), Value::Filesize { val: _, .. } => { Value::string(input.to_expanded_string(", ", config), span) diff --git a/crates/nu-command/src/database/commands/into_sqlite.rs b/crates/nu-command/src/database/commands/into_sqlite.rs index 4c231f1efc..6a7cde0783 100644 --- a/crates/nu-command/src/database/commands/into_sqlite.rs +++ b/crates/nu-command/src/database/commands/into_sqlite.rs @@ -354,6 +354,7 @@ fn nu_value_to_sqlite_type(val: &Value) -> Result<&'static str, ShellError> { | Type::Range | Type::Record(_) | Type::Signature + | Type::Glob | Type::Table(_) => Err(ShellError::OnlySupportsThisInputType { exp_input_type: "sql".into(), wrong_type: val.get_type().to_string(), diff --git a/crates/nu-command/src/debug/explain.rs b/crates/nu-command/src/debug/explain.rs index c52758a8ed..da1088b7fc 100644 --- a/crates/nu-command/src/debug/explain.rs +++ b/crates/nu-command/src/debug/explain.rs @@ -251,7 +251,7 @@ pub fn debug_string_without_formatting(value: &Value) -> String { ) } Value::String { val, .. } => val.clone(), - Value::QuotedString { val, .. } => val.clone(), + Value::Glob { val, .. } => val.clone(), Value::List { vals: val, .. } => format!( "[{}]", val.iter() diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 12815e0fda..913362f7d3 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -303,6 +303,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { IntoInt, IntoRecord, IntoString, + IntoGlob, IntoValue, }; diff --git a/crates/nu-command/src/filesystem/du.rs b/crates/nu-command/src/filesystem/du.rs index 461f9ef42f..01811047d8 100644 --- a/crates/nu-command/src/filesystem/du.rs +++ b/crates/nu-command/src/filesystem/du.rs @@ -1,10 +1,11 @@ +use super::util::opt_for_glob_pattern; use crate::{DirBuilder, DirInfo, FileInfo}; use nu_engine::{current_dir, CallExt}; use nu_glob::Pattern; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Category, Example, IntoInterruptiblePipelineData, NuPath, PipelineData, ShellError, Signature, + Category, Example, IntoInterruptiblePipelineData, NuGlob, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; use serde::Deserialize; @@ -14,7 +15,7 @@ pub struct Du; #[derive(Deserialize, Clone, Debug)] pub struct DuArgs { - path: Option>, + path: Option>, all: bool, deref: bool, exclude: Option>, @@ -66,7 +67,7 @@ impl Command for Du { "Exclude files below this size", Some('m'), ) - .category(Category::Core) + .category(Category::FileSystem) } fn run( @@ -96,7 +97,7 @@ impl Command for Du { let current_dir = current_dir(engine_state, stack)?; let args = DuArgs { - path: call.opt(engine_state, stack, 0)?, + path: opt_for_glob_pattern(engine_state, stack, call, 0)?, all: call.has_flag(engine_state, stack, "all")?, deref: call.has_flag(engine_state, stack, "deref")?, exclude: call.get_flag(engine_state, stack, "exclude")?, @@ -119,7 +120,7 @@ impl Command for Du { // The * pattern should never fail. None => nu_engine::glob_from( &Spanned { - item: NuPath::UnQuoted("*".into()), + item: NuGlob::Expand("*".into()), span: Span::unknown(), }, ¤t_dir, diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index 5a23e25a7c..c9d0533c14 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -1,3 +1,4 @@ +use super::util::opt_for_glob_pattern; use crate::DirBuilder; use crate::DirInfo; use chrono::{DateTime, Local, LocalResult, TimeZone, Utc}; @@ -7,7 +8,7 @@ use nu_glob::{MatchOptions, Pattern}; use nu_path::expand_to_real_path; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::NuPath; +use nu_protocol::NuGlob; use nu_protocol::{ Category, DataSource, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, PipelineMetadata, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, @@ -86,17 +87,16 @@ impl Command for Ls { let call_span = call.head; let cwd = current_dir(engine_state, stack)?; - let pattern_arg: Option> = call.opt(engine_state, stack, 0)?; - + let pattern_arg = opt_for_glob_pattern(engine_state, stack, call, 0)?; let pattern_arg = { if let Some(path) = pattern_arg { match path.item { - NuPath::Quoted(p) => Some(Spanned { - item: NuPath::Quoted(nu_utils::strip_ansi_string_unlikely(p)), + NuGlob::DoNotExpand(p) => Some(Spanned { + item: NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(p)), span: path.span, }), - NuPath::UnQuoted(p) => Some(Spanned { - item: NuPath::UnQuoted(nu_utils::strip_ansi_string_unlikely(p)), + NuGlob::Expand(p) => Some(Spanned { + item: NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(p)), span: path.span, }), } @@ -149,7 +149,7 @@ impl Command for Ls { p, p_tag, absolute_path, - matches!(pat.item, NuPath::Quoted(_)), + matches!(pat.item, NuGlob::DoNotExpand(_)), ) } None => { @@ -186,8 +186,8 @@ impl Command for Ls { }; let glob_path = Spanned { - // It needs to be un-quoted, the relative logic is handled previously - item: NuPath::UnQuoted(path.clone()), + // use NeedExpand, the relative escaping logic is handled previously + item: NuGlob::Expand(path.clone()), span: p_tag, }; diff --git a/crates/nu-command/src/filesystem/mv.rs b/crates/nu-command/src/filesystem/mv.rs index 9919287119..83e257f6c1 100644 --- a/crates/nu-command/src/filesystem/mv.rs +++ b/crates/nu-command/src/filesystem/mv.rs @@ -6,7 +6,7 @@ use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, IntoInterruptiblePipelineData, NuPath, PipelineData, ShellError, Signature, + Category, Example, IntoInterruptiblePipelineData, NuGlob, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; @@ -62,7 +62,7 @@ impl Command for Mv { _input: PipelineData, ) -> Result { // TODO: handle invalid directory or insufficient permissions when moving - let mut spanned_source: Spanned = call.req(engine_state, stack, 0)?; + let mut spanned_source: Spanned = call.req(engine_state, stack, 0)?; spanned_source.item = spanned_source.item.strip_ansi_string_unlikely(); let spanned_destination: Spanned = call.req(engine_state, stack, 1)?; let verbose = call.has_flag(engine_state, stack, "verbose")?; diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index 2419abbd1d..1408fe624e 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -1,10 +1,11 @@ +use super::util::get_rest_for_glob_pattern; use nu_engine::{current_dir, 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; use nu_protocol::{ - Category, DataSource, Example, IntoInterruptiblePipelineData, NuPath, PipelineData, + Category, DataSource, Example, IntoInterruptiblePipelineData, NuGlob, PipelineData, PipelineMetadata, RawStream, ShellError, Signature, Spanned, SyntaxShape, Type, }; use std::io::BufReader; @@ -58,7 +59,7 @@ impl Command for Open { let call_span = call.head; let ctrlc = engine_state.ctrlc.clone(); let cwd = current_dir(engine_state, stack)?; - let mut paths = call.rest::>(engine_state, stack, 0)?; + let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; if paths.is_empty() && call.rest_iter(0).next().is_none() { // try to use path from pipeline input if there were no positional or spread args @@ -76,7 +77,7 @@ impl Command for Open { }; paths.push(Spanned { - item: NuPath::UnQuoted(filename), + item: NuGlob::Expand(filename), span, }); } diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs index abf4cb19f6..070b3fa7c6 100644 --- a/crates/nu-command/src/filesystem/rm.rs +++ b/crates/nu-command/src/filesystem/rm.rs @@ -5,6 +5,7 @@ use std::io::ErrorKind; use std::os::unix::prelude::FileTypeExt; use std::path::PathBuf; +use super::util::get_rest_for_glob_pattern; use super::util::try_interaction; use nu_engine::env::current_dir; @@ -14,7 +15,7 @@ use nu_path::expand_path_with; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, IntoInterruptiblePipelineData, NuPath, PipelineData, ShellError, Signature, + Category, Example, IntoInterruptiblePipelineData, NuGlob, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; @@ -126,7 +127,7 @@ fn rm( let ctrlc = engine_state.ctrlc.clone(); - let mut paths: Vec> = call.rest(engine_state, stack, 0)?; + let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; if paths.is_empty() { return Err(ShellError::MissingParameter { @@ -166,8 +167,10 @@ fn rm( } let corrected_path = Spanned { item: match path.item { - NuPath::Quoted(s) => NuPath::Quoted(nu_utils::strip_ansi_string_unlikely(s)), - NuPath::UnQuoted(s) => NuPath::UnQuoted(nu_utils::strip_ansi_string_unlikely(s)), + NuGlob::DoNotExpand(s) => { + NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(s)) + } + NuGlob::Expand(s) => NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(s)), }, span: path.span, }; diff --git a/crates/nu-command/src/filesystem/ucp.rs b/crates/nu-command/src/filesystem/ucp.rs index 170d1b8a0b..f2301a97f9 100644 --- a/crates/nu-command/src/filesystem/ucp.rs +++ b/crates/nu-command/src/filesystem/ucp.rs @@ -1,9 +1,9 @@ +use super::util::get_rest_for_glob_pattern; use nu_engine::{current_dir, CallExt}; -use nu_protocol::NuPath; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value, + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, }; use std::path::PathBuf; use uu_cp::{BackupMode, CopyMode, UpdateMode}; @@ -155,7 +155,7 @@ impl Command for UCp { target_os = "macos" )))] let reflink_mode = uu_cp::ReflinkMode::Never; - let mut paths: Vec> = call.rest(engine_state, stack, 0)?; + let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; if paths.is_empty() { return Err(ShellError::GenericError { error: "Missing file operand".into(), diff --git a/crates/nu-command/src/filesystem/umv.rs b/crates/nu-command/src/filesystem/umv.rs index cc4795464c..37c9c80da3 100644 --- a/crates/nu-command/src/filesystem/umv.rs +++ b/crates/nu-command/src/filesystem/umv.rs @@ -1,11 +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_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::{ - Category, Example, NuPath, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, -}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type}; use std::ffi::OsString; use std::path::PathBuf; use uu_mv::{BackupMode, UpdateMode}; @@ -83,7 +82,7 @@ impl Command for UMv { }; let cwd = current_dir(engine_state, stack)?; - let mut paths: Vec> = call.rest(engine_state, stack, 0)?; + let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; if paths.is_empty() { return Err(ShellError::GenericError { error: "Missing file operand".into(), diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs index 22df37124e..e21e168bf6 100644 --- a/crates/nu-command/src/filesystem/util.rs +++ b/crates/nu-command/src/filesystem/util.rs @@ -1,4 +1,12 @@ use dialoguer::Input; +use nu_engine::eval_expression; +use nu_protocol::ast::Expr; +use nu_protocol::{ + ast::Call, + engine::{EngineState, Stack}, + ShellError, Spanned, Value, +}; +use nu_protocol::{FromValue, NuGlob, Type}; use std::error::Error; use std::path::{Path, PathBuf}; @@ -200,3 +208,74 @@ pub mod users { } } } + +/// Get rest arguments from given `call`, starts with `starting_pos`. +/// +/// It's similar to `call.rest`, except that it always returns NuGlob. And if input argument has +/// Type::Glob, the NuGlob is unquoted, which means it's required to expand. +pub fn get_rest_for_glob_pattern( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + starting_pos: usize, +) -> Result>, ShellError> { + let mut output = vec![]; + + for result in call.rest_iter_flattened(starting_pos, |expr| { + let result = eval_expression(engine_state, stack, expr); + match result { + Err(e) => Err(e), + Ok(result) => { + let span = result.span(); + // convert from string to quoted string if expr is a variable + // or string interpolation + match result { + Value::String { val, .. } + if matches!( + &expr.expr, + Expr::FullCellPath(_) | Expr::StringInterpolation(_) + ) => + { + // should not expand if given input type is not glob. + Ok(Value::glob(val, expr.ty != Type::Glob, span)) + } + other => Ok(other), + } + } + } + })? { + output.push(FromValue::from_value(result)?); + } + + Ok(output) +} + +/// Get optional arguments from given `call` with position `pos`. +/// +/// It's similar to `call.opt`, except that it always returns NuGlob. +pub fn opt_for_glob_pattern( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + pos: usize, +) -> Result>, ShellError> { + if let Some(expr) = call.positional_nth(pos) { + let result = eval_expression(engine_state, stack, expr)?; + let result_span = result.span(); + let result = match result { + Value::String { val, .. } + if matches!( + &expr.expr, + Expr::FullCellPath(_) | Expr::StringInterpolation(_) + ) => + { + // should quote if given input type is not glob. + Value::glob(val, expr.ty != Type::Glob, result_span) + } + other => other, + }; + FromValue::from_value(result).map(Some) + } else { + Ok(None) + } +} diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs index 8c84b322b2..84362cda60 100644 --- a/crates/nu-command/src/filters/find.rs +++ b/crates/nu-command/src/filters/find.rs @@ -534,7 +534,7 @@ fn value_should_be_printed( | Value::Nothing { .. } | Value::Error { .. } => term_equals_value(term, &lower_value, span), Value::String { .. } - | Value::QuotedString { .. } + | Value::Glob { .. } | Value::List { .. } | Value::CellPath { .. } | Value::CustomValue { .. } => term_contains_value(term, &lower_value, span), diff --git a/crates/nu-command/src/formats/to/json.rs b/crates/nu-command/src/formats/to/json.rs index 7f5eb748ed..8ad6497864 100644 --- a/crates/nu-command/src/formats/to/json.rs +++ b/crates/nu-command/src/formats/to/json.rs @@ -115,7 +115,7 @@ pub fn value_to_json_value(v: &Value) -> Result { Value::Int { val, .. } => nu_json::Value::I64(*val), Value::Nothing { .. } => nu_json::Value::Null, Value::String { val, .. } => nu_json::Value::String(val.to_string()), - Value::QuotedString { val, .. } => nu_json::Value::String(val.to_string()), + Value::Glob { val, .. } => nu_json::Value::String(val.to_string()), Value::CellPath { val, .. } => nu_json::Value::Array( val.members .iter() diff --git a/crates/nu-command/src/formats/to/nuon.rs b/crates/nu-command/src/formats/to/nuon.rs index 02754b15fd..2843c50e3a 100644 --- a/crates/nu-command/src/formats/to/nuon.rs +++ b/crates/nu-command/src/formats/to/nuon.rs @@ -279,7 +279,7 @@ pub fn value_to_string( // All strings outside data structures are quoted because they are in 'command position' // (could be mistaken for commands by the Nu parser) Value::String { val, .. } => Ok(escape_quote_string(val)), - Value::QuotedString { val, .. } => Ok(escape_quote_string(val)), + Value::Glob { val, .. } => Ok(escape_quote_string(val)), } } diff --git a/crates/nu-command/src/formats/to/text.rs b/crates/nu-command/src/formats/to/text.rs index 1c906b4e3d..f227c58472 100644 --- a/crates/nu-command/src/formats/to/text.rs +++ b/crates/nu-command/src/formats/to/text.rs @@ -127,7 +127,7 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String { ) } Value::String { val, .. } => val, - Value::QuotedString { val, .. } => val, + Value::Glob { val, .. } => val, Value::List { vals: val, .. } => val .into_iter() .map(|x| local_into_string(x, ", ", config)) diff --git a/crates/nu-command/src/formats/to/toml.rs b/crates/nu-command/src/formats/to/toml.rs index 8a7cd38cdb..27c7a57248 100644 --- a/crates/nu-command/src/formats/to/toml.rs +++ b/crates/nu-command/src/formats/to/toml.rs @@ -57,9 +57,7 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result toml::Value::String("".to_string()), Value::Float { val, .. } => toml::Value::Float(*val), - Value::String { val, .. } | Value::QuotedString { val, .. } => { - toml::Value::String(val.clone()) - } + Value::String { val, .. } | Value::Glob { val, .. } => toml::Value::String(val.clone()), Value::Record { val, .. } => { let mut m = toml::map::Map::new(); for (k, v) in val { diff --git a/crates/nu-command/src/formats/to/yaml.rs b/crates/nu-command/src/formats/to/yaml.rs index 62303b2660..eb3335b59f 100644 --- a/crates/nu-command/src/formats/to/yaml.rs +++ b/crates/nu-command/src/formats/to/yaml.rs @@ -52,7 +52,7 @@ pub fn value_to_yaml_value(v: &Value) -> Result { Value::Date { val, .. } => serde_yaml::Value::String(val.to_string()), Value::Range { .. } => serde_yaml::Value::Null, Value::Float { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)), - Value::String { val, .. } | Value::QuotedString { val, .. } => { + Value::String { val, .. } | Value::Glob { val, .. } => { serde_yaml::Value::String(val.clone()) } Value::Record { val, .. } => { diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index ed3708e0ce..3f0406f8a8 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -2,7 +2,7 @@ use nu_cmd_base::hook::eval_hook; use nu_engine::env_to_strings; use nu_engine::eval_expression; use nu_engine::CallExt; -use nu_protocol::NuPath; +use nu_protocol::NuGlob; use nu_protocol::{ ast::{Call, Expr}, did_you_mean, @@ -730,9 +730,9 @@ fn trim_expand_and_apply_arg( } let cwd = PathBuf::from(cwd); if arg.item.contains('*') && run_glob_expansion { - // we need to run glob expansion, so it's unquoted. + // we need to run glob expansion, so it's NeedExpand. let path = Spanned { - item: NuPath::UnQuoted(arg.item.clone()), + item: NuGlob::Expand(arg.item.clone()), span: arg.span, }; if let Ok((prefix, matches)) = nu_engine::glob_from(&path, &cwd, arg.span, None) { diff --git a/crates/nu-command/tests/commands/du.rs b/crates/nu-command/tests/commands/du.rs index dd6daab1bd..2b2e39b36c 100644 --- a/crates/nu-command/tests/commands/du.rs +++ b/crates/nu-command/tests/commands/du.rs @@ -62,6 +62,15 @@ fn du_files_with_glob_metachars(#[case] src_name: &str) { ); assert!(actual.err.is_empty()); + + // also test for variables. + let actual = nu!( + cwd: dirs.test(), + "let f = '{}'; du -d 1 $f", + src.display(), + ); + + assert!(actual.err.is_empty()); }); } diff --git a/crates/nu-command/tests/commands/ls.rs b/crates/nu-command/tests/commands/ls.rs index dba8a620ba..2fd1f3e216 100644 --- a/crates/nu-command/tests/commands/ls.rs +++ b/crates/nu-command/tests/commands/ls.rs @@ -493,7 +493,7 @@ fn lists_with_directory_flag() { r#" cd dir_empty; ['.' '././.' '..' '../dir_files' '../dir_files/*'] - | each { |it| ls --directory $it } + | each { |it| ls --directory ($it | into glob) } | flatten | get name | to text diff --git a/crates/nu-command/tests/commands/move_/umv.rs b/crates/nu-command/tests/commands/move_/umv.rs index 956fec566c..091ccc70b0 100644 --- a/crates/nu-command/tests/commands/move_/umv.rs +++ b/crates/nu-command/tests/commands/move_/umv.rs @@ -584,6 +584,32 @@ fn mv_files_with_glob_metachars(#[case] src_name: &str) { }); } +#[rstest] +#[case("a]c")] +#[case("a[c")] +#[case("a[bc]d")] +#[case("a][c")] +fn mv_files_with_glob_metachars_when_input_are_variables(#[case] src_name: &str) { + Playground::setup("umv_test_18", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + src_name, + "What is the sound of one hand clapping?", + )]); + + let src = dirs.test().join(src_name); + + let actual = nu!( + cwd: dirs.test(), + "let f = '{}'; umv $f {}", + src.display(), + "hello_world_dest" + ); + + assert!(actual.err.is_empty()); + assert!(dirs.test().join("hello_world_dest").exists()); + }); +} + #[cfg(not(windows))] #[rstest] #[case("a]?c")] @@ -591,6 +617,7 @@ fn mv_files_with_glob_metachars(#[case] src_name: &str) { // windows doesn't allow filename with `*`. fn mv_files_with_glob_metachars_nw(#[case] src_name: &str) { mv_files_with_glob_metachars(src_name); + mv_files_with_glob_metachars_when_input_are_variables(src_name); } #[test] diff --git a/crates/nu-command/tests/commands/open.rs b/crates/nu-command/tests/commands/open.rs index dc5379ca22..434bdbb764 100644 --- a/crates/nu-command/tests/commands/open.rs +++ b/crates/nu-command/tests/commands/open.rs @@ -357,6 +357,15 @@ fn open_files_with_glob_metachars(#[case] src_name: &str) { assert!(actual.err.is_empty()); assert!(actual.out.contains("hello")); + + // also test for variables. + let actual = nu!( + cwd: dirs.test(), + "let f = '{}'; open $f", + src.display(), + ); + assert!(actual.err.is_empty()); + assert!(actual.out.contains("hello")); }); } diff --git a/crates/nu-command/tests/commands/rm.rs b/crates/nu-command/tests/commands/rm.rs index 1cae6e29b1..62cf638dd0 100644 --- a/crates/nu-command/tests/commands/rm.rs +++ b/crates/nu-command/tests/commands/rm.rs @@ -1,6 +1,7 @@ use nu_test_support::fs::{files_exist_at, Stub::EmptyFile}; use nu_test_support::nu; use nu_test_support::playground::Playground; +use rstest::rstest; use std::fs; use std::path::Path; @@ -481,6 +482,48 @@ fn rm_files_inside_glob_metachars_dir() { }); } +#[rstest] +#[case("a]c")] +#[case("a[c")] +#[case("a[bc]d")] +#[case("a][c")] +fn rm_files_with_glob_metachars(#[case] src_name: &str) { + Playground::setup("rm_files_with_glob_metachars", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile(src_name)]); + + let src = dirs.test().join(src_name); + + let actual = nu!( + cwd: dirs.test(), + "rm '{}'", + src.display(), + ); + + assert!(actual.err.is_empty()); + assert!(!src.exists()); + + // test with variables + sandbox.with_files(vec![EmptyFile(src_name)]); + let actual = nu!( + cwd: dirs.test(), + "let f = '{}'; rm $f", + src.display(), + ); + + assert!(actual.err.is_empty()); + assert!(!src.exists()); + }); +} + +#[cfg(not(windows))] +#[rstest] +#[case("a]?c")] +#[case("a*.?c")] +// windows doesn't allow filename with `*`. +fn rm_files_with_glob_metachars_nw(#[case] src_name: &str) { + rm_files_with_glob_metachars(src_name); +} + #[test] fn force_rm_suppress_error() { Playground::setup("force_rm_suppress_error", |dirs, sandbox| { diff --git a/crates/nu-command/tests/commands/ucp.rs b/crates/nu-command/tests/commands/ucp.rs index 58d4f14dd0..d9d5568f43 100644 --- a/crates/nu-command/tests/commands/ucp.rs +++ b/crates/nu-command/tests/commands/ucp.rs @@ -1019,6 +1019,40 @@ fn copies_files_with_glob_metachars(#[case] src_name: &str) { }); } +#[rstest] +#[case("a]c")] +#[case("a[c")] +#[case("a[bc]d")] +#[case("a][c")] +fn copies_files_with_glob_metachars_when_input_are_variables(#[case] src_name: &str) { + Playground::setup("ucp_test_35", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + src_name, + "What is the sound of one hand clapping?", + )]); + + let src = dirs.test().join(src_name); + + // -- open command doesn't like file name + //// Get the hash of the file content to check integrity after copy. + //let src_hash = get_file_hash(src.display()); + + let actual = nu!( + cwd: dirs.test(), + "let f = '{}'; cp $f {}", + src.display(), + TEST_HELLO_WORLD_DEST + ); + + assert!(actual.err.is_empty()); + assert!(dirs.test().join(TEST_HELLO_WORLD_DEST).exists()); + + //// Get the hash of the copied file content to check against first_hash. + //let after_cp_hash = get_file_hash(dirs.test().join(TEST_HELLO_WORLD_DEST).display()); + //assert_eq!(src_hash, after_cp_hash); + }); +} + #[cfg(not(windows))] #[rstest] #[case(r#"'a]?c'"#)] @@ -1026,6 +1060,7 @@ fn copies_files_with_glob_metachars(#[case] src_name: &str) { // windows doesn't allow filename with `*`. fn copies_files_with_glob_metachars_nw(#[case] src_name: &str) { copies_files_with_glob_metachars(src_name); + copies_files_with_glob_metachars_when_input_are_variables(src_name); } #[cfg(not(windows))] diff --git a/crates/nu-engine/src/glob_from.rs b/crates/nu-engine/src/glob_from.rs index 7a99234023..08cd4e3fb6 100644 --- a/crates/nu-engine/src/glob_from.rs +++ b/crates/nu-engine/src/glob_from.rs @@ -5,7 +5,7 @@ use std::{ use nu_glob::MatchOptions; use nu_path::{canonicalize_with, expand_path_with}; -use nu_protocol::{NuPath, ShellError, Span, Spanned}; +use nu_protocol::{NuGlob, ShellError, Span, Spanned}; const GLOB_CHARS: &[char] = &['*', '?', '[']; @@ -18,7 +18,7 @@ const GLOB_CHARS: &[char] = &['*', '?', '[']; /// The second of the two values is an iterator over the matching filepaths. #[allow(clippy::type_complexity)] pub fn glob_from( - pattern: &Spanned, + pattern: &Spanned, cwd: &Path, span: Span, options: Option, @@ -29,7 +29,7 @@ pub fn glob_from( ), ShellError, > { - let no_glob_for_pattern = matches!(pattern.item, NuPath::Quoted(_)); + let no_glob_for_pattern = matches!(pattern.item, NuGlob::DoNotExpand(_)); let (prefix, pattern) = if pattern.item.as_ref().contains(GLOB_CHARS) { // Pattern contains glob, split it let mut p = PathBuf::new(); diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index 8c2eda8a7e..3853859909 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -63,6 +63,8 @@ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool { (Type::Record(lhs), Type::Record(rhs)) | (Type::Table(lhs), Type::Table(rhs)) => { is_compatible(lhs, rhs) } + (Type::Glob, Type::String) => true, + (Type::String, Type::Glob) => true, (lhs, rhs) => lhs == rhs, } } diff --git a/crates/nu-protocol/src/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index 52b720720b..7b0492fb8b 100644 --- a/crates/nu-protocol/src/eval_base.rs +++ b/crates/nu-protocol/src/eval_base.rs @@ -287,11 +287,7 @@ pub trait Eval { Expr::GlobPattern(pattern, quoted) => { // GlobPattern is similar to Filepath // But we don't want to expand path during eval time, it's required for `nu_engine::glob_from` to run correctly - if *quoted { - Ok(Value::quoted_string(pattern, expr.span)) - } else { - Ok(Value::string(pattern, expr.span)) - } + Ok(Value::glob(pattern, *quoted, expr.span)) } Expr::MatchBlock(_) // match blocks are handled by `match` | Expr::VarDecl(_) diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index a95e5fcfc9..bece58d59a 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -150,7 +150,7 @@ impl SyntaxShape { SyntaxShape::Float => Type::Float, SyntaxShape::Filesize => Type::Filesize, SyntaxShape::FullCellPath => Type::Any, - SyntaxShape::GlobPattern => Type::String, + SyntaxShape::GlobPattern => Type::Glob, SyntaxShape::Error => Type::Error, SyntaxShape::ImportPattern => Type::Any, SyntaxShape::Int => Type::Int, diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index d569456c9b..116f0605ab 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -31,6 +31,7 @@ pub enum Type { Record(Vec<(String, Type)>), Signature, String, + Glob, Table(Vec<(String, Type)>), } @@ -63,6 +64,8 @@ impl Type { is_subtype_collection(this, that) } (Type::Table(_), Type::List(_)) => true, + (Type::Glob, Type::String) => true, + (Type::String, Type::Glob) => true, _ => false, } } @@ -110,6 +113,7 @@ impl Type { Type::Binary => SyntaxShape::Binary, Type::Custom(_) => SyntaxShape::Any, Type::Signature => SyntaxShape::Signature, + Type::Glob => SyntaxShape::GlobPattern, } } @@ -139,6 +143,7 @@ impl Type { Type::Binary => String::from("binary"), Type::Custom(_) => String::from("custom"), Type::Signature => String::from("signature"), + Type::Glob => String::from("glob"), } } } @@ -196,6 +201,7 @@ impl Display for Type { Type::Binary => write!(f, "binary"), Type::Custom(custom) => write!(f, "{custom}"), Type::Signature => write!(f, "signature"), + Type::Glob => write!(f, "glob"), } } } diff --git a/crates/nu-protocol/src/value/from_value.rs b/crates/nu-protocol/src/value/from_value.rs index 451d66ded0..edac037650 100644 --- a/crates/nu-protocol/src/value/from_value.rs +++ b/crates/nu-protocol/src/value/from_value.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use super::NuPath; +use super::NuGlob; use crate::ast::{CellPath, PathMember}; use crate::engine::{Block, Closure}; use crate::{Range, Record, ShellError, Spanned, Value}; @@ -204,13 +204,23 @@ impl FromValue for Spanned { } } -impl FromValue for NuPath { +impl FromValue for NuGlob { fn from_value(v: Value) -> Result { // FIXME: we may want to fail a little nicer here match v { - Value::CellPath { val, .. } => Ok(NuPath::UnQuoted(val.to_string())), - Value::String { val, .. } => Ok(NuPath::UnQuoted(val)), - Value::QuotedString { val, .. } => Ok(NuPath::Quoted(val)), + Value::CellPath { val, .. } => Ok(NuGlob::Expand(val.to_string())), + Value::String { val, .. } => Ok(NuGlob::DoNotExpand(val)), + Value::Glob { + val, + no_expand: quoted, + .. + } => { + if quoted { + Ok(NuGlob::DoNotExpand(val)) + } else { + Ok(NuGlob::Expand(val)) + } + } v => Err(ShellError::CantConvert { to_type: "string".into(), from_type: v.get_type().to_string(), @@ -221,14 +231,24 @@ impl FromValue for NuPath { } } -impl FromValue for Spanned { +impl FromValue for Spanned { fn from_value(v: Value) -> Result { let span = v.span(); Ok(Spanned { item: match v { - Value::CellPath { val, .. } => NuPath::UnQuoted(val.to_string()), - Value::String { val, .. } => NuPath::UnQuoted(val), - Value::QuotedString { val, .. } => NuPath::Quoted(val), + Value::CellPath { val, .. } => NuGlob::Expand(val.to_string()), + Value::String { val, .. } => NuGlob::DoNotExpand(val), + Value::Glob { + val, + no_expand: quoted, + .. + } => { + if quoted { + NuGlob::DoNotExpand(val) + } else { + NuGlob::Expand(val) + } + } v => { return Err(ShellError::CantConvert { to_type: "string".into(), diff --git a/crates/nu-protocol/src/value/glob.rs b/crates/nu-protocol/src/value/glob.rs new file mode 100644 index 0000000000..e254ab2b70 --- /dev/null +++ b/crates/nu-protocol/src/value/glob.rs @@ -0,0 +1,37 @@ +use serde::Deserialize; +use std::fmt::Display; + +// Introduce this `NuGlob` enum rather than using `Value::Glob` directlry +// So we can handle glob easily without considering too much variant of `Value` enum. +#[derive(Debug, Clone, Deserialize)] +pub enum NuGlob { + /// Don't expand the glob pattern, normally it includes a quoted string(except backtick) + /// And a variable that doesn't annotated with `glob` type + DoNotExpand(String), + /// A glob pattern that is required to expand, it includes bare word + /// And a variable which is annotated with `glob` type + Expand(String), +} + +impl NuGlob { + pub fn strip_ansi_string_unlikely(self) -> Self { + match self { + NuGlob::DoNotExpand(s) => NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(s)), + NuGlob::Expand(s) => NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(s)), + } + } +} + +impl AsRef for NuGlob { + fn as_ref(&self) -> &str { + match self { + NuGlob::DoNotExpand(s) | NuGlob::Expand(s) => s, + } + } +} + +impl Display for NuGlob { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) + } +} diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 03c4748fbd..a8b06c4582 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -1,8 +1,8 @@ mod custom_value; mod from; mod from_value; +mod glob; mod lazy_record; -mod path; mod range; mod stream; mod unit; @@ -19,13 +19,13 @@ use chrono_humanize::HumanTime; pub use custom_value::CustomValue; use fancy_regex::Regex; pub use from_value::FromValue; +pub use glob::*; pub use lazy_record::LazyRecord; use nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR; use nu_utils::{ contains_emoji, get_system_locale, locale::get_system_locale_string, IgnoreCaseExt, }; use num_format::ToFormattedString; -pub use path::*; pub use range::*; pub use record::Record; use serde::{Deserialize, Serialize}; @@ -93,8 +93,9 @@ pub enum Value { // please use .span() instead of matching this span value internal_span: Span, }, - QuotedString { + Glob { val: String, + no_expand: bool, // note: spans are being refactored out of Value // please use .span() instead of matching this span value internal_span: Span, @@ -188,8 +189,13 @@ impl Clone for Value { val: val.clone(), internal_span: *internal_span, }, - Value::QuotedString { val, internal_span } => Value::QuotedString { + Value::Glob { + val, + no_expand: quoted, + internal_span, + } => Value::Glob { val: val.clone(), + no_expand: *quoted, internal_span: *internal_span, }, Value::Record { val, internal_span } => Value::Record { @@ -721,7 +727,7 @@ impl Value { | Value::Date { internal_span, .. } | Value::Range { internal_span, .. } | Value::String { internal_span, .. } - | Value::QuotedString { internal_span, .. } + | Value::Glob { internal_span, .. } | Value::Record { internal_span, .. } | Value::List { internal_span, .. } | Value::Block { internal_span, .. } @@ -746,7 +752,7 @@ impl Value { | Value::Date { internal_span, .. } | Value::Range { internal_span, .. } | Value::String { internal_span, .. } - | Value::QuotedString { internal_span, .. } + | Value::Glob { internal_span, .. } | Value::Record { internal_span, .. } | Value::LazyRecord { internal_span, .. } | Value::List { internal_span, .. } @@ -773,7 +779,7 @@ impl Value { Value::Date { .. } => Type::Date, Value::Range { .. } => Type::Range, Value::String { .. } => Type::String, - Value::QuotedString { .. } => Type::String, + Value::Glob { .. } => Type::Glob, Value::Record { val, .. } => { Type::Record(val.iter().map(|(x, y)| (x.clone(), y.get_type())).collect()) } @@ -903,7 +909,7 @@ impl Value { ) } Value::String { val, .. } => val.clone(), - Value::QuotedString { val, .. } => val.clone(), + Value::Glob { val, .. } => val.clone(), Value::List { vals: val, .. } => format!( "[{}]", val.iter() @@ -1891,9 +1897,10 @@ impl Value { } } - pub fn quoted_string(val: impl Into, span: Span) -> Value { - Value::QuotedString { + pub fn glob(val: impl Into, no_expand: bool, span: Span) -> Value { + Value::Glob { val: val.into(), + no_expand, internal_span: span, } } @@ -2144,7 +2151,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Less), Value::Range { .. } => Some(Ordering::Less), Value::String { .. } => Some(Ordering::Less), - Value::QuotedString { .. } => Some(Ordering::Less), + Value::Glob { .. } => Some(Ordering::Less), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2165,7 +2172,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Less), Value::Range { .. } => Some(Ordering::Less), Value::String { .. } => Some(Ordering::Less), - Value::QuotedString { .. } => Some(Ordering::Less), + Value::Glob { .. } => Some(Ordering::Less), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2186,7 +2193,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Less), Value::Range { .. } => Some(Ordering::Less), Value::String { .. } => Some(Ordering::Less), - Value::QuotedString { .. } => Some(Ordering::Less), + Value::Glob { .. } => Some(Ordering::Less), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2207,7 +2214,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Less), Value::Range { .. } => Some(Ordering::Less), Value::String { .. } => Some(Ordering::Less), - Value::QuotedString { .. } => Some(Ordering::Less), + Value::Glob { .. } => Some(Ordering::Less), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2228,7 +2235,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Less), Value::Range { .. } => Some(Ordering::Less), Value::String { .. } => Some(Ordering::Less), - Value::QuotedString { .. } => Some(Ordering::Less), + Value::Glob { .. } => Some(Ordering::Less), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2249,7 +2256,7 @@ impl PartialOrd for Value { Value::Date { val: rhs, .. } => lhs.partial_cmp(rhs), Value::Range { .. } => Some(Ordering::Less), Value::String { .. } => Some(Ordering::Less), - Value::QuotedString { .. } => Some(Ordering::Less), + Value::Glob { .. } => Some(Ordering::Less), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2270,7 +2277,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { val: rhs, .. } => lhs.partial_cmp(rhs), Value::String { .. } => Some(Ordering::Less), - Value::QuotedString { .. } => Some(Ordering::Less), + Value::Glob { .. } => Some(Ordering::Less), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2291,7 +2298,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { val: rhs, .. } => lhs.partial_cmp(rhs), - Value::QuotedString { val: rhs, .. } => lhs.partial_cmp(rhs), + Value::Glob { val: rhs, .. } => lhs.partial_cmp(rhs), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2303,7 +2310,7 @@ impl PartialOrd for Value { Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), }, - (Value::QuotedString { val: lhs, .. }, rhs) => match rhs { + (Value::Glob { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), Value::Int { .. } => Some(Ordering::Greater), Value::Float { .. } => Some(Ordering::Greater), @@ -2312,7 +2319,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { val: rhs, .. } => lhs.partial_cmp(rhs), - Value::QuotedString { val: rhs, .. } => lhs.partial_cmp(rhs), + Value::Glob { val: rhs, .. } => lhs.partial_cmp(rhs), Value::Record { .. } => Some(Ordering::Less), Value::LazyRecord { .. } => Some(Ordering::Less), Value::List { .. } => Some(Ordering::Less), @@ -2333,7 +2340,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { .. } => Some(Ordering::Greater), - Value::QuotedString { .. } => Some(Ordering::Greater), + Value::Glob { .. } => Some(Ordering::Greater), Value::Record { val: rhs, .. } => { // reorder cols and vals to make more logically compare. // more general, if two record have same col and values, @@ -2373,7 +2380,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { .. } => Some(Ordering::Greater), - Value::QuotedString { .. } => Some(Ordering::Greater), + Value::Glob { .. } => Some(Ordering::Greater), Value::Record { .. } => Some(Ordering::Greater), Value::LazyRecord { .. } => Some(Ordering::Greater), Value::List { vals: rhs, .. } => lhs.partial_cmp(rhs), @@ -2394,7 +2401,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { .. } => Some(Ordering::Greater), - Value::QuotedString { .. } => Some(Ordering::Greater), + Value::Glob { .. } => Some(Ordering::Greater), Value::Record { .. } => Some(Ordering::Greater), Value::List { .. } => Some(Ordering::Greater), Value::LazyRecord { .. } => Some(Ordering::Greater), @@ -2415,7 +2422,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { .. } => Some(Ordering::Greater), - Value::QuotedString { .. } => Some(Ordering::Greater), + Value::Glob { .. } => Some(Ordering::Greater), Value::Record { .. } => Some(Ordering::Greater), Value::LazyRecord { .. } => Some(Ordering::Greater), Value::List { .. } => Some(Ordering::Greater), @@ -2436,7 +2443,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { .. } => Some(Ordering::Greater), - Value::QuotedString { .. } => Some(Ordering::Greater), + Value::Glob { .. } => Some(Ordering::Greater), Value::Record { .. } => Some(Ordering::Greater), Value::LazyRecord { .. } => Some(Ordering::Greater), Value::List { .. } => Some(Ordering::Greater), @@ -2457,7 +2464,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { .. } => Some(Ordering::Greater), - Value::QuotedString { .. } => Some(Ordering::Greater), + Value::Glob { .. } => Some(Ordering::Greater), Value::Record { .. } => Some(Ordering::Greater), Value::LazyRecord { .. } => Some(Ordering::Greater), Value::List { .. } => Some(Ordering::Greater), @@ -2478,7 +2485,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { .. } => Some(Ordering::Greater), - Value::QuotedString { .. } => Some(Ordering::Greater), + Value::Glob { .. } => Some(Ordering::Greater), Value::Record { .. } => Some(Ordering::Greater), Value::LazyRecord { .. } => Some(Ordering::Greater), Value::List { .. } => Some(Ordering::Greater), @@ -2499,7 +2506,7 @@ impl PartialOrd for Value { Value::Date { .. } => Some(Ordering::Greater), Value::Range { .. } => Some(Ordering::Greater), Value::String { .. } => Some(Ordering::Greater), - Value::QuotedString { .. } => Some(Ordering::Greater), + Value::Glob { .. } => Some(Ordering::Greater), Value::Record { .. } => Some(Ordering::Greater), Value::LazyRecord { .. } => Some(Ordering::Greater), Value::List { .. } => Some(Ordering::Greater), diff --git a/crates/nu-protocol/src/value/path.rs b/crates/nu-protocol/src/value/path.rs index 3764637c3f..aede5b4466 100644 --- a/crates/nu-protocol/src/value/path.rs +++ b/crates/nu-protocol/src/value/path.rs @@ -1,36 +1,32 @@ use serde::Deserialize; use std::fmt::Display; -/// A simple wrapper to String. -/// -/// But it tracks if the string is originally quoted. -/// So commands can make decision on path auto-expanding behavior. #[derive(Debug, Clone, Deserialize)] -pub enum NuPath { +pub enum NuGlob { /// A quoted path(except backtick), in this case, nushell shouldn't auto-expand path. - Quoted(String), + NoExpand(String), /// An unquoted path, in this case, nushell should auto-expand path. - UnQuoted(String), + NeedExpand(String), } -impl NuPath { +impl NuGlob { pub fn strip_ansi_string_unlikely(self) -> Self { match self { - NuPath::Quoted(s) => NuPath::Quoted(nu_utils::strip_ansi_string_unlikely(s)), - NuPath::UnQuoted(s) => NuPath::UnQuoted(nu_utils::strip_ansi_string_unlikely(s)), + NuGlob::NoExpand(s) => NuGlob::NoExpand(nu_utils::strip_ansi_string_unlikely(s)), + NuGlob::NeedExpand(s) => NuGlob::NeedExpand(nu_utils::strip_ansi_string_unlikely(s)), } } } -impl AsRef for NuPath { +impl AsRef for NuGlob { fn as_ref(&self) -> &str { match self { - NuPath::Quoted(s) | NuPath::UnQuoted(s) => s, + NuGlob::NoExpand(s) | NuGlob::NeedExpand(s) => s, } } } -impl Display for NuPath { +impl Display for NuGlob { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_ref()) } diff --git a/crates/nu-std/testing.nu b/crates/nu-std/testing.nu index fcfacb3a9a..bc109059a1 100644 --- a/crates/nu-std/testing.nu +++ b/crates/nu-std/testing.nu @@ -329,7 +329,7 @@ export def run-tests [ } let modules = ( - ls ($path | path join $module_search_pattern) + ls ($path | path join $module_search_pattern | into glob) | par-each --threads $threads {|row| { file: $row.name