From d91a1216228e56b30d2347e139091b38f7d97311 Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Wed, 6 Jan 2021 14:08:28 +0100 Subject: [PATCH] initial commit of Python Bugzilla Advanced Query Signed-off-by: Frantisek Hrbata --- pbaq.py | 564 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 564 insertions(+) create mode 100755 pbaq.py diff --git a/pbaq.py b/pbaq.py new file mode 100755 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 + + ::= + ::= "||" | "" + ::= + ::= "&&" | "" + ::= + ::= "&&&" | "" + ::= "!" "(" ")" | "(" ")" | + ::= "$vars" + ::= "$ops" + ::= "$str" + +nonterminal first follow + "!", "(", "$var" ")", $ + "||", e ")", $ + "!", "(", "$var" ")", "||", $ + "&&",e ")", "||", $ + "!", "(", "$var" ")", "&&", "||", $ + "&&&", e ")", "&&", "||", $ + "!", "(", "$var" ")", "&&&", "&&", "||", $ + "$var" "$op" + "$op" "$str" + "$str" ")", "&&&", "&&", "||", $ + +first+ + -> "!", "(", "$var" + -> "||" "||" + -> e e, ")", $ + -> "!", "(", "$var" + -> "&&" "&&" + -> e e, ")", "||", $ + -> "!", "(", "$var" + -> "&&&" "&&&" + -> e e, ")", "&&", "||", $ + -> "!" "!" + -> "(" "(" + -> "$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) -- 1.7.10.4