Skip to content

Add the ability to define custom completers to the parsed_cmd template. #109062

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions lldb/bindings/python/python-wrapper.swig
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,79 @@ lldb_private::python::SWIGBridge::LLDBSwigPythonGetRepeatCommandForScriptedComma
return result.Str().GetString().str();
}

StructuredData::DictionarySP
lldb_private::python::SWIGBridge::LLDBSwigPythonHandleArgumentCompletionForScriptedCommand(PyObject *implementor,
std::vector<llvm::StringRef> &args_vec, size_t args_pos, size_t pos_in_arg) {

PyErr_Cleaner py_err_cleaner(true);

PythonObject self(PyRefType::Borrowed, implementor);
auto pfunc = self.ResolveName<PythonCallable>("handle_argument_completion");
// If this isn't implemented, return an empty dict to signal falling back to default completion:
if (!pfunc.IsAllocated())
return {};

PythonList args_list(PyInitialValue::Empty);
for (auto elem : args_vec)
args_list.AppendItem(PythonString(elem));

PythonObject result = pfunc(args_list, PythonInteger(args_pos), PythonInteger(pos_in_arg));
// Returning None means do the ordinary completion
if (result.IsNone())
return {};

// Convert the return dictionary to a DictionarySP.
StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
if (!result_obj_sp)
return {};

StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary(result_obj_sp));
if (dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
return {};
return dict_sp;
}

StructuredData::DictionarySP
lldb_private::python::SWIGBridge::LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand(PyObject *implementor,
llvm::StringRef &long_option, size_t pos_in_arg) {

PyErr_Cleaner py_err_cleaner(true);

PythonObject self(PyRefType::Borrowed, implementor);
auto pfunc = self.ResolveName<PythonCallable>("handle_option_argument_completion");
// If this isn't implemented, return an empty dict to signal falling back to default completion:
if (!pfunc.IsAllocated())
return {};

PythonObject result = pfunc(PythonString(long_option), PythonInteger(pos_in_arg));
// Returning None means do the ordinary completion
if (result.IsNone())
return {};

// Returning a boolean:
// True means the completion was handled, but there were no completions
// False means that the completion was not handled, again, do the ordinary completion:
if (result.GetObjectType() == PyObjectType::Boolean) {
if (!result.IsTrue())
return {};
// Make up a completion dictionary with the right element:
StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary());
dict_sp->AddBooleanItem("no-completion", true);
return dict_sp;
}


// Convert the return dictionary to a DictionarySP.
StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
if (!result_obj_sp)
return {};

StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary(result_obj_sp));
if (dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
return {};
return dict_sp;
}

#include "lldb/Interpreter/CommandReturnObject.h"

bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallParsedCommandObject(
Expand Down
185 changes: 179 additions & 6 deletions lldb/docs/use/python-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ command definition form can't do the right thing.
Since lldb 3.7, Python commands can also be implemented by means of a class
which should implement the following interface:

::
.. code-block:: python

class CommandObjectType:
def __init__(self, debugger, internal_dict):
Expand Down Expand Up @@ -580,20 +580,193 @@ which should implement the following interface:
As a convenience, you can treat the result object as a Python file object, and
say

::
.. code-block:: python

print >>result, "my command does lots of cool stuff"

SBCommandReturnObject and SBStream both support this file-like behavior by
providing write() and flush() calls at the Python layer.

The commands that are added using this class definition are what lldb calls
"raw" commands. The command interpreter doesn't attempt to parse the command,
doesn't handle option values, neither generating help for them, or their
completion. Raw commands are useful when the arguments passed to the command
are unstructured, and having to protect them against lldb command parsing would
be onerous. For instance, "expr" is a raw command.

You can also add scripted commands that implement the "parsed command", where
the options and their types are specified, as well as the argument and argument
types. These commands look and act like the majority of lldb commands, and you
can also add custom completions for the options and/or the arguments if you have
special needs.

The easiest way to do this is to derive your new command from the lldb.ParsedCommand
class. That responds in the same way to the help & repeat command interfaces, and
provides some convenience methods, and most importantly an LLDBOptionValueParser,
accessed throught lldb.ParsedCommand.get_parser(). The parser is used to set
your command definitions, and to retrieve option values in the __call__ method.

To set up the command definition, implement the ParsedCommand abstract method:

.. code-block:: python

def setup_command_definition(self):

This is called when your command is added to lldb. In this method you add the
options and their types, the option help strings, etc. to the command using the API:

.. code-block:: python

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. This can either be
a simple list (e.g. [1, 3, 4, 5]) or you can specify ranges by sublists:
so [1, [3,5]] is the same as [1, 3, 4, 5].
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. If
you need custom completions, implement handle_option_argument_completion.
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.
"""

Similarly, you can add argument types to the command:

.. code-block:: python

def make_argument_element(self, arg_type, repeat = "optional", groups = None):
"""
arg_type: The argument type, one of the lldb.eArgType enum values.
repeat: Choose from the following options:
"plain" - one value
"optional" - zero or more values
"plus" - one or more values
groups: As with add_option.
"""

Then implement the body of the command by defining:

.. code-block:: python

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.
"""

This differs from the "raw" command's __call__ in that the arguments are already
parsed into the args_array, and the option values are set in the parser, and
can be accessed using their property name. The LLDBOptionValueParser class has
a couple of other handy methods:

.. code-block:: python
def was_set(self, long_option_name):

returns True if the option was specified on the command line.

.. code-block:: python

def dest_for_option(self, long_option_name):
"""
This will return the value of the dest variable you defined for opt_name.
Mostly useful for handle_completion where you get passed the long option.
"""

lldb will handle completing your option names, and all your enum values
automatically. If your option or argument types have associated built-in completers,
then lldb will also handle that completion for you. But if you have a need for
custom completions, either in your arguments or option values, you can handle
completion by hand as well. To handle completion of option value arguments,
your lldb.ParsedCommand subclass should implement:

.. code-block:: python

def handle_option_argument_completion(self, long_option, cursor_pos):
"""
long_option: The long option name of the option whose value you are
asked to complete.
cursor_pos: The cursor position in the value for that option - which
you can get from the option parser.
"""

And to handle the completion of arguments:

.. code-block:: python

def handle_argument_completion(self, args, arg_pos, cursor_pos):
"""
args: A list of the arguments to the command
arg_pos: An index into the args list of the argument with the cursor
cursor_pos: The cursor position in the arg specified by arg_pos
"""

When either of these API's is called, the command line will have been parsed up to
the word containing the cursor, and any option values set in that part of the command
string are available from the option value parser. That's useful for instance
if you have a --shared-library option that would constrain the completions for,
say, a symbol name option or argument.

The return value specifies what the completion options are. You have four
choices:

- `True`: the completion was handled with no completions.

- `False`: the completion was not handled, forward it to the regular
completion machinery.

- A dictionary with the key: "completion": there is one candidate,
whose value is the value of the "completion" key. Optionally you can pass a
"mode" key whose value is either "partial" or "complete". Return partial if
the "completion" string is a prefix for all the completed value.

For instance, if the string you are completing is "Test" and the available completions are:
"Test1", "Test11" and "Test111", you should return the dictionary:

.. code-block:: python

return {"completion": "Test1", "mode" : "partial"}

and then lldb will add the "1" at the curson and advance it after the added string,
waiting for more completions. But if "Test1" is the only completion, return:

.. code-block:: python

{"completion": "Test1", "mode": "complete"}

and lldb will add "1 " at the cursor, indicating the command string is complete.

The default is "complete", you don't need to specify a "mode" in that case.

- A dictionary with the key: "values" whose value is a list of candidate completion
strings. The command interpreter will present those strings as the available choices.
You can optionally include a "descriptions" key, whose value is a parallel array
of description strings, and the completion will show the description next to
each completion.


One other handy convenience when defining lldb command-line commands is the
command command script import which will import a module specified by file
command "command script import" which will import a module specified by file
path, so you don't have to change your PYTHONPATH for temporary scripts. It
also has another convenience that if your new script module has a function of
the form:

::
.. code-block python

def __lldb_init_module(debugger, internal_dict):
# Command Initialization code goes here
Expand All @@ -609,7 +782,7 @@ creating scripts that can be run from the command line. However, for command
line scripts, the debugger instance must be created manually. Sample code would
look like:

::
.. code-block:: python

if __name__ == '__main__':
# Initialize the debugger before making any API calls.
Expand All @@ -632,7 +805,7 @@ look like:
Now we can create a module called ls.py in the file ~/ls.py that will implement
a function that can be used by LLDB's python command code:

::
.. code-block:: python

#!/usr/bin/env python

Expand Down
15 changes: 8 additions & 7 deletions lldb/examples/python/cmdtemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def register_lldb_command(cls, debugger, module_name):
)

def setup_command_definition(self):

self.ov_parser.add_option(
ov_parser = self.get_parser()
ov_parser.add_option(
"i",
"in-scope",
help = "in_scope_only = True",
Expand All @@ -36,7 +36,7 @@ def setup_command_definition(self):
default = True,
)

self.ov_parser.add_option(
ov_parser.add_option(
"i",
"in-scope",
help = "in_scope_only = True",
Expand All @@ -45,7 +45,7 @@ def setup_command_definition(self):
default=True,
)

self.ov_parser.add_option(
ov_parser.add_option(
"a",
"arguments",
help = "arguments = True",
Expand All @@ -54,7 +54,7 @@ def setup_command_definition(self):
default = True,
)

self.ov_parser.add_option(
ov_parser.add_option(
"l",
"locals",
help = "locals = True",
Expand All @@ -63,7 +63,7 @@ def setup_command_definition(self):
default = True,
)

self.ov_parser.add_option(
ov_parser.add_option(
"s",
"statics",
help = "statics = True",
Expand Down Expand Up @@ -100,8 +100,9 @@ def __call__(self, debugger, command, exe_ctx, result):
result.SetError("invalid frame")
return

ov_parser = self.get_parser()
variables_list = frame.GetVariables(
self.ov_parser.arguments, self.ov_parser.locals, self.ov_parser.statics, self.ov_parser.inscope
ov_parser.arguments, ov_parser.locals, ov_parser.statics, ov_parser.inscope
)
variables_count = variables_list.GetSize()
if variables_count == 0:
Expand Down
Loading
Loading