diff --git a/src/conditional_operator.rs b/src/conditional_operator.rs index bb297c22ab..d3fe826b9f 100644 --- a/src/conditional_operator.rs +++ b/src/conditional_operator.rs @@ -9,6 +9,8 @@ pub(crate) enum ConditionalOperator { Inequality, /// `=~` RegexMatch, + /// `!~` + RegexNotMatch, } impl Display for ConditionalOperator { @@ -17,6 +19,7 @@ impl Display for ConditionalOperator { Self::Equality => write!(f, "=="), Self::Inequality => write!(f, "!="), Self::RegexMatch => write!(f, "=~"), + Self::RegexNotMatch => write!(f, "!~"), } } } diff --git a/src/evaluator.rs b/src/evaluator.rs index b83c8cf70c..c69ea1fc78 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -245,6 +245,9 @@ impl<'src, 'run> Evaluator<'src, 'run> { ConditionalOperator::RegexMatch => Regex::new(&rhs_value) .map_err(|source| Error::RegexCompile { source })? .is_match(&lhs_value), + ConditionalOperator::RegexNotMatch => !Regex::new(&rhs_value) + .map_err(|source| Error::RegexCompile { source })? + .is_match(&lhs_value), }; Ok(condition) } diff --git a/src/lexer.rs b/src/lexer.rs index 2c56db9dcf..eaa4417535 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -475,7 +475,7 @@ impl<'src> Lexer<'src> { match start { ' ' | '\t' => self.lex_whitespace(), '!' if self.rest().starts_with("!include") => Err(self.error(Include)), - '!' => self.lex_digraph('!', '=', BangEquals), + '!' => self.lex_choices('!', &[('=', BangEquals), ('~', BangTilde)], Unspecified), '#' => self.lex_comment(), '$' => self.lex_single(Dollar), '&' => self.lex_digraph('&', '&', AmpersandAmpersand), @@ -949,6 +949,7 @@ mod tests { Asterisk => "*", At => "@", BangEquals => "!=", + BangTilde => "!~", BarBar => "||", BraceL => "{", BraceR => "}", diff --git a/src/parser.rs b/src/parser.rs index 32a5575159..adfe8fbd9e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -614,6 +614,8 @@ impl<'run, 'src> Parser<'run, 'src> { ConditionalOperator::Inequality } else if self.accepted(EqualsTilde)? { ConditionalOperator::RegexMatch + } else if self.accepted(BangTilde)? { + ConditionalOperator::RegexNotMatch } else { self.expect(EqualsEquals)?; ConditionalOperator::Equality diff --git a/src/summary.rs b/src/summary.rs index 76483d63ec..bab3d5fd5e 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -360,6 +360,7 @@ pub enum ConditionalOperator { Equality, Inequality, RegexMatch, + RegexNotMatch, } impl ConditionalOperator { @@ -368,6 +369,7 @@ impl ConditionalOperator { full::ConditionalOperator::Equality => Self::Equality, full::ConditionalOperator::Inequality => Self::Inequality, full::ConditionalOperator::RegexMatch => Self::RegexMatch, + full::ConditionalOperator::RegexNotMatch => Self::RegexNotMatch, } } } diff --git a/src/token_kind.rs b/src/token_kind.rs index 850afa9629..be8af19f5f 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -7,6 +7,7 @@ pub(crate) enum TokenKind { At, Backtick, BangEquals, + BangTilde, BarBar, BraceL, BraceR, @@ -51,6 +52,7 @@ impl Display for TokenKind { At => "'@'", Backtick => "backtick", BangEquals => "'!='", + BangTilde => "'!~'", BarBar => "'||'", BraceL => "'{'", BraceR => "'}'", diff --git a/tests/conditional.rs b/tests/conditional.rs index 4eab2f4d72..8eae1351d6 100644 --- a/tests/conditional.rs +++ b/tests/conditional.rs @@ -136,7 +136,7 @@ test! { ", stdout: "", stderr: " - error: Expected '&&', '!=', '||', '==', '=~', '+', or '/', but found identifier + error: Expected '&&', '!=', '!~', '||', '==', '=~', '+', or '/', but found identifier ——▶ justfile:1:12 │ 1 │ a := if '' a '' { '' } else { b } diff --git a/tests/regexes.rs b/tests/regexes.rs index 7a53a0af5c..3b13a72578 100644 --- a/tests/regexes.rs +++ b/tests/regexes.rs @@ -64,3 +64,23 @@ fn bad_regex_fails_at_runtime() { .status(EXIT_FAILURE) .run(); } + +#[test] +fn not_matching_regex() { + Test::new() + .justfile( + " + foo := if 'Foo' !~ '^ab+c' { + 'no' + } else { + 'yes' + } + + default: + echo {{ foo }} + ", + ) + .stderr("echo no\n") + .stdout("no\n") + .run(); +}