diff shelltools/query-pr/query.py @ 52:c0be30249ffe

Fix the argument handling.
author David A. Holland
date Sat, 02 Apr 2022 21:13:01 -0400
parents ef6d572c4e1e
children 40f64a96481f
line wrap: on
line diff
--- a/shelltools/query-pr/query.py	Sat Apr 02 18:14:40 2022 -0400
+++ b/shelltools/query-pr/query.py	Sat Apr 02 21:13:01 2022 -0400
@@ -4,9 +4,6 @@
 import argparse
 import psycopg2
 
-program_description = """
-Search for and retrieve problem reports.
-"""
 program_version = "@VERSION@"
 
 ############################################################
@@ -17,6 +14,20 @@
 outformat = "text"
 
 ############################################################
+# simple dump widget
+
+class Dumper:
+        def __init__(self, f):
+                self.f = f
+                self.indentation = 0
+        def indent(self):
+                self.indentation += 3
+        def unindent(self):
+                self.indentation -= 3
+        def write(self, msg):
+                self.f.write(" " * self.indentation + msg + "\n")
+
+############################################################
 # database field access
 
 #
@@ -667,62 +678,44 @@
 # end print_attachment
 
 ############################################################
-# AST for query
+# AST for input query
 
 class Invocation:
+        Q_TERM = 1
+        Q_SQL = 2
         class Query:
-                Q_TERM = 1  # XXX unused so far
-                Q_SQL = 2
-                Q_AND = 3
-                Q_OR = 4
-                Q_TSTRING = 5
-                Q_QSTRING = 6
-
                 def __init__(self, type):
                         self.type = type
-                def doterm(term):
-                        self = Query(Q_TERM)
-                        self.term = term
-                        return self
-                def dosql(s):
-                        self = Query(Q_SQL)
-                        self.sql = s
-                        return self
-                def doand(qs):
-                        self = Query(Q_AND)
-                        self.args = qs
-                        return self
-                def door(qs):
-                        self = Query(Q_OR)
-                        self.args = qs
-                        return self
-                # query term string
-                def dotstring(q):
-                        self = Query(Q_TSTRING)
-                        self.string = q
-                        return self
-                # whole query string
-                def doqstring(q):
-                        self = Query(Q_QSTRING)
-                        self.string = q
-                        return self
-        # end class Query
+                def dump(self, d):
+                        if self.type == Invocation.Q_TERM:
+                                d.write("query.term({})".format(self.term))
+                        else:
+                                d.write("query.sql({})".format(self.sql))
+        def mkterm(term):
+                self = Invocation.Query(Invocation.Q_TERM)
+                self.term = term
+                return self
+        def mksql(s):
+                self = Invocation.Query(Invocation.Q_SQL)
+                self.sql = s
+                return self
 
         class Order:
                 def __init__(self, field, rev = False):
                         self.field = field
                         self.rev = rev
-                def dooldest(ign):
-                        return Order("number")
-                def donewest(ign):
-                        return Order("number", True)
-                def dostaleness(ign):
-                        return Order("modified_date", True)
-                def dofield(field):
-                        return Order(field)
-                def dorevfield(field):
-                        return Order(field, True)
-        # end class Order
+                def dump(self, d):
+                        d.write("order({}, {})".format(self.field, self.rev))
+        def mkoldest():
+                return Invocation.Order("number")
+        def mknewest():
+                return Invocation.Order("number", True)
+        def mkstaleness():
+                return Invocation.Order("modified_date", True)
+        def mkfield(field):
+                return Invocation.Order(field)
+        def mkrevfield(field):
+                return Invocation.Order(field, True)
 
         class Search:
                 def __init__(self, qs, openonly, publiconly, os):
@@ -730,59 +723,98 @@
                         self.openonly = openonly
                         self.publiconly = publiconly
                         self.orders = os
-        # end class Search
+                def dump(self, d):
+                        d.write("search({}, {})".format(
+                                "open" if self.openonly else "closed",
+                                "public" if self.publiconly else "privileged"))
+                        d.indent()
+                        d.write("queries")
+                        d.indent()
+                        for query in self.queries:
+                                query.dump(d)
+                        d.unindent()
+                        d.write("orders")
+                        d.indent()
+                        for order in self.orders:
+                                order.dump(d)
+                        d.unindent()
+                        d.unindent()
 
