diff --git a/jmespath/lexer.py b/jmespath/lexer.py index 91376e09..d89b732a 100644 --- a/jmespath/lexer.py +++ b/jmespath/lexer.py @@ -90,10 +90,15 @@ def tokenize(self, expression): elif self._current == '!': yield self._match_or_else('=', 'ne', 'not') elif self._current == '=': - if self._next() == '=': + next_char = self._next() + if next_char == '=': yield {'type': 'eq', 'value': '==', 'start': self._position - 1, 'end': self._position} self._next() + elif next_char == '~': + yield {'type': 'regex_match', 'value': '=~', + 'start': self._position - 1, 'end': self._position} + self._next() else: if self._current is None: # If we're at the EOF, we never advanced diff --git a/jmespath/parser.py b/jmespath/parser.py index 4d5ba38a..724eb92d 100644 --- a/jmespath/parser.py +++ b/jmespath/parser.py @@ -57,6 +57,7 @@ class Parser(object): 'gte': 5, 'lte': 5, 'ne': 5, + 'regex_match': 5, 'flatten': 9, # Everything above stops a projection. 'star': 20, @@ -306,6 +307,9 @@ def _token_led_eq(self, left): def _token_led_ne(self, left): return self._parse_comparator(left, 'ne') + def _token_led_regex_match(self, left): + return self._parse_comparator(left, 'regex_match') + def _token_led_gt(self, left): return self._parse_comparator(left, 'gt') diff --git a/jmespath/visitor.py b/jmespath/visitor.py index 2c783e5e..ba185910 100644 --- a/jmespath/visitor.py +++ b/jmespath/visitor.py @@ -1,4 +1,5 @@ import operator +import re from jmespath import functions from jmespath.compat import string_type @@ -12,6 +13,17 @@ def _equals(x, y): return x == y +def _regex_match(lhs, rhs): + try: + if hasattr(rhs, 'search'): + return rhs.search(lhs) is not None + if hasattr(lhs, 'search'): + return lhs.search(rhs) is not None + return re.search(rhs, lhs) is not None + except TypeError: + return None + + def _is_special_integer_case(x, y): # We need to special case comparing 0 or 1 to # True/False. While normally comparing any @@ -101,12 +113,13 @@ class TreeInterpreter(Visitor): COMPARATOR_FUNC = { 'eq': _equals, 'ne': lambda x, y: not _equals(x, y), + 'regex_match': _regex_match, 'lt': operator.lt, 'gt': operator.gt, 'lte': operator.le, 'gte': operator.ge } - _EQUALITY_OPS = ['eq', 'ne'] + _EQUALITY_OPS = ['eq', 'ne', 'regex_match'] MAP_TYPE = dict def __init__(self, options=None): diff --git a/tests/compliance/filters.json b/tests/compliance/filters.json index 5b9f52b1..54588c69 100644 --- a/tests/compliance/filters.json +++ b/tests/compliance/filters.json @@ -464,5 +464,54 @@ "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] } ] + }, + { + "given": { + "foo": [ + {"name": "ax"}, + {"name": "Ax"}, + {"name": "bx"}, + {"name": "Bx"} + ] + }, + "cases": [ + { + "comment": "Using regex in a filter expression", + "expression": "foo[? name =~ '^a']", + "result": [ + {"name": "ax"} + ] + }, + { + "comment": "Using regex in a filter expression (pre-compiled)", + "expression": "foo[? name =~ /^a/]", + "result": [ + {"name": "ax"} + ] + }, + { + "comment": "Using regex in a filter expression (pre-compiled with flag)", + "expression": "foo[? name =~ /^a/i]", + "result": [ + {"name": "ax"}, + {"name": "Ax"} + ] + }, + { + "comment": "Using regex as a lhs in a filter expression (pre-compiled)", + "expression": "foo[? /^a/ =~ name]", + "result": [ + {"name": "ax"} + ] + }, + { + "comment": "Using regex as a lhs in a filter expression (pre-compiled with flag)", + "expression": "foo[? /^a/i =~ name]", + "result": [ + {"name": "ax"}, + {"name": "Ax"} + ] + } + ] } ]