initial commit of Python Bugzilla Advanced Query
authorFrantisek Hrbata <frantisek@hrbata.com>
Wed, 6 Jan 2021 13:08:28 +0000 (14:08 +0100)
committerFrantisek Hrbata <frantisek@hrbata.com>
Wed, 6 Jan 2021 13:08:28 +0000 (14:08 +0100)
Signed-off-by: Frantisek Hrbata <frantisek@hrbata.com>
pbaq.py [new file with mode: 0755]

diff --git a/pbaq.py b/pbaq.py
new file mode 100755 (executable)
index 0000000..859789f
--- /dev/null
+++ b/pbaq.py
@@ -0,0 +1,564 @@
+#!/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)