+        S_PR = 1
+        S_MESSAGE = 2
+        S_ATTACHMENT = 3
         class Selection:
-                S_PR = 1
-                S_MESSAGE = 2
-                S_ATTACHMENT = 3
-
                 def __init__(self, type):
                         self.type = type
-                def dopr(output, outformat):
-                        self = Selection(S_PR)
-                        self.output = output
-                        self.outformat = outformat
-                        return self
-                def domessage(arg):
-                        self = Selection(S_MESSAGE)
-                        self.message = arg
-                        return self
-                def doattachment(arg):
-                        self = Selection(S_ATTACHMENT)
-                        self.attachment = arg
-                        return self
-        # end class Selection
+                def dump(self, d):
+                        if self.type == Invocation.S_PR:
+                                d.write("selection.pr({}, {})".format(
+                                        self.output, self.outformat))
+                        elif self.type == Invocation.S_MESSAGE:
+                                d.write("selection.message({})".format(
+                                        self.message))
+                        else:
+                                d.write("selection.attachment({})".format(
+                                        self.attachment))
+        def mkpr(output, outformat):
+                self = Invocation.Selection(Invocation.S_PR)
+                self.output = output
+                self.outformat = outformat
+                return self
+        def mkmessage(arg):
+                self = Invocation.Selection(Invocation.S_MESSAGE)
+                self.message = arg
+                return self
+        def mkattachment(arg):
+                self = Invocation.Selection(Invocation.S_ATTACHMENT)
+                self.attachment = arg
+                return self
 
+        OP_FIELDS = 1
+        OP_SHOW = 2
+        OP_RANGE = 3
+        OP_SEARCH = 4
         class Op:
-                # operation codes
-                OP_FIELDS = 1
-                OP_SHOW = 2
-                OP_RANGE = 3
-                OP_SEARCH = 4
-
                 def __init__(self, type):
                         self.type = type
-
-                def dofields():
-                        return Op(OP_FIELDS)
-                def doshow(field):
-                        self = Op(OP_SHOW)
-                        self.field = field
-                        return self
-                def dorange(field):
-                        self = Op(OP_RANGE)
-                        self.field = field
-                        return self
-                def dosearch(s, sels):
-                        self = Op(OP_SEARCH)
-                        self.search = s
-                        self.sels = sels
-                        return self
-        # end class Op
+                def dump(self, d):
+                        if self.type == Invocation.OP_FIELDS:
+                                d.write("op.fields")
+                        elif self.type == Invocation.OP_SHOW:
+                                d.write("op.show({})".format(self.field))
+                        elif self.type == Invocation.OP_RANGE:
+                                d.write("op.range({})".format(self.field))
+                        else:
+                                d.write("op.search:")
+                                d.indent()
+                                self.search.dump(d)
+                                for sel in self.sels:
+                                        sel.dump(d)
+                                d.unindent()
+        def mkfields():
+                return Invocation.Op(Invocation.OP_FIELDS)
+        def mkshow(field):
+                self = Invocation.Op(Invocation.OP_SHOW)
+                self.field = field
+                return self
+        def mkrange(field):
+                self = Invocation.Op(Invocation.OP_RANGE)
+                self.field = field
+                return self
+        def mksearch(s, sels):
+                self = Invocation.Op(Invocation.OP_SEARCH)
+                self.search = s
+                self.sels = sels
+                return self
 
         def __init__(self, ops):
                 self.ops = ops
+        def dump(self, d):
+                d.write("invocation: {} ops".format(len(self.ops)))
+                d.indent()
+                for op in self.ops:
+                        op.dump(d)
+                d.unindent()
 # end class Invocation
 
 ############################################################
@@ -969,183 +1001,341 @@
 # arg handling
 
 #
-# I swear, all getopt interfaces suck.
-#
-# Provide an argparse action for constructing something out of the
-# argument value and appending that somewhere, since it can't do this
-# on its own.
-#
-# The way you use this:
-#    p.add_argument("--foo", action = CtorAppend, dest = 'mylist',
-#                   const = ctor)
-# where ctor is a function taking the option arg(s) and producing
-# a value to append to mylist.
-#
-# This itself is mangy even for what it is -- it seems like we should
-# be able to pass action=CtorAppend(ctor), but since it has to be a
-# class that doesn't work... unless you make a new class for every
-# ctor you want to use, which seems completely insane.
+# I swear, all getopt interfaces suck. You have to write your own to
+# not go mad.
 #
-class CtorAppend(argparse.Action):
-        def __call__(self, parser, namespace, values, option_string=None):
-                items = getattr(namespace, self.dest)
-                item = self.const(values)
-                items.append(item)
-                setattr(namespace, self.dest, items)
 
-def getargs():
-        p = argparse.ArgumentParser(program_description)
-
-        # note: -h/--help is built in by default
-        p.add_argument("-v", "--version",
-                        action='version', version=program_version,
-                        help="Print program version and exit")
-
-        p.add_argument("--show", nargs=1,
-                        action=CtorAppend, dest='ops',
-                        const=Invocation.Op.doshow,
-                        help="Show description of field")
-        p.add_argument("--range", nargs=1,
-                        action=CtorAppend, dest='ops',
-                        const=Invocation.Op.dorange,
-                        help="Show range of extant values for field")
+# Usage.
+def usage():
+        sys.stderr.write("""
+query-pr: search for and retrieve problem reports
+usage: query-pr [options] [searchterms] Query the database.
+       query-pr [options] --sql QUERY   Execute QUERY as the search.
+       query-pr [options] -s QUERY      Same as --sql.
+       query-pr --fields                List database fields.
+       query-pr --show FIELD            Print info about database field.
+       query-pr --range FIELD           Print extant range for database field.
+       query-pr --help / -h             Print this message.
+       query-pr --version / -v          Print version and exit.
+options:
+       --search-string STRING           Forcibly treat STRING as a search term.
+       --message NUM / -m NUM           Print a message by its ID number.
+       --attachment NUM / -a NUM        Print an attachment by its ID number.
+       --paranoid                       Deny unsafe settings.
+filter options:
+       --open                           Exclude closed PRs. (default)
+       --closed                         Include closed PRs.
+       --public                         Exclude confidential PRs.
+       --privileged                     Include confidential PRs. (default)
+sort options:
+       --oldest                         Sort with oldest PRs first. (default)
+       --newest                         Sort with newest PRs first.
+       --staleness                      Sort by last modification time.
+       --orderby FIELD                  Sort by specific field.
+output options:
+       --raw / -r                       Print raw SQL output.
+       --list / -l                      Print in list form.
+       --headers                        Print headers only.
+       --meta / --metadata              Print all metadata.
+       --full / -f                      Print entire PR.
+       --text                           Print text. (default)
+       --csv                            Print CSV.
+       --xml                            Print XML.
+       --json                           Print JSON.
+       --rdf                            Print RDF.
+       --rdflike                        Print RDF-like text.
+search terms:
+       NUM                              Single PR by number.
+       NUM-[NUM]                        Range of PRs by number.
+       TEXT                             Search string
+       FIELD:TEXT                       Search string for a particular field.
+       FIELD:                           Use the field's default search string.
+derived fields:
+       arrived-before:DATE              Arrival date before DATE.
+       arrived-after:DATE               Arrival date after DATE.
+       closed-before:DATE               Close date before DATE.
+       closed-after:DATE                Close date after DATE, or none.
+       last-modified-before:DATE        Last modified before DATE.
+       last-modified-after:DATE         Last modified after DATE.
+       stale:TIME                       Last modified at least TIME ago.
+Explicit SQL queries should return lists of PR numbers (only).
+""")
 
