[Lldb-commits] [lldb] c5011ae - Add a "command container" hierarchy to allow users to add container nodes.

Jim Ingham via lldb-commits lldb-commits at lists.llvm.org
Mon Oct 18 15:29:31 PDT 2021


Author: Jim Ingham
Date: 2021-10-18T15:29:24-07:00
New Revision: c5011aed9c297d6ddd8ee4f77453b215aa27554a

URL: https://github.com/llvm/llvm-project/commit/c5011aed9c297d6ddd8ee4f77453b215aa27554a
DIFF: https://github.com/llvm/llvm-project/commit/c5011aed9c297d6ddd8ee4f77453b215aa27554a.diff

LOG: Add a "command container" hierarchy to allow users to add container nodes.

The point is to allow users with a related set of script based commands
to organize their commands in a hierarchy in the command set, rather than
having to have only top-level commands.

Differential Revision: https://reviews.llvm.org/D110298

Added: 
    lldb/test/API/commands/command/container/TestContainerCommands.py
    lldb/test/API/commands/command/container/welcome.py
    lldb/unittests/Interpreter/TestCommandPaths.cpp

Modified: 
    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/script/TestCommandScript.py
    lldb/test/API/commands/expression/char/main.cpp
    lldb/test/API/functionalities/completion/TestCompletion.py
    lldb/unittests/Interpreter/CMakeLists.txt

Removed: 
    


################################################################################
diff  --git a/lldb/include/lldb/Interpreter/CommandCompletions.h b/lldb/include/lldb/Interpreter/CommandCompletions.h
index c80bde0e719bf..c13bc4997ff3e 100644
--- 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 @@ class CommandCompletions {
   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
index 3b3daced3e336..e6f0d5f9c4d43 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -231,11 +231,12 @@ class CommandInterpreter : public Broadcaster,
   };
 
   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 @@ class CommandInterpreter : public Broadcaster,
   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 @@ class CommandInterpreter : public Broadcaster,
                                   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 @@ class CommandInterpreter : public Broadcaster,
 
   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 @@ class CommandInterpreter : public Broadcaster,
 
   bool HasUserCommands() const;
 
