Source code for crash.commands
# -*- coding: utf-8 -*-
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
"""
The crash.commands module is the interface for implementing commands
in crash-python.
The only mandatory part of implementing a command is to derive a class
from :class:`.Command`, implement the :meth:`.Command.execute` method,
and instantiate it. If the command should have multiple aliases,
accept a name in the constructor and instantiate it multiple times.
Optional extensions:
- Adding a parser (derived from :class:`.ArgumentParser`) that parses
arguments. If not provided, an empty parser will be used.
- Adding a module docstring to be used as help text. If not provided,
the argparse generic help text will be used instead.
The module docstring will be placed automatically in the command reference
section of the user guide and will also be converted into plaintext help
for use in command execution. It should be in `reStructuredText
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html>`_
format.
Example:
::
\"\"\"
NAME
----
helloworld
SYNOPSYS
--------
``helloworld`` -- a command that prints hello world
\"\"\"
import crash.commands
class HelloWorld(crash.commands.Command):
def __init__(self) -> None:
parser = crash.commands.ArgumentParser(prog='helloworld')
super().__init__('helloworld', parser)
def execute(self, args: argparse.Namespace) -> None:
print("hello world")
HelloWorld()
"""
from typing import Dict, Any, Optional, Tuple
import os
import glob
import importlib
import argparse
from crash.exceptions import DelayedAttributeError, ArgumentTypeError
import gdb
[docs]class CommandError(RuntimeError):
"""An error occured while executing this command"""
[docs]class CommandLineError(RuntimeError):
"""An error occured while handling the command line for this command"""
[docs]class ArgumentParser(argparse.ArgumentParser):
"""
A simple extension to :class:`argparse.ArgumentParser` that:
- Requires a command name be set
- Loads help text automatically from files
- Handles errors by raising :obj:`.CommandLineError`
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
if not self.prog:
raise CommandError("Cannot build command with no name")
[docs] def error(self, message: str) -> Any:
"""
An error callback that raises the :obj:`CommandLineError` exception.
"""
raise CommandLineError(message)
[docs]class Command(gdb.Command):
"""
The Command class is the starting point for implementing a new command.
The :meth:`.Command.execute` method will be invoked when the user
invokes the command.
Once the constructor returns, the command will be registered with
``gdb`` and the command will be available for use.
Args:
name: The name of the command. The string ``py`` will be prefixed
to it.
parser: The parser to use to handle the arguments. It must be derived
from the :class:`.ArgumentParser` class.
Raises:
ArgumentTypeError: The parser is not derived from
:class:`.ArgumentParser`.
"""
_commands: Dict[str, 'Command'] = dict()
def __init__(self, name: str, parser: ArgumentParser = None) -> None:
"""
"""
self.name = "py" + name
if parser is None:
parser = ArgumentParser(prog=self.name)
elif not isinstance(parser, ArgumentParser):
raise ArgumentTypeError('parser', parser, ArgumentParser)
self._parser = parser
self._commands[self.name] = self
gdb.Command.__init__(self, self.name, gdb.COMMAND_USER)
# pylint: disable=unused-argument
[docs] def invoke_uncaught(self, argstr: str, from_tty: bool = False) -> None:
"""
Invokes the command directly and does not catch exceptions.
This is used mainly for unit testing to ensure proper exceptions
are raised.
Unless you are doing something special, see :meth:`execute` instead.
Args:
argstr: The command arguments
from_tty (default=False): Whether the command was invoked from a
tty.
"""
argv = gdb.string_to_argv(argstr)
args = self._parser.parse_args(argv)
self.execute(args)
[docs] def invoke(self, argstr: str, from_tty: bool = False) -> None:
"""
Invokes the command directly and translates exceptions.
This method is called by ``gdb`` to implement the command.
It translates the :class:`.CommandError`, :class:`.CommandLineError`,
and :class:`.DelayedAttributeError` exceptions into readable
error messages.
Unless you are doing something special, see :meth:`execute` instead.
Args:
argstr: The command arguments
from_tty (default=False): Whether the command was invoked from a
tty.
"""
try:
self.invoke_uncaught(argstr, from_tty)
except CommandError as e:
print(f"{self.name}: {str(e)}")
except CommandLineError as e:
print(f"{self.name}: {str(e)}")
self._parser.print_usage()
except DelayedAttributeError as e:
print(f"{self.name}: command unavailable, {str(e)}")
except (SystemExit, KeyboardInterrupt):
pass
[docs] def execute(self, args: argparse.Namespace) -> None:
"""
This method implements the command functionality.
Each command has a derived class associated with it that,
minimally, implements this method.
Args:
args: The arguments to this command already parsed by the
commmand's parser.
"""
raise NotImplementedError("Command should not be called directly")
[docs]def discover() -> None:
modules = glob.glob(os.path.dirname(__file__)+"/[A-Za-z]*.py")
__all__ = [os.path.basename(f)[:-3] for f in modules]
mods = __all__
for mod in mods:
# pylint: disable=unused-variable
x = importlib.import_module("crash.commands.{}".format(mod))