-        p.add_argument("--search", nargs=1,
-                        action=CtorAppend, dest='queries',
-                        const=Invocation.Query.doqstring,
-                        help="Force string to be read as a search string")
-        p.add_argument("-s", "--sql", nargs=1,
-                        action=CtorAppend, dest='queries',
-                        const=Invocation.Query.dosql,
-                        help="Supply explicit sql query as search")
+# Widget to hold argv and allow peeling args off one at a time.
+class ArgHolder:
+        def __init__(self, argv):
+                self.argc = len(argv)
+                self.argv = argv
+                self.pos = 1
+        def next(self):
+                if self.pos >= self.argc:
+                        return None
+                ret = self.argv[self.pos]
+                self.pos += 1
+                return ret
+        def getarg(self, opt):
+                ret = self.next()
+                if ret is None:
+                        msg = "Option {} requires an argument\n".format(opt)
+                        sys.stderr.write(msg)
+                        exit(1)
+                return ret
+
+# Read the argument list and convert it into an Invocation.
+def getargs(argv):
+        # Results
+        ops = []
+        orders = []
+        queries = []
+        selections = []
+        output = "LIST"
+        outformat = "TEXT"
+        openonly = True
+        publiconly = False
+        paranoid = False
+        nomoreoptions = False
+
+        #
+        # Functions for the options
+        #
+
+        def do_paranoid():
+                nonlocal paranoid, publiconly
+                paranoid = True
+                publiconly = True
 
-        p.add_argument("--open",
-                        action='store_const', dest='openonly', const="True",
-                        help="Exclude closed PRs (default)")
-        p.add_argument("--closed",
-                        action='store_const', dest='openonly', const="False",
-                        help="Include closed PRs in search")
-        p.add_argument("--public",
-                        action='store_const', dest='publiconly', const="True",
-                        help="Exclude confidential PRs")
-        p.add_argument("--privileged",
-                        action='store_const', dest='publiconly', const="False",
-                        help="Allow confidential PRs (default)")
+        def do_help():
+                usage()
+                exit(0)
+        def do_version():
+                msg = "query-pr {}\n".format(program_version)
+                sys.stdout.write(msg)
+                exit(0)
+        def do_fields():
+                ops.append(Invocation.mkfields())
+        def do_show(field):
+                ops.append(Invocation.mkshow(field))
+        def do_range(field):
+                ops.append(Invocation.mkrange(field))
+
+        def do_search(term):
+                queries.append(Invocation.mkterm(term))
+        def do_sql(text):
+                assert(not paranoid)
+                queries.append(Invocation.mksql(text))
 
-        p.add_argument("--oldest",
-                        action=CtorAppend, dest='orders',
-                        const=Invocation.Order.dooldest,
-                        help="Sort output with oldest PRs first")
-        p.add_argument("--newest",
-                        action=CtorAppend, dest='orders',
-                        const=Invocation.Order.donewest,
-                        help="Sort output with newest PRs first")
-        p.add_argument("--staleness",
-                        action=CtorAppend, dest='orders',
-                        const=Invocation.Order.dostaleness,
-                        help="Sort output by time since last modification")
-        p.add_argument("--orderby", nargs=1,
-                        action=CtorAppend, dest='orders',
-                        const=Invocation.Order.dofield,
-                        help="Sort output by specific field")
-        p.add_argument("--revorderby", nargs=1,
-                        action=CtorAppend, dest='orders',
-                        const=Invocation.Order.dorevfield,
-                        help="Sort output by specific field, reversed")
+        def do_open():
+                nonlocal openonly
+                openonly = True
+        def do_closed():
+                nonlocal openonly
+                openonly = False
+        def do_public():
+                nonlocal publiconly
+                publiconly = True
+        def do_privileged():
+                nonlocal publiconly
+                assert(not paranoid)
+                publiconly = False
+
+        def do_oldest():
+                orders.append(Invocation.mkoldest())
+        def do_newest():
+                orders.append(Invocation.mknewest())
+        def do_staleness():
+                orders.append(Invocation.mkstaleness())
+        def do_orderby(field):
+                orders.append(Invocation.mkfield(field))
+        def do_revorderby(field):
+                orders.append(Invocation.mkrevfield(field))
+
+        def do_message(n):
+                selections.append(Invocation.mkmessage(n))
+        def do_attachment(n):
+                selections.append(Invocation.mkattachment(n))
 
-        p.add_argument("-m", "--message", nargs=1,
-                        action=CtorAppend, dest='selections',
-                        const=Invocation.Selection.domessage,
-                        help="Print selected message (single PR only)")
-        p.add_argument("-a", "--attachment", nargs=1,
-                        action=CtorAppend, dest='selections',
-                        const=Invocation.Selection.doattachment,
-                        help="Print selected attachment (single PR only)")
+        def do_raw():
+                nonlocal output
+                output = "RAW"
+        def do_list():
+                nonlocal output
+                output = "LIST"
+        def do_headers():
+                nonlocal output
+                output = "HEADERS"
+        def do_meta():
+                nonlocal output
+                output = "META"
+        def do_full():
+                nonlocal output
+                output = "FULL"
 