+  bool HasUserMultiwordCommands() const;
+
   bool HasAliasOptions() const;
 
   void BuildAliasCommandArgs(CommandObject *alias_cmd_obj,
@@ -421,6 +466,7 @@ class CommandInterpreter : public Broadcaster,
                              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 @@ class CommandInterpreter : public Broadcaster,
                               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 @@ class CommandInterpreter : public Broadcaster,
     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 @@ class CommandInterpreter : public Broadcaster,
   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
index 8bc5d3e22355d..89cc161993a9f 100644
--- a/lldb/include/lldb/Interpreter/CommandObject.h
+++ b/lldb/include/lldb/Interpreter/CommandObject.h
@@ -145,6 +145,10 @@ class CommandObject {
 
   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 @@ class CommandObject {
     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 @@ class CommandObject {
     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 @@ class CommandObject {
   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
index f330a745f9bd2..a0e8d163c4b6d 100644
--- a/lldb/include/lldb/Interpreter/CommandObjectMultiword.h
+++ b/lldb/include/lldb/Interpreter/CommandObjectMultiword.h
@@ -35,11 +35,19 @@ class CommandObjectMultiword : public CommandObject {
   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
index b4a69c3e972a4..3830f6ed80baf 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -574,12 +574,11 @@ lldb::SBCommand SBCommandInterpreter::AddMultiwordCommand(const char *name,
   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 @@ lldb::SBCommand SBCommandInterpreter::AddCommand(
       *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
index 191c25f5e6771..42b0bac717bd4 100644
--- 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 @@ void CommandCompletions::TypeCategoryNames(CommandInterpreter &interpreter,
         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<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);
+}

diff  --git a/lldb/source/Commands/CommandObjectApropos.cpp b/lldb/source/Commands/CommandObjectApropos.cpp
index 656487169a34d..c6680f8b140d1 100644
--- a/lldb/source/Commands/CommandObjectApropos.cpp
+++ b/lldb/source/Commands/CommandObjectApropos.cpp
@@ -49,8 +49,8 @@ bool CommandObjectApropos::DoExecute(Args &args, CommandReturnObject &result) {
       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
index 639279875e715..1ec54cf7ededa 100644
--- a/lldb/source/Commands/CommandObjectCommands.cpp
+++ b/lldb/source/Commands/CommandObjectCommands.cpp
@@ -443,6 +443,14 @@ rather than using a positional placeholder:"
       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 @@ rather than using a positional placeholder:"
       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 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
   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 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
 
   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 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
         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 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
       m_class_name.clear();
       m_funct_name.clear();
       m_short_help.clear();
+      m_overwrite = false;
       m_synchronicity = eScriptedCommandSynchronicitySynchronous;
     }
 
@@ -1450,6 +1484,7 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
     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 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
             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 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
       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 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
         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 @@ class CommandObjectCommandsScriptList : public CommandObjectParsed {
 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 CommandObjectCommandsScriptClear : public CommandObjectParsed {
 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 @@ class CommandObjectCommandsScriptDelete : public CommandObjectParsed {
   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 @@ class CommandObjectMultiwordCommandsScript : public CommandObjectMultiword {
   ~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<OptionDefinition> 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 <subcommand> [<subcommand-options>]") {
+    LoadSubCommand("add", CommandObjectSP(new CommandObjectCommandsContainerAdd(
+                              interpreter)));
+    LoadSubCommand(
+        "delete",
+        CommandObjectSP(new CommandObjectCommandsContainerDelete(interpreter)));
+  }
+
+  ~CommandObjectCommandContainer() override = default;
+};
+
 #pragma mark CommandObjectMultiwordCommands
 
 // CommandObjectMultiwordCommands
@@ -1727,6 +2123,8 @@ CommandObjectMultiwordCommands::CommandObjectMultiwordCommands(
                                 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
index 4643ee30f0f9b..8c24efaa08ee3 100644
--- a/lldb/source/Commands/CommandObjectHelp.cpp
+++ b/lldb/source/Commands/CommandObjectHelp.cpp
@@ -51,8 +51,9 @@ CommandObjectHelp::CommandObjectHelp(CommandInterpreter &interpreter)
   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 @@ bool CommandObjectHelp::DoExecute(Args &command, CommandReturnObject &result) {
     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
index a523fd0b1560c..e800bcc12bd36 100644
--- a/lldb/source/Commands/CommandObjectMultiword.cpp
+++ b/lldb/source/Commands/CommandObjectMultiword.cpp
@@ -26,36 +26,48 @@ CommandObjectMultiword::CommandObjectMultiword(CommandInterpreter &interpreter,
 
 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 @@ CommandObjectMultiword::GetSubcommandObject(llvm::StringRef sub_cmd,
 }
 
 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 
diff erent interpreter");
 
   CommandMap::iterator pos;
@@ -76,13 +88,76 @@ bool CommandObjectMultiword::LoadSubCommand(llvm::StringRef name,
 
   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 
diff erent 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
index 3d69bb8ad8d05..b9f8950d2cace 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -787,12 +787,23 @@ let Command = "script add" in {
   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
index 223cccf28d74b..1c43ea15af01b 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -897,6 +897,63 @@ int CommandInterpreter::GetCommandNamesMatchingPartialString(
   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 @@ CommandInterpreter::GetCommandSP(llvm::StringRef cmd_str, bool include_aliases,
       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 @@ CommandInterpreter::GetCommandSP(llvm::StringRef cmd_str, bool include_aliases,
     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 @@ CommandInterpreter::GetCommandSP(llvm::StringRef cmd_str, bool include_aliases,
         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 @@ bool CommandInterpreter::AddCommand(llvm::StringRef name,
   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 @@ bool CommandInterpreter::AddCommand(llvm::StringRef name,
   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 
diff erent 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 @@ CommandInterpreter::GetCommandObject(llvm::StringRef cmd_str,
   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 @@ bool CommandInterpreter::UserCommandExists(llvm::StringRef cmd) const {
   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 @@ bool CommandInterpreter::RemoveCommand(llvm::StringRef cmd) {
   }
   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 @@ bool CommandInterpreter::RemoveUser(llvm::StringRef alias_name) {
   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 @@ void CommandInterpreter::GetHelp(CommandReturnObject &result,
     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 <command-name>'.\n",
       GetCommandPrefix());
@@ -1931,6 +2097,10 @@ bool CommandInterpreter::HasAliases() const { return (!m_alias_dict.empty()); }
 
 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 @@ void CommandInterpreter::OutputFormattedHelpText(Stream &strm,
 
   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 @@ void CommandInterpreter::FindCommandsForApropos(llvm::StringRef search_word,
                                                 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 @@ void CommandInterpreter::FindCommandsForApropos(llvm::StringRef search_word,
     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
index a7dcd56827017..64b23d04abea3 100644
--- a/lldb/source/Interpreter/CommandObject.cpp
+++ b/lldb/source/Interpreter/CommandObject.cpp
@@ -1120,7 +1120,7 @@ CommandObject::ArgumentTableEntry CommandObject::g_arguments_data[] = {
     { 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
index 0000000000000..408303dd43a54
--- /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 
diff erent 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 
diff erent 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
index 0000000000000..6283e69564006
--- /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
index 47d77b0e56962..3a9a4a39d89d8 100644
--- a/lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py
+++ b/lldb/test/API/commands/command/invalid-args/TestInvalidArgsCommand.py
@@ -9,10 +9,10 @@ class InvalidArgsCommandTestCase(TestBase):
     @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
index b103298f24e10..975a0a538ed67 100644
--- a/lldb/test/API/commands/command/script/TestCommandScript.py
+++ b/lldb/test/API/commands/command/script/TestCommandScript.py
@@ -147,7 +147,7 @@ def cleanup():
         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
index c8b0beb1b3553..9ff4436d88a08 100644
--- a/lldb/test/API/commands/expression/char/main.cpp
+++ b/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 @@ int main() {
   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
index 1688612c57854..ed901890f7dfe 100644
--- a/lldb/test/API/functionalities/completion/TestCompletion.py
+++ b/lldb/test/API/functionalities/completion/TestCompletion.py
@@ -510,7 +510,7 @@ def test_command_argument_completion(self):
 
     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
index 6ea5996e2b036..2080ce9085400 100644
--- 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
index 0000000000000..2a654d7fe7042
--- /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");
+}


        


More information about the lldb-commits mailing list