mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 02:35:00 +02:00
do not attempt to glob expand if the file path is wrapped in quotes (#11569)
# Description Fixes: #11455 ### For arguments which is annotated with `:path/:directory/:glob` To fix the issue, we need to have a way to know if a path is originally quoted during runtime. So the information needed to be added at several levels: * parse time (from user input to expression) We need to add quoted information into `Expr::Filepath`, `Expr::Directory`, `Expr::GlobPattern` * eval time When convert from `Expr::Filepath`, `Expr::Directory`, `Expr::GlobPattern` to `Value::String` during runtime, we won't auto expanded the path if it's quoted ### For `ls` It's really special, because it accepts a `String` as a pattern, and it generates `glob` expression inside the command itself. So the idea behind the change is introducing a special SyntaxShape to ls: `SyntaxShape::LsGlobPattern`. So we can track if the pattern is originally quoted easier, and we don't auto expand the path either. Then when constructing a glob pattern inside ls, we check if input pattern is quoted, if so: we escape the input pattern, so we can run `ls a[123]b`, because it's already escaped. Finally, to accomplish the checking process, we also need to introduce a new value type called `Value::QuotedString` to differ from `Value::String`, it's used to generate an enum called `NuPath`, which is finally used in `ls` function. `ls` learned from `NuPath` to know if user input is quoted. # User-Facing Changes Actually it contains several changes ### For arguments which is annotated with `:path/:directory/:glob` #### Before ```nushell > def foo [p: path] { echo $p }; print (foo "~/a"); print (foo '~/a') /home/windsoilder/a /home/windsoilder/a > def foo [p: directory] { echo $p }; print (foo "~/a"); print (foo '~/a') /home/windsoilder/a /home/windsoilder/a > def foo [p: glob] { echo $p }; print (foo "~/a"); print (foo '~/a') /home/windsoilder/a /home/windsoilder/a ``` #### After ```nushell > def foo [p: path] { echo $p }; print (foo "~/a"); print (foo '~/a') ~/a ~/a > def foo [p: directory] { echo $p }; print (foo "~/a"); print (foo '~/a') ~/a ~/a > def foo [p: glob] { echo $p }; print (foo "~/a"); print (foo '~/a') ~/a ~/a ``` ### For ls command `touch '[uwu]'` #### Before ``` ❯ ls -D "[uwu]" Error: × No matches found for [uwu] ╭─[entry #6:1:1] 1 │ ls -D "[uwu]" · ───┬─── · ╰── Pattern, file or folder not found ╰──── help: no matches found ``` #### After ``` ❯ ls -D "[uwu]" ╭───┬───────┬──────┬──────┬──────────╮ │ # │ name │ type │ size │ modified │ ├───┼───────┼──────┼──────┼──────────┤ │ 0 │ [uwu] │ file │ 0 B │ now │ ╰───┴───────┴──────┴──────┴──────────╯ ``` # Tests + Formatting Done # After Submitting NaN
This commit is contained in:
@ -251,6 +251,7 @@ pub fn debug_string_without_formatting(value: &Value) -> String {
|
||||
)
|
||||
}
|
||||
Value::String { val, .. } => val.clone(),
|
||||
Value::QuotedString { val, .. } => val.clone(),
|
||||
Value::List { vals: val, .. } => format!(
|
||||
"[{}]",
|
||||
val.iter()
|
||||
|
@ -3,10 +3,11 @@ use crate::DirInfo;
|
||||
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
|
||||
use nu_engine::env::current_dir;
|
||||
use nu_engine::CallExt;
|
||||
use nu_glob::MatchOptions;
|
||||
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::{
|
||||
Category, DataSource, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
|
||||
PipelineMetadata, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
|
||||
@ -38,8 +39,9 @@ impl Command for Ls {
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("ls")
|
||||
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
|
||||
// Using a string instead of a glob pattern shape so it won't auto-expand
|
||||
.optional("pattern", SyntaxShape::String, "The glob pattern to use.")
|
||||
// LsGlobPattern is similar to string, it won't auto-expand
|
||||
// and we use it to track if the user input is quoted.
|
||||
.optional("pattern", SyntaxShape::LsGlobPattern, "The glob pattern to use.")
|
||||
.switch("all", "Show hidden files", Some('a'))
|
||||
.switch(
|
||||
"long",
|
||||
@ -84,23 +86,32 @@ impl Command for Ls {
|
||||
let call_span = call.head;
|
||||
let cwd = current_dir(engine_state, stack)?;
|
||||
|
||||
let pattern_arg: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
|
||||
let pattern_arg: Option<Spanned<NuPath>> = call.opt(engine_state, stack, 0)?;
|
||||
|
||||
let pattern_arg = {
|
||||
if let Some(path) = pattern_arg {
|
||||
Some(Spanned {
|
||||
item: nu_utils::strip_ansi_string_unlikely(path.item),
|
||||
span: path.span,
|
||||
})
|
||||
match path.item {
|
||||
NuPath::Quoted(p) => Some(Spanned {
|
||||
item: NuPath::Quoted(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)),
|
||||
span: path.span,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
pattern_arg
|
||||
}
|
||||
};
|
||||
|
||||
let (path, p_tag, absolute_path) = match pattern_arg {
|
||||
Some(p) => {
|
||||
let p_tag = p.span;
|
||||
let mut p = expand_to_real_path(p.item);
|
||||
// it indicates we need to append an extra '*' after pattern for listing given directory
|
||||
// Example: 'ls directory' -> 'ls directory/*'
|
||||
let mut extra_star_under_given_directory = false;
|
||||
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);
|
||||
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
|
||||
@ -131,27 +142,50 @@ impl Command for Ls {
|
||||
if is_empty_dir(&expanded) {
|
||||
return Ok(Value::list(vec![], call_span).into_pipeline_data());
|
||||
}
|
||||
p.push("*");
|
||||
extra_star_under_given_directory = true;
|
||||
}
|
||||
let absolute_path = p.is_absolute();
|
||||
(p, p_tag, absolute_path)
|
||||
(
|
||||
p,
|
||||
p_tag,
|
||||
absolute_path,
|
||||
matches!(pat.item, NuPath::Quoted(_)),
|
||||
)
|
||||
}
|
||||
None => {
|
||||
// Avoid pushing "*" to the default path when directory (do not show contents) flag is true
|
||||
if directory {
|
||||
(PathBuf::from("."), call_span, false)
|
||||
(PathBuf::from("."), call_span, false, false)
|
||||
} else if is_empty_dir(current_dir(engine_state, stack)?) {
|
||||
return Ok(Value::list(vec![], call_span).into_pipeline_data());
|
||||
} else {
|
||||
(PathBuf::from("*"), call_span, false)
|
||||
(PathBuf::from("*"), call_span, false, false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let hidden_dir_specified = is_hidden_dir(&path);
|
||||
// when it's quoted, we need to escape our glob pattern
|
||||
// so we can do ls for a file or directory like `a[123]b`
|
||||
let path = if quoted {
|
||||
let p = path.display().to_string();
|
||||
let mut glob_escaped = Pattern::escape(&p);
|
||||
if extra_star_under_given_directory {
|
||||
glob_escaped.push(std::path::MAIN_SEPARATOR);
|
||||
glob_escaped.push('*');
|
||||
}
|
||||
glob_escaped
|
||||
} else {
|
||||
let mut p = path.display().to_string();
|
||||
if extra_star_under_given_directory {
|
||||
p.push(std::path::MAIN_SEPARATOR);
|
||||
p.push('*');
|
||||
}
|
||||
p
|
||||
};
|
||||
|
||||
let glob_path = Spanned {
|
||||
item: path.display().to_string(),
|
||||
item: path.clone(),
|
||||
span: p_tag,
|
||||
};
|
||||
|
||||
@ -169,7 +203,7 @@ impl Command for Ls {
|
||||
let mut paths_peek = paths.peekable();
|
||||
if paths_peek.peek().is_none() {
|
||||
return Err(ShellError::GenericError {
|
||||
error: format!("No matches found for {}", &path.display().to_string()),
|
||||
error: format!("No matches found for {}", &path),
|
||||
msg: "Pattern, file or folder not found".into(),
|
||||
span: Some(p_tag),
|
||||
help: Some("no matches found".into()),
|
||||
|
@ -495,6 +495,7 @@ fn value_should_be_printed(
|
||||
| Value::Nothing { .. }
|
||||
| Value::Error { .. } => term_equals_value(term, &lower_value, span),
|
||||
Value::String { .. }
|
||||
| Value::QuotedString { .. }
|
||||
| Value::List { .. }
|
||||
| Value::CellPath { .. }
|
||||
| Value::CustomValue { .. } => term_contains_value(term, &lower_value, span),
|
||||
|
@ -220,8 +220,8 @@ fn convert_to_value(
|
||||
msg: "calls not supported in nuon".into(),
|
||||
span: expr.span,
|
||||
}),
|
||||
Expr::Filepath(val) => Ok(Value::string(val, span)),
|
||||
Expr::Directory(val) => Ok(Value::string(val, span)),
|
||||
Expr::Filepath(val, _) => Ok(Value::string(val, span)),
|
||||
Expr::Directory(val, _) => Ok(Value::string(val, span)),
|
||||
Expr::Float(val) => Ok(Value::float(val, span)),
|
||||
Expr::FullCellPath(full_cell_path) => {
|
||||
if !full_cell_path.tail.is_empty() {
|
||||
@ -242,7 +242,8 @@ fn convert_to_value(
|
||||
msg: "extra tokens in input file".into(),
|
||||
span: expr.span,
|
||||
}),
|
||||
Expr::GlobPattern(val) => Ok(Value::string(val, span)),
|
||||
Expr::GlobPattern(val, _) => Ok(Value::string(val, span)),
|
||||
Expr::LsGlobPattern(val, _) => Ok(Value::string(val, span)),
|
||||
Expr::ImportPattern(..) => Err(ShellError::OutsideSpannedLabeledError {
|
||||
src: original_text.to_string(),
|
||||
error: "Error when loading".into(),
|
||||
|
@ -115,6 +115,7 @@ pub fn value_to_json_value(v: &Value) -> Result<nu_json::Value, ShellError> {
|
||||
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::CellPath { val, .. } => nu_json::Value::Array(
|
||||
val.members
|
||||
.iter()
|
||||
|
@ -279,6 +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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,6 +127,7 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String {
|
||||
)
|
||||
}
|
||||
Value::String { val, .. } => val,
|
||||
Value::QuotedString { val, .. } => val,
|
||||
Value::List { vals: val, .. } => val
|
||||
.into_iter()
|
||||
.map(|x| local_into_string(x, ", ", config))
|
||||
|
@ -54,7 +54,9 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellErr
|
||||
Value::Date { val, .. } => toml::Value::String(val.to_string()),
|
||||
Value::Range { .. } => toml::Value::String("<Range>".to_string()),
|
||||
Value::Float { val, .. } => toml::Value::Float(*val),
|
||||
Value::String { val, .. } => toml::Value::String(val.clone()),
|
||||
Value::String { val, .. } | Value::QuotedString { val, .. } => {
|
||||
toml::Value::String(val.clone())
|
||||
}
|
||||
Value::Record { val, .. } => {
|
||||
let mut m = toml::map::Map::new();
|
||||
for (k, v) in val {
|
||||
|
@ -52,7 +52,9 @@ pub fn value_to_yaml_value(v: &Value) -> Result<serde_yaml::Value, ShellError> {
|
||||
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, .. } => serde_yaml::Value::String(val.clone()),
|
||||
Value::String { val, .. } | Value::QuotedString { val, .. } => {
|
||||
serde_yaml::Value::String(val.clone())
|
||||
}
|
||||
Value::Record { val, .. } => {
|
||||
let mut m = serde_yaml::Mapping::new();
|
||||
for (k, v) in val {
|
||||
|
Reference in New Issue
Block a user