summaryrefslogtreecommitdiff
path: root/lldb/examples/python
diff options
context:
space:
mode:
authorjimingham <jingham@apple.com>2024-02-13 11:09:47 -0800
committerGitHub <noreply@github.com>2024-02-13 11:09:47 -0800
commita69ecb2420f644e31f18fcc61a07b3ca627e8939 (patch)
tree3ef79076597be772ad71ff8b1d9c2fff4e333bcd /lldb/examples/python
parenta04c6366b156f508cdf84a32ef4484b53a6dabee (diff)
Add the ability to define a Python based command that uses CommandObjectParsed (#70734)
This allows you to specify options and arguments and their definitions and then have lldb handle the completions, help, etc. in the same way that lldb does for its parsed commands internally. This feature has some design considerations as well as the code, so I've also set up an RFC, but I did this one first and will put the RFC address in here once I've pushed it... Note, the lldb "ParsedCommand interface" doesn't actually do all the work that it should. For instance, saying the type of an option that has a completer doesn't automatically hook up the completer, and ditto for argument values. We also do almost no work to verify that the arguments match their definition, or do auto-completion for them. This patch allows you to make a command that's bug-for-bug compatible with built-in ones, but I didn't want to stall it on getting the auto-command checking to work all the way correctly. As an overall design note, my primary goal here was to make an interface that worked well in the script language. For that I needed, for instance, to have a property-based way to get all the option values that were specified. It was much more convenient to do that by making a fairly bare-bones C interface to define the options and arguments of a command, and set their values, and then wrap that in a Python class (installed along with the other bits of the lldb python module) which you can then derive from to make your new command. This approach will also make it easier to experiment. See the file test_commands.py in the test case for examples of how this works.
Diffstat (limited to 'lldb/examples/python')
-rw-r--r--lldb/examples/python/cmdtemplate.py129
-rw-r--r--lldb/examples/python/templates/parsed_cmd.py360
2 files changed, 409 insertions, 80 deletions
diff --git a/lldb/examples/python/cmdtemplate.py b/lldb/examples/python/cmdtemplate.py
index a3c30f30afea..49a08365268f 100644
--- a/lldb/examples/python/cmdtemplate.py
+++ b/lldb/examples/python/cmdtemplate.py
@@ -11,115 +11,84 @@
import inspect
import lldb
-import optparse
-import shlex
import sys
+from lldb.plugins.parsed_cmd import ParsedCommand
-
-class FrameStatCommand:
+class FrameStatCommand(ParsedCommand):
program = "framestats"
@classmethod
def register_lldb_command(cls, debugger, module_name):
- parser = cls.create_options()
- cls.__doc__ = parser.format_help()
- # Add any commands contained in this module to LLDB
- command = "command script add -o -c %s.%s %s" % (
- module_name,
- cls.__name__,
- cls.program,
- )
- debugger.HandleCommand(command)
+ ParsedCommandBase.do_register_cmd(cls, debugger, module_name)
print(
'The "{0}" command has been installed, type "help {0}" or "{0} '
'--help" for detailed help.'.format(cls.program)
)
- @classmethod
- def create_options(cls):
- usage = "usage: %prog [options]"
- description = (
- "This command is meant to be an example of how to make "
- "an LLDB command that does something useful, follows "
- "best practices, and exploits the SB API. "
- "Specifically, this command computes the aggregate "
- "and average size of the variables in the current "
- "frame and allows you to tweak exactly which variables "
- "are to be accounted in the computation."
- )
+ def setup_command_definition(self):
- # Pass add_help_option = False, since this keeps the command in line
- # with lldb commands, and we wire up "help command" to work by
- # providing the long & short help methods below.
- parser = optparse.OptionParser(
- description=description,
- prog=cls.program,
- usage=usage,
- add_help_option=False,
+ self.ov_parser.add_option(
+ "i",
+ "in-scope",
+ help = "in_scope_only = True",
+ value_type = lldb.eArgTypeBoolean,
+ dest = "bool_arg",
+ default = True,
)
- parser.add_option(
- "-i",
- "--in-scope",
- action="store_true",
- dest="inscope",
- help="in_scope_only = True",
+ self.ov_parser.add_option(
+ "i",
+ "in-scope",
+ help = "in_scope_only = True",
+ value_type = lldb.eArgTypeBoolean,
+ dest = "inscope",
default=True,
)
-
- parser.add_option(
- "-a",
- "--arguments",
- action="store_true",
- dest="arguments",
- help="arguments = True",
- default=True,
+
+ self.ov_parser.add_option(
+ "a",
+ "arguments",
+ help = "arguments = True",
+ value_type = lldb.eArgTypeBoolean,
+ dest = "arguments",
+ default = True,
)
- parser.add_option(
- "-l",
- "--locals",
- action="store_true",
- dest="locals",
- help="locals = True",
- default=True,
+ self.ov_parser.add_option(
+ "l",
+ "locals",
+ help = "locals = True",
+ value_type = lldb.eArgTypeBoolean,
+ dest = "locals",
+ default = True,
)
- parser.add_option(
- "-s",
- "--statics",
- action="store_true",
- dest="statics",
- help="statics = True",
- default=True,
+ self.ov_parser.add_option(
+ "s",
+ "statics",
+ help = "statics = True",
+ value_type = lldb.eArgTypeBoolean,
+ dest = "statics",
+ default = True,
)
- return parser
-
def get_short_help(self):
return "Example command for use in debugging"
def get_long_help(self):
- return self.help_string
+ return ("This command is meant to be an example of how to make "
+ "an LLDB command that does something useful, follows "
+ "best practices, and exploits the SB API. "
+ "Specifically, this command computes the aggregate "
+ "and average size of the variables in the current "
+ "frame and allows you to tweak exactly which variables "
+ "are to be accounted in the computation.")
+
def __init__(self, debugger, unused):
- self.parser = self.create_options()
- self.help_string = self.parser.format_help()
+ super().__init__(debugger, unused)
def __call__(self, debugger, command, exe_ctx, result):
- # Use the Shell Lexer to properly parse up command options just like a
- # shell would
- command_args = shlex.split(command)
-
- try:
- (options, args) = self.parser.parse_args(command_args)
- except:
- # if you don't handle exceptions, passing an incorrect argument to
- # the OptionParser will cause LLDB to exit (courtesy of OptParse
- # dealing with argument errors by throwing SystemExit)
- result.SetError("option parsing failed")
- return
-
# Always get program state from the lldb.SBExecutionContext passed
# in as exe_ctx
frame = exe_ctx.GetFrame()
@@ -128,7 +97,7 @@ class FrameStatCommand:
return
variables_list = frame.GetVariables(
- options.arguments, options.locals, options.statics, options.inscope
+ self.ov_parser.arguments, self.ov_parser.locals, self.ov_parser.statics, self.ov_parser.inscope
)
variables_count = variables_list.GetSize()
if variables_count == 0:
diff --git a/lldb/examples/python/templates/parsed_cmd.py b/lldb/examples/python/templates/parsed_cmd.py
new file mode 100644
index 000000000000..61ea57c275aa
--- /dev/null
+++ b/lldb/examples/python/templates/parsed_cmd.py
@@ -0,0 +1,360 @@
+"""
+This module implements a couple of utility classes to make writing
+lldb parsed commands more Pythonic.
+The way to use it is to make a class for your command that inherits from ParsedCommandBase.
+That will make an LLDBOptionValueParser which you will use for your
+option definition, and to fetch option values for the current invocation
+of your command. Access to the OV parser is through:
+
+ParsedCommandBase.get_parser()
+
+Next, implement setup_command_definition() in your new command class, and call:
+
+ self.get_parser().add_option()
+
+to add all your options. The order doesn't matter for options, lldb will sort them
+alphabetically for you when it prints help.
+
+Similarly you can define the arguments with:
+
+ self.get_parser().add_argument()
+
+At present, lldb doesn't do as much work as it should verifying arguments, it
+only checks that commands that take no arguments don't get passed arguments.
+
+Then implement the execute function for your command as:
+
+ def __call__(self, debugger, args_list, exe_ctx, result):
+
+The arguments will be a list of strings.
+
+You can access the option values using the 'dest' string you passed in when defining the option.
+And if you need to know whether a given option was set by the user or not, you can
+use the was_set API.
+
+So for instance, if you have an option whose "dest" is "my_option", then:
+
+ self.get_parser().my_option
+
+will fetch the value, and:
+
+ self.get_parser().was_set("my_option")
+
+will return True if the user set this option, and False if it was left at its default
+value.
+
+There are example commands in the lldb testsuite at:
+
+llvm-project/lldb/test/API/commands/command/script/add/test_commands.py
+"""
+import inspect
+import lldb
+import sys
+from abc import abstractmethod
+
+class LLDBOptionValueParser:
+ """
+ This class holds the option definitions for the command, and when
+ the command is run, you can ask the parser for the current values. """
+
+ def __init__(self):
+ # This is a dictionary of dictionaries. The key is the long option
+ # name, and the value is the rest of the definition.
+ self.options_dict = {}
+ self.args_array = []
+
+ # Some methods to translate common value types. Should return a
+ # tuple of the value and an error value (True => error) if the
+ # type can't be converted. These are called internally when the
+ # command line is parsed into the 'dest' properties, you should
+ # not need to call them directly.
+ # FIXME: Need a way to push the conversion error string back to lldb.
+ @staticmethod
+ def to_bool(in_value):
+ error = True
+ value = False
+ if type(in_value) != str or len(in_value) == 0:
+ return (value, error)
+
+ low_in = in_value.lower()
+ if low_in in ["y", "yes", "t", "true", "1"]:
+ value = True
+ error = False
+
+ if not value and low_in in ["n", "no", "f", "false", "0"]:
+ value = False
+ error = False
+
+ return (value, error)
+
+ @staticmethod
+ def to_int(in_value):
+ #FIXME: Not doing errors yet...
+ return (int(in_value), False)
+
+ @staticmethod
+ def to_unsigned(in_value):
+ # FIXME: find an unsigned converter...
+ # And handle errors.
+ return (int(in_value), False)
+
+ translators = {
+ lldb.eArgTypeBoolean : to_bool,
+ lldb.eArgTypeBreakpointID : to_unsigned,
+ lldb.eArgTypeByteSize : to_unsigned,
+ lldb.eArgTypeCount : to_unsigned,
+ lldb.eArgTypeFrameIndex : to_unsigned,
+ lldb.eArgTypeIndex : to_unsigned,
+ lldb.eArgTypeLineNum : to_unsigned,
+ lldb.eArgTypeNumLines : to_unsigned,
+ lldb.eArgTypeNumberPerLine : to_unsigned,
+ lldb.eArgTypeOffset : to_int,
+ lldb.eArgTypeThreadIndex : to_unsigned,
+ lldb.eArgTypeUnsignedInteger : to_unsigned,
+ lldb.eArgTypeWatchpointID : to_unsigned,
+ lldb.eArgTypeColumnNum : to_unsigned,
+ lldb.eArgTypeRecognizerID : to_unsigned,
+ lldb.eArgTypeTargetID : to_unsigned,
+ lldb.eArgTypeStopHookID : to_unsigned
+ }
+
+ @classmethod
+ def translate_value(cls, value_type, value):
+ try:
+ return cls.translators[value_type](value)
+ except KeyError:
+ # If we don't have a translator, return the string value.
+ return (value, False)
+
+ # FIXME: would this be better done on the C++ side?
+ # The common completers are missing some useful ones.
+ # For instance there really should be a common Type completer
+ # And an "lldb command name" completer.
+ completion_table = {
+ lldb.eArgTypeAddressOrExpression : lldb.eVariablePathCompletion,
+ lldb.eArgTypeArchitecture : lldb.eArchitectureCompletion,
+ lldb.eArgTypeBreakpointID : lldb.eBreakpointCompletion,
+ lldb.eArgTypeBreakpointIDRange : lldb.eBreakpointCompletion,
+ lldb.eArgTypeBreakpointName : lldb.eBreakpointNameCompletion,
+ lldb.eArgTypeClassName : lldb.eSymbolCompletion,
+ lldb.eArgTypeDirectoryName : lldb.eDiskDirectoryCompletion,
+ lldb.eArgTypeExpression : lldb.eVariablePathCompletion,
+ lldb.eArgTypeExpressionPath : lldb.eVariablePathCompletion,
+ lldb.eArgTypeFilename : lldb.eDiskFileCompletion,
+ lldb.eArgTypeFrameIndex : lldb.eFrameIndexCompletion,
+ lldb.eArgTypeFunctionName : lldb.eSymbolCompletion,
+ lldb.eArgTypeFunctionOrSymbol : lldb.eSymbolCompletion,
+ lldb.eArgTypeLanguage : lldb.eTypeLanguageCompletion,
+ lldb.eArgTypePath : lldb.eDiskFileCompletion,
+ lldb.eArgTypePid : lldb.eProcessIDCompletion,
+ lldb.eArgTypeProcessName : lldb.eProcessNameCompletion,
+ lldb.eArgTypeRegisterName : lldb.eRegisterCompletion,
+ lldb.eArgTypeRunArgs : lldb.eDiskFileCompletion,
+ lldb.eArgTypeShlibName : lldb.eModuleCompletion,
+ lldb.eArgTypeSourceFile : lldb.eSourceFileCompletion,
+ lldb.eArgTypeSymbol : lldb.eSymbolCompletion,
+ lldb.eArgTypeThreadIndex : lldb.eThreadIndexCompletion,
+ lldb.eArgTypeVarName : lldb.eVariablePathCompletion,
+ lldb.eArgTypePlatform : lldb.ePlatformPluginCompletion,
+ lldb.eArgTypeWatchpointID : lldb.eWatchpointIDCompletion,
+ lldb.eArgTypeWatchpointIDRange : lldb.eWatchpointIDCompletion,
+ lldb.eArgTypeModuleUUID : lldb.eModuleUUIDCompletion,
+ lldb.eArgTypeStopHookID : lldb.eStopHookIDCompletion
+ }
+
+ @classmethod
+ def determine_completion(cls, arg_type):
+ return cls.completion_table.get(arg_type, lldb.eNoCompletion)
+
+ def add_argument_set(self, arguments):
+ self.args_array.append(arguments)
+
+ def get_option_element(self, long_name):
+ return self.options_dict.get(long_name, None)
+
+ def is_enum_opt(self, opt_name):
+ elem = self.get_option_element(opt_name)
+ if not elem:
+ return False
+ return "enum_values" in elem
+
+ def option_parsing_started(self):
+ """ This makes the ivars for all the "dest" values in the array and gives them
+ their default values. You should not have to call this by hand, though if
+ you have some option that needs to do some work when a new command invocation
+ starts, you can override this to handle your special option. """
+ for key, elem in self.options_dict.items():
+ elem['_value_set'] = False
+ try:
+ object.__setattr__(self, elem["dest"], elem["default"])
+ except AttributeError:
+ # It isn't an error not to have a "dest" variable name, you'll
+ # just have to manage this option's value on your own.
+ continue
+
+ def set_enum_value(self, enum_values, input):
+ """ This sets the value for an enum option, you should not have to call this
+ by hand. """
+ candidates = []
+ for candidate in enum_values:
+ # The enum_values are a two element list of value & help string.
+ value = candidate[0]
+ if value.startswith(input):
+ candidates.append(value)
+
+ if len(candidates) == 1:
+ return (candidates[0], False)
+ else:
+ return (input, True)
+
+ def set_option_value(self, exe_ctx, opt_name, opt_value):
+ """ This sets a single option value. This will handle most option
+ value types, but if you have an option that has some complex behavior,
+ you can override this to implement that behavior, and then pass the
+ rest of the options to the base class implementation. """
+ elem = self.get_option_element(opt_name)
+ if not elem:
+ return False
+
+ if "enum_values" in elem:
+ (value, error) = self.set_enum_value(elem["enum_values"], opt_value)
+ else:
+ (value, error) = __class__.translate_value(elem["value_type"], opt_value)
+
+ if error:
+ return False
+
+ object.__setattr__(self, elem["dest"], value)
+ elem["_value_set"] = True
+ return True
+
+ def was_set(self, opt_name):
+ """ Call this in the __call__ method of your command to determine
+ whether this option was set on the command line. It is sometimes
+ useful to know whether an option has the default value because the
+ user set it explicitly (was_set -> True) or not. """
+
+ elem = self.get_option_element(opt_name)
+ if not elem:
+ return False
+ try:
+ return elem["_value_set"]
+ except AttributeError:
+ return False
+
+ def add_option(self, short_option, long_option, help, default,
+ dest = None, required=False, groups = None,
+ value_type=lldb.eArgTypeNone, completion_type=None,
+ enum_values=None):
+ """
+ short_option: one character, must be unique, not required
+ long_option: no spaces, must be unique, required
+ help: a usage string for this option, will print in the command help
+ default: the initial value for this option (if it has a value)
+ dest: the name of the property that gives you access to the value for
+ this value. Defaults to the long option if not provided.
+ required: if true, this option must be provided or the command will error out
+ groups: Which "option groups" does this option belong to
+ value_type: one of the lldb.eArgType enum values. Some of the common arg
+ types also have default completers, which will be applied automatically.
+ completion_type: currently these are values form the lldb.CompletionType enum, I
+ haven't done custom completions yet.
+ enum_values: An array of duples: ["element_name", "element_help"]. If provided,
+ only one of the enum elements is allowed. The value will be the
+ element_name for the chosen enum element as a string.
+ """
+ if not dest:
+ dest = long_option
+
+ if not completion_type:
+ completion_type = self.determine_completion(value_type)
+
+ dict = {"short_option" : short_option,
+ "required" : required,
+ "help" : help,
+ "value_type" : value_type,
+ "completion_type" : completion_type,
+ "dest" : dest,
+ "default" : default}
+
+ if enum_values:
+ dict["enum_values"] = enum_values
+ if groups:
+ dict["groups"] = groups
+
+ self.options_dict[long_option] = dict
+
+ def make_argument_element(self, arg_type, repeat = "optional", groups = None):
+ element = {"arg_type" : arg_type, "repeat" : repeat}
+ if groups:
+ element["groups"] = groups
+ return element
+
+class ParsedCommand:
+ def __init__(self, debugger, unused):
+ self.debugger = debugger
+ self.ov_parser = LLDBOptionValueParser()
+ self.setup_command_definition()
+
+ def get_options_definition(self):
+ return self.get_parser().options_dict
+
+ def get_flags(self):
+ return 0
+
+ def get_args_definition(self):
+ return self.get_parser().args_array
+
+ # The base class will handle calling these methods
+ # when appropriate.
+
+ def option_parsing_started(self):
+ self.get_parser().option_parsing_started()
+
+ def set_option_value(self, exe_ctx, opt_name, opt_value):
+ return self.get_parser().set_option_value(exe_ctx, opt_name, opt_value)
+
+ def get_parser(self):
+ """Returns the option value parser for this command.
+ When defining the command, use the parser to add
+ argument and option definitions to the command.
+ When you are in the command callback, the parser
+ gives you access to the options passes to this
+ invocation"""
+
+ return self.ov_parser
+
+ # These are the two "pure virtual" methods:
+ @abstractmethod
+ def __call__(self, debugger, args_array, exe_ctx, result):
+ """This is the command callback. The option values are
+ provided by the 'dest' properties on the parser.
+
+ args_array: This is the list of arguments provided.
+ exe_ctx: Gives the SBExecutionContext on which the
+ command should operate.
+ result: Any results of the command should be
+ written into this SBCommandReturnObject.
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def setup_command_definition(self):
+ """This will be called when your command is added to
+ the command interpreter. Here is where you add your
+ options and argument definitions for the command."""
+ raise NotImplementedError()
+
+ @staticmethod
+ def do_register_cmd(cls, debugger, module_name):
+ """ Add any commands contained in this module to LLDB """
+ command = "command script add -o -p -c %s.%s %s" % (
+ module_name,
+ cls.__name__,
+ cls.program,
+ )
+ debugger.HandleCommand(command)
+ print(
+ 'The "{0}" command has been installed, type "help {0}"'
+ 'for detailed help.'.format(cls.program)
+ )