jingham created this revision. jingham added reviewers: clayborg, JDevlieghere. Herald added subscribers: dang, mgorny. jingham requested review of this revision. Herald added a project: LLDB. Herald added a subscriber: lldb-commits.
One of the ways that we organize all the functionality provided by lldb is to use a hierarchical command structure, with nested commands. That means that when you first look at lldb, you see an organized set of commands mirroring the basic constructs in a debugger. Then we use aliases to make the most commonly used commands available in a couple of keystrokes. But when you start adding user commands, since they have to be root commands, you quickly overwhelm this orderly setup. To fix this problem, I'm adding the ability for users who add commands to also nest them in a sensible hierarchy. This change keeps the "built-in" and the "user added" command hierarchies separate, you can't add user commands or multiword subtrees into the builtin commands. I think it would be too confusing if we started having user commands sprinkled among the built-in commands. I also don't allow you to add aliases into multiword command trees. The whole point of aliases is to make something available at the top level, so this didn't seem to make sense. I apologize in advance for the size of the patch, but there really wasn't anything testable up till it was actually working, which is pretty much this patch. Repository: rG LLVM Github Monorepo https://reviews.llvm.org/D110298 Files: lldb/include/lldb/Interpreter/CommandCompletions.h lldb/include/lldb/Interpreter/CommandInterpreter.h lldb/include/lldb/Interpreter/CommandObject.h lldb/include/lldb/Interpreter/CommandObjectMultiword.h lldb/source/API/SBCommandInterpreter.cpp lldb/source/Commands/CommandCompletions.cpp lldb/source/Commands/CommandObjectApropos.cpp lldb/source/Commands/CommandObjectCommands.cpp lldb/source/Commands/CommandObjectHelp.cpp lldb/source/Commands/CommandObjectMultiword.cpp lldb/source/Commands/Options.td lldb/source/Interpreter/CommandInterpreter.cpp lldb/source/Interpreter/CommandObject.cpp lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py lldb/test/API/commands/command/multiword/TestMultiwordCommands.py lldb/test/API/commands/command/multiword/welcome.py lldb/test/API/commands/command/script/TestCommandScript.py lldb/test/API/commands/expression/char/main.cpp lldb/test/API/functionalities/completion/TestCompletion.py lldb/unittests/Interpreter/CMakeLists.txt lldb/unittests/Interpreter/TestCommandPaths.cpp
Index: lldb/unittests/Interpreter/TestCommandPaths.cpp =================================================================== --- /dev/null +++ lldb/unittests/Interpreter/TestCommandPaths.cpp @@ -0,0 +1,159 @@ +//===-- ProcessEventDataTest.cpp ------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "Plugins/Platform/MacOSX/PlatformMacOSX.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Interpreter/CommandObject.h" +#include "lldb/Interpreter/CommandObjectMultiword.h" +#include "lldb/Interpreter/CommandReturnObject.h" +#include "lldb/Utility/Args.h" +#include "lldb/Utility/Reproducer.h" +#include "lldb/Utility/Status.h" + +#include "gtest/gtest.h" + +using namespace lldb_private; +using namespace lldb_private::repro; +using namespace lldb; + +namespace { +class VerifyUserMultiwordCmdPathTest : public ::testing::Test { + void SetUp() override { + llvm::cantFail(Reproducer::Initialize(ReproducerMode::Off, llvm::None)); + FileSystem::Initialize(); + HostInfo::Initialize(); + PlatformMacOSX::Initialize(); + } + void TearDown() override { + PlatformMacOSX::Terminate(); + HostInfo::Terminate(); + FileSystem::Terminate(); + Reproducer::Terminate(); + } +}; +} // namespace + +class CommandObjectLeaf : public CommandObjectParsed { +public: + CommandObjectLeaf(CommandInterpreter &interpreter) + : CommandObjectParsed(interpreter, "dummy subcommand leaf", + "Does nothing", "dummy subcommand leaf") { + SetIsUserCommand(true); + } + +protected: + virtual bool DoExecute(Args &command, CommandReturnObject &result) { + result.SetStatus(eReturnStatusSuccessFinishResult); + result.AppendMessage("I did nothing"); + return true; + } +}; + +class CommandObjectMultiwordSubDummy : public CommandObjectMultiword { +public: + CommandObjectMultiwordSubDummy(CommandInterpreter &interpreter) + : CommandObjectMultiword(interpreter, "dummy subcommand", "Does nothing", + "dummy subcommand") { + SetIsUserCommand(true); + LoadSubCommand("leaf", CommandObjectSP(new CommandObjectLeaf(interpreter))); + } + + ~CommandObjectMultiwordSubDummy() override = default; +}; + +class CommandObjectMultiwordDummy : public CommandObjectMultiword { +public: + CommandObjectMultiwordDummy(CommandInterpreter &interpreter) + : CommandObjectMultiword(interpreter, "dummy", "Does nothing", "dummy") { + SetIsUserCommand(true); + LoadSubCommand( + "subcommand", + CommandObjectSP(new CommandObjectMultiwordSubDummy(interpreter))); + } + + ~CommandObjectMultiwordDummy() override = default; +}; + +// Pass in the command path to args. If success is true, we make sure the MWC +// returned matches the test string. If success is false, we make sure the +// lookup error matches test_str. +void RunTest(CommandInterpreter &interp, const char *args, bool is_leaf, + bool success, const char *test_str) { + CommandObjectMultiword *multi_word_cmd = nullptr; + Args test_args(args); + Status error; + multi_word_cmd = + interp.VerifyUserMultiwordCmdPath(test_args, is_leaf, error); + if (success) { + ASSERT_NE(multi_word_cmd, nullptr); + ASSERT_TRUE(error.Success()); + ASSERT_STREQ(multi_word_cmd->GetCommandName().str().c_str(), test_str); + } else { + ASSERT_EQ(multi_word_cmd, nullptr); + ASSERT_TRUE(error.Fail()); + ASSERT_STREQ(error.AsCString(), test_str); + } +} + +TEST_F(VerifyUserMultiwordCmdPathTest, TestErrors) { + DebuggerSP debugger_sp = Debugger::CreateInstance(); + ASSERT_TRUE(debugger_sp); + + CommandInterpreter &interp = debugger_sp->GetCommandInterpreter(); + + Status error; + bool success; + bool is_leaf; + + // Test that we reject non-user path components: + success = false; + is_leaf = true; + RunTest(interp, "process launch", is_leaf, success, + "Path component: 'process' is not a user command"); + + // Test that we reject non-existent commands: + is_leaf = true; + success = false; + RunTest(interp, "wewouldnevernameacommandthis subcommand", is_leaf, success, + "Path component: 'wewouldnevernameacommandthis' not found"); + + // Now we have to add a multiword command, and then probe it. + error = interp.AddUserCommand( + "dummy", CommandObjectSP(new CommandObjectMultiwordDummy(interp)), true); + ASSERT_TRUE(error.Success()); + + // Now pass the correct path, and make sure we get back the right MWC. + is_leaf = false; + success = true; + RunTest(interp, "dummy subcommand", is_leaf, success, "dummy subcommand"); + + is_leaf = true; + RunTest(interp, "dummy subcommand", is_leaf, success, "dummy"); + + // If you tell us the last node is a leaf, we don't check that. Make sure + // that is true: + is_leaf = true; + success = true; + RunTest(interp, "dummy subcommand leaf", is_leaf, success, + "dummy subcommand"); + // But we should fail if we say the last component is a multiword: + + is_leaf = false; + success = false; + RunTest(interp, "dummy subcommand leaf", is_leaf, success, + "Path component: 'leaf' is not a multiword command"); + + // We should fail if we get the second path component wrong: + is_leaf = false; + success = false; + RunTest(interp, "dummy not-subcommand", is_leaf, success, + "Path component: 'not-subcommand' not found"); +} Index: lldb/unittests/Interpreter/CMakeLists.txt =================================================================== --- lldb/unittests/Interpreter/CMakeLists.txt +++ lldb/unittests/Interpreter/CMakeLists.txt @@ -1,10 +1,17 @@ add_lldb_unittest(InterpreterTests + TestCommandPaths.cpp TestCompletion.cpp TestOptionArgParser.cpp TestOptionValue.cpp TestOptionValueFileColonLine.cpp LINK_LIBS - lldbInterpreter - lldbUtilityHelpers - ) + lldbCore + lldbHost + lldbTarget + lldbSymbol + lldbUtility + lldbUtilityHelpers + lldbInterpreter + lldbPluginPlatformMacOSX +) Index: lldb/test/API/functionalities/completion/TestCompletion.py =================================================================== --- lldb/test/API/functionalities/completion/TestCompletion.py +++ lldb/test/API/functionalities/completion/TestCompletion.py @@ -511,7 +511,7 @@ def test_command_script_delete(self): self.runCmd("command script add -h test_desc -f none -s current usercmd1") - self.check_completion_with_desc('command script delete ', [['usercmd1', 'test_desc']]) + self.check_completion_with_desc('command script delete ', [['usercmd1', '']]) def test_command_delete(self): self.runCmd(r"command regex test_command s/^$/finish/ 's/([0-9]+)/frame select %1/'") Index: lldb/test/API/commands/expression/char/main.cpp =================================================================== --- lldb/test/API/commands/expression/char/main.cpp +++ lldb/test/API/commands/expression/char/main.cpp @@ -1,3 +1,5 @@ +#include <stdio.h> + int foo(char c) { return 1; } int foo(signed char c) { return 2; } int foo(unsigned char c) { return 3; } @@ -6,5 +8,6 @@ char c = 0; signed char sc = 0; unsigned char uc = 0; + printf("%d %d %d\n", foo(c), foo(sc), foo(uc)); return 0; // Break here } Index: lldb/test/API/commands/command/script/TestCommandScript.py =================================================================== --- lldb/test/API/commands/command/script/TestCommandScript.py +++ lldb/test/API/commands/command/script/TestCommandScript.py @@ -148,7 +148,7 @@ self.expect('my_command Blah', substrs=['Hello Blah, welcome to LLDB']) self.runCmd( - 'command script add my_command --class welcome.TargetnameCommand') + 'command script add my_command -o --class welcome.TargetnameCommand') self.expect('my_command', substrs=['a.out']) self.runCmd("command script clear") Index: lldb/test/API/commands/command/multiword/welcome.py =================================================================== --- /dev/null +++ lldb/test/API/commands/command/multiword/welcome.py @@ -0,0 +1,28 @@ +from __future__ import print_function +import lldb +import sys + + +class WelcomeCommand(object): + + def __init__(self, debugger, session_dict): + pass + + def get_short_help(self): + return "Just a docstring for Welcome\nA command that says hello to LLDB users" + + def __call__(self, debugger, args, exe_ctx, result): + print('Hello ' + args + ', welcome to LLDB', file=result) + return None + +class WelcomeCommand2(object): + + def __init__(self, debugger, session_dict): + pass + + def get_short_help(self): + return "Just a docstring for the second Welcome\nA command that says hello to LLDB users" + + def __call__(self, debugger, args, exe_ctx, result): + print('Hello ' + args + ', welcome again to LLDB', file=result) + return None Index: lldb/test/API/commands/command/multiword/TestMultiwordCommands.py =================================================================== --- /dev/null +++ lldb/test/API/commands/command/multiword/TestMultiwordCommands.py @@ -0,0 +1,127 @@ +""" +Test user added multiword commands +""" + + +import sys +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * + + +class TestCmdMultiword(TestBase): + + mydir = TestBase.compute_mydir(__file__) + NO_DEBUG_INFO_TESTCASE = True + + def test_multiword_add(self): + self.multiword_add() + + def check_command_tree_exists(self): + """This makes sure we can still run the command tree we added.""" + self.runCmd("test-multi") + self.runCmd("test-multi test-multi-sub") + self.runCmd("test-multi test-multi-sub welcome") + + def multiword_add(self): + # Make sure we can't overwrite built-in commands: + self.expect("command multiword add process", "Can't replace builtin multiword command", + substrs=["can't replace builtin command"], error=True) + self.expect("command multiword add process non_such_subcommand", "Can't add to built-in subcommand", + substrs=["Path component: 'process' is not a user command"], error=True) + self.expect("command multiword add process launch", "Can't replace builtin subcommand", + substrs=["Path component: 'process' is not a user command"], error=True) + + # Now lets make a multiword command: + self.runCmd("command multiword add -h 'A test multiword command' test-multi") + # Make sure the help works: + self.expect("help test-multi", "Help works for top-level multi", + substrs=["A test multiword command"]) + # Add a subcommand: + self.runCmd("command multiword add -h 'A test multiword sub-command' test-multi test-multi-sub") + # Make sure the help works: + self.expect("help test-multi", "Help shows sub-multi", + substrs=["A test multiword command", "test-multi-sub -- A test multiword sub-command"]) + self.expect("help test-multi test-multi-sub", "Help shows sub-multi", + substrs=["A test multiword sub-command"]) + + # Now add a script based command to the multiword command: + self.runCmd("command script import welcome.py") + self.runCmd("command script add -c welcome.WelcomeCommand test-multi test-multi-sub welcome") + # Make sure the help still works: + self.expect("help test-multi test-multi-sub", "Listing subcommands works", + substrs=["A test multiword sub-command", "welcome -- Just a docstring for Welcome"]) + self.expect("help test-multi test-multi-sub welcome", "Subcommand help works", + substrs=["Just a docstring for Welcome"]) + # And make sure it actually works: + self.expect("test-multi test-multi-sub welcome friend", "Test command works", + substrs=["Hello friend, welcome to LLDB"]) + + # Make sure overwriting works, first the leaf command: + # We should not be able to remove extant commands by default: + self.expect("command script add -c welcome.WelcomeCommand2 test-multi test-multi-sub welcome", + "overwrite command w/o -o", + substrs=["cannot add command: sub-command already exists"], error=True) + # But we can with the -o option: + self.runCmd("command script add -c welcome.WelcomeCommand2 -o test-multi test-multi-sub welcome") + # Make sure we really did overwrite: + self.expect("test-multi test-multi-sub welcome friend", "Used the new command class", + substrs=["Hello friend, welcome again to LLDB"]) + + self.expect("apropos welcome", "welcome should show up in apropos", substrs=["Just a docstring for the second Welcome"]) + + # Make sure we give good errors when the input is wrong: + self.expect("command script delete test-mult test-multi-sub welcome", "Delete script command - wrong first path component", + substrs=["'test-mult' not found"], error=True) + + self.expect("command script delete test-multi test-multi-su welcome", "Delete script command - wrong second path component", + substrs=["'test-multi-su' not found"], error=True) + self.check_command_tree_exists() + + self.expect("command script delete test-multi test-multi-sub welcom", "Delete script command - wrong leaf component", + substrs=["'welcom' not found"], error=True) + self.check_command_tree_exists() + + self.expect("command script delete test-multi test-multi-sub", "Delete script command - no leaf component", + substrs=["subcommand 'test-multi-sub' is not a user command"], error=True) + self.check_command_tree_exists() + + # You can't use command script delete to delete multiword commands: + self.expect("command script delete test-multi", "Delete script - can't delete multiword", + substrs=["command 'test-multi' is a multi-word command."], error=True) + self.expect("command script delete test-multi test-multi-sub", "Delete script - can't delete multiword", + substrs=["subcommand 'test-multi-sub' is not a user command"], error = True) + + # You can't use command multiword delete to delete scripted commands: + self.expect("command multiword delete test-multi test-multi-sub welcome", "command multiword can't delete user commands", + substrs=["subcommand 'welcome' is not a multiword command"], error = True) + + # Also make sure you can't alias on top of multiword commands: + self.expect("command alias test-multi process launch", "Tried to alias on top of a multiword command", + substrs=["'test-multi' is a user multiword command and cannot be overwritten."], error=True) + self.check_command_tree_exists() + + # Also assert that we can't delete builtin commands: + self.expect("command script delete process launch", "Delete builtin command fails", substrs=["command 'process' is not a user command"], error=True) + # Now let's do the version that works + self.expect("command script delete test-multi test-multi-sub welcome", "Delete script command by path", substrs=["Deleted command: test-multi test-multi-sub welcome"]) + + # Now overwrite the sub-command, it should end up empty: + self.runCmd("command multiword add -h 'A different help string' -o test-multi test-multi-sub") + # welcome should be gone: + self.expect("test-multi test-multi-sub welcome friend", "did remove subcommand", + substrs=["'test-multi-sub' does not have any subcommands."], error=True) + # We should have the new help: + self.expect("help test-multi test-multi-sub", "help changed", + substrs=["A different help string"]) + + # Now try deleting commands. + self.runCmd("command multiword delete test-multi test-multi-sub") + self.expect("test-multi test-multi-sub", "Command is not active", error=True, + substrs = ["'test-multi' does not have any subcommands"]) + self.expect("help test-multi", matching=False, substrs=["test-multi-sub"]) + + + # Next the root command: + self.runCmd("command multiword delete test-multi") + self.expect("test-multi", "Root command gone", substrs=["'test-multi' is not a valid command."], error=True) Index: lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py =================================================================== --- lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py +++ lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py @@ -9,10 +9,10 @@ @no_debug_info_test def test_script_add(self): self.expect("command script add 1 2", error=True, - substrs=["'command script add' requires one argument"]) + substrs=["Path component: '1' not found"]) self.expect("command script add", error=True, - substrs=["'command script add' requires one argument"]) + substrs=["'command script add' requires at least one argument"]) @no_debug_info_test def test_script_clear(self): Index: lldb/source/Interpreter/CommandObject.cpp =================================================================== --- lldb/source/Interpreter/CommandObject.cpp +++ lldb/source/Interpreter/CommandObject.cpp @@ -1120,7 +1120,7 @@ { eArgTypeWatchpointIDRange, "watchpt-id-list", CommandCompletions::eNoCompletion, { nullptr, false }, "For example, '1-3' or '1 to 3'." }, { eArgTypeWatchType, "watch-type", CommandCompletions::eNoCompletion, { nullptr, false }, "Specify the type for a watchpoint." }, { eArgRawInput, "raw-input", CommandCompletions::eNoCompletion, { nullptr, false }, "Free-form text passed to a command without prior interpretation, allowing spaces without requiring quotes. To pass arguments and free form text put two dashes ' -- ' between the last argument and any raw input." }, - { eArgTypeCommand, "command", CommandCompletions::eNoCompletion, { nullptr, false }, "An LLDB Command line command." }, + { eArgTypeCommand, "command", CommandCompletions::eNoCompletion, { nullptr, false }, "An LLDB Command line command element." }, { eArgTypeColumnNum, "column", CommandCompletions::eNoCompletion, { nullptr, false }, "Column number in a source file." }, { eArgTypeModuleUUID, "module-uuid", CommandCompletions::eModuleUUIDCompletion, { nullptr, false }, "A module UUID value." }, { eArgTypeSaveCoreStyle, "corefile-style", CommandCompletions::eNoCompletion, { nullptr, false }, "The type of corefile that lldb will try to create, dependant on this target's capabilities." } Index: lldb/source/Interpreter/CommandInterpreter.cpp =================================================================== --- lldb/source/Interpreter/CommandInterpreter.cpp +++ lldb/source/Interpreter/CommandInterpreter.cpp @@ -897,6 +897,63 @@ return matches.GetSize(); } +CommandObjectMultiword *CommandInterpreter::VerifyUserMultiwordCmdPath( + Args &path, bool leaf_is_command, Status &result) { + result.Clear(); + + auto get_multi_or_report_error = + [&result](CommandObjectSP cmd_sp, + const char *name) -> CommandObjectMultiword * { + if (!cmd_sp) { + result.SetErrorStringWithFormat("Path component: '%s' not found", name); + return nullptr; + } + if (!cmd_sp->IsUserCommand()) { + result.SetErrorStringWithFormat("Path component: '%s' is not a user " + "command", + name); + return nullptr; + } + CommandObjectMultiword *cmd_as_multi = cmd_sp->GetAsMultiwordCommand(); + if (!cmd_as_multi) { + result.SetErrorStringWithFormat("Path component: '%s' is not a multiword " + "command", + name); + return nullptr; + } + return cmd_as_multi; + }; + + size_t num_args = path.GetArgumentCount(); + if (num_args == 0) { + result.SetErrorString("empty command path"); + return nullptr; + } + + if (num_args == 1 && leaf_is_command) { + // We just got a leaf command to be added to the root. That's not an error, + // just return null for the container. + return nullptr; + } + + // Start by getting the root command from the interpreter. + const char *cur_name = path.GetArgumentAtIndex(0); + CommandObjectSP cur_cmd_sp = GetCommandSPExact(cur_name); + CommandObjectMultiword *cur_as_multi = + get_multi_or_report_error(cur_cmd_sp, cur_name); + if (cur_as_multi == nullptr) + return nullptr; + + size_t num_path_elements = num_args - (leaf_is_command ? 1 : 0); + for (size_t cursor = 1; cursor < num_path_elements && cur_as_multi != nullptr; + cursor++) { + cur_name = path.GetArgumentAtIndex(cursor); + cur_cmd_sp = cur_as_multi->GetSubcommandSPExact(cur_name); + cur_as_multi = get_multi_or_report_error(cur_cmd_sp, cur_name); + } + return cur_as_multi; +} + CommandObjectSP CommandInterpreter::GetCommandSP(llvm::StringRef cmd_str, bool include_aliases, bool exact, StringList *matches, @@ -923,10 +980,17 @@ command_sp = pos->second; } + if (HasUserMultiwordCommands()) { + auto pos = m_user_mw_dict.find(cmd); + if (pos != m_user_mw_dict.end()) + command_sp = pos->second; + } + if (!exact && !command_sp) { // We will only get into here if we didn't find any exact matches. - CommandObjectSP user_match_sp, alias_match_sp, real_match_sp; + CommandObjectSP user_match_sp, user_mw_match_sp, alias_match_sp, + real_match_sp; StringList local_matches; if (matches == nullptr) @@ -935,6 +999,7 @@ unsigned int num_cmd_matches = 0; unsigned int num_alias_matches = 0; unsigned int num_user_matches = 0; + unsigned int num_user_mw_matches = 0; // Look through the command dictionaries one by one, and if we get only one // match from any of them in toto, then return that, otherwise return an @@ -978,14 +1043,32 @@ user_match_sp = pos->second; } + if (HasUserMultiwordCommands()) { + num_user_mw_matches = AddNamesMatchingPartialString( + m_user_mw_dict, cmd_str, *matches, descriptions); + } + + if (num_user_mw_matches == 1) { + cmd.assign(matches->GetStringAtIndex(num_cmd_matches + num_alias_matches + + num_user_matches)); + + auto pos = m_user_mw_dict.find(cmd); + if (pos != m_user_mw_dict.end()) + user_mw_match_sp = pos->second; + } + // If we got exactly one match, return that, otherwise return the match // list. - if (num_user_matches + num_cmd_matches + num_alias_matches == 1) { + if (num_user_matches + num_user_mw_matches + num_cmd_matches + + num_alias_matches == + 1) { if (num_cmd_matches) return real_match_sp; else if (num_alias_matches) return alias_match_sp; + else if (num_user_mw_matches) + return user_mw_match_sp; else return user_match_sp; } @@ -1008,6 +1091,8 @@ if (name.empty()) return false; + cmd_sp->SetIsUserCommand(false); + std::string name_sstr(name); auto name_iter = m_command_dict.find(name_sstr); if (name_iter != m_command_dict.end()) { @@ -1020,33 +1105,49 @@ return true; } -bool CommandInterpreter::AddUserCommand(llvm::StringRef name, - const lldb::CommandObjectSP &cmd_sp, - bool can_replace) { +Status CommandInterpreter::AddUserCommand(llvm::StringRef name, + const lldb::CommandObjectSP &cmd_sp, + bool can_replace) { + Status result; if (cmd_sp.get()) lldbassert((this == &cmd_sp->GetCommandInterpreter()) && "tried to add a CommandObject from a different interpreter"); - - if (!name.empty()) { - // do not allow replacement of internal commands - if (CommandExists(name)) { - if (!can_replace) - return false; - if (!m_command_dict[std::string(name)]->IsRemovable()) - return false; + if (name.empty()) { + result.SetErrorString("can't use the empty string for a command name"); + return result; + } + // do not allow replacement of internal commands + if (CommandExists(name)) { + result.SetErrorString("can't replace builtin command"); + return result; + } + + if (UserCommandExists(name)) { + if (!can_replace) { + result.SetErrorString("user command exists and force replace not set"); + return result; + } + if (cmd_sp->IsMultiwordObject()) { + if (!m_user_mw_dict[std::string(name)]->IsRemovable()) { + result.SetErrorString( + "can't replace explicitly non-removable multi-word command"); + return result; + } + } else { + if (!m_user_dict[std::string(name)]->IsRemovable()) { + result.SetErrorString("can't replace explicitly non-removable command"); + return result; + } } + } - if (UserCommandExists(name)) { - if (!can_replace) - return false; - if (!m_user_dict[std::string(name)]->IsRemovable()) - return false; - } + cmd_sp->SetIsUserCommand(true); + if (cmd_sp->IsMultiwordObject()) + m_user_mw_dict[std::string(name)] = cmd_sp; + else m_user_dict[std::string(name)] = cmd_sp; - return true; - } - return false; + return result; } CommandObjectSP @@ -1127,6 +1228,46 @@ return GetCommandSP(cmd_str, true, false, matches, descriptions).get(); } +CommandObject *CommandInterpreter::GetUserCommandObject( + llvm::StringRef cmd, StringList *matches, StringList *descriptions) const { + std::string cmd_str(cmd); + auto find_exact = [&](const CommandObject::CommandMap &map) { + auto found_elem = map.find(std::string(cmd)); + if (found_elem == map.end()) + return (CommandObject *)nullptr; + CommandObject *exact_cmd = found_elem->second.get(); + if (exact_cmd) { + if (matches) + matches->AppendString(exact_cmd->GetCommandName()); + if (descriptions) + descriptions->AppendString(exact_cmd->GetHelp()); + return exact_cmd; + } + return (CommandObject *)nullptr; + }; + + CommandObject *exact_cmd = find_exact(GetUserCommands()); + if (exact_cmd) + return exact_cmd; + + exact_cmd = find_exact(GetUserMultiwordCommands()); + if (exact_cmd) + return exact_cmd; + + // We didn't have an exact command, so now look for partial matches. + size_t num_found; + StringList *matches_ptr = matches; + StringList tmp_list; + if (!matches_ptr) + matches_ptr = &tmp_list; + num_found = + AddNamesMatchingPartialString(GetUserCommands(), cmd_str, *matches); + num_found += AddNamesMatchingPartialString(GetUserMultiwordCommands(), + cmd_str, *matches); + + return {}; +} + bool CommandInterpreter::CommandExists(llvm::StringRef cmd) const { return m_command_dict.find(std::string(cmd)) != m_command_dict.end(); } @@ -1169,6 +1310,10 @@ return m_user_dict.find(std::string(cmd)) != m_user_dict.end(); } +bool CommandInterpreter::UserMultiwordCommandExists(llvm::StringRef cmd) const { + return m_user_mw_dict.find(std::string(cmd)) != m_user_mw_dict.end(); +} + CommandAlias * CommandInterpreter::AddAlias(llvm::StringRef alias_name, lldb::CommandObjectSP &command_obj_sp, @@ -1209,9 +1354,10 @@ } return false; } -bool CommandInterpreter::RemoveUser(llvm::StringRef alias_name) { + +bool CommandInterpreter::RemoveUser(llvm::StringRef user_name) { CommandObject::CommandMap::iterator pos = - m_user_dict.find(std::string(alias_name)); + m_user_dict.find(std::string(user_name)); if (pos != m_user_dict.end()) { m_user_dict.erase(pos); return true; @@ -1219,6 +1365,16 @@ return false; } +bool CommandInterpreter::RemoveUserMultiword(llvm::StringRef multi_name) { + CommandObject::CommandMap::iterator pos = + m_user_mw_dict.find(std::string(multi_name)); + if (pos != m_user_mw_dict.end()) { + m_user_mw_dict.erase(pos); + return true; + } + return false; +} + void CommandInterpreter::GetHelp(CommandReturnObject &result, uint32_t cmd_types) { llvm::StringRef help_prologue(GetDebugger().GetIOHandlerHelpPrologue()); @@ -1274,6 +1430,18 @@ result.AppendMessage(""); } + if (!m_user_mw_dict.empty() && + ((cmd_types & eCommandTypesUserMW) == eCommandTypesUserMW)) { + result.AppendMessage("Current user-defined multiword commands:"); + result.AppendMessage(""); + max_len = FindLongestCommandWord(m_user_mw_dict); + for (pos = m_user_dict.begin(); pos != m_user_mw_dict.end(); ++pos) { + OutputFormattedHelpText(result.GetOutputStream(), pos->first, "--", + pos->second->GetHelp(), max_len); + } + result.AppendMessage(""); + } + result.AppendMessageWithFormat( "For more information on any command, type '%shelp <command-name>'.\n", GetCommandPrefix()); @@ -1931,6 +2099,10 @@ bool CommandInterpreter::HasUserCommands() const { return (!m_user_dict.empty()); } +bool CommandInterpreter::HasUserMultiwordCommands() const { + return (!m_user_mw_dict.empty()); +} + bool CommandInterpreter::HasAliasOptions() const { return HasAliases(); } void CommandInterpreter::BuildAliasCommandArgs(CommandObject *alias_cmd_obj, @@ -2587,6 +2759,9 @@ strm.IndentMore(prefix.size()); bool prefixed_yet = false; + // Even if we have no help text we still want to emit the command name. + if (help_text.empty()) + help_text = "No help text"; while (!help_text.empty()) { // Prefix the first line, indent subsequent lines to line up if (!prefixed_yet) { @@ -2706,7 +2881,8 @@ StringList &commands_help, bool search_builtin_commands, bool search_user_commands, - bool search_alias_commands) { + bool search_alias_commands, + bool search_user_mw_commands) { CommandObject::CommandMap::const_iterator pos; if (search_builtin_commands) @@ -2717,6 +2893,10 @@ FindCommandsForApropos(search_word, commands_found, commands_help, m_user_dict); + if (search_user_mw_commands) + FindCommandsForApropos(search_word, commands_found, commands_help, + m_user_mw_dict); + if (search_alias_commands) FindCommandsForApropos(search_word, commands_found, commands_help, m_alias_dict); Index: lldb/source/Commands/Options.td =================================================================== --- lldb/source/Commands/Options.td +++ lldb/source/Commands/Options.td @@ -783,12 +783,23 @@ Desc<"Name of the Python class to bind to this command name.">; def script_add_help : Option<"help", "h">, Group<1>, Arg<"HelpText">, Desc<"The help text to display for this command.">; + def script_add_overwrite : Option<"overwrite", "o">, Groups<[1,2]>, + Desc<"Overwrite an existing command at this node.">; def script_add_synchronicity : Option<"synchronicity", "s">, EnumArg<"ScriptedCommandSynchronicity", "ScriptSynchroType()">, Desc<"Set the synchronicity of this command's executions with regard to " "LLDB event system.">; } +let Command = "multiword add" in { + def multiword_add_help : Option<"help", "h">, Arg<"HelpText">, + Desc<"Help text for this command">; + def multiword_add_long_help : Option<"long-help", "H">, Arg<"HelpText">, + Desc<"Long help text for this command">; + def multiword_add_overwrite : Option<"overwrite", "o">, Group<1>, + Desc<"Overwrite an existing command at this node.">; +} + let Command = "script" in { def script_language : Option<"language", "l">, EnumArg<"ScriptLang", "ScriptOptionEnum()">, Desc<"Specify the scripting " Index: lldb/source/Commands/CommandObjectMultiword.cpp =================================================================== --- lldb/source/Commands/CommandObjectMultiword.cpp +++ lldb/source/Commands/CommandObjectMultiword.cpp @@ -26,36 +26,49 @@ CommandObjectMultiword::~CommandObjectMultiword() = default; +CommandObjectSP +CommandObjectMultiword::GetSubcommandSPExact(llvm::StringRef sub_cmd) { + if (m_subcommand_dict.empty()) + return {}; + + CommandObject::CommandMap::iterator pos; + pos = m_subcommand_dict.find(std::string(sub_cmd)); + if (pos == m_subcommand_dict.end()) + return {}; + + return pos->second; +} + CommandObjectSP CommandObjectMultiword::GetSubcommandSP(llvm::StringRef sub_cmd, StringList *matches) { - CommandObjectSP return_cmd_sp; + if (m_subcommand_dict.empty()) + return {}; + + CommandObjectSP return_cmd_sp = GetSubcommandSPExact(sub_cmd); + if (return_cmd_sp) { + if (matches) + matches->AppendString(sub_cmd); + return return_cmd_sp; + } + CommandObject::CommandMap::iterator pos; - if (!m_subcommand_dict.empty()) { + StringList local_matches; + if (matches == nullptr) + matches = &local_matches; + int num_matches = + AddNamesMatchingPartialString(m_subcommand_dict, sub_cmd, *matches); + + if (num_matches == 1) { + // Cleaner, but slightly less efficient would be to call back into this + // function, since I now know I have an exact match... + + sub_cmd = matches->GetStringAtIndex(0); pos = m_subcommand_dict.find(std::string(sub_cmd)); - if (pos != m_subcommand_dict.end()) { - // An exact match; append the sub_cmd to the 'matches' string list. - if (matches) - matches->AppendString(sub_cmd); + if (pos != m_subcommand_dict.end()) return_cmd_sp = pos->second; - } else { - StringList local_matches; - if (matches == nullptr) - matches = &local_matches; - int num_matches = - AddNamesMatchingPartialString(m_subcommand_dict, sub_cmd, *matches); - - if (num_matches == 1) { - // Cleaner, but slightly less efficient would be to call back into this - // function, since I now know I have an exact match... - - sub_cmd = matches->GetStringAtIndex(0); - pos = m_subcommand_dict.find(std::string(sub_cmd)); - if (pos != m_subcommand_dict.end()) - return_cmd_sp = pos->second; - } - } } + return return_cmd_sp; } @@ -83,6 +96,77 @@ return success; } +Status CommandObjectMultiword::LoadUserSubcommand( + llvm::StringRef name, const CommandObjectSP &cmd_obj, bool can_replace) { + Status result; + if (cmd_obj) + assert((&GetCommandInterpreter() == &cmd_obj->GetCommandInterpreter()) && + "tried to add a CommandObject from a different interpreter"); + if (!IsUserCommand()) { + result.SetErrorString("can't add a user subcommand to a builtin " + "multiword command."); + return result; + } + // Make sure this a user command if it isn't already: + cmd_obj->SetIsUserCommand(true); + + CommandMap::iterator pos; + std::string str_name(name); + + pos = m_subcommand_dict.find(str_name); + if (pos == m_subcommand_dict.end()) { + m_subcommand_dict[str_name] = cmd_obj; + return result; + } + + const char *error_str = nullptr; + if (!can_replace) + error_str = "sub-command already exists"; + if (!(*pos).second->IsUserCommand()) + error_str = "can't replace a builtin subcommand"; + + if (error_str) { + result.SetErrorString(error_str); + return result; + } + m_subcommand_dict[str_name] = cmd_obj; + return result; +} + +Status CommandObjectMultiword::RemoveUserSubcommand(llvm::StringRef cmd_name, + bool must_be_multiword) { + Status error; + CommandMap::iterator pos; + std::string str_name(cmd_name); + + pos = m_subcommand_dict.find(str_name); + if (pos == m_subcommand_dict.end()) { + error.SetErrorStringWithFormat("subcommand '%s' not found.", + str_name.c_str()); + return error; + } + if (!(*pos).second->IsUserCommand()) { + error.SetErrorStringWithFormat("subcommand '%s' not a user command.", + str_name.c_str()); + return error; + } + + if (must_be_multiword && !(*pos).second->IsMultiwordObject()) { + error.SetErrorStringWithFormat("subcommand '%s' is not a multiword command", + str_name.c_str()); + return error; + } + if (!must_be_multiword && (*pos).second->IsMultiwordObject()) { + error.SetErrorStringWithFormat("subcommand '%s' is not a user command", + str_name.c_str()); + return error; + } + + m_subcommand_dict.erase(pos); + + return error; +} + bool CommandObjectMultiword::Execute(const char *args_string, CommandReturnObject &result) { Args args(args_string); Index: lldb/source/Commands/CommandObjectHelp.cpp =================================================================== --- lldb/source/Commands/CommandObjectHelp.cpp +++ lldb/source/Commands/CommandObjectHelp.cpp @@ -51,8 +51,9 @@ CommandArgumentEntry arg; CommandArgumentData command_arg; - // Define the first (and only) variant of this arg. - command_arg.arg_type = eArgTypeCommandName; + // A list of command names forming a path to the command we want help on. + // No names is allowed - in which case we dump the top-level help. + command_arg.arg_type = eArgTypeCommand; command_arg.arg_repetition = eArgRepeatStar; // There is only one variant this argument could be; put it into the argument @@ -85,8 +86,10 @@ uint32_t cmd_types = CommandInterpreter::eCommandTypesBuiltin; if (m_options.m_show_aliases) cmd_types |= CommandInterpreter::eCommandTypesAliases; - if (m_options.m_show_user_defined) + if (m_options.m_show_user_defined) { cmd_types |= CommandInterpreter::eCommandTypesUserDef; + cmd_types |= CommandInterpreter::eCommandTypesUserMW; + } if (m_options.m_show_hidden) cmd_types |= CommandInterpreter::eCommandTypesHidden; Index: lldb/source/Commands/CommandObjectCommands.cpp =================================================================== --- lldb/source/Commands/CommandObjectCommands.cpp +++ lldb/source/Commands/CommandObjectCommands.cpp @@ -415,6 +415,14 @@ return false; } + if (m_interpreter.UserMultiwordCommandExists(alias_command)) { + result.AppendErrorWithFormat( + "'%s' is a user multiword command and cannot be overwritten.\n" + "Delete it first with 'command multiword delete'\n", + args[0].c_str()); + return false; + } + // Get CommandObject that is being aliased. The command name is read from // the front of raw_command_string. raw_command_string is returned with the // name of the command object stripped off the front. @@ -500,6 +508,14 @@ return false; } + if (m_interpreter.UserMultiwordCommandExists(alias_command)) { + result.AppendErrorWithFormat( + "'%s' is user multiword command and cannot be overwritten.\n" + "Delete it first with 'command multiword delete'", + alias_command.c_str()); + return false; + } + CommandObjectSP command_obj_sp( m_interpreter.GetCommandSPExact(actual_command, true)); CommandObjectSP subcommand_obj_sp; @@ -1343,14 +1359,21 @@ CommandObjectCommandsScriptAdd(CommandInterpreter &interpreter) : CommandObjectParsed(interpreter, "command script add", "Add a scripted function as an LLDB command.", - nullptr), + "Add a scripted function as an lldb command. " + "If you provide a single argument, the command " + "will be added at the root level of the command " + "hierarchy. If there are more arguments they " + "must be a path to a user-added multiword " + "command, and the last element will be the new " + "command name."), IOHandlerDelegateMultiline("DONE"), m_options() { CommandArgumentEntry arg1; CommandArgumentData cmd_arg; - // Define the first (and only) variant of this arg. - cmd_arg.arg_type = eArgTypeCommandName; - cmd_arg.arg_repetition = eArgRepeatPlain; + // This is one or more command names, which form the path to the command + // you want to add. + cmd_arg.arg_type = eArgTypeCommand; + cmd_arg.arg_repetition = eArgRepeatPlus; // There is only one variant this argument could be; put it into the // argument entry. @@ -1364,6 +1387,13 @@ Options *GetOptions() override { return &m_options; } + void + HandleArgumentCompletion(CompletionRequest &request, + OptionElementVector &opt_element_vector) override { + CommandCompletions::CompleteModifiableCmdPathArgs(m_interpreter, request, + opt_element_vector); + } + protected: class CommandOptions : public Options { public: @@ -1390,6 +1420,9 @@ if (!option_arg.empty()) m_short_help = std::string(option_arg); break; + case 'o': + m_overwrite = true; + break; case 's': m_synchronicity = (ScriptedCommandSynchronicity)OptionArgParser::ToOptionEnum( @@ -1410,6 +1443,7 @@ m_class_name.clear(); m_funct_name.clear(); m_short_help.clear(); + m_overwrite = false; m_synchronicity = eScriptedCommandSynchronicitySynchronous; } @@ -1422,6 +1456,7 @@ std::string m_class_name; std::string m_funct_name; std::string m_short_help; + bool m_overwrite; ScriptedCommandSynchronicity m_synchronicity = eScriptedCommandSynchronicitySynchronous; }; @@ -1456,11 +1491,16 @@ CommandObjectSP command_obj_sp(new CommandObjectPythonFunction( m_interpreter, m_cmd_name, funct_name_str, m_short_help, m_synchronicity)); - - if (!m_interpreter.AddUserCommand(m_cmd_name, command_obj_sp, - true)) { - error_sp->Printf("error: unable to add selected command, didn't " - "add python command.\n"); + Status add_result; + if (!m_container) + add_result = m_interpreter.AddUserCommand( + m_cmd_name, command_obj_sp, m_overwrite); + else + add_result = m_container->LoadUserSubcommand( + m_cmd_name, command_obj_sp, m_overwrite); + if (add_result.Fail()) { + error_sp->Printf("error: unable to add selected command: '%s'", + add_result.AsCString()); error_sp->Flush(); } } @@ -1489,31 +1529,45 @@ return false; } - if (command.GetArgumentCount() != 1) { - result.AppendError("'command script add' requires one argument"); + if (command.GetArgumentCount() == 0) { + result.AppendError("'command script add' requires at least one argument"); return false; } - // Store the options in case we get multi-line input - m_cmd_name = std::string(command[0].ref()); + m_overwrite = m_options.m_overwrite; + Status path_error; + m_container = GetCommandInterpreter().VerifyUserMultiwordCmdPath( + command, true, path_error); + + if (path_error.Fail()) { + result.AppendErrorWithFormat("Error in command path: %s", + path_error.AsCString()); + return false; + } + + if (!m_container) { + // This is getting inserted into the root of the interpreter. + m_cmd_name = std::string(command[0].ref()); + } else { + size_t num_args = command.GetArgumentCount(); + m_cmd_name = std::string(command[num_args - 1].ref()); + } + m_short_help.assign(m_options.m_short_help); m_synchronicity = m_options.m_synchronicity; + // Handle the case where we prompt for the script code first: + if (m_options.m_class_name.empty() && m_options.m_funct_name.empty()) { + m_interpreter.GetPythonCommandsFromIOHandler(" ", // Prompt + *this); // IOHandlerDelegate + return result.Succeeded(); + } + + CommandObjectSP new_cmd_sp; if (m_options.m_class_name.empty()) { - if (m_options.m_funct_name.empty()) { - m_interpreter.GetPythonCommandsFromIOHandler( - " ", // Prompt - *this); // IOHandlerDelegate - } else { - CommandObjectSP new_cmd(new CommandObjectPythonFunction( - m_interpreter, m_cmd_name, m_options.m_funct_name, - m_options.m_short_help, m_synchronicity)); - if (m_interpreter.AddUserCommand(m_cmd_name, new_cmd, true)) { - result.SetStatus(eReturnStatusSuccessFinishNoResult); - } else { - result.AppendError("cannot add command"); - } - } + new_cmd_sp.reset(new CommandObjectPythonFunction( + m_interpreter, m_cmd_name, m_options.m_funct_name, + m_options.m_short_help, m_synchronicity)); } else { ScriptInterpreter *interpreter = GetDebugger().GetScriptInterpreter(); if (!interpreter) { @@ -1528,13 +1582,22 @@ return false; } - CommandObjectSP new_cmd(new CommandObjectScriptingObject( + new_cmd_sp.reset(new CommandObjectScriptingObject( m_interpreter, m_cmd_name, cmd_obj_sp, m_synchronicity)); - if (m_interpreter.AddUserCommand(m_cmd_name, new_cmd, true)) { - result.SetStatus(eReturnStatusSuccessFinishNoResult); - } else { - result.AppendError("cannot add command"); - } + } + Status add_error; + if (!m_container) + add_error = + m_interpreter.AddUserCommand(m_cmd_name, new_cmd_sp, m_overwrite); + else + add_error = + m_container->LoadUserSubcommand(m_cmd_name, new_cmd_sp, m_overwrite); + + if (add_error.Success()) { + result.SetStatus(eReturnStatusSuccessFinishNoResult); + } else { + result.AppendErrorWithFormat("cannot add command: %s", + add_error.AsCString()); } return result.Succeeded(); @@ -1542,7 +1605,9 @@ CommandOptions m_options; std::string m_cmd_name; + CommandObjectMultiword *m_container = nullptr; std::string m_short_help; + bool m_overwrite; ScriptedCommandSynchronicity m_synchronicity; }; @@ -1552,7 +1617,8 @@ public: CommandObjectCommandsScriptList(CommandInterpreter &interpreter) : CommandObjectParsed(interpreter, "command script list", - "List defined scripted commands.", nullptr) {} + "List defined top-level scripted commands.", + nullptr) {} ~CommandObjectCommandsScriptList() override = default; @@ -1600,14 +1666,17 @@ class CommandObjectCommandsScriptDelete : public CommandObjectParsed { public: CommandObjectCommandsScriptDelete(CommandInterpreter &interpreter) - : CommandObjectParsed(interpreter, "command script delete", - "Delete a scripted command.", nullptr) { + : CommandObjectParsed( + interpreter, "command script delete", + "Delete a scripted command by specifying the path to the command.", + nullptr) { CommandArgumentEntry arg1; CommandArgumentData cmd_arg; - // Define the first (and only) variant of this arg. - cmd_arg.arg_type = eArgTypeCommandName; - cmd_arg.arg_repetition = eArgRepeatPlain; + // This is a list of command names forming the path to the command + // to be deleted. + cmd_arg.arg_type = eArgTypeCommand; + cmd_arg.arg_repetition = eArgRepeatPlus; // There is only one variant this argument could be; put it into the // argument entry. @@ -1622,30 +1691,85 @@ void HandleArgumentCompletion(CompletionRequest &request, OptionElementVector &opt_element_vector) override { - if (!m_interpreter.HasCommands() || request.GetCursorIndex() != 0) - return; - - for (const auto &c : m_interpreter.GetUserCommands()) - request.TryCompleteCurrentArg(c.first, c.second->GetHelp()); + CommandCompletions::CompleteModifiableCmdPathArgs(m_interpreter, request, + opt_element_vector); } protected: bool DoExecute(Args &command, CommandReturnObject &result) override { - if (command.GetArgumentCount() != 1) { - result.AppendError("'command script delete' requires one argument"); + llvm::StringRef root_cmd = command[0].ref(); + size_t num_args = command.GetArgumentCount(); + + if (root_cmd.empty()) { + result.AppendErrorWithFormat("empty root command name"); + return false; + } + if (!m_interpreter.HasUserCommands() && + !m_interpreter.HasUserMultiwordCommands()) { + result.AppendErrorWithFormat("Can only delete user defined commands, " + "but no user defined commands found"); return false; } - auto cmd_name = command[0].ref(); + CommandObjectSP cmd_sp = m_interpreter.GetCommandSPExact(root_cmd); + if (!cmd_sp) { + result.AppendErrorWithFormat("command '%s' not found.", + command[0].c_str()); + return false; + } + if (!cmd_sp->IsUserCommand()) { + result.AppendErrorWithFormat("command '%s' is not a user command.", + command[0].c_str()); + return false; + } + if (cmd_sp->GetAsMultiwordCommand() && num_args == 1) { + result.AppendErrorWithFormat("command '%s' is a multi-word command.\n " + "Delete with \"command multiword delete\"", + command[0].c_str()); + return false; + } - if (cmd_name.empty() || !m_interpreter.HasUserCommands() || - !m_interpreter.UserCommandExists(cmd_name)) { - result.AppendErrorWithFormat("command %s not found", command[0].c_str()); + if (command.GetArgumentCount() == 1) { + m_interpreter.RemoveUser(root_cmd); + result.SetStatus(eReturnStatusSuccessFinishResult); + return true; + } + // We're deleting a command from a multiword command. Verify the command + // path: + Status error; + CommandObjectMultiword *container = + GetCommandInterpreter().VerifyUserMultiwordCmdPath(command, true, + error); + if (error.Fail()) { + result.AppendErrorWithFormat("could not resolve command path: %s", + error.AsCString()); + return false; + } + if (!container) { + // This means that command only had a leaf command, so the container is + // the root. That should have been handled above. + result.AppendErrorWithFormat("could not find a container for '%s'", + command[0].c_str()); + return false; + } + const char *leaf_cmd = command[num_args - 1].c_str(); + error = container->RemoveUserSubcommand(leaf_cmd, + /* multiword not okay */ false); + if (error.Fail()) { + result.AppendErrorWithFormat("could not delete command '%s': %s", + leaf_cmd, error.AsCString()); return false; } - m_interpreter.RemoveUser(cmd_name); + Stream &out_stream = result.GetOutputStream(); + + out_stream << "Deleted command:"; + for (size_t idx = 0; idx < num_args; idx++) { + out_stream << ' '; + out_stream << command[idx].c_str(); + } + out_stream << '\n'; result.SetStatus(eReturnStatusSuccessFinishResult); return true; } @@ -1682,6 +1806,271 @@ ~CommandObjectMultiwordCommandsScript() override = default; }; +#pragma mark CommandObjectCommandMultiword +#define LLDB_OPTIONS_multiword_add +#include "CommandOptions.inc" + +class CommandObjectCommandsMultiwordAdd : public CommandObjectParsed { +public: + CommandObjectCommandsMultiwordAdd(CommandInterpreter &interpreter) + : CommandObjectParsed( + interpreter, "command multiword add", + "Add a multiword command to lldb. Adding to built-" + "in multiword commands is not allowed.", + "command multiword add [[path1]...] multiword-name") { + CommandArgumentEntry arg1; + CommandArgumentData cmd_arg; + + // This is one or more command names, which form the path to the command + // you want to add. + cmd_arg.arg_type = eArgTypeCommand; + cmd_arg.arg_repetition = eArgRepeatPlus; + + // There is only one variant this argument could be; put it into the + // argument entry. + arg1.push_back(cmd_arg); + + // Push the data for the first argument into the m_arguments vector. + m_arguments.push_back(arg1); + } + + ~CommandObjectCommandsMultiwordAdd() override = default; + + Options *GetOptions() override { return &m_options; } + + void + HandleArgumentCompletion(CompletionRequest &request, + OptionElementVector &opt_element_vector) override { + CommandCompletions::CompleteModifiableCmdPathArgs(m_interpreter, request, + opt_element_vector); + } + +protected: + class CommandOptions : public Options { + public: + CommandOptions() : Options(), m_short_help(), m_long_help() {} + + ~CommandOptions() override = default; + + Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg, + ExecutionContext *execution_context) override { + Status error; + const int short_option = m_getopt_table[option_idx].val; + + switch (short_option) { + case 'h': + if (!option_arg.empty()) + m_short_help = std::string(option_arg); + break; + case 'o': + m_overwrite = true; + break; + case 'H': + if (!option_arg.empty()) + m_long_help = std::string(option_arg); + break; + default: + llvm_unreachable("Unimplemented option"); + } + + return error; + } + + void OptionParsingStarting(ExecutionContext *execution_context) override { + m_short_help.clear(); + m_long_help.clear(); + m_overwrite = false; + } + + llvm::ArrayRef<OptionDefinition> GetDefinitions() override { + return llvm::makeArrayRef(g_multiword_add_options); + } + + // Instance variables to hold the values for command options. + + std::string m_short_help; + std::string m_long_help; + bool m_overwrite = false; + }; + bool DoExecute(Args &command, CommandReturnObject &result) override { + size_t num_args = command.GetArgumentCount(); + + if (num_args == 0) { + result.AppendError("No command was specified."); + return false; + } + + if (num_args == 1) { + // We're adding this as a root command, so use the interpreter. + const char *cmd_name = command.GetArgumentAtIndex(0); + auto cmd_sp = CommandObjectSP(new CommandObjectMultiword( + GetCommandInterpreter(), cmd_name, m_options.m_short_help.c_str(), + m_options.m_long_help.c_str())); + cmd_sp->GetAsMultiwordCommand()->SetRemovable(true); + Status add_error = GetCommandInterpreter().AddUserCommand( + cmd_name, cmd_sp, m_options.m_overwrite); + if (add_error.Fail()) { + result.AppendErrorWithFormat("Error adding command: %s.", + add_error.AsCString()); + return false; + } + result.SetStatus(eReturnStatusSuccessFinishNoResult); + return true; + } + + // We're adding this to a subcommand, first find the subcommand: + Status path_error; + CommandObjectMultiword *add_to_me = + GetCommandInterpreter().VerifyUserMultiwordCmdPath(command, true, + path_error); + + if (!add_to_me) { + result.AppendErrorWithFormat("Error adding command: %s", + path_error.AsCString()); + return false; + } + + const char *cmd_name = command.GetArgumentAtIndex(num_args - 1); + auto cmd_sp = CommandObjectSP(new CommandObjectMultiword( + GetCommandInterpreter(), cmd_name, m_options.m_short_help.c_str(), + m_options.m_long_help.c_str())); + Status add_error = + add_to_me->LoadUserSubcommand(cmd_name, cmd_sp, m_options.m_overwrite); + if (add_error.Fail()) { + result.AppendErrorWithFormat("Error adding subcommand: %s", + add_error.AsCString()); + return false; + } + + result.SetStatus(eReturnStatusSuccessFinishNoResult); + return true; + } + +private: + CommandOptions m_options; +}; + +#define LLDB_OPTIONS_multiword_delete +#include "CommandOptions.inc" +class CommandObjectCommandsMultiwordDelete : public CommandObjectParsed { +public: + CommandObjectCommandsMultiwordDelete(CommandInterpreter &interpreter) + : CommandObjectParsed( + interpreter, "command multiword delete", + "Delete a multiword command previously added to " + "lldb.", + "command multiword delete [[path1] ...] multiword-cmd") { + CommandArgumentEntry arg1; + CommandArgumentData cmd_arg; + + // This is one or more command names, which form the path to the command + // you want to add. + cmd_arg.arg_type = eArgTypeCommand; + cmd_arg.arg_repetition = eArgRepeatPlus; + + // There is only one variant this argument could be; put it into the + // argument entry. + arg1.push_back(cmd_arg); + + // Push the data for the first argument into the m_arguments vector. + m_arguments.push_back(arg1); + } + + ~CommandObjectCommandsMultiwordDelete() override = default; + + void + HandleArgumentCompletion(CompletionRequest &request, + OptionElementVector &opt_element_vector) override { + CommandCompletions::CompleteModifiableCmdPathArgs(m_interpreter, request, + opt_element_vector); + } + +protected: + bool DoExecute(Args &command, CommandReturnObject &result) override { + size_t num_args = command.GetArgumentCount(); + + if (num_args == 0) { + result.AppendError("No command was specified."); + return false; + } + + if (num_args == 1) { + // We're removing a root command, so we need to delete it from the + // interpreter. + const char *cmd_name = command.GetArgumentAtIndex(0); + // Let's do a little more work here so we can do better error reporting. + CommandInterpreter &interp = GetCommandInterpreter(); + CommandObjectSP cmd_sp = interp.GetCommandSPExact(cmd_name); + if (!cmd_sp) { + result.AppendErrorWithFormat("multiword command %s doesn't exist.", + cmd_name); + return false; + } + if (!cmd_sp->IsUserCommand()) { + result.AppendErrorWithFormat( + "multiword command %s is not a user command", cmd_name); + return false; + } + if (!cmd_sp->GetAsMultiwordCommand()) { + result.AppendErrorWithFormat("command %s is not a multiword command", + cmd_name); + return false; + } + + bool did_remove = GetCommandInterpreter().RemoveUserMultiword(cmd_name); + if (!did_remove) { + result.AppendErrorWithFormat("Error removing command %s.", cmd_name); + return false; + } + + result.SetStatus(eReturnStatusSuccessFinishNoResult); + return true; + } + + // We're removing a subcommand, first find the subcommand's owner: + Status path_error; + CommandObjectMultiword *container = + GetCommandInterpreter().VerifyUserMultiwordCmdPath(command, true, + path_error); + + if (!container) { + result.AppendErrorWithFormat("Error removing multiword command: %s", + path_error.AsCString()); + return false; + } + const char *leaf = command.GetArgumentAtIndex(num_args - 1); + path_error = + container->RemoveUserSubcommand(leaf, /* multiword okay */ true); + if (path_error.Fail()) { + result.AppendErrorWithFormat("Error removing multiword command: %s", + path_error.AsCString()); + return false; + } + result.SetStatus(eReturnStatusSuccessFinishNoResult); + return true; + } +}; + +class CommandObjectCommandMultiword : public CommandObjectMultiword { +public: + CommandObjectCommandMultiword(CommandInterpreter &interpreter) + : CommandObjectMultiword( + interpreter, "command multiword", + "Commands for adding multiword commands to lldb. " + "Multiword commands are containers for other commands. You can" + "add nested multiword commands by specifying a command path, but " + "but you can't add commands into the built-in command hierarchy.", + "command multiword <subcommand> [<subcommand-options>]") { + LoadSubCommand("add", CommandObjectSP(new CommandObjectCommandsMultiwordAdd( + interpreter))); + LoadSubCommand( + "delete", + CommandObjectSP(new CommandObjectCommandsMultiwordDelete(interpreter))); + } + + ~CommandObjectCommandMultiword() override = default; +}; + #pragma mark CommandObjectMultiwordCommands // CommandObjectMultiwordCommands @@ -1699,6 +2088,8 @@ new CommandObjectCommandsUnalias(interpreter))); LoadSubCommand("delete", CommandObjectSP(new CommandObjectCommandsDelete(interpreter))); + LoadSubCommand("multiword", CommandObjectSP(new CommandObjectCommandMultiword( + interpreter))); LoadSubCommand( "regex", CommandObjectSP(new CommandObjectCommandsAddRegex(interpreter))); LoadSubCommand( Index: lldb/source/Commands/CommandObjectApropos.cpp =================================================================== --- lldb/source/Commands/CommandObjectApropos.cpp +++ lldb/source/Commands/CommandObjectApropos.cpp @@ -49,8 +49,8 @@ StringList commands_found; StringList commands_help; - m_interpreter.FindCommandsForApropos(search_word, commands_found, - commands_help, true, true, true); + m_interpreter.FindCommandsForApropos( + search_word, commands_found, commands_help, true, true, true, true); if (commands_found.GetSize() == 0) { result.AppendMessageWithFormat("No commands found pertaining to '%s'. " Index: lldb/source/Commands/CommandCompletions.cpp =================================================================== --- lldb/source/Commands/CommandCompletions.cpp +++ lldb/source/Commands/CommandCompletions.cpp @@ -17,6 +17,8 @@ #include "lldb/Host/FileSystem.h" #include "lldb/Interpreter/CommandCompletions.h" #include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Interpreter/CommandObject.h" +#include "lldb/Interpreter/CommandObjectMultiword.h" #include "lldb/Interpreter/OptionValueProperties.h" #include "lldb/Symbol/CompileUnit.h" #include "lldb/Symbol/Variable.h" @@ -792,3 +794,60 @@ return true; }); } + +void CommandCompletions::CompleteModifiableCmdPathArgs( + CommandInterpreter &interpreter, CompletionRequest &request, + OptionElementVector &opt_element_vector) { + // The only arguments constitute a command path, however, there might be + // options interspersed among the options, and we need to skip those. Do that + // by copying the args vector, and just dropping all the option bits: + Args args = request.GetParsedLine(); + std::vector<size_t> to_delete; + for (auto &elem : opt_element_vector) { + to_delete.push_back(elem.opt_pos); + if (elem.opt_arg_pos != 0) + to_delete.push_back(elem.opt_arg_pos); + } + sort(to_delete.begin(), to_delete.end(), std::greater<size_t>()); + for (size_t idx : to_delete) + args.DeleteArgumentAtIndex(idx); + + // At this point, we should only have args, so now lookup the command up to + // the cursor element. + + // There's nothing here but options. It doesn't seem very useful here to + // dump all the commands, so just return. + size_t num_args = args.GetArgumentCount(); + if (num_args == 0) + return; + + // There's just one argument, so we should complete its name: + StringList matches; + if (num_args == 1) { + interpreter.GetUserCommandObject(args.GetArgumentAtIndex(0), &matches, + nullptr); + request.AddCompletions(matches); + return; + } + + // There was more than one path element, lets find the containing command: + Status error; + CommandObjectMultiword *mwc = + interpreter.VerifyUserMultiwordCmdPath(args, true, error); + + // Something was wrong somewhere along the path, but I don't think there's + // a good way to go back and fill in the missing elements: + if (error.Fail()) + return; + + // This should never happen. We already handled the case of one argument + // above, and we can only get Success & nullptr back if there's a one-word + // leaf. + assert(mwc != nullptr); + + mwc->GetSubcommandObject(args.GetArgumentAtIndex(num_args - 1), &matches); + if (matches.GetSize() == 0) + return; + + request.AddCompletions(matches); +} Index: lldb/source/API/SBCommandInterpreter.cpp =================================================================== --- lldb/source/API/SBCommandInterpreter.cpp +++ lldb/source/API/SBCommandInterpreter.cpp @@ -574,12 +574,11 @@ LLDB_RECORD_METHOD(lldb::SBCommand, SBCommandInterpreter, AddMultiwordCommand, (const char *, const char *), name, help); - CommandObjectMultiword *new_command = - new CommandObjectMultiword(*m_opaque_ptr, name, help); - new_command->SetRemovable(true); - lldb::CommandObjectSP new_command_sp(new_command); - if (new_command_sp && - m_opaque_ptr->AddUserCommand(name, new_command_sp, true)) + lldb::CommandObjectSP new_command_sp( + new CommandObjectMultiword(*m_opaque_ptr, name, help)); + new_command_sp->GetAsMultiwordCommand()->SetRemovable(true); + Status add_error = m_opaque_ptr->AddUserCommand(name, new_command_sp, true); + if (add_error.Success()) return LLDB_RECORD_RESULT(lldb::SBCommand(new_command_sp)); return LLDB_RECORD_RESULT(lldb::SBCommand()); } @@ -620,8 +619,8 @@ *m_opaque_ptr, name, impl, help, syntax, /*flags=*/0, auto_repeat_command); - if (new_command_sp && - m_opaque_ptr->AddUserCommand(name, new_command_sp, true)) + Status add_error = m_opaque_ptr->AddUserCommand(name, new_command_sp, true); + if (add_error.Success()) return LLDB_RECORD_RESULT(lldb::SBCommand(new_command_sp)); return LLDB_RECORD_RESULT(lldb::SBCommand()); } Index: lldb/include/lldb/Interpreter/CommandObjectMultiword.h =================================================================== --- lldb/include/lldb/Interpreter/CommandObjectMultiword.h +++ lldb/include/lldb/Interpreter/CommandObjectMultiword.h @@ -35,11 +35,19 @@ bool LoadSubCommand(llvm::StringRef cmd_name, const lldb::CommandObjectSP &command_obj) override; + Status LoadUserSubcommand(llvm::StringRef cmd_name, + const lldb::CommandObjectSP &command_obj, + bool can_replace) override; + + Status RemoveUserSubcommand(llvm::StringRef cmd_name, bool multiword_okay); + void GenerateHelpText(Stream &output_stream) override; lldb::CommandObjectSP GetSubcommandSP(llvm::StringRef sub_cmd, StringList *matches = nullptr) override; + lldb::CommandObjectSP GetSubcommandSPExact(llvm::StringRef sub_cmd) override; + CommandObject *GetSubcommandObject(llvm::StringRef sub_cmd, StringList *matches = nullptr) override; Index: lldb/include/lldb/Interpreter/CommandObject.h =================================================================== --- lldb/include/lldb/Interpreter/CommandObject.h +++ lldb/include/lldb/Interpreter/CommandObject.h @@ -145,6 +145,10 @@ virtual bool IsMultiwordObject() { return false; } + bool IsUserCommand() { return m_is_user_command; } + + void SetIsUserCommand(bool is_user) { m_is_user_command = is_user; } + virtual CommandObjectMultiword *GetAsMultiwordCommand() { return nullptr; } virtual bool IsAlias() { return false; } @@ -159,6 +163,10 @@ return lldb::CommandObjectSP(); } + virtual lldb::CommandObjectSP GetSubcommandSPExact(llvm::StringRef sub_cmd) { + return lldb::CommandObjectSP(); + } + virtual CommandObject *GetSubcommandObject(llvm::StringRef sub_cmd, StringList *matches = nullptr) { return nullptr; @@ -183,6 +191,14 @@ return false; } + virtual Status LoadUserSubcommand(llvm::StringRef cmd_name, + const lldb::CommandObjectSP &command_obj, + bool can_replace) { + Status result; + result.SetErrorString("can't add a subcommand to a plain command"); + return result; + } + virtual bool WantsRawCommandString() = 0; // By default, WantsCompletion = !WantsRawCommandString. Subclasses who want @@ -367,6 +383,7 @@ lldb::CommandOverrideCallback m_deprecated_command_override_callback; lldb::CommandOverrideCallbackWithResult m_command_override_callback; void *m_command_override_baton; + bool m_is_user_command = false; // Helper function to populate IDs or ID ranges as the command argument data // to the specified command argument entry. Index: lldb/include/lldb/Interpreter/CommandInterpreter.h =================================================================== --- lldb/include/lldb/Interpreter/CommandInterpreter.h +++ lldb/include/lldb/Interpreter/CommandInterpreter.h @@ -233,8 +233,9 @@ enum CommandTypes { eCommandTypesBuiltin = 0x0001, // native commands such as "frame" eCommandTypesUserDef = 0x0002, // scripted commands - eCommandTypesAliases = 0x0004, // aliases such as "po" - eCommandTypesHidden = 0x0008, // commands prefixed with an underscore + eCommandTypesUserMW = 0x0004, + eCommandTypesAliases = 0x0008, // aliases such as "po" + eCommandTypesHidden = 0x0010, // commands prefixed with an underscore eCommandTypesAllThem = 0xFFFF // all commands }; @@ -256,8 +257,8 @@ bool AddCommand(llvm::StringRef name, const lldb::CommandObjectSP &cmd_sp, bool can_replace); - bool AddUserCommand(llvm::StringRef name, const lldb::CommandObjectSP &cmd_sp, - bool can_replace); + Status AddUserCommand(llvm::StringRef name, + const lldb::CommandObjectSP &cmd_sp, bool can_replace); lldb::CommandObjectSP GetCommandSPExact(llvm::StringRef cmd, bool include_aliases = false) const; @@ -266,12 +267,49 @@ StringList *matches = nullptr, StringList *descriptions = nullptr) const; + CommandObject *GetUserCommandObject(llvm::StringRef cmd, + StringList *matches = nullptr, + StringList *descriptions = nullptr) const; + + /// Determine whether a root level, built-in command with this name exists. bool CommandExists(llvm::StringRef cmd) const; + /// Determine whether an alias command with this name exists bool AliasExists(llvm::StringRef cmd) const; + /// Determine whether a root-level user command with this name exists. bool UserCommandExists(llvm::StringRef cmd) const; + /// Determine whether a root-level user multiword command with this name + /// exists. + bool UserMultiwordCommandExists(llvm::StringRef cmd) const; + + /// Look up the command pointed to by path encoded in the arguments of + /// the incoming command object. If all the path components exist + /// and are all actual commands - not aliases, and the leaf command is a + /// multiword command, return the command. Otherwise return nullptr, and put + /// a useful diagnostic in the Status object. + /// + /// \param[in] path + /// An Args object holding the path in its arguments + /// \param[in] leaf_is_command + /// If true, return the container of the leaf name rather than looking up + /// the whole path as a leaf command. The leaf needn't exist in this case. + /// \param[in,out] result + /// If the path is not found, this error shows where we got off track. + /// \return + /// If found, a pointer to the CommandObjectMultiword pointed to by path, + /// or to the container of the leaf element is is_leaf_command. + /// Returns nullptr under two circumstances: + /// 1) The command in not found (check error.Fail) + /// 2) is_leaf is true and the path has only a leaf. We don't have a + /// dummy "contains everything MWC, so we return null here, but + /// in this case error.Success is true. + + CommandObjectMultiword *VerifyUserMultiwordCmdPath(Args &path, + bool leaf_is_command, + Status &result); + CommandAlias *AddAlias(llvm::StringRef alias_name, lldb::CommandObjectSP &command_obj_sp, llvm::StringRef args_string = llvm::StringRef()); @@ -283,6 +321,11 @@ bool GetAliasFullName(llvm::StringRef cmd, std::string &full_name) const; + bool RemoveUserMultiword(llvm::StringRef alias_name); + + // Do we want to allow top-level user multiword commands to be deleted? + void RemoveAllUserMultiword() { m_user_mw_dict.clear(); } + bool RemoveUser(llvm::StringRef alias_name); void RemoveAllUser() { m_user_dict.clear(); } @@ -414,6 +457,8 @@ bool HasUserCommands() const; + bool HasUserMultiwordCommands() const; + bool HasAliasOptions() const; void BuildAliasCommandArgs(CommandObject *alias_cmd_obj, @@ -421,6 +466,7 @@ std::string &raw_input_string, CommandReturnObject &result); + /// Picks the number out of a string of the form "%NNN", otherwise return 0. int GetOptionArgumentPosition(const char *in_string); void SkipLLDBInitFiles(bool skip_lldbinit_files) { @@ -437,7 +483,8 @@ StringList &commands_help, bool search_builtin_commands, bool search_user_commands, - bool search_alias_commands); + bool search_alias_commands, + bool search_user_mw_commands); bool GetBatchCommandMode() { return m_batch_command_mode; } @@ -506,6 +553,10 @@ return m_user_dict; } + const CommandObject::CommandMap &GetUserMultiwordCommands() const { + return m_user_mw_dict; + } + const CommandObject::CommandMap &GetCommands() const { return m_command_dict; } @@ -636,6 +687,8 @@ CommandObject::CommandMap m_alias_dict; // Stores user aliases/abbreviations for commands CommandObject::CommandMap m_user_dict; // Stores user-defined commands + CommandObject::CommandMap + m_user_mw_dict; // Stores user-defined multiword commands CommandHistory m_command_history; std::string m_repeat_command; // Stores the command that will be executed for // an empty command string. Index: lldb/include/lldb/Interpreter/CommandCompletions.h =================================================================== --- lldb/include/lldb/Interpreter/CommandCompletions.h +++ lldb/include/lldb/Interpreter/CommandCompletions.h @@ -13,6 +13,7 @@ #include "lldb/Core/FileSpecList.h" #include "lldb/Core/SearchFilter.h" +#include "lldb/Interpreter/Options.h" #include "lldb/Utility/CompletionRequest.h" #include "lldb/Utility/RegularExpression.h" #include "lldb/lldb-private.h" @@ -151,6 +152,15 @@ static void TypeCategoryNames(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 + // hand. + static void + CompleteModifiableCmdPathArgs(CommandInterpreter &interpreter, + CompletionRequest &request, + OptionElementVector &opt_element_vector); }; } // namespace lldb_private
_______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits