mirror of
https://github.com/openSUSE/osc.git
synced 2024-12-26 09:56:13 +01:00
Daniel Mach
be8a5268a8
Cmdln.py is unmaintained for years and uses deprecated optparse. Let's replace it with a simpler custom code. Also remove code that generates man page, we'll replace it with a 3rd party tool.
240 lines
7.5 KiB
Python
240 lines
7.5 KiB
Python
"""
|
|
A modern, lightweight alternative to cmdln.py from https://github.com/trentm/cmdln
|
|
"""
|
|
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
|
|
def option(*args, **kwargs):
|
|
"""
|
|
Decorator to add an option to the optparser argument of a Cmdln subcommand.
|
|
|
|
Example:
|
|
class MyShell(cmdln.Cmdln):
|
|
@cmdln.option("-f", "--force", help="force removal")
|
|
def do_remove(self, subcmd, opts, *args):
|
|
#...
|
|
"""
|
|
def decorate(f):
|
|
if not hasattr(f, "options"):
|
|
f.options = []
|
|
new_args = [i for i in args if i]
|
|
f.options.append((new_args, kwargs))
|
|
return f
|
|
return decorate
|
|
|
|
|
|
def alias(*aliases):
|
|
"""
|
|
Decorator to add aliases for Cmdln.do_* command handlers.
|
|
|
|
Example:
|
|
class MyShell(cmdln.Cmdln):
|
|
@cmdln.alias("!", "sh")
|
|
def do_shell(self, argv):
|
|
#...implement 'shell' command
|
|
"""
|
|
def decorate(f):
|
|
if not hasattr(f, "aliases"):
|
|
f.aliases = []
|
|
f.aliases += aliases
|
|
return f
|
|
return decorate
|
|
|
|
|
|
def hide():
|
|
"""
|
|
For obsolete calls, hide them in help listings.
|
|
|
|
Example:
|
|
class MyShell(cmdln.Cmdln):
|
|
@cmdln.hide()
|
|
def do_shell(self, argv):
|
|
#...implement 'shell' command
|
|
"""
|
|
def decorate(f):
|
|
f.hidden = True
|
|
return f
|
|
return decorate
|
|
|
|
|
|
class HelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
def _format_action(self, action):
|
|
if isinstance(action, argparse._SubParsersAction):
|
|
parts = []
|
|
for i in action._get_subactions():
|
|
if i.help == argparse.SUPPRESS:
|
|
# don't display commands with suppressed help
|
|
continue
|
|
if len(i.metavar) > 20:
|
|
parts.append("%*s%-21s" % (self._current_indent, "", i.metavar))
|
|
parts.append("%*s %s" % (self._current_indent + 21, "", i.help))
|
|
else:
|
|
parts.append("%*s%-21s %s" % (self._current_indent, "", i.metavar, i.help))
|
|
return "\n".join(parts)
|
|
return super()._format_action(action)
|
|
|
|
|
|
class Cmdln:
|
|
def get_argparser_usage(self):
|
|
return "%(prog)s [global opts] <command> [--help] [opts] [args]"
|
|
|
|
def get_subcommand_prog(self, subcommand):
|
|
return f"{self.argparser.prog} [global opts] {subcommand}"
|
|
|
|
def _remove_leading_spaces_from_text(self, text):
|
|
lines = text.splitlines()
|
|
lines = self._remove_leading_spaces_from_lines(lines)
|
|
return "\n".join(lines)
|
|
|
|
def _remove_leading_spaces_from_lines(self, lines):
|
|
# compute the indentation (leading spaces) in the docstring
|
|
leading_spaces = 0
|
|
for line in lines:
|
|
line_leading_spaces = len(line) - len(line.lstrip(' '))
|
|
if leading_spaces == 0:
|
|
leading_spaces = line_leading_spaces
|
|
leading_spaces = min(leading_spaces, line_leading_spaces)
|
|
# dedent the lines (remove leading spaces)
|
|
lines = [line[leading_spaces:] for line in lines]
|
|
return lines
|
|
|
|
def create_argparser(self):
|
|
"""
|
|
Create `.argparser` and `.subparsers`.
|
|
Override this method to replace them with your own.
|
|
"""
|
|
self.argparser = argparse.ArgumentParser(
|
|
usage=self.get_argparser_usage(),
|
|
description=self._remove_leading_spaces_from_text(self.__doc__),
|
|
formatter_class=HelpFormatter,
|
|
)
|
|
self.subparsers = self.argparser.add_subparsers(
|
|
title="commands",
|
|
dest="command",
|
|
)
|
|
|
|
self.pre_argparse()
|
|
|
|
# map command name to `do_*` function that runs the command
|
|
self.cmd_map = {}
|
|
|
|
# map aliases back to the command names
|
|
self.alias_to_cmd_name_map = {}
|
|
|
|
for attr in dir(self):
|
|
if not attr.startswith("do_"):
|
|
continue
|
|
|
|
cmd_name = attr[3:]
|
|
cmd_func = getattr(self, attr)
|
|
|
|
# extract data from the function
|
|
options = getattr(cmd_func, "options", [])
|
|
aliases = getattr(cmd_func, "aliases", [])
|
|
hidden = getattr(cmd_func, "hidden", False)
|
|
|
|
# map command name and aliases to the function
|
|
self.cmd_map[cmd_name] = cmd_func
|
|
self.alias_to_cmd_name_map[cmd_name] = cmd_name
|
|
for i in aliases:
|
|
self.cmd_map[i] = cmd_func
|
|
self.alias_to_cmd_name_map[i] = cmd_name
|
|
|
|
# split doctext into lines, allow the first line to start at a new line
|
|
help_lines = cmd_func.__doc__.lstrip().splitlines()
|
|
|
|
# use the first line as help text
|
|
help_text = help_lines.pop(0)
|
|
|
|
# use the remaining lines as description
|
|
help_lines = self._remove_leading_spaces_from_lines(help_lines)
|
|
help_desc = "\n".join(help_lines)
|
|
help_desc = help_desc.strip()
|
|
|
|
if hidden:
|
|
help_text = argparse.SUPPRESS
|
|
|
|
subparser = self.subparsers.add_parser(
|
|
cmd_name,
|
|
aliases=aliases,
|
|
help=help_text,
|
|
description=help_desc,
|
|
prog=self.get_subcommand_prog(cmd_name),
|
|
formatter_class=HelpFormatter
|
|
)
|
|
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.
|
|
Automatically pick the right parser for the main program or a subcommand.
|
|
"""
|
|
if not self.options.command:
|
|
parser = self.argparser
|
|
else:
|
|
parser = self.subparsers._name_parser_map.get(self.options.command, self.argparser)
|
|
parser.error(*args, **kwargs)
|
|
|
|
def pre_argparse(self):
|
|
"""
|
|
Hook method executed after `.main()` creates `.argparser` instance
|
|
and before `parse_args()` is called.
|
|
"""
|
|
pass
|
|
|
|
def post_argparse(self):
|
|
"""
|
|
Hook method executed after `.main()` calls `parse_args()`.
|
|
When called, `.options` and `.args` hold the results of `parse_args()`.
|
|
"""
|
|
pass
|
|
|
|
def main(self, argv=None):
|
|
if argv is None:
|
|
argv = sys.argv
|
|
else:
|
|
argv = argv[:] # don't modify caller's list
|
|
|
|
self.create_argparser()
|
|
|
|
self.options = self.argparser.parse_args(argv[1:])
|
|
self.args = getattr(self.options, "args", [])
|
|
|
|
self.post_argparse()
|
|
|
|
if not self.options.command:
|
|
self.argparser.error("Please specify a command")
|
|
|
|
# find the `do_*` function to call by its name
|
|
cmd = self.cmd_map[self.options.command]
|
|
# run the command with parsed args
|
|
cmd(self.options.command, self.options, *self.args)
|
|
|
|
@alias("?")
|
|
def do_help(self, subcmd, opts, *args):
|
|
"""
|
|
Give detailed help on a specific sub-command
|
|
|
|
usage:
|
|
%(prog)s [SUBCOMMAND]
|
|
"""
|
|
if not args:
|
|
self.argparser.print_help()
|
|
return
|
|
|
|
for action in self.argparser._actions:
|
|
if not isinstance(action, argparse._SubParsersAction):
|
|
continue
|
|
|
|
for choice, subparser in action.choices.items():
|
|
if choice == args[0]:
|
|
subparser.print_help()
|
|
return
|