diff --git a/lldb/include/lldb/Interpreter/CommandCompletions.h b/lldb/include/lldb/Interpreter/CommandCompletions.h --- a/lldb/include/lldb/Interpreter/CommandCompletions.h +++ b/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 diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h --- a/lldb/include/lldb/Interpreter/CommandInterpreter.h +++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h @@ -231,11 +231,12 @@ }; 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 - eCommandTypesAllThem = 0xFFFF // all commands + eCommandTypesBuiltin = 0x0001, //< native commands such as "frame" + eCommandTypesUserDef = 0x0002, //< scripted commands + eCommandTypesUserMW = 0x0004, //< multiword commands (command containers) + eCommandTypesAliases = 0x0008, //< aliases such as "po" + eCommandTypesHidden = 0x0010, //< commands prefixed with an underscore + eCommandTypesAllThem = 0xFFFF //< all commands }; CommandInterpreter(Debugger &debugger, bool synchronous_execution); @@ -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 multiword_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. diff --git a/lldb/include/lldb/Interpreter/CommandObject.h b/lldb/include/lldb/Interpreter/CommandObject.h --- a/lldb/include/lldb/Interpreter/CommandObject.h +++ b/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,13 @@ return false; } + virtual llvm::Error LoadUserSubcommand(llvm::StringRef cmd_name, + const lldb::CommandObjectSP &command_obj, + bool can_replace) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "can only add commands to container commands"); + } + virtual bool WantsRawCommandString() = 0; // By default, WantsCompletion = !WantsRawCommandString. Subclasses who want @@ -367,6 +382,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. diff --git a/lldb/include/lldb/Interpreter/CommandObjectMultiword.h b/lldb/include/lldb/Interpreter/CommandObjectMultiword.h --- a/lldb/include/lldb/Interpreter/CommandObjectMultiword.h +++ b/lldb/include/lldb/Interpreter/CommandObjectMultiword.h @@ -35,11 +35,19 @@ bool LoadSubCommand(llvm::StringRef cmd_name, const lldb::CommandObjectSP &command_obj) override; + llvm::Error LoadUserSubcommand(llvm::StringRef cmd_name, + const lldb::CommandObjectSP &command_obj, + bool can_replace) override; + + llvm::Error 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; diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp --- a/lldb/source/API/SBCommandInterpreter.cpp +++ b/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()); } diff --git a/lldb/source/Commands/CommandCompletions.cpp b/lldb/source/Commands/CommandCompletions.cpp --- a/lldb/source/Commands/CommandCompletions.cpp +++ b/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 arguments, 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 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()); + 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); +} diff --git a/lldb/source/Commands/CommandObjectApropos.cpp b/lldb/source/Commands/CommandObjectApropos.cpp --- a/lldb/source/Commands/CommandObjectApropos.cpp +++ b/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'. " diff --git a/lldb/source/Commands/CommandObjectCommands.cpp b/lldb/source/Commands/CommandObjectCommands.cpp --- a/lldb/source/Commands/CommandObjectCommands.cpp +++ b/lldb/source/Commands/CommandObjectCommands.cpp @@ -443,6 +443,14 @@ return false; } + if (m_interpreter.UserMultiwordCommandExists(alias_command)) { + result.AppendErrorWithFormat( + "'%s' is a user container command and cannot be overwritten.\n" + "Delete it first with 'command container 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. @@ -528,6 +536,14 @@ return false; } + if (m_interpreter.UserMultiwordCommandExists(alias_command)) { + result.AppendErrorWithFormat( + "'%s' is user container command and cannot be overwritten.\n" + "Delete it first with 'command container delete'", + alias_command.c_str()); + return false; + } + CommandObjectSP command_obj_sp( m_interpreter.GetCommandSPExact(actual_command, true)); CommandObjectSP subcommand_obj_sp; @@ -1371,14 +1387,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 container " + "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. @@ -1392,6 +1415,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: @@ -1418,6 +1448,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( @@ -1438,6 +1471,7 @@ m_class_name.clear(); m_funct_name.clear(); m_short_help.clear(); + m_overwrite = false; m_synchronicity = eScriptedCommandSynchronicitySynchronous; } @@ -1450,6 +1484,7 @@ std::string m_class_name; std::string m_funct_name; std::string m_short_help; + bool m_overwrite; ScriptedCommandSynchronicity m_synchronicity = eScriptedCommandSynchronicitySynchronous; }; @@ -1484,26 +1519,36 @@ 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"); - error_sp->Flush(); + if (!m_container) { + Status error = m_interpreter.AddUserCommand( + m_cmd_name, command_obj_sp, m_overwrite); + if (error.Fail()) { + error_sp->Printf("error: unable to add selected command: '%s'", + error.AsCString()); + error_sp->Flush(); + } + } else { + llvm::Error llvm_error = m_container->LoadUserSubcommand( + m_cmd_name, command_obj_sp, m_overwrite); + if (llvm_error) { + error_sp->Printf("error: unable to add selected command: '%s'", + llvm::toString(std::move(llvm_error)).c_str()); + error_sp->Flush(); + } } } } else { error_sp->Printf( - "error: unable to create function, didn't add python command.\n"); + "error: unable to create function, didn't add python command\n"); error_sp->Flush(); } } else { - error_sp->Printf("error: empty function, didn't add python command.\n"); + error_sp->Printf("error: empty function, didn't add python command\n"); error_sp->Flush(); } } else { error_sp->Printf( - "error: script interpreter missing, didn't add python command.\n"); + "error: script interpreter missing, didn't add python command\n"); error_sp->Flush(); } @@ -1517,31 +1562,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) { @@ -1556,21 +1615,33 @@ 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"); - } } - + + // Assume we're going to succeed... + result.SetStatus(eReturnStatusSuccessFinishNoResult); + if (!m_container) { + Status add_error = + m_interpreter.AddUserCommand(m_cmd_name, new_cmd_sp, m_overwrite); + if (add_error.Fail()) + result.AppendErrorWithFormat("cannot add command: %s", + add_error.AsCString()); + } else { + llvm::Error llvm_error = + m_container->LoadUserSubcommand(m_cmd_name, new_cmd_sp, m_overwrite); + if (llvm_error) + result.AppendErrorWithFormat("cannot add command: %s", + llvm::toString(std::move(llvm_error)).c_str()); + } return result.Succeeded(); } CommandOptions m_options; std::string m_cmd_name; + CommandObjectMultiword *m_container = nullptr; std::string m_short_help; + bool m_overwrite; ScriptedCommandSynchronicity m_synchronicity; }; @@ -1580,7 +1651,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; @@ -1628,14 +1700,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. @@ -1650,30 +1725,86 @@ 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 container 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(); + llvm::Error llvm_error = container->RemoveUserSubcommand(leaf_cmd, + /* multiword not okay */ false); + if (llvm_error) { + result.AppendErrorWithFormat("could not delete command '%s': %s", + leaf_cmd, + llvm::toString(std::move(llvm_error)).c_str()); 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; } @@ -1710,6 +1841,271 @@ ~CommandObjectMultiwordCommandsScript() override = default; }; +#pragma mark CommandObjectCommandContainer +#define LLDB_OPTIONS_container_add +#include "CommandOptions.inc" + +class CommandObjectCommandsContainerAdd : public CommandObjectParsed { +public: + CommandObjectCommandsContainerAdd(CommandInterpreter &interpreter) + : CommandObjectParsed( + interpreter, "command container add", + "Add a container command to lldb. Adding to built-" + "in container commands is not allowed.", + "command container add [[path1]...] container-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); + } + + ~CommandObjectCommandsContainerAdd() 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 GetDefinitions() override { + return llvm::makeArrayRef(g_container_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())); + llvm::Error llvm_error = + add_to_me->LoadUserSubcommand(cmd_name, cmd_sp, m_options.m_overwrite); + if (llvm_error) { + result.AppendErrorWithFormat("error adding subcommand: %s", + llvm::toString(std::move(llvm_error)).c_str()); + return false; + } + + result.SetStatus(eReturnStatusSuccessFinishNoResult); + return true; + } + +private: + CommandOptions m_options; +}; + +#define LLDB_OPTIONS_multiword_delete +#include "CommandOptions.inc" +class CommandObjectCommandsContainerDelete : public CommandObjectParsed { +public: + CommandObjectCommandsContainerDelete(CommandInterpreter &interpreter) + : CommandObjectParsed( + interpreter, "command container delete", + "Delete a container command previously added to " + "lldb.", + "command container delete [[path1] ...] container-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); + } + + ~CommandObjectCommandsContainerDelete() 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("container command %s doesn't exist.", + cmd_name); + return false; + } + if (!cmd_sp->IsUserCommand()) { + result.AppendErrorWithFormat( + "container command %s is not a user command", cmd_name); + return false; + } + if (!cmd_sp->GetAsMultiwordCommand()) { + result.AppendErrorWithFormat("command %s is not a container 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 container command: %s", + path_error.AsCString()); + return false; + } + const char *leaf = command.GetArgumentAtIndex(num_args - 1); + llvm::Error llvm_error = + container->RemoveUserSubcommand(leaf, /* multiword okay */ true); + if (llvm_error) { + result.AppendErrorWithFormat("error removing container command: %s", + llvm::toString(std::move(llvm_error)).c_str()); + return false; + } + result.SetStatus(eReturnStatusSuccessFinishNoResult); + return true; + } +}; + +class CommandObjectCommandContainer : public CommandObjectMultiword { +public: + CommandObjectCommandContainer(CommandInterpreter &interpreter) + : CommandObjectMultiword( + interpreter, "command container", + "Commands for adding container commands to lldb. " + "Container commands are containers for other commands. You can" + "add nested container commands by specifying a command path, but " + "but you can't add commands into the built-in command hierarchy.", + "command container []") { + LoadSubCommand("add", CommandObjectSP(new CommandObjectCommandsContainerAdd( + interpreter))); + LoadSubCommand( + "delete", + CommandObjectSP(new CommandObjectCommandsContainerDelete(interpreter))); + } + + ~CommandObjectCommandContainer() override = default; +}; + #pragma mark CommandObjectMultiwordCommands // CommandObjectMultiwordCommands @@ -1727,6 +2123,8 @@ new CommandObjectCommandsUnalias(interpreter))); LoadSubCommand("delete", CommandObjectSP(new CommandObjectCommandsDelete(interpreter))); + LoadSubCommand("container", CommandObjectSP(new CommandObjectCommandContainer( + interpreter))); LoadSubCommand( "regex", CommandObjectSP(new CommandObjectCommandsAddRegex(interpreter))); LoadSubCommand( diff --git a/lldb/source/Commands/CommandObjectHelp.cpp b/lldb/source/Commands/CommandObjectHelp.cpp --- a/lldb/source/Commands/CommandObjectHelp.cpp +++ b/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; diff --git a/lldb/source/Commands/CommandObjectMultiword.cpp b/lldb/source/Commands/CommandObjectMultiword.cpp --- a/lldb/source/Commands/CommandObjectMultiword.cpp +++ b/lldb/source/Commands/CommandObjectMultiword.cpp @@ -26,36 +26,48 @@ CommandObjectMultiword::~CommandObjectMultiword() = default; +CommandObjectSP +CommandObjectMultiword::GetSubcommandSPExact(llvm::StringRef sub_cmd) { + if (m_subcommand_dict.empty()) + return {}; + + auto 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; } @@ -66,9 +78,9 @@ } bool CommandObjectMultiword::LoadSubCommand(llvm::StringRef name, - const CommandObjectSP &cmd_obj) { - if (cmd_obj) - assert((&GetCommandInterpreter() == &cmd_obj->GetCommandInterpreter()) && + const CommandObjectSP &cmd_obj_sp) { + if (cmd_obj_sp) + lldbassert((&GetCommandInterpreter() == &cmd_obj_sp->GetCommandInterpreter()) && "tried to add a CommandObject from a different interpreter"); CommandMap::iterator pos; @@ -76,13 +88,76 @@ pos = m_subcommand_dict.find(std::string(name)); if (pos == m_subcommand_dict.end()) { - m_subcommand_dict[std::string(name)] = cmd_obj; + m_subcommand_dict[std::string(name)] = cmd_obj_sp; } else success = false; return success; } +llvm::Error CommandObjectMultiword::LoadUserSubcommand( + llvm::StringRef name, const CommandObjectSP &cmd_obj_sp, bool can_replace) { + Status result; + if (cmd_obj_sp) + lldbassert((&GetCommandInterpreter() == &cmd_obj_sp->GetCommandInterpreter()) && + "tried to add a CommandObject from a different interpreter"); + if (!IsUserCommand()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "can't add a user subcommand to a builtin container command."); + } + // Make sure this a user command if it isn't already: + cmd_obj_sp->SetIsUserCommand(true); + + std::string str_name(name); + + auto pos = m_subcommand_dict.find(str_name); + if (pos == m_subcommand_dict.end()) { + m_subcommand_dict[str_name] = cmd_obj_sp; + return llvm::Error::success(); + } + + 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) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), error_str); + } + m_subcommand_dict[str_name] = cmd_obj_sp; + return llvm::Error::success(); +} + +llvm::Error CommandObjectMultiword::RemoveUserSubcommand(llvm::StringRef cmd_name, + bool must_be_multiword) { + CommandMap::iterator pos; + std::string str_name(cmd_name); + + pos = m_subcommand_dict.find(str_name); + if (pos == m_subcommand_dict.end()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(),"subcommand '%s' not found.", + str_name.c_str()); + } + if (!(*pos).second->IsUserCommand()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(),"subcommand '%s' not a user command.", + str_name.c_str()); + } + + if (must_be_multiword && !(*pos).second->IsMultiwordObject()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(),"subcommand '%s' is not a container command", + str_name.c_str()); + } + if (!must_be_multiword && (*pos).second->IsMultiwordObject()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(),"subcommand '%s' is not a user command", + str_name.c_str()); + } + + m_subcommand_dict.erase(pos); + + return llvm::Error::success(); +} + bool CommandObjectMultiword::Execute(const char *args_string, CommandReturnObject &result) { Args args(args_string); diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td --- a/lldb/source/Commands/Options.td +++ b/lldb/source/Commands/Options.td @@ -787,12 +787,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 = "container add" in { + def container_add_help : Option<"help", "h">, Arg<"HelpText">, + Desc<"Help text for this command">; + def container_add_long_help : Option<"long-help", "H">, Arg<"HelpText">, + Desc<"Long help text for this command">; + def container_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 " diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/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 container " + "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,44 @@ 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 tmp_list; + StringList *matches_ptr = matches ? matches : &tmp_list; + num_found = + AddNamesMatchingPartialString(GetUserCommands(), cmd_str, *matches_ptr); + num_found += AddNamesMatchingPartialString(GetUserMultiwordCommands(), + cmd_str, *matches_ptr); + + return {}; +} + bool CommandInterpreter::CommandExists(llvm::StringRef cmd) const { return m_command_dict.find(std::string(cmd)) != m_command_dict.end(); } @@ -1169,6 +1308,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 +1352,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 +1363,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 +1428,18 @@ result.AppendMessage(""); } + if (!m_user_mw_dict.empty() && + ((cmd_types & eCommandTypesUserMW) == eCommandTypesUserMW)) { + result.AppendMessage("Current user-defined container 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 '.\n", GetCommandPrefix()); @@ -1931,6 +2097,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, @@ -2581,6 +2751,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) { @@ -2700,7 +2873,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) @@ -2711,6 +2885,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); diff --git a/lldb/source/Interpreter/CommandObject.cpp b/lldb/source/Interpreter/CommandObject.cpp --- a/lldb/source/Interpreter/CommandObject.cpp +++ b/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." } diff --git a/lldb/test/API/commands/command/container/TestContainerCommands.py b/lldb/test/API/commands/command/container/TestContainerCommands.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/commands/command/container/TestContainerCommands.py @@ -0,0 +1,127 @@ +""" +Test user added container commands +""" + + +import sys +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * + + +class TestCmdContainer(TestBase): + + mydir = TestBase.compute_mydir(__file__) + NO_DEBUG_INFO_TESTCASE = True + + def test_container_add(self): + self.container_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 container_add(self): + # Make sure we can't overwrite built-in commands: + self.expect("command container add process", "Can't replace builtin container command", + substrs=["can't replace builtin command"], error=True) + self.expect("command container 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 container add process launch", "Can't replace builtin subcommand", + substrs=["Path component: 'process' is not a user command"], error=True) + + # Now lets make a container command: + self.runCmd("command container add -h 'A test container command' test-multi") + # Make sure the help works: + self.expect("help test-multi", "Help works for top-level multi", + substrs=["A test container command"]) + # Add a subcommand: + self.runCmd("command container add -h 'A test container sub-command' test-multi test-multi-sub") + # Make sure the help works: + self.expect("help test-multi", "Help shows sub-multi", + substrs=["A test container command", "test-multi-sub -- A test container sub-command"]) + self.expect("help test-multi test-multi-sub", "Help shows sub-multi", + substrs=["A test container sub-command"]) + + # Now add a script based command to the container 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 container 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 container commands: + self.expect("command script delete test-multi", "Delete script - can't delete container", + 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 container", + substrs=["subcommand 'test-multi-sub' is not a user command"], error = True) + + # You can't use command container delete to delete scripted commands: + self.expect("command container delete test-multi test-multi-sub welcome", "command container can't delete user commands", + substrs=["subcommand 'welcome' is not a container command"], error = True) + + # Also make sure you can't alias on top of container commands: + self.expect("command alias test-multi process launch", "Tried to alias on top of a container command", + substrs=["'test-multi' is a user container 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 container 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 container 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 container delete test-multi") + self.expect("test-multi", "Root command gone", substrs=["'test-multi' is not a valid command."], error=True) diff --git a/lldb/test/API/commands/command/container/welcome.py b/lldb/test/API/commands/command/container/welcome.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/commands/command/container/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 diff --git a/lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py b/lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py --- a/lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py +++ b/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): diff --git a/lldb/test/API/commands/command/script/TestCommandScript.py b/lldb/test/API/commands/command/script/TestCommandScript.py --- a/lldb/test/API/commands/command/script/TestCommandScript.py +++ b/lldb/test/API/commands/command/script/TestCommandScript.py @@ -147,7 +147,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") diff --git a/lldb/test/API/commands/expression/char/main.cpp b/lldb/test/API/commands/expression/char/main.cpp --- a/lldb/test/API/commands/expression/char/main.cpp +++ b/lldb/test/API/commands/expression/char/main.cpp @@ -1,3 +1,5 @@ +#include + 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 } diff --git a/lldb/test/API/functionalities/completion/TestCompletion.py b/lldb/test/API/functionalities/completion/TestCompletion.py --- a/lldb/test/API/functionalities/completion/TestCompletion.py +++ b/lldb/test/API/functionalities/completion/TestCompletion.py @@ -510,7 +510,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/'") diff --git a/lldb/unittests/Interpreter/CMakeLists.txt b/lldb/unittests/Interpreter/CMakeLists.txt --- a/lldb/unittests/Interpreter/CMakeLists.txt +++ b/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 +) diff --git a/lldb/unittests/Interpreter/TestCommandPaths.cpp b/lldb/unittests/Interpreter/TestCommandPaths.cpp new file mode 100644 --- /dev/null +++ b/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 container 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"); +}