diff --git a/crates/nu-cli/src/completions/operator_completions.rs b/crates/nu-cli/src/completions/operator_completions.rs index aebb176b76..0e5e1d9c90 100644 --- a/crates/nu-cli/src/completions/operator_completions.rs +++ b/crates/nu-cli/src/completions/operator_completions.rs @@ -107,10 +107,14 @@ impl Completer for OperatorCompletion { ("not-in", "Is not a member of (doesn't use regex)"), ], Expr::FullCellPath(path) => match path.head.expr { - Expr::List(_) => vec![( - "++", - "Concatenates two lists, two strings, or two binary values", - )], + Expr::List(_) => vec![ + ( + "++", + "Concatenates two lists, two strings, or two binary values", + ), + ("has", "Contains a value of (doesn't use regex)"), + ("not-has", "Does not contain a value of (doesn't use regex)"), + ], Expr::Var(id) => get_variable_completions(id, working_set), _ => vec![], }, diff --git a/crates/nu-command/src/help/help_operators.rs b/crates/nu-command/src/help/help_operators.rs index 28bb223e61..9000bdacad 100644 --- a/crates/nu-command/src/help/help_operators.rs +++ b/crates/nu-command/src/help/help_operators.rs @@ -141,6 +141,12 @@ fn description(operator: &Operator) -> &'static str { Operator::Comparison(Comparison::NotIn) => { "Checks if a value is not in a list, is not part of a string, or is not a key in a record." } + Operator::Comparison(Comparison::Has) => { + "Checks if a list contains a value, a string contains another, or if a record has a key." + } + Operator::Comparison(Comparison::NotHas) => { + "Checks if a list does not contain a value, a string does not contain another, or if a record does not have a key." + } Operator::Comparison(Comparison::StartsWith) => "Checks if a string starts with another.", Operator::Comparison(Comparison::EndsWith) => "Checks if a string ends with another.", Operator::Math(Math::Plus) => "Adds two values.", diff --git a/crates/nu-command/tests/commands/where_.rs b/crates/nu-command/tests/commands/where_.rs index 870d74fbc6..7c1fa37f23 100644 --- a/crates/nu-command/tests/commands/where_.rs +++ b/crates/nu-command/tests/commands/where_.rs @@ -187,3 +187,25 @@ fn where_gt_null() { let actual = nu!("[{foo: 123} {}] | where foo? > 10 | to nuon"); assert_eq!(actual.out, "[[foo]; [123]]"); } + +#[test] +fn has_operator() { + let actual = nu!( + r#"[[name, children]; [foo, [a, b]], [bar [b, c]], [baz, [c, d]]] | where children has "a" | to nuon"# + ); + assert_eq!(actual.out, r#"[[name, children]; [foo, [a, b]]]"#); + + let actual = nu!( + r#"[[name, children]; [foo, [a, b]], [bar [b, c]], [baz, [c, d]]] | where children not-has "a" | to nuon"# + ); + assert_eq!( + actual.out, + r#"[[name, children]; [bar, [b, c]], [baz, [c, d]]]"# + ); + + let actual = nu!(r#"{foo: 1} has foo"#); + assert_eq!(actual.out, "true"); + + let actual = nu!(r#"{foo: 1} has bar "#); + assert_eq!(actual.out, "false"); +} diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs index 0091963b53..20672484fa 100644 --- a/crates/nu-engine/src/eval_ir.rs +++ b/crates/nu-engine/src/eval_ir.rs @@ -952,6 +952,8 @@ fn binary_op( } Comparison::In => lhs_val.r#in(op_span, &rhs_val, span)?, Comparison::NotIn => lhs_val.not_in(op_span, &rhs_val, span)?, + Comparison::Has => lhs_val.has(op_span, &rhs_val, span)?, + Comparison::NotHas => lhs_val.not_has(op_span, &rhs_val, span)?, Comparison::StartsWith => lhs_val.starts_with(op_span, &rhs_val, span)?, Comparison::EndsWith => lhs_val.ends_with(op_span, &rhs_val, span)?, }, diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 3d9038856a..6f4b1e1b81 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -5147,6 +5147,8 @@ pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expressi b"//" => Operator::Math(Math::FloorDivision), b"in" => Operator::Comparison(Comparison::In), b"not-in" => Operator::Comparison(Comparison::NotIn), + b"has" => Operator::Comparison(Comparison::Has), + b"not-has" => Operator::Comparison(Comparison::NotHas), b"mod" => Operator::Math(Math::Modulo), b"bit-or" => Operator::Bits(Bits::BitOr), b"bit-xor" => Operator::Bits(Bits::BitXor), @@ -5187,7 +5189,7 @@ pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expressi b"contains" => { working_set.error(ParseError::UnknownOperator( "contains", - "Did you mean '$string =~ $pattern' or '$element in $container'?", + "Did you mean 'has'?", span, )); return garbage(working_set, span); diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index fd57acd5ca..413a3c3218 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -821,7 +821,7 @@ pub fn math_result_type( ) } }, - Operator::Comparison(Comparison::In) => match (&lhs.ty, &rhs.ty) { + Operator::Comparison(Comparison::In | Comparison::NotIn) => match (&lhs.ty, &rhs.ty) { (t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None), (Type::Int | Type::Float | Type::Number, Type::Range) => (Type::Bool, None), (Type::String, Type::String) => (Type::Bool, None), @@ -859,44 +859,48 @@ pub fn math_result_type( ) } }, - Operator::Comparison(Comparison::NotIn) => match (&lhs.ty, &rhs.ty) { - (t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None), - (Type::Int | Type::Float | Type::Number, Type::Range) => (Type::Bool, None), - (Type::String, Type::String) => (Type::Bool, None), - (Type::String, Type::Record(_)) => (Type::Bool, None), + Operator::Comparison(Comparison::Has | Comparison::NotHas) => { + let container = lhs; + let element = rhs; + match (&element.ty, &container.ty) { + (t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None), + (Type::Int | Type::Float | Type::Number, Type::Range) => (Type::Bool, None), + (Type::String, Type::String) => (Type::Bool, None), + (Type::String, Type::Record(_)) => (Type::Bool, None), - (Type::Custom(a), Type::Custom(b)) if a == b => (Type::Custom(a.clone()), None), - (Type::Custom(a), _) => (Type::Custom(a.clone()), None), + (Type::Custom(a), Type::Custom(b)) if a == b => (Type::Custom(a.clone()), None), + (Type::Custom(a), _) => (Type::Custom(a.clone()), None), - (Type::Any, _) => (Type::Bool, None), - (_, Type::Any) => (Type::Bool, None), - (Type::Int | Type::Float | Type::String, _) => { - *op = Expression::garbage(working_set, op.span); - ( - Type::Any, - Some(ParseError::UnsupportedOperationRHS( - "subset comparison".into(), - op.span, - lhs.span, - lhs.ty.clone(), - rhs.span, - rhs.ty.clone(), - )), - ) + (Type::Any, _) => (Type::Bool, None), + (_, Type::Any) => (Type::Bool, None), + (Type::Int | Type::Float | Type::String, _) => { + *op = Expression::garbage(working_set, op.span); + ( + Type::Any, + Some(ParseError::UnsupportedOperationLHS( + "subset comparison".into(), + op.span, + element.span, + element.ty.clone(), + )), + ) + } + _ => { + *op = Expression::garbage(working_set, op.span); + ( + Type::Any, + Some(ParseError::UnsupportedOperationRHS( + "subset comparison".into(), + op.span, + element.span, + element.ty.clone(), + container.span, + container.ty.clone(), + )), + ) + } } - _ => { - *op = Expression::garbage(working_set, op.span); - ( - Type::Any, - Some(ParseError::UnsupportedOperationLHS( - "subset comparison".into(), - op.span, - lhs.span, - lhs.ty.clone(), - )), - ) - } - }, + } Operator::Bits(Bits::ShiftLeft) | Operator::Bits(Bits::ShiftRight) | Operator::Bits(Bits::BitOr) diff --git a/crates/nu-protocol/src/ast/operator.rs b/crates/nu-protocol/src/ast/operator.rs index bde866498c..0f6fd0b556 100644 --- a/crates/nu-protocol/src/ast/operator.rs +++ b/crates/nu-protocol/src/ast/operator.rs @@ -17,6 +17,8 @@ pub enum Comparison { NotRegexMatch, In, NotIn, + Has, + NotHas, StartsWith, EndsWith, } @@ -90,6 +92,8 @@ impl Operator { | Self::Comparison(Comparison::NotEqual) | Self::Comparison(Comparison::In) | Self::Comparison(Comparison::NotIn) + | Self::Comparison(Comparison::Has) + | Self::Comparison(Comparison::NotHas) | Self::Math(Math::Concat) => 80, Self::Bits(Bits::BitAnd) => 75, Self::Bits(Bits::BitXor) => 70, @@ -123,6 +127,8 @@ impl Display for Operator { Operator::Comparison(Comparison::EndsWith) => write!(f, "ends-with"), Operator::Comparison(Comparison::In) => write!(f, "in"), Operator::Comparison(Comparison::NotIn) => write!(f, "not-in"), + Operator::Comparison(Comparison::Has) => write!(f, "has"), + Operator::Comparison(Comparison::NotHas) => write!(f, "not-has"), Operator::Math(Math::Plus) => write!(f, "+"), Operator::Math(Math::Concat) => write!(f, "++"), Operator::Math(Math::Minus) => write!(f, "-"), diff --git a/crates/nu-protocol/src/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index 0a65c50817..65f4dd7da0 100644 --- a/crates/nu-protocol/src/eval_base.rs +++ b/crates/nu-protocol/src/eval_base.rs @@ -256,6 +256,8 @@ pub trait Eval { Comparison::NotEqual => lhs.ne(op_span, &rhs, expr_span), Comparison::In => lhs.r#in(op_span, &rhs, expr_span), Comparison::NotIn => lhs.not_in(op_span, &rhs, expr_span), + Comparison::Has => lhs.has(op_span, &rhs, expr_span), + Comparison::NotHas => lhs.not_has(op_span, &rhs, expr_span), Comparison::StartsWith => lhs.starts_with(op_span, &rhs, expr_span), Comparison::EndsWith => lhs.ends_with(op_span, &rhs, expr_span), Comparison::RegexMatch => { diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 5ffe376269..58cde75b50 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -3540,6 +3540,14 @@ impl Value { } } + pub fn has(&self, op: Span, rhs: &Value, span: Span) -> Result { + rhs.r#in(op, self, span) + } + + pub fn not_has(&self, op: Span, rhs: &Value, span: Span) -> Result { + rhs.r#not_in(op, self, span) + } + pub fn regex_match( &self, engine_state: &EngineState,