--- /dev/null
+#!/usr/bin/env python
+
+'''
+Python Bugzilla Advanced Query
+
+Dumb recursive descent parser for bugzilla's advanced queries. Input is
+bugzilla query described as expression with hopefully friendly syntax. Output
+is a string with bugzilla query parameters which can be used in HTTP and REST
+API requests.
+
+Input examples:
+'classification == "Red Hat" && product == "Red Hat Enterprise Linux 7" && component == kernel && (status == ASSIGNED || status == POST || status == NEW) && flags =~ .*rhel-7\.7\.z.* &&& flags !~ .*rhel-7\.[0-9]+\.0'
+'classification == "Red Hat" && product == "Red Hat Enterprise Linux 8" && component == kernel && (status == ASSIGNED || status == POST || status == NEW) && ztr == 8.1.0 && (itr == --- || itr isempty NONE)'
+'(!(!(status == ASSIGNED) && !(status == POST) && !(status == NEW)) && (assignee == fhrbata@redhat.com || assignee == "dhoward@redhat.com")) || ((status == ON_QE || status == VERIFIED) && assignee == fhrbata@redhat.com)'
+
+
+precedence operator description associativity
+-----------------------------------------------------------------------------------
+1 ==, =~, regexp, ... Bugzilla operators None
+ see TokenMap for
+ full list
+-----------------------------------------------------------------------------------
+2 () Grouping operator
+ !() Negate grouping operator None
+-----------------------------------------------------------------------------------
+3 &&& Field logical AND aka match Left-to-right
+ all against the same field
+-----------------------------------------------------------------------------------
+4 && Logical AND aka match all Left-to-right
+-----------------------------------------------------------------------------------
+5 || Logical OR aka match any Left-to-right
+-----------------------------------------------------------------------------------
+
+LL1 in pseudo BNF
+
+<exp> ::= <subexp> <orexp>
+<orexp> ::= "||" <subexp> <orexp> | ""
+<subexp> ::= <subsubexp> <andexp>
+<andexp> ::= "&&" <subsubexp> <andexp> | ""
+<subsubexp> ::= <term> <fandexp>
+<fandexp> ::= "&&&" <term> <fandexp> | ""
+<term> ::= "!" "(" <exp> ")" | "(" <exp> ")" | <var> <op> <value>
+<var> ::= "$vars"
+<op> ::= "$ops"
+<value> ::= "$str"
+
+nonterminal first follow
+<exp> "!", "(", "$var" ")", $
+<orexp> "||", e ")", $
+<subexp> "!", "(", "$var" ")", "||", $
+<andexp> "&&",e ")", "||", $
+<subsubexp> "!", "(", "$var" ")", "&&", "||", $
+<fandexp> "&&&", e ")", "&&", "||", $
+<term> "!", "(", "$var" ")", "&&&", "&&", "||", $
+<var> "$var" "$op"
+<op> "$op" "$str"
+<value> "$str" ")", "&&&", "&&", "||", $
+
+first+
+<exp> -> <subexp> "!", "(", "$var"
+<orexp> -> "||" "||"
+<orexp> -> e e, ")", $
+<subexp> -> <subsubexp> "!", "(", "$var"
+<andexp> -> "&&" "&&"
+<andexp> -> e e, ")", "||", $
+<subsubexp> -> <term> "!", "(", "$var"
+<fandexp> -> "&&&" "&&&"
+<fandexp> -> e e, ")", "&&", "||", $
+<term> -> "!" "!"
+<term> -> "(" "("
+<term> -> <var> "$vars"
+
+e - empty string
+$ - end of file/input
+$vars - bugzilla variable/filed
+ e.g. "cf_zstream_target_release", "bug_id", "alias", ...
+ for all values see TokenMap
+$ops - bugzilla operator
+ e.g. "==", "!=", "anywordssubstr"
+ for all values see TokenMap
+$str - string, may be quoted if necessary
+'''
+
+import sys
+from enum import Enum
+
+class TokenType(Enum):
+ UNKNOWN = 0
+ AND = 1
+ FAND = 2
+ OR = 3
+ NOT = 4
+ LPAR = 5
+ RPAR = 6
+ VAR = 7
+ OPERATOR = 8
+ VALUE = 9
+ EOF = 10
+
+class TokenIdx:
+ TYPE = 0 # token type
+ STR = 1 # bugzilla field or operator string
+ DOC = 2 # bugzilla field or operator docs
+
+TokenMap = {
+ "&&": (TokenType.AND, "", ""),
+ "&&&": (TokenType.FAND, "", ""),
+ "||": (TokenType.OR, "", ""),
+ "!": (TokenType.NOT, "", ""),
+ "(": (TokenType.LPAR, "", ""),
+ ")": (TokenType.RPAR, "", ""),
+
+ # Bugzilla operators
+ # "operator accepted in expression": (token type, bugzilla operator used in query, description)
+ "equals": (TokenType.OPERATOR, "equals", "Is equal to"),
+ "==": (TokenType.OPERATOR, "equals", "Is equal to"),
+ "notequals": (TokenType.OPERATOR, "notequals", "Is not equal to"),
+ "!=": (TokenType.OPERATOR, "notequals", "Is not equal to"),
+ "anyexact": (TokenType.OPERATOR, "anyexact", "Is equal to any of the strings"),
+ "substring": (TokenType.OPERATOR, "substring", "Contains the string"),
+ "casesubstring": (TokenType.OPERATOR, "casesubstring", "Contains the string (exact case)"),
+ "notsubstring": (TokenType.OPERATOR, "notsubstring", "Does not contains the string"),
+ "anywordssubstr": (TokenType.OPERATOR, "anywordssubstr", "Contains any of the strings"),
+ "allwordssubstr": (TokenType.OPERATOR, "allwordssubstr", "Contains all of the strings"),
+ "nowordssubstr": (TokenType.OPERATOR, "nowordssubstr", "Contains none of the strings"),
+ "regexp": (TokenType.OPERATOR, "regexp", "Matches regular exression"),
+ "=~": (TokenType.OPERATOR, "regexp", "Matches regular exression"),
+ "notregexp": (TokenType.OPERATOR, "notregexp", "Does not match regular exression"),
+ "!~": (TokenType.OPERATOR, "notregexp", "Does not match regular exression"),
+ "lessthan": (TokenType.OPERATOR, "lessthan", "Is less than"),
+ "<": (TokenType.OPERATOR, "lessthan", "Is less than"),
+ "lessthaneq": (TokenType.OPERATOR, "lessthaneq", "Is less than or equal to"),
+ "<=": (TokenType.OPERATOR, "lessthaneq", "Is less than or equal to"),
+ "greaterthan": (TokenType.OPERATOR, "greaterthan", "Is greater than"),
+ ">": (TokenType.OPERATOR, "greaterthan", "Is greater than"),
+ "greaterthaneq": (TokenType.OPERATOR, "greaterthaneq", "Is greater than or equal to"),
+ ">=": (TokenType.OPERATOR, "greaterthaneq", "Is greater than or equal to"),
+ "anywords": (TokenType.OPERATOR, "anywords", "Contains any of the words"),
+ "allwords": (TokenType.OPERATOR, "allwords", "Contains all of the words"),
+ "nowords": (TokenType.OPERATOR, "nowords", "Contains none of the words"),
+ "changedbefore": (TokenType.OPERATOR, "changedbefore", "Changed before"),
+ "changedafter": (TokenType.OPERATOR, "changedafter", "Changed after"),
+ "changedfrom": (TokenType.OPERATOR, "changedfrom", "Changed from"),
+ "changedto": (TokenType.OPERATOR, "changedto", "Changed to"),
+ "changedby": (TokenType.OPERATOR, "changedby", "Changed by"),
+ "matches": (TokenType.OPERATOR, "matches", "Matches"),
+ "notmatches": (TokenType.OPERATOR, "notmatches", "Does not match"),
+ "isempty": (TokenType.OPERATOR, "isempty", "Is empty"),
+ "isnotempty": (TokenType.OPERATOR, "isnotempty", "Is not empty"),
+ "listofbugs": (TokenType.OPERATOR, "listofbugs", "In the list of bugs"),
+
+ # Bugzilla fields/variables
+ # "filed accepted in expression": (token type, bugzilla filed used in query, description)
+ "percentage_complete": (TokenType.VAR, "percentage_complete", "%Complete"),
+ "alias": (TokenType.VAR, "alias", "Alias"),
+ "cf_approved_release": (TokenType.VAR, "cf_approved_release", "Approved Release"),
+ "assigned_to": (TokenType.VAR, "assigned_to", "Assignee"),
+ "assignee": (TokenType.VAR, "assigned_to", "Assignee"),
+ "owner": (TokenType.VAR, "assigned_to", "Assignee"),
+ "assigned_to_realname": (TokenType.VAR, "assigned_to_realname", "Assignee Real Name"),
+ "assigned_to_realname": (TokenType.VAR, "assigned_to_realname", "Assignee Real Name"),
+ "attachmentdata": (TokenType.VAR, "attachmentdata", "Attachment data"),
+ "attachdata": (TokenType.VAR, "attachdata", "Attachment data"),
+ "attachmentdesc": (TokenType.VAR, "attachmentdesc", "Attachment description"),
+ "attachment": (TokenType.VAR, "attachment", "Attachment description"),
+ "attachdesc": (TokenType.VAR, "attachdesc", "Attachment description"),
+ "attachmentmimetype": (TokenType.VAR, "attachmentmimetype", "Attachment mime type"),
+ "attachmimetype": (TokenType.VAR, "attachmimetype", "Attachment mime type"),
+ "blocked": (TokenType.VAR, "blocked", "Blocks"),
+ "bug_id": (TokenType.VAR, "bug_id", "Bug ID"),
+ "bug": (TokenType.VAR, "bug_id", "Bug ID"),
+ "cf_build_id": (TokenType.VAR, "cf_build_id", "Build ID"),
+ "cf_category": (TokenType.VAR, "cf_category", "Category"),
+ "cc": (TokenType.VAR, "cc", "CC"),
+ "delta_ts": (TokenType.VAR, "delta_ts", "Changed"),
+ "classification": (TokenType.VAR, "classification", "Classification"),
+ "cf_clone_of": (TokenType.VAR, "cf_clone_of", "Clone Of"),
+ "cf_epm_cdp": (TokenType.VAR, "cf_epm_cdp", "Close Duplicate Candidate"),
+ "cf_cloudforms_team": (TokenType.VAR, "cf_cloudforms_team", "Cloudforms Team"),
+ "description": (TokenType.VAR, "description", "Comment"),
+ "longdesc": (TokenType.VAR, "longdesc", "Comment"),
+ "comment": (TokenType.VAR, "comment", "Comment"),
+ "comment_tag": (TokenType.VAR, "comment_tag", "Comment Tag"),
+ "commenter": (TokenType.VAR, "commenter", "Commenter"),
+ "cf_compliance_control_group": (TokenType.VAR, "cf_compliance_control_group", "Compliance Control Group"),
+ "cf_compliance_level": (TokenType.VAR, "cf_compliance_level", "Compliance Level"),
+ "component": (TokenType.VAR, "component", "Component"),
+ "content": (TokenType.VAR, "content", "Content"),
+ "creation_ts": (TokenType.VAR, "creation_ts", "Creation date"),
+ "cf_crm": (TokenType.VAR, "cf_crm", "CRM"),
+ "cf_deadline": (TokenType.VAR, "cf_deadline", "Current Deadline"),
+ "cf_deadline_type": (TokenType.VAR, "cf_deadline_type", "Current Deadline Type"),
+ "cf_cust_facing": (TokenType.VAR, "cf_cust_facing", "Customer Escalation"),
+ "days_elapsed": (TokenType.VAR, "days_elapsed", "Days since bug changed"),
+ "deadline": (TokenType.VAR, "deadline", "Deadline"),
+ "dependent_products": (TokenType.VAR, "dependent_products", "Dependent Products"),
+ "dependson": (TokenType.VAR, "dependson", "Depends On"),
+ "cf_conditional_nak": (TokenType.VAR, "cf_conditional_nak", "Devel Conditional NAK"),
+ "cf_devel_whiteboard": (TokenType.VAR, "cf_devel_whiteboard", "Devel Whiteboard"),
+ "cf_release_notes": (TokenType.VAR, "cf_release_notes", "Doc Text"),
+ "cf_doc_type": (TokenType.VAR, "cf_doc_type", "Doc Type"),
+ "docs_contact": (TokenType.VAR, "docs_contact", "Docs Contact"),
+ "docs_contact_realname": (TokenType.VAR, "docs_contact_realname", "Docs Contact Real Name"),
+ "cf_docs_score": (TokenType.VAR, "cf_docs_score", "Docs Score"),
+ "cf_documentation_action": (TokenType.VAR, "cf_documentation_action", "Documentation"),
+ "cf_environment": (TokenType.VAR, "cf_environment", "Environment"),
+ "cf_epm_pri": (TokenType.VAR, "cf_epm_pri", "EPM Priority"),
+ "everconfirmed": (TokenType.VAR, "everconfirmed", "Ever confirmed"),
+ "extra_components": (TokenType.VAR, "extra_components", "Extra Components"),
+ "extra_versions": (TokenType.VAR, "extra_versions", "Extra Versions"),
+ "cf_fixed_in": (TokenType.VAR, "cf_fixed_in", "Fixed In Version"),
+ "requestee": (TokenType.VAR, "requestees.login_name", "Flag Requestee"),
+ "setter": (TokenType.VAR, "setters.login_name", "Flag Setter"),
+ "flagtypes.name": (TokenType.VAR, "flagtypes.name", "Flags"),
+ "flags": (TokenType.VAR, "flagtypes.name", "Flags"),
+ "group": (TokenType.VAR, "group", "Group"),
+ "platform": (TokenType.VAR, "platform", "Hardware"),
+ "cf_srtnotes": (TokenType.VAR, "cf_srtnotes", "Internal SRT notes"),
+ "cf_internal_target_milestone": (TokenType.VAR, "cf_internal_target_milestone", "Internal Target Milestone"),
+ "cf_internal_target_release": (TokenType.VAR, "cf_internal_target_release", "Internal Target Release"),
+ "itr": (TokenType.VAR, "cf_internal_target_release", "Internal Target Release"),
+ "cf_internal_whiteboard": (TokenType.VAR, "cf_internal_whiteboard", "Internal Whiteboard"),
+ "keywords": (TokenType.VAR, "keywords", "Keywords"),
+ "kw": (TokenType.VAR, "kw", "Keywords"),
+ "cf_last_closed": (TokenType.VAR, "cf_last_closed", "Last Closed"),
+ "last_visit_ts": (TokenType.VAR, "last_visit_ts", "Last Visit"),
+ "cf_mount_type": (TokenType.VAR, "cf_mount_type", "Mount Type"),
+ "cf_epm_phd": (TokenType.VAR, "cf_epm_phd", "Onsite Hardware Date"),
+ "estimated_time": (TokenType.VAR, "estimated_time", "Orig. Est."),
+ "op_sys": (TokenType.VAR, "op_sys", "OS"),
+ "os": (TokenType.VAR, "os", "OS"),
+ "cf_ovirt_team": (TokenType.VAR, "cf_ovirt_team", "oVirt Team"),
+ "cf_partner": (TokenType.VAR, "cf_partner", "Partner"),
+ "cf_epm_prf_state": (TokenType.VAR, "cf_epm_prf_state", "Partner Requirement State"),
+ "tag": (TokenType.VAR, "tag", "Personal Tags"),
+ "cf_pgm_internal": (TokenType.VAR, "cf_pgm_internal", "PgM Internal"),
+ "cf_pm_score": (TokenType.VAR, "cf_pm_score", "PM Score"),
+ "remaining_time": (TokenType.VAR, "remaining_time", "Points Left"),
+ "work_time": (TokenType.VAR, "work_time", "Points Worked"),
+ "pool": (TokenType.VAR, "pool", "Pool"),
+ "priority": (TokenType.VAR, "priority", "Priority"),
+ "product": (TokenType.VAR, "product", "Product"),
+ "cf_epm_ptl": (TokenType.VAR, "cf_epm_ptl", "Public Target Launch Date"),
+ "qa_contact": (TokenType.VAR, "qa_contact", "QA Contact"),
+ "qa_contact_realname": (TokenType.VAR, "qa_contact_realname", "QA Contact Real Name"),
+ "cf_qa_whiteboard": (TokenType.VAR, "cf_qa_whiteboard", "QA Whiteboard"),
+ "cf_qe_conditional_nak": (TokenType.VAR, "cf_qe_conditional_nak", "QE Conditional NAK"),
+ "cf_regression_status": (TokenType.VAR, "cf_regression_status", "Regression"),
+ "reporter": (TokenType.VAR, "reporter", "Reporter"),
+ "reporter_realname": (TokenType.VAR, "reporter_realname", "Reporter Real Name"),
+ "resolution": (TokenType.VAR, "resolution", "Resolution"),
+ "cf_atomic": (TokenType.VAR, "cf_atomic", "RHEL 7.3 requirements from Atomic Host"),
+ "rh_rule": (TokenType.VAR, "rh_rule", "Rule Engine Rule"),
+ "see_also": (TokenType.VAR, "see_also", "See Also"),
+ "severity": (TokenType.VAR, "severity", "Severity"),
+ "bug_status": (TokenType.VAR, "bug_status", "Status"),
+ "status": (TokenType.VAR, "bug_status", "Status"),
+ "cf_story_points": (TokenType.VAR, "cf_story_points", "Story Points"),
+ "rh_sub_components": (TokenType.VAR, "rh_sub_components", "Sub Component"),
+ "short_desc": (TokenType.VAR, "short_desc", "Summary"),
+ "summary": (TokenType.VAR, "summary", "Summary"),
+ "target_milestone": (TokenType.VAR, "target_milestone", "Target Milestone"),
+ "milestone": (TokenType.VAR, "milestone", "Target Milestone"),
+ "target_release": (TokenType.VAR, "target_release", "Target Release"),
+ "cf_target_upstream_version": (TokenType.VAR, "cf_target_upstream_version", "Target Upstream Version"),
+ "owner_idle_time": (TokenType.VAR, "owner_idle_time", "Time Since Assignee Touched"),
+ "cf_type": (TokenType.VAR, "cf_type", "Type"),
+ "cf_epm_put": (TokenType.VAR, "cf_epm_put", "Upstream Kernel Target"),
+ "url": (TokenType.VAR, "url", "URL"),
+ "cf_verified": (TokenType.VAR, "cf_verified", "Verified"),
+ "cf_verified_branch": (TokenType.VAR, "cf_verified_branch", "Verified Versions"),
+ "version": (TokenType.VAR, "version", "Version"),
+ "view": (TokenType.VAR, "view", "view"),
+ "votes": (TokenType.VAR, "votes", "Votes"),
+ "whiteboard": (TokenType.VAR, "whiteboard", "Whiteboard"),
+ "sw": (TokenType.VAR, "sw", "Whiteboard"),
+ "cf_zstream_target_release": (TokenType.VAR, "cf_zstream_target_release", "ZStream Target Release"),
+ "ztr": (TokenType.VAR, "cf_zstream_target_release", "ZStream Target Release"),
+}
+
+class Scanner:
+
+ def __init__(self, query):
+ self.query = query
+ self.lexeme = ""
+ self.curr = 0
+ self.tokens = []
+ self.__get_tokens()
+
+ def peek(self):
+ if self.curr == len(self.tokens):
+ return None
+
+ token = self.tokens[self.curr]
+ return token
+
+ def next(self):
+ token = self.peek()
+ self.curr += 1
+ return token
+
+ def back(self):
+ if self.curr > 0:
+ self.curr -= 1
+
+ def top(self):
+ if len(self.tokens) == 0:
+ return None
+ else:
+ return self.tokens[len(self.tokens) - 1]
+
+ def __str__(self):
+ s = ""
+ for ttype, tvalue, tpos in self.tokens:
+ s += "{} {}: {}\n".format(tpos, ttype, tvalue)
+ return s
+
+ def __add_token(self, ttype, pos):
+ if not self.lexeme:
+ return
+
+ if ttype == TokenType.UNKNOWN:
+ if self.lexeme in TokenMap:
+ ttype = TokenMap[self.lexeme][TokenIdx.TYPE]
+ else:
+ ttype = TokenType.VALUE
+
+ self.tokens.append((ttype, self.lexeme, pos - len(self.lexeme)))
+
+ self.lexeme = ""
+
+ def __get_tokens(self):
+ quote = False
+ last = ""
+ pos = 0
+
+ for c in self.query:
+ if quote:
+ if c == '"':
+ if last == "\\":
+ self.lexeme = self.lexeme[:-1] + "\""
+ else:
+ self.__add_token(TokenType.VALUE, pos)
+ quote = False
+ else:
+ self.lexeme += c
+
+ elif c == '"':
+ self.__add_token(TokenType.UNKNOWN, pos)
+ quote = True
+
+ elif c.isspace():
+ self.__add_token(TokenType.UNKNOWN, pos)
+
+ elif c == '(':
+ self.__add_token(TokenType.UNKNOWN, pos)
+ self.lexeme = "("
+ self.__add_token(TokenType.LPAR, pos + 1)
+
+ elif c == ')':
+ self.__add_token(TokenType.UNKNOWN, pos)
+ self.lexeme = ")"
+ self.__add_token(TokenType.RPAR, pos + 1)
+
+ elif c == '!':
+ top_token = self.top()
+ if not top_token or top_token[TokenIdx.TYPE] != TokenType.VAR:
+ self.__add_token(TokenType.UNKNOWN, pos)
+ self.lexeme = "!"
+ self.__add_token(TokenType.NOT, pos + 1)
+ else:
+ self.lexeme += c
+
+ else:
+ self.lexeme += c
+
+ last = c
+ pos += 1
+
+ self.__add_token(TokenType.UNKNOWN, pos)
+
+ self.tokens.append((TokenType.EOF, "", pos))
+
+ if quote:
+ raise Exception("lexical error: unmatched quotation mark")
+
+class Parser:
+ def __init__(self, query):
+ self.code = ""
+ self.idx = 0
+ self.scanner = Scanner(query)
+ self.__parse()
+
+ def __str__(self):
+ return self.code
+
+ def __exp(self):
+ self.idx += 1
+ op_idx = self.idx # open parenthesis
+ depth = 0
+
+ self.__subexp()
+ self.__orexp(op_idx, depth)
+
+ def __orexp(self, op_idx, depth):
+ ttype, tvalue, tpos = self.scanner.next()
+
+ if ttype == TokenType.OR:
+ self.__subexp()
+ self.__orexp(op_idx, depth + 1)
+
+ if depth == 0:
+ self.code += "&f{0}=OP&j{0}=OR".format(op_idx)
+ self.idx += 1
+ self.code += "&f{0}=CP".format(self.idx)
+
+ elif ttype not in {TokenType.EOF, TokenType.RPAR}:
+ raise Exception("syntax error[{}]: expecting EOF or \")\", got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ else:
+ self.scanner.back()
+
+ def __subexp(self):
+ self.idx += 1
+ op_idx = self.idx # open parenthesis
+ depth = 0
+
+ self.__subsubexp()
+ self.__andexp(op_idx, depth)
+
+ def __andexp(self, op_idx, depth):
+ ttype, tvalue, tpos = self.scanner.next()
+
+ if ttype == TokenType.AND:
+ self.__subsubexp()
+ self.__andexp(op_idx, depth + 1)
+
+ if depth == 0:
+ self.code += "&f{0}=OP&j{0}=AND".format(op_idx)
+ self.idx += 1
+ self.code += "&f{0}=CP".format(self.idx)
+
+ elif ttype not in {TokenType.EOF, TokenType.RPAR, TokenType.OR}:
+ raise Exception("syntax error[{}]: expecting EOF, \"||\" or \")\", got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ else:
+ self.scanner.back()
+
+ def __subsubexp(self):
+ self.idx += 1
+ op_idx = self.idx # open parenthesis
+ depth = 0
+
+ self.__term()
+ self.__fandexp(op_idx, depth)
+
+ def __fandexp(self, op_idx, depth):
+ ttype, tvalue, tpos = self.scanner.next()
+
+ if ttype == TokenType.FAND:
+ self.__term()
+ self.__fandexp(op_idx, depth + 1)
+
+ if depth == 0:
+ self.code += "&f{0}=OP&j{0}=AND_G".format(op_idx)
+ self.idx += 1
+ self.code += "&f{0}=CP".format(self.idx)
+
+ elif ttype not in {TokenType.EOF, TokenType.RPAR, TokenType.OR, TokenType.AND}:
+ raise Exception("syntax error[{}]: expecting EOF, \"&&\", \"||\" or \")\", got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ else:
+ self.scanner.back()
+
+ def __term(self):
+ ttype, tvalue, tpos = self.scanner.next()
+
+ if ttype == TokenType.NOT:
+ ttype, tvalue, tpos = self.scanner.next()
+ if ttype != TokenType.LPAR:
+ raise Exception("syntax error[{}]: expecting \"(\", got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ self.idx += 1
+ self.code += "&f{0}=OP&n{0}=1".format(self.idx)
+
+ self.__exp()
+
+ self.idx += 1
+ self.code += "&f{0}=CP".format(self.idx)
+
+ ttype, tvalue, tpos = self.scanner.next()
+ if ttype != TokenType.RPAR:
+ raise Exception("syntax error[{}]: expecting \")\", got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ elif ttype == TokenType.LPAR:
+ self.__exp()
+
+ ttype, tvalue, tpos = self.scanner.next()
+ if ttype != TokenType.RPAR:
+ raise Exception("syntax error[{}]: expecting \")\", got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ elif ttype == TokenType.VAR:
+ self.scanner.back()
+
+ self.idx += 1
+
+ self.__var()
+ self.__op()
+ self.__value()
+
+ else:
+ raise Exception("syntax error[{}]: expecting variable, got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ def __var(self):
+ ttype, tvalue, tpos = self.scanner.next()
+ if ttype != TokenType.VAR:
+ raise Exception("syntax error[{}]: expecting variable, got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ self.code += "&f{0}={1}".format(self.idx, self.__encode(TokenMap[tvalue][TokenIdx.STR]))
+
+ def __op(self):
+ ttype, tvalue, tpos = self.scanner.next()
+ if ttype != TokenType.OPERATOR:
+ raise Exception("syntax error[{}]: expecting operator, got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ self.code += "&o{0}={1}".format(self.idx, self.__encode(TokenMap[tvalue][TokenIdx.STR]))
+
+ def __value(self):
+ ttype, tvalue, tpos = self.scanner.next()
+ if ttype != TokenType.VALUE:
+ raise Exception("syntax error[{}]: expecting value, got \"{}: {}\" instead".format(tpos, ttype, tvalue))
+
+ self.code += "&v{0}={1}".format(self.idx, self.__encode(tvalue))
+
+ def __parse(self):
+ self.__exp()
+
+ def __encode(self, src):
+ dst = ""
+
+ '''rfc2396 - 2.2. Reserved Characters'''
+ rmap = {';': "%3b", '/': "%2f", '?': "3f", ':': "%3a", '@': "%40", '&': "%26", '=': "%3d", '+': "%2b", '$': "%24", ',': "%2c"}
+
+ for c in src:
+ if c in rmap:
+ dst += rmap[c]
+ else:
+ dst += c
+
+ return dst
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("usage: pbqa.py 'expression'")
+ sys.exit(1)
+ try:
+ p = Parser(sys.argv[1])
+
+ print("Scanner output:\n{}".format(str(p.scanner)))
+ print("Parser output:\n{}\n".format(str(p)))
+ print("Bugzilla link:\nhttps://bugzilla.redhat.com/buglist.cgi?query_format=advanced{}".format(p))
+
+ except Exception as err:
+ print(err)