[Lldb-commits] [lldb] [lldb/script] Improve `scripting extension list` output and filtering (PR #209400)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Tue Jul 14 13:00:50 PDT 2026
https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/209400
>From 68ff8c0eb55aca6fe722fc73971e37ede2106c4a Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 14 Jul 2026 10:47:59 -0700
Subject: [PATCH] [lldb/script] Improve `scripting extension list` output and
filtering
This patch improves `scripting extension list` in three ways.
First, it groups the output by `ScriptedExtension`: instead of one row
per registered plugin instance, one entry per extension is printed with
a combined `Language` field.
Second, it colorizes and visually separates the output. Each entry is
preceded by a dimmed dashed separator; field labels are printed in bold
green, the extension name value in bold cyan as a mini-heading, and
`None` usage values are dimmed, all via the same
`ansi::FormatAnsiTerminalCodes(..., use_color)` idiom
`Breakpoint::GetDescription` uses elsewhere, gracefully no-op when color
is disabled or unsupported. `ScriptedInterfaceUsages::Dump` takes an
optional `use_color` parameter so its own `API Usages:` /
`Command Interpreter Usages:` labels can match.
Third, it adds `-j`/`--json` to emit a JSON array of
`{name, description, languages, api_usages, command_interpreter_usages}`
per extension, mirroring `plugin list`'s existing `-j`. `DoExecute` now
computes the extension grouping once and branches into
`OutputJsonFormat` or `OutputTextFormat`, sharing a
`GetLanguagesForExtension` helper. It also accepts optional positional
`<extension-name>` arguments to filter the listing to specific
extensions (matched via the same case-insensitive
`ScriptInterpreter::StringToExtension` used by `scripting extension
generate`), with completion wired up via `eScriptedExtensionCompletion`.
Groups whose plugin instances don't match the requested `-l` language
are dropped from the output entirely, so `scripting extension list -l
lua` prints `Available scripted extension templates: None` (and the
JSON path returns `[]`) rather than every group with an empty
`Language:` field.
Also fix a latent lifetime bug in the shared language-collection helper
so it owns its `std::string` values instead of holding `StringRef`s to
temporaries; the JSON path made it observable.
Add `TestScriptingExtensionListJSON.py`, which drives `scripting
extension list -j` (bare, filtered, and language-restricted) through
`SBCommandInterpreter` and parses the output back through
`SBStructuredData::SetFromJSON` to verify the array/object shape and
the presence of a known extension.
Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
lldb/include/lldb/Core/PluginManager.h | 3 +
.../lldb/Interpreter/CommandCompletions.h | 4 +
.../Interpreter/CommandOptionArgumentTable.h | 1 +
.../Interfaces/ScriptedInterfaceUsages.h | 2 +-
.../lldb/Interpreter/ScriptInterpreter.h | 5 +
lldb/include/lldb/lldb-enumerations.h | 4 +-
lldb/source/Commands/CommandCompletions.cpp | 9 +
.../Commands/CommandObjectScripting.cpp | 192 +++++++++++++++---
lldb/source/Commands/Options.td | 3 +
lldb/source/Core/PluginManager.cpp | 13 ++
.../Interfaces/ScriptedInterfaceUsages.cpp | 16 +-
lldb/source/Interpreter/ScriptInterpreter.cpp | 45 ++++
.../TestScriptingExtensionListJSON.py | 103 ++++++++++
.../command-scripting-extension-list.test | 101 ++++-----
14 files changed, 420 insertions(+), 81 deletions(-)
create mode 100644 lldb/test/API/commands/scripting/extension/TestScriptingExtensionListJSON.py
diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h
index 9d36a9361dbc4..230a025c2f9e1 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -687,6 +687,9 @@ class PluginManager {
static ScriptedInterfaceUsages
GetScriptedInterfaceUsagesAtIndex(uint32_t idx);
+ static void AutoCompleteScriptedExtension(llvm::StringRef partial_name,
+ CompletionRequest &request);
+
// REPL
static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
REPLCreateInstance create_callback,
diff --git a/lldb/include/lldb/Interpreter/CommandCompletions.h b/lldb/include/lldb/Interpreter/CommandCompletions.h
index 0c0424cbac6eb..55de91e294e5d 100644
--- a/lldb/include/lldb/Interpreter/CommandCompletions.h
+++ b/lldb/include/lldb/Interpreter/CommandCompletions.h
@@ -127,6 +127,10 @@ class CommandCompletions {
CompletionRequest &request,
SearchFilter *searcher);
+ static void ScriptedExtensions(CommandInterpreter &interpreter,
+ CompletionRequest &request,
+ SearchFilter *searcher);
+
/// This completer works for commands whose only arguments are a command path.
/// It isn't tied to an argument type because it completes not on a single
/// argument but on the sequence of arguments, so you have to invoke it by
diff --git a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
index abb056d9d51df..7075a7acb9733 100644
--- a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
+++ b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
@@ -364,6 +364,7 @@ static constexpr CommandObject::ArgumentTableEntry g_argument_table[] = {
{ lldb::eArgTypeNameMatchStyle, "match-style", lldb::CompletionType::eNoCompletion, g_name_match_style, { nullptr, false }, "Specify the kind of match to use when looking up names." },
{ lldb::eArgTypePluginDomain, "plugin-domain", lldb::CompletionType::eNoCompletion, g_plugin_domain_values, { nullptr, false }, "The domain to apply the plugin operation to." },
{ lldb::eArgTypeBreakpointResolverMask, "resolver-mask", lldb::CompletionType::eNoCompletion, g_resolver_mask_values, { nullptr, false }, "Specify the breakpoint resolver type your override will handle. Can be specified more than once to specify a mask of resolver types." },
+ { lldb::eArgTypeScriptedExtension, "scripting-extension", lldb::CompletionType::eScriptedExtensionCompletion, {}, { nullptr, false }, "The name of a scripting extension." },
// clang-format on
};
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h
index d958e782d86bd..48f38eb70bdb3 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h
@@ -32,7 +32,7 @@ class ScriptedInterfaceUsages {
enum class UsageKind { CommandInterpreter, API };
- void Dump(Stream &s, UsageKind kind) const;
+ void Dump(Stream &s, UsageKind kind, bool use_color = false) const;
private:
std::vector<llvm::StringRef> m_command_interpreter_usages;
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 7ab098f177a80..2b9cb8f7bb866 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -526,6 +526,11 @@ class ScriptInterpreter : public PluginInterface {
static lldb::ScriptLanguage StringToLanguage(const llvm::StringRef &string);
+ static llvm::StringLiteral
+ ExtensionToString(lldb::ScriptedExtension extension);
+
+ static lldb::ScriptedExtension StringToExtension(llvm::StringRef string);
+
lldb::ScriptLanguage GetLanguage() { return m_script_lang; }
virtual lldb::ScriptedProcessInterfaceUP CreateScriptedProcessInterface() {
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index d21081ff754e5..b36ad09f2e892 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -797,6 +797,7 @@ enum CommandArgumentType {
eArgTypeNameMatchStyle,
eArgTypePluginDomain,
eArgTypeBreakpointResolverMask,
+ eArgTypeScriptedExtension,
eArgTypeLastArg // Always keep this entry as the last entry in this
// enumeration!!
};
@@ -1477,10 +1478,11 @@ enum CompletionType {
eCustomCompletion = (1ul << 25),
eThreadIDCompletion = (1ul << 26),
eManagedPluginCompletion = (1ul << 27),
+ eScriptedExtensionCompletion = (1ul << 28),
// This last enum element is just for input validation.
// Add new completions before this element,
// and then increment eTerminatorCompletion's shift value
- eTerminatorCompletion = (1ul << 28)
+ eTerminatorCompletion = (1ul << 29)
};
/// Specifies if children need to be re-computed after a call to \ref
diff --git a/lldb/source/Commands/CommandCompletions.cpp b/lldb/source/Commands/CommandCompletions.cpp
index 9804e579a252d..18ee8f1a12fc0 100644
--- a/lldb/source/Commands/CommandCompletions.cpp
+++ b/lldb/source/Commands/CommandCompletions.cpp
@@ -89,6 +89,8 @@ bool CommandCompletions::InvokeCommonCompletionCallbacks(
CommandCompletions::TypeCategoryNames},
{lldb::eThreadIDCompletion, CommandCompletions::ThreadIDs},
{lldb::eManagedPluginCompletion, CommandCompletions::ManagedPlugins},
+ {lldb::eScriptedExtensionCompletion,
+ CommandCompletions::ScriptedExtensions},
{lldb::eTerminatorCompletion,
nullptr} // This one has to be last in the list.
};
@@ -869,6 +871,13 @@ void CommandCompletions::ManagedPlugins(CommandInterpreter &interpreter,
request);
}
+void CommandCompletions::ScriptedExtensions(CommandInterpreter &interpreter,
+ CompletionRequest &request,
+ SearchFilter *searcher) {
+ PluginManager::AutoCompleteScriptedExtension(
+ request.GetCursorArgumentPrefix(), request);
+}
+
void CommandCompletions::CompleteModifiableCmdPathArgs(
CommandInterpreter &interpreter, CompletionRequest &request,
OptionElementVector &opt_element_vector) {
diff --git a/lldb/source/Commands/CommandObjectScripting.cpp b/lldb/source/Commands/CommandObjectScripting.cpp
index 21400a62d697f..ff02169df1172 100644
--- a/lldb/source/Commands/CommandObjectScripting.cpp
+++ b/lldb/source/Commands/CommandObjectScripting.cpp
@@ -18,8 +18,11 @@
#include "lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h"
#include "lldb/Interpreter/OptionArgParser.h"
#include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Utility/AnsiTerminal.h"
#include "lldb/Utility/Args.h"
+#include "llvm/ADT/StringMap.h"
+
using namespace lldb;
using namespace lldb_private;
@@ -138,12 +141,23 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
: CommandObjectParsed(
interpreter, "scripting extension list",
"List all the available scripting extension templates. ",
- "scripting template list [--language <scripting-language> --]") {}
+ "scripting extension list [--language <scripting-language> --] "
+ "[--json --] [<extension-name> ...]") {
+ AddSimpleArgumentList(eArgTypeScriptedExtension, eArgRepeatStar);
+ }
~CommandObjectScriptingExtensionList() override = default;
Options *GetOptions() override { return &m_options; }
+ void
+ HandleArgumentCompletion(CompletionRequest &request,
+ OptionElementVector &opt_element_vector) override {
+ lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
+ GetCommandInterpreter(), lldb::eScriptedExtensionCompletion, request,
+ nullptr);
+ }
+
class CommandOptions : public Options {
public:
CommandOptions() = default;
@@ -162,6 +176,9 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
error = Status::FromErrorStringWithFormatv(
"unrecognized value for language '{0}'", option_arg);
break;
+ case 'j':
+ m_json_format = true;
+ break;
default:
llvm_unreachable("Unimplemented option");
}
@@ -171,6 +188,7 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
void OptionParsingStarting(ExecutionContext *execution_context) override {
m_language = lldb::eScriptLanguageDefault;
+ m_json_format = false;
}
llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
@@ -178,53 +196,169 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
}
lldb::ScriptLanguage m_language = lldb::eScriptLanguageDefault;
+ bool m_json_format = false;
};
protected:
void DoExecute(Args &command, CommandReturnObject &result) override {
- Stream &s = result.GetOutputStream();
- s.Printf("Available scripted extension templates:");
+ llvm::StringMap<std::vector<size_t>> grouped_by_extension;
+ for (size_t i = 0; i < PluginManager::GetNumScriptedInterfaces(); i++) {
+ lldb::ScriptedExtension extension =
+ PluginManager::GetScriptedInterfaceExtensionAtIndex(i);
+ if (extension == eScriptedExtensionInvalid)
+ continue;
+
+ llvm::StringLiteral extension_name =
+ ScriptInterpreter::ExtensionToString(extension);
+ if (grouped_by_extension.contains(extension_name))
+ grouped_by_extension[extension_name].push_back(i);
+ else
+ grouped_by_extension[extension_name] = {i};
+ }
- auto print_field = [&s](llvm::StringRef key, llvm::StringRef value) {
- if (!value.empty()) {
- s.IndentMore();
- s.Indent();
- s << key << ": " << value << '\n';
- s.IndentLess();
+ if (command.GetArgumentCount() > 0) {
+ llvm::StringMap<std::vector<size_t>> filtered;
+ for (const Args::ArgEntry &arg : command.entries()) {
+ lldb::ScriptedExtension extension =
+ ScriptInterpreter::StringToExtension(arg.ref());
+ if (extension == eScriptedExtensionInvalid) {
+ result.AppendErrorWithFormat("no scripted extension named '%s'",
+ arg.c_str());
+ return;
+ }
+ llvm::StringLiteral extension_name =
+ ScriptInterpreter::ExtensionToString(extension);
+ auto it = grouped_by_extension.find(extension_name);
+ if (it != grouped_by_extension.end())
+ filtered[extension_name] = it->second;
}
- };
+ grouped_by_extension = std::move(filtered);
+ }
- size_t num_listed_interface = 0;
- size_t num_extensions = PluginManager::GetNumScriptedInterfaces();
- for (size_t i = 0; i < num_extensions; i++) {
- llvm::StringRef plugin_name =
- PluginManager::GetScriptedInterfaceNameAtIndex(i);
- if (plugin_name.empty())
- break;
+ if (m_options.m_json_format)
+ OutputJsonFormat(grouped_by_extension, result);
+ else
+ OutputTextFormat(grouped_by_extension, result);
+ }
+private:
+ std::vector<std::string>
+ GetLanguagesForExtension(const std::vector<size_t> &indices) {
+ std::vector<std::string> languages;
+ for (const size_t idx : indices) {
lldb::ScriptLanguage lang =
- PluginManager::GetScriptedInterfaceLanguageAtIndex(i);
+ PluginManager::GetScriptedInterfaceLanguageAtIndex(idx);
if (lang != m_options.m_language)
continue;
+ languages.push_back(ScriptInterpreter::LanguageToString(lang));
+ }
+ return languages;
+ }
- if (!num_listed_interface)
- s.EOL();
+ void OutputJsonFormat(
+ const llvm::StringMap<std::vector<size_t>> &grouped_by_extension,
+ CommandReturnObject &result) {
+ llvm::json::Array extensions;
+ for (const auto &extension_pair : grouped_by_extension) {
+ // llvm::json::Value's StringRef constructor does not copy the
+ // underlying characters, so every string handed to the JSON structure
+ // below must be an owned std::string -- otherwise it dangles by the
+ // time the object tree is serialized at the end of this function.
+ llvm::json::Array languages;
+ for (const std::string &lang :
+ GetLanguagesForExtension(extension_pair.second))
+ languages.push_back(lang);
+ if (languages.empty())
+ continue;
+ llvm::StringRef desc =
+ PluginManager::GetScriptedInterfaceDescriptionAtIndex(
+ extension_pair.second[0]);
+ ScriptedInterfaceUsages usages =
+ PluginManager::GetScriptedInterfaceUsagesAtIndex(
+ extension_pair.second[0]);
+
+ llvm::json::Array api_usages;
+ for (llvm::StringRef usage : usages.GetSBAPIUsages())
+ api_usages.push_back(usage.str());
+
+ llvm::json::Array cmd_usages;
+ for (llvm::StringRef usage : usages.GetCommandInterpreterUsages())
+ cmd_usages.push_back(usage.str());
+
+ extensions.push_back(llvm::json::Object{
+ {"name", extension_pair.first().str()},
+ {"description", desc.str()},
+ {"languages", std::move(languages)},
+ {"api_usages", std::move(api_usages)},
+ {"command_interpreter_usages", std::move(cmd_usages)},
+ });
+ }
+
+ std::string str;
+ llvm::raw_string_ostream os(str);
+ os << llvm::formatv("{0:2}", llvm::json::Value(std::move(extensions)));
+ result.AppendMessage(str);
+ result.SetStatus(eReturnStatusSuccessFinishResult);
+ }
+
+ void OutputTextFormat(
+ const llvm::StringMap<std::vector<size_t>> &grouped_by_extension,
+ CommandReturnObject &result) {
+ Stream &s = result.GetOutputStream();
+ const bool use_color = s.AsRawOstream().colors_enabled();
+ auto ansi_code = [use_color](llvm::StringRef code) {
+ return ansi::FormatAnsiTerminalCodes(code, use_color);
+ };
+ const std::string label_color = ansi_code("${ansi.fg.green}${ansi.bold}");
+ const std::string name_color = ansi_code("${ansi.fg.cyan}${ansi.bold}");
+ const std::string sep_color = ansi_code("${ansi.faint}");
+ const std::string reset = ansi_code("${ansi.normal}");
+ const std::string separator(
+ std::min<uint64_t>(GetDebugger().GetTerminalWidth(), 80), '-');
+
+ s.Printf("Available scripted extension templates:");
+
+ auto print_field = [&](llvm::StringRef key, llvm::StringRef value,
+ const std::string &value_color = std::string()) {
+ if (value.empty())
+ return;
+ s.IndentMore();
+ s.Indent();
+ s << label_color << key << ": " << reset;
+ if (!value_color.empty())
+ s << value_color << value << reset;
+ else
+ s << value;
+ s << '\n';
+ s.IndentLess();
+ };
+
+ size_t num_listed_interface = 0;
+ for (const auto &extension_pair : grouped_by_extension) {
+ std::vector<std::string> languages =
+ GetLanguagesForExtension(extension_pair.second);
+ if (languages.empty())
+ continue;
num_listed_interface++;
+ s.EOL();
+ s << sep_color << separator << reset;
+ s.EOL();
+
llvm::StringRef desc =
- PluginManager::GetScriptedInterfaceDescriptionAtIndex(i);
+ PluginManager::GetScriptedInterfaceDescriptionAtIndex(
+ extension_pair.second[0]);
ScriptedInterfaceUsages usages =
- PluginManager::GetScriptedInterfaceUsagesAtIndex(i);
+ PluginManager::GetScriptedInterfaceUsagesAtIndex(
+ extension_pair.second[0]);
- print_field("Name", plugin_name);
- print_field("Language", ScriptInterpreter::LanguageToString(lang));
+ print_field("Name", extension_pair.first(), name_color);
print_field("Description", desc);
- usages.Dump(s, ScriptedInterfaceUsages::UsageKind::API);
- usages.Dump(s, ScriptedInterfaceUsages::UsageKind::CommandInterpreter);
-
- if (i != num_extensions - 1)
- s.EOL();
+ print_field("Language", llvm::join(languages, ""));
+ usages.Dump(s, ScriptedInterfaceUsages::UsageKind::API, use_color);
+ usages.Dump(s, ScriptedInterfaceUsages::UsageKind::CommandInterpreter,
+ use_color);
}
if (!num_listed_interface)
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index 7e6e2c173d67f..bd6fa8efad0a5 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1582,6 +1582,9 @@ let Command = "scripting extension list" in {
Desc<"Specify the scripting "
" language. If none is specified the default scripting language "
"is used.">;
+ def scripting_extension_list_json
+ : Option<"json", "j">,
+ Desc<"Output the scripted extension list in json format.">;
}
let Command = "source info" in {
diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp
index 34b7f87b09af8..893993d2b24a1 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -2126,6 +2126,19 @@ PluginManager::GetScriptedInterfaceUsagesAtIndex(uint32_t idx) {
return {};
}
+void PluginManager::AutoCompleteScriptedExtension(llvm::StringRef name,
+ CompletionRequest &request) {
+ for (size_t idx = 0; idx < GetNumScriptedInterfaces(); idx++) {
+ if (auto instance =
+ GetScriptedInterfaceInstances().GetInstanceAtIndex(idx)) {
+ llvm::StringLiteral extension_name =
+ ScriptInterpreter::ExtensionToString(instance->extension);
+ if (extension_name.starts_with(name))
+ request.AddCompletion(extension_name);
+ }
+ }
+}
+
#pragma mark REPL
struct REPLInstance : public PluginInstance<REPLCreateInstance> {
diff --git a/lldb/source/Interpreter/Interfaces/ScriptedInterfaceUsages.cpp b/lldb/source/Interpreter/Interfaces/ScriptedInterfaceUsages.cpp
index 05d7a5d852f8c..0f1cc83a70a38 100644
--- a/lldb/source/Interpreter/Interfaces/ScriptedInterfaceUsages.cpp
+++ b/lldb/source/Interpreter/Interfaces/ScriptedInterfaceUsages.cpp
@@ -8,20 +8,30 @@
#include "lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h"
+#include "lldb/Utility/AnsiTerminal.h"
+
using namespace lldb;
using namespace lldb_private;
-void ScriptedInterfaceUsages::Dump(Stream &s, UsageKind kind) const {
+void ScriptedInterfaceUsages::Dump(Stream &s, UsageKind kind,
+ bool use_color) const {
+ const std::string label =
+ ansi::FormatAnsiTerminalCodes("${ansi.fg.green}${ansi.bold}", use_color);
+ const std::string dim =
+ ansi::FormatAnsiTerminalCodes("${ansi.faint}", use_color);
+ const std::string reset =
+ ansi::FormatAnsiTerminalCodes("${ansi.normal}", use_color);
+
s.IndentMore();
s.Indent();
llvm::StringRef usage_kind =
(kind == UsageKind::CommandInterpreter) ? "Command Interpreter" : "API";
- s << usage_kind << " Usages:";
+ s << label << usage_kind << " Usages:" << reset;
const std::vector<llvm::StringRef> &usages =
(kind == UsageKind::CommandInterpreter) ? GetCommandInterpreterUsages()
: GetSBAPIUsages();
if (usages.empty())
- s << " None\n";
+ s << ' ' << dim << "None" << reset << '\n';
else if (usages.size() == 1)
s << " " << usages.front() << '\n';
else {
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 8dafb7f823e70..8420782b02814 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -16,6 +16,7 @@
#include "lldb/Utility/Stream.h"
#include "lldb/Utility/StringList.h"
#include "lldb/ValueObject/ValueObject.h"
+#include "llvm/ADT/StringSwitch.h"
#if defined(_WIN32)
#include "lldb/Host/windows/ConnectionGenericFileWindows.h"
#endif
@@ -188,6 +189,50 @@ ScriptInterpreter::StringToLanguage(const llvm::StringRef &language) {
return eScriptLanguageUnknown;
}
+llvm::StringLiteral
+ScriptInterpreter::ExtensionToString(lldb::ScriptedExtension extension) {
+ switch (extension) {
+ case eScriptedExtensionInvalid:
+ return "Invalid";
+ case eScriptedExtensionOperatingSystem:
+ return "OperatingSystem";
+ case eScriptedExtensionScriptedPlatform:
+ return "ScriptedPlatform";
+ case eScriptedExtensionScriptedProcess:
+ return "ScriptedProcess";
+ case eScriptedExtensionScriptedBreakpointResolver:
+ return "ScriptedBreakpointResolver";
+ case eScriptedExtensionScriptedThreadPlan:
+ return "ScriptedThreadPlan";
+ case eScriptedExtensionScriptedFrameProvider:
+ return "ScriptedFrameProvider";
+ case eScriptedExtensionScriptedHook:
+ return "ScriptedHook";
+ case eScriptedExtensionScriptedThread:
+ return "ScriptedThread";
+ case eScriptedExtensionScriptedFrame:
+ return "ScriptedFrame";
+ }
+ llvm_unreachable("unhandled ScriptedExtension");
+}
+
+lldb::ScriptedExtension
+ScriptInterpreter::StringToExtension(llvm::StringRef string) {
+ return llvm::StringSwitch<lldb::ScriptedExtension>(string)
+ .CaseLower("OperatingSystem", eScriptedExtensionOperatingSystem)
+ .CaseLower("ScriptedPlatform", eScriptedExtensionScriptedPlatform)
+ .CaseLower("ScriptedProcess", eScriptedExtensionScriptedProcess)
+ .CaseLower("ScriptedBreakpointResolver",
+ eScriptedExtensionScriptedBreakpointResolver)
+ .CaseLower("ScriptedThreadPlan", eScriptedExtensionScriptedThreadPlan)
+ .CaseLower("ScriptedFrameProvider",
+ eScriptedExtensionScriptedFrameProvider)
+ .CaseLower("ScriptedHook", eScriptedExtensionScriptedHook)
+ .CaseLower("ScriptedThread", eScriptedExtensionScriptedThread)
+ .CaseLower("ScriptedFrame", eScriptedExtensionScriptedFrame)
+ .Default(eScriptedExtensionInvalid);
+}
+
Status ScriptInterpreter::SetBreakpointCommandCallback(
std::vector<std::reference_wrapper<BreakpointOptions>> &bp_options_vec,
const char *callback_text) {
diff --git a/lldb/test/API/commands/scripting/extension/TestScriptingExtensionListJSON.py b/lldb/test/API/commands/scripting/extension/TestScriptingExtensionListJSON.py
new file mode 100644
index 0000000000000..5d92717babc4b
--- /dev/null
+++ b/lldb/test/API/commands/scripting/extension/TestScriptingExtensionListJSON.py
@@ -0,0 +1,103 @@
+"""
+Verify that `scripting extension list -j` emits a well-formed JSON array of
+scripted-extension records that can be round-tripped through SBStructuredData.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TestScriptingExtensionListJSON(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def run_command(self, command):
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand(command, result)
+ self.assertTrue(result.Succeeded(), result.GetError())
+ return result.GetOutput()
+
+ def run_and_parse(self, command):
+ data = lldb.SBStructuredData()
+ self.assertSuccess(data.SetFromJSON(self.run_command(command)))
+ return data
+
+ @skipIfNoSBHeaders
+ def test_json_output_shape(self):
+ """Every entry has the expected top-level keys and array-valued
+ `languages`, `api_usages`, `command_interpreter_usages`."""
+ data = self.run_and_parse("scripting extension list -j")
+ self.assertEqual(data.GetType(), lldb.eStructuredDataTypeArray)
+ self.assertGreater(data.GetSize(), 0)
+
+ expected_keys = {
+ "name",
+ "description",
+ "languages",
+ "api_usages",
+ "command_interpreter_usages",
+ }
+ for i in range(data.GetSize()):
+ entry = data.GetItemAtIndex(i)
+ self.assertEqual(entry.GetType(), lldb.eStructuredDataTypeDictionary)
+ keys = lldb.SBStringList()
+ entry.GetKeys(keys)
+ got = {keys.GetStringAtIndex(k) for k in range(keys.GetSize())}
+ self.assertEqual(got, expected_keys)
+ self.assertEqual(
+ entry.GetValueForKey("name").GetType(),
+ lldb.eStructuredDataTypeString,
+ )
+ self.assertEqual(
+ entry.GetValueForKey("description").GetType(),
+ lldb.eStructuredDataTypeString,
+ )
+ for array_key in ("languages", "api_usages", "command_interpreter_usages"):
+ self.assertEqual(
+ entry.GetValueForKey(array_key).GetType(),
+ lldb.eStructuredDataTypeArray,
+ array_key,
+ )
+
+ def test_json_includes_known_extension(self):
+ """`ScriptedProcess` is registered unconditionally, so it must appear
+ with `SBTarget.Launch` in its `api_usages`."""
+ data = self.run_and_parse("scripting extension list -j")
+
+ names = set()
+ scripted_process_entry = None
+ for i in range(data.GetSize()):
+ entry = data.GetItemAtIndex(i)
+ name = entry.GetValueForKey("name").GetStringValue(256)
+ names.add(name)
+ if name == "ScriptedProcess":
+ scripted_process_entry = entry
+ self.assertIn("ScriptedProcess", names)
+
+ api_usages = scripted_process_entry.GetValueForKey("api_usages")
+ found_launch = False
+ for i in range(api_usages.GetSize()):
+ usage = api_usages.GetItemAtIndex(i).GetStringValue(256)
+ if usage == "SBTarget.Launch":
+ found_launch = True
+ break
+ self.assertTrue(
+ found_launch,
+ f"expected SBTarget.Launch in ScriptedProcess.api_usages",
+ )
+
+ def test_json_name_filter(self):
+ """`scripting extension list -j <name>` restricts output to that
+ one extension."""
+ data = self.run_and_parse("scripting extension list -j ScriptedProcess")
+ self.assertEqual(data.GetSize(), 1)
+ entry = data.GetItemAtIndex(0)
+ self.assertEqual(
+ entry.GetValueForKey("name").GetStringValue(256), "ScriptedProcess"
+ )
+
+ def test_json_lua_yields_empty_array(self):
+ """No lua-registered extensions -> empty JSON array."""
+ data = self.run_and_parse("scripting extension list -j -l lua --")
+ self.assertEqual(data.GetType(), lldb.eStructuredDataTypeArray)
+ self.assertEqual(data.GetSize(), 0)
diff --git a/lldb/test/Shell/Commands/command-scripting-extension-list.test b/lldb/test/Shell/Commands/command-scripting-extension-list.test
index 8ca183a609671..880d4035516de 100644
--- a/lldb/test/Shell/Commands/command-scripting-extension-list.test
+++ b/lldb/test/Shell/Commands/command-scripting-extension-list.test
@@ -2,53 +2,60 @@
# RUN: %lldb -s %s -o exit | FileCheck %s
scripting extension list
-# CHECK:Available scripted extension templates:
-
-# CHECK: Name: OperatingSystemPythonInterface
-# CHECK-NEXT: Language: Python
-# CHECK-NEXT: Description: Mock thread state
-# CHECK-NEXT: API Usages: None
-# CHECK-NEXT: Command Interpreter Usages:
-# CHECK-NEXT: settings set target.process.python-os-plugin-path <script-path>
-# CHECK-NEXT: settings set process.experimental.os-plugin-reports-all-threads [0/1]
-
-# CHECK: Name: ScriptedPlatformPythonInterface
-# CHECK-NEXT: Language: Python
-# CHECK-NEXT: Description: Mock platform and interact with its processes.
-# CHECK-NEXT: API Usages: None
-# CHECK-NEXT: Command Interpreter Usages: None
-
-# CHECK: Name: ScriptedProcessPythonInterface
-# CHECK-NEXT: Language: Python
-# CHECK-NEXT: Description: Mock process state
-# CHECK-NEXT: API Usages:
-# CHECK-NEXT: SBAttachInfo.SetScriptedProcessClassName
-# CHECK-NEXT: SBAttachInfo.SetScriptedProcessDictionary
-# CHECK-NEXT: SBTarget.Attach
-# CHECK-NEXT: SBLaunchInfo.SetScriptedProcessClassName
-# CHECK-NEXT: SBLaunchInfo.SetScriptedProcessDictionary
-# CHECK-NEXT: SBTarget.Launch
-# CHECK-NEXT: Command Interpreter Usages:
-# CHECK-NEXT: process attach -C <script-name> [-k key -v value ...]
-# CHECK-NEXT: process launch -C <script-name> [-k key -v value ...]
-
-# CHECK: Name: ScriptedThreadPlanPythonInterface
-# CHECK-NEXT: Language: Python
-# CHECK-NEXT: Description: Alter thread stepping logic and stop reason
-# CHECK-NEXT: API Usages: SBThread.StepUsingScriptedThreadPlan
-# CHECK-NEXT: Command Interpreter Usages: thread step-scripted -C <script-name> [-k key -v value ...]
-
-# CHECK: Name: ScriptedThreadPythonInterface
-# CHECK-NEXT: Language: Python
-# CHECK-NEXT: Description: Provide thread state for a scripted process.
-# CHECK-NEXT: API Usages: None
-# CHECK-NEXT: Command Interpreter Usages: None
-
-# CHECK: Name: ScriptedFramePythonInterface
-# CHECK-NEXT: Language: Python
-# CHECK-NEXT: Description: Provide frame state for scripted threads and frame providers.
-# CHECK-NEXT: API Usages: None
-# CHECK-NEXT: Command Interpreter Usages: None
+# CHECK: Available scripted extension templates:
+
+# The extensions are grouped by lldb::ScriptedExtension in a hash map, so
+# groups are not printed in a fixed order across builds. Match each group's
+# fields with CHECK-DAG instead of CHECK-NEXT.
+# CHECK-DAG: Name: OperatingSystem
+# CHECK-DAG: Language: Python
+# CHECK-DAG: Description: Mock thread state
+# CHECK-DAG: settings set target.process.python-os-plugin-path <script-path>
+# CHECK-DAG: settings set process.experimental.os-plugin-reports-all-threads [0/1]
+
+# CHECK-DAG: Name: ScriptedPlatform
+# CHECK-DAG: Description: Mock platform and interact with its processes.
+
+# CHECK-DAG: Name: ScriptedProcess
+# CHECK-DAG: Description: Mock process state
+# CHECK-DAG: SBAttachInfo.SetScriptedProcessClassName
+# CHECK-DAG: SBAttachInfo.SetScriptedProcessDictionary
+# CHECK-DAG: SBTarget.Attach
+# CHECK-DAG: SBLaunchInfo.SetScriptedProcessClassName
+# CHECK-DAG: SBLaunchInfo.SetScriptedProcessDictionary
+# CHECK-DAG: SBTarget.Launch
+# CHECK-DAG: process attach -C <script-name> [-k key -v value ...]
+# CHECK-DAG: process launch -C <script-name> [-k key -v value ...]
+
+# CHECK-DAG: Name: ScriptedThreadPlan
+# CHECK-DAG: Description: Alter thread stepping logic and stop reason
+# CHECK-DAG: SBThread.StepUsingScriptedThreadPlan
+# CHECK-DAG: thread step-scripted -C <script-name> [-k key -v value ...]
+
+# CHECK-DAG: Name: ScriptedBreakpointResolver
+# CHECK-DAG: Description: Create a breakpoint that chooses locations based on user-created callbacks
+# CHECK-DAG: SBTarget.BreakpointCreateFromScript
+# CHECK-DAG: breakpoint set -P classname [-k key -v value ...]
+
+# CHECK-DAG: Name: ScriptedFrameProvider
+# CHECK-DAG: Description: Provide scripted stack frames for threads
+# CHECK-DAG: SBTarget.RegisterScriptedFrameProvider
+# CHECK-DAG: SBTarget.RemoveScriptedFrameProvider
+# CHECK-DAG: SBTarget.ClearScriptedFrameProvider
+# CHECK-DAG: target frame-provider register -C <script-name> [-k key -v value ...]
+# CHECK-DAG: target frame-provider list
+# CHECK-DAG: target frame-provider remove <provider-name>
+# CHECK-DAG: target frame-provider clear
+
+# CHECK-DAG: Name: ScriptedHook
+# CHECK-DAG: Description: Perform actions on target lifecycle events (module load/unload, process stop).
+# CHECK-DAG: target hook add -P <script-name> [-k key -v value ...]
+
+# CHECK-DAG: Name: ScriptedThread
+# CHECK-DAG: Description: Provide thread state for a scripted process.
+
+# CHECK-DAG: Name: ScriptedFrame
+# CHECK-DAG: Description: Provide frame state for scripted threads and frame providers.
scripting extension list -l lua
# CHECK: Available scripted extension templates: None
More information about the lldb-commits
mailing list