-        p.add_argument("-r", "--raw",
-                        action = 'store_const', const="RAW",
-                        dest = 'output',
-                        help="Print exactly what the database returns")
-        p.add_argument("-l", "--list",
-                        action = 'store_const', const="LIST",
-                        dest = 'output',
-                        help="Print in list form (default)")
-        p.add_argument("--headers",
-                        action = 'store_const', const="HEADERS",
-                        dest = 'output',
-                        help="Print header information only")
-        p.add_argument("--meta",
-                        action = 'store_const', const="META",
-                        dest = 'output',
-                        help="Print all metadata")
-        p.add_argument("--metadata",
-                        action = 'store_const', const="META",
-                        dest = 'output')
-        p.add_argument("-f", "--full",
-                        action = 'store_const', const="FULL",
-                        dest = 'output',
-                        help="Print everything")
+        def do_text():
+                nonlocal outformat
+                outformat = "TEXT"
+        def do_csv():
+                nonlocal outformat
+                outformat = "CSV"
+        def do_xml():
+                nonlocal outformat
+                outformat = "XML"
+        def do_json():
+                nonlocal outformat
+                outformat = "JSON"
+        def do_rdf():
+                nonlocal outformat
+                outformat = "RDF"
+        def do_rdflike():
+                nonlocal outformat
+                outformat = "RDFLIKE"
+
+        def do_unknown(opt):
+                sys.stderr.write("Unknown option {}\n".format(opt))
+                exit(1)
+
+        args = ArgHolder(argv)
+        while True:
+                arg = args.next()
+                if arg is None:
+                        break
 
-        p.add_argument("--text",
-                        action = 'store_const', const="TEXT",
-                        dest = 'outformat',
-                        help="Print in text format (default)")
-        p.add_argument("--csv",
-                        action = 'store_const', const="CSV",
-                        dest = 'outformat',
-                        help="Print a CSV file")
-        p.add_argument("--xml",
-                        action = 'store_const', const="XML",
-                        dest = 'outformat',
-                        help="Print in XML")
-        p.add_argument("--json",
-                        action = 'store_const', const="JSON",
-                        dest = 'outformat',
-                        help="Print in JSON")
-        p.add_argument("--rdf",
-                        action = 'store_const', const="RDF",
-                        dest = 'outbformat',
-                        help="Print in RDF")
-        p.add_argument("--rdflike",
-                        action = 'store_const', const="RDFLIKE",
-                        dest = 'outformat',
-                        help="Print RDF-like text")
-
-        p.add_argument("TERM", nargs='*',
-                        action=CtorAppend, dest='queries',
-                        const=Invocation.Query.doqstring,
-                        help="Search term")
+                if nomoreoptions or arg[0] != "-":
+                        do_search(arg)
+                elif arg == "--":
+                        nomoreoptions = True
+                # Long options
+                elif arg == "--attachment":
+                        do_attachment(args.getarg(arg))
+                elif arg.startswith("--attachment="):
+                        do_message(arg[13:])
+                elif arg == "--closed":
+                        do_closed()
+                elif arg == "--csv":
+                        do_csv()
+                elif arg == "--fields":
+                        do_fields()
+                elif arg == "--full":
+                        do_full()
+                elif arg == "--headers":
+                        do_headers()
+                elif arg == "--help":
+                        do_help()
+                elif arg == "--json":
+                        do_json()
+                elif arg == "--list":
+                        do_list()
+                elif arg == "--message":
+                        do_message(args.getarg(arg))
+                elif arg.startswith("--message="):
+                        do_message(arg[10:])
+                elif arg == "--meta":
+                        do_meta()
+                elif arg == "--metadata":
+                        do_meta()
+                elif arg == "--newest":
+                        do_newest()
+                elif arg == "--oldest":
+                        do_oldest()
+                elif arg == "--orderby":
+                        do_orderby(args.getarg(arg))
+                elif arg.startswith("--orderby="):
+                        do_orderby(arg[10:])
+                elif arg == "--open":
+                        do_open()
+                elif arg == "--paranoid":
+                        do_paranoid()
+                elif arg == "--public":
+                        do_public()
+                elif arg == "--privileged" and not paranoid:
+                        do_privileged()
+                elif arg == "--range":
+                        do_range(args.getarg(arg))
+                elif arg.startswith("--range="):
+                        do_range(arg[8:])
+                elif arg == "--raw":
+                        do_raw()
+                elif arg == "--rdf":
+                        do_rdf()
+                elif arg == "--rdflike":
+                        do_rdflike()
+                elif arg == "--revorderby":
+                        do_revorderby(args.getarg(arg))
+                elif arg.startswith("--revorderby="):
+                        do_revorderby(arg[13:])
+                elif arg == "--search":
+                        do_search(args.getarg(arg))
+                elif arg.startswith("--search="):
+                        do_search(arg[9:])
+                elif arg == "--show":
+                        do_show(args.getarg(arg))
+                elif arg.startswith("--show="):
+                        do_show(arg[7:])
+                elif arg == "--sql" and not paranoid:
+                        do_sql(args.getarg(arg))
+                elif arg.startswith("--sql=") and not paranoid:
+                        do_sql(arg[7:])
+                elif arg == "--staleness":
+                        do_staleness()
+                elif arg == "--text":
+                        do_text()
+                elif arg == "--version":
+                        do_version()
+                elif arg == "--xml":
+                        do_xml()
+                elif arg.startswith("--"):
+                        do_unknown(arg)
+                else:
+                        # short options
+                        i = 1
+                        n = len(arg)
+                        while i < n:
+                                opt = arg[i]
+                                i += 1
+                                def getarg():
+                                        nonlocal i
+                                        if i < n:
+                                                ret = arg[i:]
+                                        else:
+                                                ret = args.getarg("-" + opt)
+                                        i = n
+                                        return ret
 
-        args = p.parse_args()
+                                if opt == "a":
+                                        do_attachment(getarg())
+                                elif opt == "f":
+                                        do_full()
+                                elif opt == "h":
+                                        do_help()
+                                elif opt == "l":
+                                        do_list()
+                                elif opt == "m":
+                                        do_message(getarg())
+                                elif opt == "s" and not paranoid:
+                                        do_sql(getarg())
+                                elif opt == "r":
+                                        do_raw()
+                                elif opt == "v":
+                                        do_version()
+                                else:
+                                        do_unknown("-" + opt)
 
-        ops = args.ops
-        if ops is None:
-                ops = []
-        queries = args.queries
-        if queries is not None:
-                openonly = args.openonly
-                if openonly is None:
-                        openonly = True
-                publiconly = args.publiconly
-                if publiconly is None:
-                        publiconly = False
-                orders = args.orders
-                if orders is None:
-                        orders = [Invocation.Order.dooldest(None)]
-                output = args.output
-                if output is None:
-                        output = "LIST"
-                outformat = args.outformat
-                if outformat is None:
-                        outformat = "TEXT"
-                selections = args.selections
-                if selections is None:
-                        sel = Invocation.Selection.dopr(output, outformat)
-                        selections = [sel]
-                search = Search(queries, openonly, publiconly, orders)
-                op = dosearch(search, selections)
-                ops.append(op)
-        # endif
+        # Now convert what we got to a single thing.
+        if queries != []:
+                if orders == []:
+                        orders = [Invocation.mkoldest()]
+                if selections == []:
+                        selections = [Invocation.mkpr(output, outformat)]
+                search = Invocation.Search(queries, openonly, publiconly, orders)
+                ops.append(Invocation.mksearch(search, selections))
+        else:
+                if orders != []:
+                        msg = "No queries given for requested orderings\n"
+                        sys.stderr.write(msg)
+                        exit(1)
+                if selections != []:
+                        msg = "No queries given for requested selections\n"
+                        sys.stderr.write(msg)
+                        exit(1)
 
         return Invocation(ops)
 # end getargs
@@ -1153,7 +1343,9 @@
 ############################################################
 # main
 
-todo = getargs()
+todo = getargs(sys.argv)
+#todo.dump(Dumper(sys.stdout))
+
 opendb()
 fetch_classifications()
 todo = compile(todo)