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:


0487e9ffcb/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.
This commit is contained in:
Wind
2024-02-23 09:17:09 +08:00
committed by GitHub
parent a2a1c1656f
commit f7d647ac3c
41 changed files with 534 additions and 109 deletions

View File

@ -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<Spanned<NuPath>>,
path: Option<Spanned<NuGlob>>,
all: bool,
deref: bool,
exclude: Option<Spanned<String>>,
@ -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(),
},
&current_dir,

View File

@ -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<Spanned<NuPath>> = 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,
};

View File

@ -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<PipelineData, ShellError> {
// TODO: handle invalid directory or insufficient permissions when moving
let mut spanned_source: Spanned<NuPath> = call.req(engine_state, stack, 0)?;
let mut spanned_source: Spanned<NuGlob> = call.req(engine_state, stack, 0)?;
spanned_source.item = spanned_source.item.strip_ansi_string_unlikely();
let spanned_destination: Spanned<String> = call.req(engine_state, stack, 1)?;
let verbose = call.has_flag(engine_state, stack, "verbose")?;

View File

@ -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::<Spanned<NuPath>>(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,
});
}

View File

@ -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<Spanned<NuPath>> = 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,
};

View File

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

View File

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

View File

@ -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<Vec<Spanned<NuGlob>>, 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<Option<Spanned<NuGlob>>, 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)
}
}