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

@ -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<Vec<CellPath>>,
}
impl CmdArgument for Arguments {
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
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<PipelineData, ShellError> {
glob_helper(engine_state, stack, call, input)
}
fn examples(&self) -> Vec<Example> {
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<PipelineData, ShellError> {
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 {})
}
}

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -303,6 +303,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
IntoInt,
IntoRecord,
IntoString,
IntoGlob,
IntoValue,
};

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)
}
}

View File

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

View File

@ -115,7 +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::Glob { val, .. } => nu_json::Value::String(val.to_string()),
Value::CellPath { val, .. } => nu_json::Value::Array(
val.members
.iter()

View File

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

View File

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

View File

@ -57,9 +57,7 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellErr
}
Value::Range { .. } => toml::Value::String("<Range>".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 {

View File

@ -52,7 +52,7 @@ 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, .. } | Value::QuotedString { val, .. } => {
Value::String { val, .. } | Value::Glob { val, .. } => {
serde_yaml::Value::String(val.clone())
}
Value::Record { val, .. } => {

View File

@ -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) {