nushell/crates/nu-protocol/src/ty.rs
Wind f7d647ac3c
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.
2024-02-23 09:17:09 +08:00

249 lines
8.4 KiB
Rust

use serde::{Deserialize, Serialize};
#[cfg(test)]
use strum_macros::EnumIter;
use std::fmt::Display;
use crate::SyntaxShape;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[cfg_attr(test, derive(EnumIter))]
pub enum Type {
Any,
Binary,
Block,
Bool,
CellPath,
Closure,
Custom(String),
Date,
Duration,
Error,
Filesize,
Float,
Int,
List(Box<Type>),
ListStream,
#[default]
Nothing,
Number,
Range,
Record(Vec<(String, Type)>),
Signature,
String,
Glob,
Table(Vec<(String, Type)>),
}
impl Type {
pub fn is_subtype(&self, other: &Type) -> bool {
// Structural subtyping
let is_subtype_collection = |this: &[(String, Type)], that: &[(String, Type)]| {
if this.is_empty() || that.is_empty() {
true
} else if this.len() < that.len() {
false
} else {
that.iter().all(|(col_y, ty_y)| {
if let Some((_, ty_x)) = this.iter().find(|(col_x, _)| col_x == col_y) {
ty_x.is_subtype(ty_y)
} else {
false
}
})
}
};
match (self, other) {
(t, u) if t == u => true,
(Type::Float, Type::Number) => true,
(Type::Int, Type::Number) => true,
(_, Type::Any) => true,
(Type::List(t), Type::List(u)) if t.is_subtype(u) => true, // List is covariant
(Type::Record(this), Type::Record(that)) | (Type::Table(this), Type::Table(that)) => {
is_subtype_collection(this, that)
}
(Type::Table(_), Type::List(_)) => true,
(Type::Glob, Type::String) => true,
(Type::String, Type::Glob) => true,
_ => false,
}
}
pub fn is_numeric(&self) -> bool {
matches!(self, Type::Int | Type::Float | Type::Number)
}
pub fn is_list(&self) -> bool {
matches!(self, Type::List(_))
}
/// Does this type represent a data structure containing values that can be addressed using 'cell paths'?
pub fn accepts_cell_paths(&self) -> bool {
matches!(self, Type::List(_) | Type::Record(_) | Type::Table(_))
}
pub fn to_shape(&self) -> SyntaxShape {
let mk_shape = |tys: &[(String, Type)]| {
tys.iter()
.map(|(key, val)| (key.clone(), val.to_shape()))
.collect()
};
match self {
Type::Int => SyntaxShape::Int,
Type::Float => SyntaxShape::Float,
Type::Range => SyntaxShape::Range,
Type::Bool => SyntaxShape::Boolean,
Type::String => SyntaxShape::String,
Type::Block => SyntaxShape::Block, // FIXME needs more accuracy
Type::Closure => SyntaxShape::Closure(None), // FIXME needs more accuracy
Type::CellPath => SyntaxShape::CellPath,
Type::Duration => SyntaxShape::Duration,
Type::Date => SyntaxShape::DateTime,
Type::Filesize => SyntaxShape::Filesize,
Type::List(x) => SyntaxShape::List(Box::new(x.to_shape())),
Type::Number => SyntaxShape::Number,
Type::Nothing => SyntaxShape::Nothing,
Type::Record(entries) => SyntaxShape::Record(mk_shape(entries)),
Type::Table(columns) => SyntaxShape::Table(mk_shape(columns)),
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
Type::Any => SyntaxShape::Any,
Type::Error => SyntaxShape::Any,
Type::Binary => SyntaxShape::Binary,
Type::Custom(_) => SyntaxShape::Any,
Type::Signature => SyntaxShape::Signature,
Type::Glob => SyntaxShape::GlobPattern,
}
}
/// Get a string representation, without inner type specification of lists,
/// tables and records (get `list` instead of `list<any>`
pub fn get_non_specified_string(&self) -> String {
match self {
Type::Block => String::from("block"),
Type::Closure => String::from("closure"),
Type::Bool => String::from("bool"),
Type::CellPath => String::from("cell-path"),
Type::Date => String::from("date"),
Type::Duration => String::from("duration"),
Type::Filesize => String::from("filesize"),
Type::Float => String::from("float"),
Type::Int => String::from("int"),
Type::Range => String::from("range"),
Type::Record(_) => String::from("record"),
Type::Table(_) => String::from("table"),
Type::List(_) => String::from("list"),
Type::Nothing => String::from("nothing"),
Type::Number => String::from("number"),
Type::String => String::from("string"),
Type::ListStream => String::from("list-stream"),
Type::Any => String::from("any"),
Type::Error => String::from("error"),
Type::Binary => String::from("binary"),
Type::Custom(_) => String::from("custom"),
Type::Signature => String::from("signature"),
Type::Glob => String::from("glob"),
}
}
}
impl Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Type::Block => write!(f, "block"),
Type::Closure => write!(f, "closure"),
Type::Bool => write!(f, "bool"),
Type::CellPath => write!(f, "cell-path"),
Type::Date => write!(f, "date"),
Type::Duration => write!(f, "duration"),
Type::Filesize => write!(f, "filesize"),
Type::Float => write!(f, "float"),
Type::Int => write!(f, "int"),
Type::Range => write!(f, "range"),
Type::Record(fields) => {
if fields.is_empty() {
write!(f, "record")
} else {
write!(
f,
"record<{}>",
fields
.iter()
.map(|(x, y)| format!("{x}: {y}"))
.collect::<Vec<String>>()
.join(", "),
)
}
}
Type::Table(columns) => {
if columns.is_empty() {
write!(f, "table")
} else {
write!(
f,
"table<{}>",
columns
.iter()
.map(|(x, y)| format!("{x}: {y}"))
.collect::<Vec<String>>()
.join(", ")
)
}
}
Type::List(l) => write!(f, "list<{l}>"),
Type::Nothing => write!(f, "nothing"),
Type::Number => write!(f, "number"),
Type::String => write!(f, "string"),
Type::ListStream => write!(f, "list-stream"),
Type::Any => write!(f, "any"),
Type::Error => write!(f, "error"),
Type::Binary => write!(f, "binary"),
Type::Custom(custom) => write!(f, "{custom}"),
Type::Signature => write!(f, "signature"),
Type::Glob => write!(f, "glob"),
}
}
}
#[cfg(test)]
mod tests {
use super::Type;
use strum::IntoEnumIterator;
mod subtype_relation {
use super::*;
#[test]
fn test_reflexivity() {
for ty in Type::iter() {
assert!(ty.is_subtype(&ty));
}
}
#[test]
fn test_any_is_top_type() {
for ty in Type::iter() {
assert!(ty.is_subtype(&Type::Any));
}
}
#[test]
fn test_number_supertype() {
assert!(Type::Int.is_subtype(&Type::Number));
assert!(Type::Float.is_subtype(&Type::Number));
}
#[test]
fn test_list_covariance() {
for ty1 in Type::iter() {
for ty2 in Type::iter() {
let list_ty1 = Type::List(Box::new(ty1.clone()));
let list_ty2 = Type::List(Box::new(ty2.clone()));
assert_eq!(list_ty1.is_subtype(&list_ty2), ty1.is_subtype(&ty2));
}
}
}
}
}