diff --git a/crates/nu-command/src/dataframe/values/nu_dataframe/between_values.rs b/crates/nu-command/src/dataframe/values/nu_dataframe/between_values.rs index 8abb290d39..de075b0d01 100644 --- a/crates/nu-command/src/dataframe/values/nu_dataframe/between_values.rs +++ b/crates/nu-command/src/dataframe/values/nu_dataframe/between_values.rs @@ -273,7 +273,7 @@ pub(super) fn compute_series_single_value( compare_series_decimal(&lhs, *val, ChunkedArray::equal, lhs_span) } Value::String { val, .. } => { - let equal_pattern = format!("^{}$", val); + let equal_pattern = format!("^{}$", regex::escape(val)); contains_series_pat(&lhs, &equal_pattern, lhs_span) } Value::Date { val, .. } => { @@ -385,7 +385,7 @@ pub(super) fn compute_series_single_value( }, Operator::StartsWith => match &right { Value::String { val, .. } => { - let starts_with_pattern = format!("^{}", val); + let starts_with_pattern = format!("^{}", regex::escape(val)); contains_series_pat(&lhs, &starts_with_pattern, lhs_span) } _ => Err(ShellError::OperatorMismatch { @@ -396,6 +396,19 @@ pub(super) fn compute_series_single_value( rhs_span: right.span()?, }), }, + Operator::EndsWith => match &right { + Value::String { val, .. } => { + let ends_with_pattern = format!("{}$", regex::escape(val)); + contains_series_pat(&lhs, &ends_with_pattern, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, _ => Err(ShellError::OperatorMismatch { op_span: operator.span, lhs_ty: left.get_type(), diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 75dadae108..7d7735dc1a 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -427,6 +427,10 @@ pub fn eval_expression( let rhs = eval_expression(engine_state, stack, rhs)?; lhs.starts_with(op_span, &rhs, expr.span) } + Operator::EndsWith => { + let rhs = eval_expression(engine_state, stack, rhs)?; + lhs.ends_with(op_span, &rhs, expr.span) + } } } Expr::Subexpression(block_id) => { diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 49280dacc6..9708a2f516 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -4049,6 +4049,7 @@ pub fn parse_operator( b"not-in" => Operator::NotIn, b"mod" => Operator::Modulo, b"starts-with" => Operator::StartsWith, + b"ends-with" => Operator::EndsWith, b"&&" | b"and" => Operator::And, b"||" | b"or" => Operator::Or, b"**" => Operator::Pow, diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index 11a16d3ea6..9b2bf3696f 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -337,6 +337,24 @@ pub fn math_result_type( ) } }, + Operator::EndsWith => match (&lhs.ty, &rhs.ty) { + (Type::String, Type::String) => (Type::Bool, None), + (Type::Any, _) => (Type::Bool, None), + (_, Type::Any) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Any, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, Operator::In => match (&lhs.ty, &rhs.ty) { (t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None), (Type::Int | Type::Float, Type::Range) => (Type::Bool, None), diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index c6ceeac41e..4a93841ef2 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -35,6 +35,7 @@ impl Expression { Operator::NotRegexMatch | Operator::RegexMatch | Operator::StartsWith + | Operator::EndsWith | Operator::LessThan | Operator::LessThanOrEqual | Operator::GreaterThan diff --git a/crates/nu-protocol/src/ast/operator.rs b/crates/nu-protocol/src/ast/operator.rs index 72a6d0f82e..6d305c732d 100644 --- a/crates/nu-protocol/src/ast/operator.rs +++ b/crates/nu-protocol/src/ast/operator.rs @@ -24,6 +24,7 @@ pub enum Operator { Or, Pow, StartsWith, + EndsWith, } impl Display for Operator { @@ -48,6 +49,7 @@ impl Display for Operator { Operator::LessThanOrEqual => write!(f, "<="), Operator::GreaterThanOrEqual => write!(f, ">="), Operator::StartsWith => write!(f, "starts-with"), + Operator::EndsWith => write!(f, "ends-with"), } } } diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 0aa79152f6..6554670572 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -2080,6 +2080,25 @@ impl Value { } } + pub fn ends_with(&self, op: Span, rhs: &Value, span: Span) -> Result { + match (self, rhs) { + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::Bool { + val: lhs.ends_with(rhs), + span, + }), + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::EndsWith, op, rhs) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn modulo(&self, op: Span, rhs: &Value, span: Span) -> Result { match (self, rhs) { (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { diff --git a/src/tests/test_parser.rs b/src/tests/test_parser.rs index 56481704b2..3f75a0956c 100644 --- a/src/tests/test_parser.rs +++ b/src/tests/test_parser.rs @@ -326,6 +326,14 @@ fn starts_with_operator_succeeds() -> TestResult { ) } +#[test] +fn ends_with_operator_succeeds() -> TestResult { + run_test( + r#"[Moe Larry Curly] | where $it ends-with ly | str collect"#, + "Curly", + ) +} + #[test] fn proper_missing_param() -> TestResult { fail_test(r#"def foo [x y z w] { }; foo a b c"#, "missing w")