123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- import re
- class BooleanExpression:
- # A simple evaluator of boolean expressions.
- #
- # Grammar:
- # expr :: or_expr
- # or_expr :: and_expr ('||' and_expr)*
- # and_expr :: not_expr ('&&' not_expr)*
- # not_expr :: '!' not_expr
- # '(' or_expr ')'
- # identifier
- # identifier :: [-+=._a-zA-Z0-9]+
- # Evaluates `string` as a boolean expression.
- # Returns True or False. Throws a ValueError on syntax error.
- #
- # Variables in `variables` are true.
- # Substrings of `triple` are true.
- # 'true' is true.
- # All other identifiers are false.
- @staticmethod
- def evaluate(string, variables, triple=""):
- try:
- parser = BooleanExpression(string, set(variables), triple)
- return parser.parseAll()
- except ValueError as e:
- raise ValueError(str(e) + ('\nin expression: %r' % string))
- #####
- def __init__(self, string, variables, triple=""):
- self.tokens = BooleanExpression.tokenize(string)
- self.variables = variables
- self.variables.add('true')
- self.triple = triple
- self.value = None
- self.token = None
- # Singleton end-of-expression marker.
- END = object()
- # Tokenization pattern.
- Pattern = re.compile(r'\A\s*([()]|[-+=._a-zA-Z0-9]+|&&|\|\||!)\s*(.*)\Z')
- @staticmethod
- def tokenize(string):
- while True:
- m = re.match(BooleanExpression.Pattern, string)
- if m is None:
- if string == "":
- yield BooleanExpression.END;
- return
- else:
- raise ValueError("couldn't parse text: %r" % string)
- token = m.group(1)
- string = m.group(2)
- yield token
- def quote(self, token):
- if token is BooleanExpression.END:
- return '<end of expression>'
- else:
- return repr(token)
- def accept(self, t):
- if self.token == t:
- self.token = next(self.tokens)
- return True
- else:
- return False
- def expect(self, t):
- if self.token == t:
- if self.token != BooleanExpression.END:
- self.token = next(self.tokens)
- else:
- raise ValueError("expected: %s\nhave: %s" %
- (self.quote(t), self.quote(self.token)))
- def isIdentifier(self, t):
- if (t is BooleanExpression.END or t == '&&' or t == '||' or
- t == '!' or t == '(' or t == ')'):
- return False
- return True
- def parseNOT(self):
- if self.accept('!'):
- self.parseNOT()
- self.value = not self.value
- elif self.accept('('):
- self.parseOR()
- self.expect(')')
- elif not self.isIdentifier(self.token):
- raise ValueError("expected: '!' or '(' or identifier\nhave: %s" %
- self.quote(self.token))
- else:
- self.value = (self.token in self.variables or
- self.token in self.triple)
- self.token = next(self.tokens)
- def parseAND(self):
- self.parseNOT()
- while self.accept('&&'):
- left = self.value
- self.parseNOT()
- right = self.value
- # this is technically the wrong associativity, but it
- # doesn't matter for this limited expression grammar
- self.value = left and right
- def parseOR(self):
- self.parseAND()
- while self.accept('||'):
- left = self.value
- self.parseAND()
- right = self.value
- # this is technically the wrong associativity, but it
- # doesn't matter for this limited expression grammar
- self.value = left or right
- def parseAll(self):
- self.token = next(self.tokens)
- self.parseOR()
- self.expect(BooleanExpression.END)
- return self.value
- #######
- # Tests
- import unittest
- class TestBooleanExpression(unittest.TestCase):
- def test_variables(self):
- variables = {'its-true', 'false-lol-true', 'under_score',
- 'e=quals', 'd1g1ts'}
- self.assertTrue(BooleanExpression.evaluate('true', variables))
- self.assertTrue(BooleanExpression.evaluate('its-true', variables))
- self.assertTrue(BooleanExpression.evaluate('false-lol-true', variables))
- self.assertTrue(BooleanExpression.evaluate('under_score', variables))
- self.assertTrue(BooleanExpression.evaluate('e=quals', variables))
- self.assertTrue(BooleanExpression.evaluate('d1g1ts', variables))
- self.assertFalse(BooleanExpression.evaluate('false', variables))
- self.assertFalse(BooleanExpression.evaluate('True', variables))
- self.assertFalse(BooleanExpression.evaluate('true-ish', variables))
- self.assertFalse(BooleanExpression.evaluate('not_true', variables))
- self.assertFalse(BooleanExpression.evaluate('tru', variables))
- def test_triple(self):
- triple = 'arch-vendor-os'
- self.assertTrue(BooleanExpression.evaluate('arch-', {}, triple))
- self.assertTrue(BooleanExpression.evaluate('ar', {}, triple))
- self.assertTrue(BooleanExpression.evaluate('ch-vend', {}, triple))
- self.assertTrue(BooleanExpression.evaluate('-vendor-', {}, triple))
- self.assertTrue(BooleanExpression.evaluate('-os', {}, triple))
- self.assertFalse(BooleanExpression.evaluate('arch-os', {}, triple))
- def test_operators(self):
- self.assertTrue(BooleanExpression.evaluate('true || true', {}))
- self.assertTrue(BooleanExpression.evaluate('true || false', {}))
- self.assertTrue(BooleanExpression.evaluate('false || true', {}))
- self.assertFalse(BooleanExpression.evaluate('false || false', {}))
- self.assertTrue(BooleanExpression.evaluate('true && true', {}))
- self.assertFalse(BooleanExpression.evaluate('true && false', {}))
- self.assertFalse(BooleanExpression.evaluate('false && true', {}))
- self.assertFalse(BooleanExpression.evaluate('false && false', {}))
- self.assertFalse(BooleanExpression.evaluate('!true', {}))
- self.assertTrue(BooleanExpression.evaluate('!false', {}))
- self.assertTrue(BooleanExpression.evaluate(' ((!((false) )) ) ', {}))
- self.assertTrue(BooleanExpression.evaluate('true && (true && (true))', {}))
- self.assertTrue(BooleanExpression.evaluate('!false && !false && !! !false', {}))
- self.assertTrue(BooleanExpression.evaluate('false && false || true', {}))
- self.assertTrue(BooleanExpression.evaluate('(false && false) || true', {}))
- self.assertFalse(BooleanExpression.evaluate('false && (false || true)', {}))
- # Evaluate boolean expression `expr`.
- # Fail if it does not throw a ValueError containing the text `error`.
- def checkException(self, expr, error):
- try:
- BooleanExpression.evaluate(expr, {})
- self.fail("expression %r didn't cause an exception" % expr)
- except ValueError as e:
- if -1 == str(e).find(error):
- self.fail(("expression %r caused the wrong ValueError\n" +
- "actual error was:\n%s\n" +
- "expected error was:\n%s\n") % (expr, e, error))
- except BaseException as e:
- self.fail(("expression %r caused the wrong exception; actual " +
- "exception was: \n%r") % (expr, e))
- def test_errors(self):
- self.checkException("ba#d",
- "couldn't parse text: '#d'\n" +
- "in expression: 'ba#d'")
- self.checkException("true and true",
- "expected: <end of expression>\n" +
- "have: 'and'\n" +
- "in expression: 'true and true'")
- self.checkException("|| true",
- "expected: '!' or '(' or identifier\n" +
- "have: '||'\n" +
- "in expression: '|| true'")
- self.checkException("true &&",
- "expected: '!' or '(' or identifier\n" +
- "have: <end of expression>\n" +
- "in expression: 'true &&'")
- self.checkException("",
- "expected: '!' or '(' or identifier\n" +
- "have: <end of expression>\n" +
- "in expression: ''")
- self.checkException("*",
- "couldn't parse text: '*'\n" +
- "in expression: '*'")
- self.checkException("no wait stop",
- "expected: <end of expression>\n" +
- "have: 'wait'\n" +
- "in expression: 'no wait stop'")
- self.checkException("no-$-please",
- "couldn't parse text: '$-please'\n" +
- "in expression: 'no-$-please'")
- self.checkException("(((true && true) || true)",
- "expected: ')'\n" +
- "have: <end of expression>\n" +
- "in expression: '(((true && true) || true)'")
- self.checkException("true (true)",
- "expected: <end of expression>\n" +
- "have: '('\n" +
- "in expression: 'true (true)'")
- self.checkException("( )",
- "expected: '!' or '(' or identifier\n" +
- "have: ')'\n" +
- "in expression: '( )'")
- if __name__ == '__main__':
- unittest.main()
|