diff --git a/osc/cmdln.py b/osc/cmdln.py index bb346dd6..5d69264d 100644 --- a/osc/cmdln.py +++ b/osc/cmdln.py @@ -117,6 +117,7 @@ class Cmdln: ) self.pre_argparse() + self.add_global_options(self.argparser) # map command name to `do_*` function that runs the command self.cmd_map = {} @@ -169,12 +170,11 @@ class Cmdln: prog=self.get_subcommand_prog(cmd_name), formatter_class=HelpFormatter ) + # add hidden copy of global options so they can be used in any place + self.add_global_options(subparser, suppress=True) for option_args, option_kwargs in options: subparser.add_argument(*option_args, **option_kwargs) - # HACK: inject 'args' to all commands so we don't have to decorate all of them - subparser.add_argument('args', nargs='*') - def argparse_error(self, *args, **kwargs): """ Raise an argument parser error. @@ -193,6 +193,12 @@ class Cmdln: """ pass + def add_global_options(self, parser, suppress=False): + """ + Add options to the main argument parser and all subparsers. + """ + pass + def post_argparse(self): """ Hook method executed after `.main()` calls `parse_args()`. @@ -208,8 +214,10 @@ class Cmdln: self.create_argparser() - self.options = self.argparser.parse_args(argv[1:]) - self.args = getattr(self.options, "args", []) + self.options, self.args = self.argparser.parse_known_args(argv[1:]) + unrecognized = [i for i in self.args if i.startswith("-")] + if unrecognized: + self.argparser.error(f"unrecognized arguments: " + " ".join(unrecognized)) self.post_argparse() diff --git a/osc/commandline.py b/osc/commandline.py index c7b1c264..e00d6e68 100644 --- a/osc/commandline.py +++ b/osc/commandline.py @@ -3,6 +3,7 @@ # and distributed under the terms of the GNU General Public Licence, # either version 2, or version 3 (at your option). +import argparse import importlib.util import inspect import os @@ -69,39 +70,84 @@ class Osc(cmdln.Cmdln): return project.replace(conf.config['project_separator'], ':') return project - def pre_argparse(self): - """Add global options to the parser (options that are not specific to any subcommand)""" + def add_global_options(self, parser, suppress=False): - optparser = self.argparser - optparser.add_argument('--debugger', action='store_true', - help='jump into the debugger before executing anything') - optparser.add_argument('--post-mortem', action='store_true', - help='jump into the debugger in case of errors') - optparser.add_argument('-t', '--traceback', action='store_true', - help='print call trace in case of errors') - optparser.add_argument('-H', '--http-debug', action='store_true', - help='debug HTTP traffic (filters some headers)') - optparser.add_argument('--http-full-debug', action='store_true', - help='debug HTTP traffic (filters no headers)') - optparser.add_argument('-d', '--debug', action='store_true', - help='print info useful for debugging') - optparser.add_argument('-A', '--apiurl', dest='apiurl', - metavar='URL/alias', - help='specify URL to access API server at or an alias') - optparser.add_argument('-c', '--config', dest='conffile', - metavar='FILE', - help='specify alternate configuration file') - optparser.add_argument('--no-keyring', action='store_true', - help='disable usage of desktop keyring system') - verbose_group = optparser.add_mutually_exclusive_group() - verbose_group.add_argument('-v', '--verbose', action='store_true', - help='increase verbosity') - verbose_group.add_argument('-q', '--quiet', action='store_true', - help='be quiet, not verbose') + def _add_parser_arguments_from_data(argument_parser, data): + for kwargs in data: + args = kwargs.pop("names") + if suppress: + kwargs["help"] = argparse.SUPPRESS + kwargs["default"] = argparse.SUPPRESS + argument_parser.add_argument(*args, **kwargs) + + arguments = [] + arguments.append(dict( + names=['-v', '--verbose'], + action='store_true', + help='increase verbosity', + )) + arguments.append(dict( + names=['-q', '--quiet'], + action='store_true', + help='be quiet, not verbose', + )) + arguments.append(dict( + names=['--debugger'], + action='store_true', + help='jump into the debugger before executing anything', + )) + arguments.append(dict( + names=['--post-mortem'], + action='store_true', + help='jump into the debugger in case of errors', + )) + arguments.append(dict( + names=['--traceback'], + action='store_true', + help='print call trace in case of errors', + )) + arguments.append(dict( + names=['-H', '--http-debug'], + action='store_true', + help='debug HTTP traffic (filters some headers)', + )) + arguments.append(dict( + names=['--http-full-debug'], + action='store_true', + help='debug HTTP traffic (filters no headers)', + )) + arguments.append(dict( + names=['--debug'], + action='store_true', + help='print info useful for debugging', + )) + arguments.append(dict( + names=['-A', '--apiurl'], + metavar='URL/alias', + help='specify URL to access API server at or an alias', + )) + arguments.append(dict( + names=['--config'], + dest='conffile', + metavar='FILE', + help='specify alternate configuration file', + )) + arguments.append(dict( + names=['--no-keyring'], + action='store_true', + help='disable usage of desktop keyring system', + )) + + _add_parser_arguments_from_data(parser, arguments) def post_argparse(self): """merge commandline options into the config""" + # handle conflicting options manually because the mutually exclusive group is buggy + # https://github.com/python/cpython/issues/96310 + if self.options.quiet and self.options.verbose: + self.argparse_error("argument -q/--quiet: not allowed with argument -v/--verbose") + # avoid loading config that may trigger prompt for username, password etc. if not self.options.command: # no command specified @@ -5321,8 +5367,6 @@ Please submit there instead, or use --nodevelproject to force direct submission. # WARNING: this function is also called by do_results. You need to set a default there # as well when adding a new option! - @cmdln.option('-q', '--hide-legend', action='store_true', - help='hide the legend') @cmdln.option('-b', '--brief', action='store_true', help='show the result in "pkgname repo arch result"') @cmdln.option('-w', '--watch', action='store_true', @@ -5378,7 +5422,7 @@ Please submit there instead, or use --nodevelproject to force direct submission. print('Please implement support for osc prjresults --watch without --xml.') return 2 - print('\n'.join(get_prj_results(apiurl, project, hide_legend=opts.hide_legend, \ + print('\n'.join(get_prj_results(apiurl, project, hide_legend=opts.quiet, \ csv=opts.csv, status_filter=opts.status_filter, \ name_filter=opts.name_filter, repo=opts.repo, \ arch=opts.arch, vertical=opts.vertical, \ @@ -8048,7 +8092,7 @@ Please submit there instead, or use --nodevelproject to force direct submission. help='define the project where this package is primarily developed') @cmdln.option('-a', '--add', metavar='user', help='add a new person for given role ("maintainer" by default)') - @cmdln.option('-A', '--all', action='store_true', + @cmdln.option('--all', action='store_true', help='list all found entries not just the first one') @cmdln.option('-s', '--set-bugowner', metavar='user', help='Set the bugowner to specified person (or group via group: prefix)')