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:
WindSoilder
2024-01-21 23:22:25 +08:00
committed by GitHub
parent 64f34e0287
commit c59d6d31bc
26 changed files with 310 additions and 60 deletions

View File

@ -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()),