1
0
mirror of https://github.com/openSUSE/osc.git synced 2024-12-26 09:56:13 +01:00
github.com_openSUSE_osc/osc/cmdln.py
Daniel Mach be8a5268a8 Replace unmaintained cmdln.py with a custom code
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.
2022-08-12 10:05:59 +02:00

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