Repository: redis/redis

Problem statement:
Iterating all the commands
starting from https://github.com/redis/redis/pull/9504 not all commands are in `server.commands` (that is, if we are counting subcommands as commands, which we do)

we need to go over all references to `server.commands` and `server.orig_commands` and make sure that the usage is as intended (i.e. if the functions need to process only "top level" commands, and ignore subcommands. i assume this is not the case for most usages, but rather just to iterate all commands, in which case we will need to replace the logic with some recursive function tat takes. `redisCommand` - see `populateCommandStructure`)


Hints:

Patch:
diff --git a/src/acl.c b/src/acl.c
index 24a9e35b41e..174861f4aae 100644
--- a/src/acl.c
+++ b/src/acl.c
@@ -644,9 +644,7 @@ sds ACLDescribeSelectorCommandRulesSingleCommands(aclSelector *selector, aclSele
         int fakebit = ACLGetSelectorCommandBit(fake_selector,cmd->id);
         if (userbit != fakebit) {
             rules = sdscatlen(rules, userbit ? "+" : "-", 1);
-            sds fullname = getFullCommandName(cmd);
-            rules = sdscat(rules,fullname);
-            sdsfree(fullname);
+            rules = sdscatsds(rules,cmd->fullname);
             rules = sdscatlen(rules," ",1);
             ACLChangeSelectorPerm(fake_selector,cmd,userbit);
         }
@@ -660,9 +658,7 @@ sds ACLDescribeSelectorCommandRulesSingleCommands(aclSelector *selector, aclSele
         {
             for (int j = 0; selector->allowed_firstargs[cmd->id][j]; j++) {
                 rules = sdscatlen(rules,"+",1);
-                sds fullname = getFullCommandName(cmd);
-                rules = sdscat(rules,fullname);
-                sdsfree(fullname);
+                rules = sdscatsds(rules,cmd->fullname);
                 rules = sdscatlen(rules,"|",1);
                 rules = sdscatsds(rules,selector->allowed_firstargs[cmd->id][j]);
                 rules = sdscatlen(rules," ",1);
@@ -994,10 +990,10 @@ aclSelector *aclCreateSelectorFromOpSet(const char *opset, size_t opsetlen) {
  *              commands. For instance ~* allows all the keys. The pattern
  *              is a glob-style pattern like the one of KEYS.
  *              It is possible to specify multiple patterns.
- * %R~<pattern> Add key read pattern that specifies which keys can be read 
+ * %R~<pattern> Add key read pattern that specifies which keys can be read
  *              from.
  * %W~<pattern> Add key write pattern that specifies which keys can be
- *              written to. 
+ *              written to.
  * allkeys      Alias for ~*
  * resetkeys    Flush the list of allowed keys patterns.
  * &<pattern>   Add a pattern of channels that can be mentioned as part of
@@ -1005,7 +1001,7 @@ aclSelector *aclCreateSelectorFromOpSet(const char *opset, size_t opsetlen) {
  *              pattern is a glob-style pattern like the one of PSUBSCRIBE.
  *              It is possible to specify multiple patterns.
  * allchannels              Alias for &*
- * resetchannels            Flush the list of allowed keys patterns.
+ * resetchannels            Flush the list of allowed channel patterns.
  */
 int ACLSetSelector(aclSelector *selector, const char* op, size_t oplen) {
     if (!strcasecmp(op,"allkeys") ||
@@ -1456,9 +1452,12 @@ int ACLAuthenticateUser(client *c, robj *username, robj *password) {
  * should have an assigned ID (that is used to index the bitmap). This function
  * creates such an ID: it uses sequential IDs, reusing the same ID for the same
  * command name, so that a command retains the same ID in case of modules that
- * are unloaded and later reloaded. */
-unsigned long ACLGetCommandID(const char *cmdname) {
-    sds lowername = sdsnew(cmdname);
+ * are unloaded and later reloaded.
+ *
+ * The function does not take ownership of the 'cmdname' SDS string.
+ * */
+unsigned long ACLGetCommandID(sds cmdname) {
+    sds lowername = sdsdup(cmdname);
     sdstolower(lowername);
     if (commandId == NULL) commandId = raxNew();
     void *id = raxFind(commandId,(unsigned char*)lowername,sdslen(lowername));
@@ -2374,7 +2373,7 @@ void addACLLogEntry(client *c, int reason, int context, int argpos, sds username
         le->object = object;
     } else {
         switch(reason) {
-            case ACL_DENIED_CMD: le->object = getFullCommandName(c->cmd); break;
+            case ACL_DENIED_CMD: le->object = sdsdup(c->cmd->fullname); break;
             case ACL_DENIED_KEY: le->object = sdsdup(c->argv[argpos]->ptr); break;
             case ACL_DENIED_CHANNEL: le->object = sdsdup(c->argv[argpos]->ptr); break;
             case ACL_DENIED_AUTH: le->object = sdsdup(c->argv[0]->ptr); break;
@@ -2435,6 +2434,26 @@ void addACLLogEntry(client *c, int reason, int context, int argpos, sds username
  * ACL related commands
  * ==========================================================================*/
 
+/* ACL CAT category */
+void aclCatWithFlags(client *c, dict *commands, uint64_t cflag, int *arraylen) {
+    dictEntry *de;
+    dictIterator *di = dictGetIterator(commands);
+
+    while ((de = dictNext(di)) != NULL) {
+        struct redisCommand *cmd = dictGetVal(de);
+        if (cmd->flags & CMD_MODULE) continue;
+        if (cmd->acl_categories & cflag) {
+            addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
+            (*arraylen)++;
+        }
+
+        if (cmd->subcommands_dict) {
+            aclCatWithFlags(c, cmd->subcommands_dict, cflag, arraylen);
+        }
+    }
+    dictReleaseIterator(di);
+}
+
 /* Add the formatted response from a single selector to the ACL GETUSER
  * response. This function returns the number of fields added. 
  * 
@@ -2686,22 +2705,12 @@ void aclCommand(client *c) {
     } else if (!strcasecmp(sub,"cat") && c->argc == 3) {
         uint64_t cflag = ACLGetCommandCategoryFlagByName(c->argv[2]->ptr);
         if (cflag == 0) {
-            addReplyErrorFormat(c, "Unknown category '%s'", (char*)c->argv[2]->ptr);
+            addReplyErrorFormat(c, "Unknown category '%.128s'", (char*)c->argv[2]->ptr);
             return;
         }
         int arraylen = 0;
         void *dl = addReplyDeferredLen(c);
-        dictIterator *di = dictGetIterator(server.orig_commands);
-        dictEntry *de;
-        while ((de = dictNext(di)) != NULL) {
-            struct redisCommand *cmd = dictGetVal(de);
-            if (cmd->flags & CMD_MODULE) continue;
-            if (cmd->acl_categories & cflag) {
-                addReplyBulkCString(c,cmd->name);
-                arraylen++;
-            }
-        }
-        dictReleaseIterator(di);
+        aclCatWithFlags(c, server.orig_commands, cflag, &arraylen);
         setDeferredArrayLen(c,dl,arraylen);
     } else if (!strcasecmp(sub,"genpass") && (c->argc == 2 || c->argc == 3)) {
         #define GENPASS_MAX_BITS 4096
@@ -2809,7 +2818,7 @@ void aclCommand(client *c) {
             sds err = sdsempty();
             if (result == ACL_DENIED_CMD) {
                 err = sdscatfmt(err, "This user has no permissions to run "
-                    "the '%s' command or its subcommand", c->cmd->name);
+                    "the '%s' command", c->cmd->fullname);
             } else if (result == ACL_DENIED_KEY) {
                 err = sdscatfmt(err, "This user has no permissions to access "
                     "the '%s' key", c->argv[idx + 3]->ptr);
diff --git a/src/cluster.c b/src/cluster.c
index df59f9ae72e..d2d179e6fd6 100644
--- a/src/cluster.c
+++ b/src/cluster.c
@@ -5131,8 +5131,7 @@ NULL
     } else if ((!strcasecmp(c->argv[1]->ptr,"addslotsrange") ||
                !strcasecmp(c->argv[1]->ptr,"delslotsrange")) && c->argc >= 4) {
         if (c->argc % 2 == 1) {
-            addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
-                            c->cmd->name);
+            addReplyErrorArity(c);
             return;
         }
         /* CLUSTER ADDSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...] */
diff --git a/src/expire.c b/src/expire.c
index 059d0c344c3..f49e371b6ac 100644
--- a/src/expire.c
+++ b/src/expire.c
@@ -571,14 +571,14 @@ void expireGenericCommand(client *c, long long basetime, int unit) {
      * overflow by either unit conversion or basetime addition. */
     if (unit == UNIT_SECONDS) {
         if (when > LLONG_MAX / 1000 || when < LLONG_MIN / 1000) {
-            addReplyErrorFormat(c, "invalid expire time in %s", c->cmd->name);
+            addReplyErrorExpireTime(c);
             return;
         }
         when *= 1000;
     }
 
     if (when > LLONG_MAX - basetime) {
-        addReplyErrorFormat(c, "invalid expire time in %s", c->cmd->name);
+        addReplyErrorExpireTime(c);
         return;
     }
     when += basetime;
diff --git a/src/latency.c b/src/latency.c
index 1eac48191a9..95e5a8a1d85 100644
--- a/src/latency.c
+++ b/src/latency.c
@@ -522,33 +522,24 @@ void fillCommandCDF(client *c, struct hdr_histogram* histogram) {
 
 /* latencyCommand() helper to produce for all commands,
  * a per command cumulative distribution of latencies. */
-void latencyAllCommandsFillCDF(client *c) {
-    dictIterator *di = dictGetSafeIterator(server.commands);
+void latencyAllCommandsFillCDF(client *c, dict *commands, int *command_with_data) {
+    dictIterator *di = dictGetSafeIterator(commands);
     dictEntry *de;
     struct redisCommand *cmd;
-    void *replylen = addReplyDeferredLen(c);
-    int command_with_data = 0;
+
     while((de = dictNext(di)) != NULL) {
         cmd = (struct redisCommand *) dictGetVal(de);
         if (cmd->latency_histogram) {
-            addReplyBulkCString(c,cmd->name);
+            addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
             fillCommandCDF(c, cmd->latency_histogram);
-            command_with_data++;
+            (*command_with_data)++;
         }
 
         if (cmd->subcommands) {
-            for (int j = 0; cmd->subcommands[j].name; j++) {
-                struct redisCommand *sub = cmd->subcommands+j;
-                if (sub->latency_histogram) {
-                    addReplyBulkSds(c,getFullCommandName(sub));
-                    fillCommandCDF(c, sub->latency_histogram);
-                    command_with_data++;
-                }
-            }
+            latencyAllCommandsFillCDF(c, cmd->subcommands_dict, command_with_data);
         }
     }
     dictReleaseIterator(di);
-    setDeferredMapLen(c,replylen,command_with_data);
 }
 
 /* latencyCommand() helper to produce for a specific command set,
@@ -564,20 +555,24 @@ void latencySpecificCommandsFillCDF(client *c) {
         }
 
         if (cmd->latency_histogram) {
-            addReplyBulkSds(c,getFullCommandName(cmd));
+            addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
             fillCommandCDF(c, cmd->latency_histogram);
             command_with_data++;
         }
 
-        if (cmd->subcommands) {
-            for (int j = 0; cmd->subcommands[j].name; j++) {
-                struct redisCommand *sub = cmd->subcommands+j;
+        if (cmd->subcommands_dict) {
+            dictEntry *de;
+            dictIterator *di = dictGetSafeIterator(cmd->subcommands_dict);
+
+            while ((de = dictNext(di)) != NULL) {
+                struct redisCommand *sub = dictGetVal(de);
                 if (sub->latency_histogram) {
-                    addReplyBulkSds(c,getFullCommandName(sub));
+                    addReplyBulkCBuffer(c, sub->fullname, sdslen(sub->fullname));
                     fillCommandCDF(c, sub->latency_histogram);
                     command_with_data++;
                 }
             }
+            dictReleaseIterator(di);
         }
     }
     setDeferredMapLen(c,replylen,command_with_data);
@@ -725,7 +720,10 @@ void latencyCommand(client *c) {
     } else if (!strcasecmp(c->argv[1]->ptr,"histogram") && c->argc >= 2) {
         /* LATENCY HISTOGRAM*/
         if (c->argc == 2) {
-            latencyAllCommandsFillCDF(c);
+            int command_with_data = 0;
+            void *replylen = addReplyDeferredLen(c);
+            latencyAllCommandsFillCDF(c, server.commands, &command_with_data);
+            setDeferredMapLen(c, replylen, command_with_data);
         } else {
             latencySpecificCommandsFillCDF(c);
         }
diff --git a/src/module.c b/src/module.c
index 2e26527173d..0384f2d1e96 100644
--- a/src/module.c
+++ b/src/module.c
@@ -877,7 +877,7 @@ int64_t commandKeySpecsFlagsFromString(const char *s) {
     return flags;
 }
 
-RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, const char *name, RedisModuleCmdFunc cmdfunc, int64_t flags, int firstkey, int lastkey, int keystep);
+RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds declared_name, sds fullname, RedisModuleCmdFunc cmdfunc, int64_t flags, int firstkey, int lastkey, int keystep);
 
 /* Register a new command in the Redis server, that will be handled by
  * calling the function pointer 'cmdfunc' using the RedisModule calling
@@ -978,19 +978,26 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
     if (lookupCommandByCString(name) != NULL)
         return REDISMODULE_ERR;
 
-    RedisModuleCommand *cp = moduleCreateCommandProxy(ctx->module, name, cmdfunc, flags, firstkey, lastkey, keystep);
+    sds declared_name = sdsnew(name);
+    RedisModuleCommand *cp = moduleCreateCommandProxy(ctx->module, declared_name, sdsdup(declared_name), cmdfunc, flags, firstkey, lastkey, keystep);
     cp->rediscmd->arity = cmdfunc ? -1 : -2; /* Default value, can be changed later via dedicated API */
 
-    dictAdd(server.commands,sdsnew(name),cp->rediscmd);
-    dictAdd(server.orig_commands,sdsnew(name),cp->rediscmd);
-    cp->rediscmd->id = ACLGetCommandID(name); /* ID used for ACL. */
+    serverAssert(dictAdd(server.commands, sdsdup(declared_name), cp->rediscmd) == DICT_OK);
+    serverAssert(dictAdd(server.orig_commands, sdsdup(declared_name), cp->rediscmd) == DICT_OK);
+    cp->rediscmd->id = ACLGetCommandID(declared_name); /* ID used for ACL. */
     return REDISMODULE_OK;
 }
 
-RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, const char *name, RedisModuleCmdFunc cmdfunc, int64_t flags, int firstkey, int lastkey, int keystep) {
+/* A proxy that help create a module command / subcommand.
+ *
+ * 'declared_name': it contains the sub_name, which is just the fullname for non-subcommands.
+ * 'fullname': sds string representing the command fullname.
+ *
+ * Function will take the ownership of both 'declared_name' and 'fullname' SDS.
+ */
+RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds declared_name, sds fullname, RedisModuleCmdFunc cmdfunc, int64_t flags, int firstkey, int lastkey, int keystep) {
     struct redisCommand *rediscmd;
     RedisModuleCommand *cp;
-    sds cmdname = sdsnew(name);
 
     /* Create a command "proxy", which is a structure that is referenced
      * in the command table, so that the generic command that works as
@@ -1003,7 +1010,8 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, const c
     cp->module = module;
     cp->func = cmdfunc;
     cp->rediscmd = zcalloc(sizeof(*rediscmd));
-    cp->rediscmd->name = cmdname;
+    cp->rediscmd->declared_name = declared_name; /* SDS for module commands */
+    cp->rediscmd->fullname = fullname;
     cp->rediscmd->group = COMMAND_GROUP_MODULE;
     cp->rediscmd->proc = RedisModuleCommandDispatcher;
     cp->rediscmd->flags = flags | CMD_MODULE;
@@ -1099,13 +1107,17 @@ int RM_CreateSubcommand(RedisModuleCommand *parent, const char *name, RedisModul
         return REDISMODULE_ERR; /* A parent command should be a pure container of subcommands */
 
     /* Check if the command name is busy within the parent command. */
-    if (parent_cmd->subcommands_dict && lookupCommandByCStringLogic(parent_cmd->subcommands_dict, name) != NULL)
+    sds declared_name = sdsnew(name);
+    if (parent_cmd->subcommands_dict && lookupSubcommand(parent_cmd, declared_name) != NULL) {
+        sdsfree(declared_name);
         return REDISMODULE_ERR;
+    }
 
-    RedisModuleCommand *cp = moduleCreateCommandProxy(parent->module, name, cmdfunc, flags, firstkey, lastkey, keystep);
+    sds fullname = catSubCommandFullname(parent_cmd->fullname, name);
+    RedisModuleCommand *cp = moduleCreateCommandProxy(parent->module, declared_name, fullname, cmdfunc, flags, firstkey, lastkey, keystep);
     cp->rediscmd->arity = -2;
 
-    commandAddSubcommand(parent_cmd, cp->rediscmd);
+    commandAddSubcommand(parent_cmd, cp->rediscmd, name);
     return REDISMODULE_OK;
 }
 
@@ -5056,7 +5068,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
         }
         acl_retval = ACLCheckAllUserCommandPerm(ctx->client->user,c->cmd,c->argv,c->argc,&acl_errpos);
         if (acl_retval != ACL_OK) {
-            sds object = (acl_retval == ACL_DENIED_CMD) ? sdsnew(c->cmd->name) : sdsdup(c->argv[acl_errpos]->ptr);
+            sds object = (acl_retval == ACL_DENIED_CMD) ? sdsdup(c->cmd->fullname) : sdsdup(c->argv[acl_errpos]->ptr);
             addACLLogEntry(ctx->client, acl_retval, ACL_LOG_CTX_MODULE, -1, ctx->client->user->name, object);
             errno = EACCES;
             goto cleanup;
@@ -9965,40 +9977,71 @@ void moduleFreeModuleStructure(struct RedisModule *module) {
     zfree(module);
 }
 
+/* Free the command registered with the specified module.
+ * On success C_OK is returned, otherwise C_ERR is returned.
+ *
+ * Note that caller needs to handle the deletion of the command table dict,
+ * and after that needs to free the command->fullname and the command itself.
+ */
+int moduleFreeCommand(struct RedisModule *module, struct redisCommand *cmd) {
+    if (cmd->proc != RedisModuleCommandDispatcher)
+        return C_ERR;
+
+    RedisModuleCommand *cp = (void*)(unsigned long)cmd->getkeys_proc;
+    if (cp->module != module)
+        return C_ERR;
+
+    /* Free everything except cmd->fullname and cmd itself. */
+    if (cmd->key_specs != cmd->key_specs_static)
+        zfree(cmd->key_specs);
+    for (int j = 0; cmd->tips && cmd->tips[j]; j++)
+        sdsfree((sds)cmd->tips[j]);
+    for (int j = 0; cmd->history && cmd->history[j].since; j++) {
+        sdsfree((sds)cmd->history[j].since);
+        sdsfree((sds)cmd->history[j].changes);
+    }
+    sdsfree((sds)cmd->summary);
+    sdsfree((sds)cmd->since);
+    sdsfree((sds)cmd->complexity);
+    if (cmd->latency_histogram) {
+        hdr_close(cmd->latency_histogram);
+        cmd->latency_histogram = NULL;
+    }
+    zfree(cmd->args);
+    zfree(cp);
+
+    if (cmd->subcommands_dict) {
+        dictEntry *de;
+        dictIterator *di = dictGetSafeIterator(cmd->subcommands_dict);
+        while ((de = dictNext(di)) != NULL) {
+            struct redisCommand *sub = dictGetVal(de);
+            if (moduleFreeCommand(module, sub) != C_OK) continue;
+
+            serverAssert(dictDelete(cmd->subcommands_dict, sub->declared_name) == DICT_OK);
+            sdsfree((sds)sub->declared_name);
+            sdsfree(sub->fullname);
+            zfree(sub);
+        }
+        dictReleaseIterator(di);
+        dictRelease(cmd->subcommands_dict);
+    }
+
+    return C_OK;
+}
+
 void moduleUnregisterCommands(struct RedisModule *module) {
     /* Unregister all the commands registered by this module. */
     dictIterator *di = dictGetSafeIterator(server.commands);
     dictEntry *de;
     while ((de = dictNext(di)) != NULL) {
         struct redisCommand *cmd = dictGetVal(de);
-        if (cmd->proc == RedisModuleCommandDispatcher) {
-            RedisModuleCommand *cp =
-                (void*)(unsigned long)cmd->getkeys_proc;
-            sds cmdname = (sds)cmd->name;
-            if (cp->module == module) {
-                if (cmd->key_specs != cmd->key_specs_static)
-                    zfree(cmd->key_specs);
-                for (int j = 0; cmd->tips && cmd->tips[j]; j++)
-                    sdsfree((sds)cmd->tips[j]);
-                for (int j = 0; cmd->history && cmd->history[j].since; j++) {
-                    sdsfree((sds)cmd->history[j].since);
-                    sdsfree((sds)cmd->history[j].changes);
-                }
-                dictDelete(server.commands,cmdname);
-                dictDelete(server.orig_commands,cmdname);
-                sdsfree(cmdname);
-                sdsfree((sds)cmd->summary);
-                sdsfree((sds)cmd->since);
-                sdsfree((sds)cmd->complexity);
-                if (cmd->latency_histogram) {
-                    hdr_close(cmd->latency_histogram);
-                    cmd->latency_histogram = NULL;
-                }
-                zfree(cmd->args);
-                zfree(cmd);
-                zfree(cp);
-            }
-        }
+        if (moduleFreeCommand(module, cmd) != C_OK) continue;
+
+        serverAssert(dictDelete(server.commands, cmd->fullname) == DICT_OK);
+        serverAssert(dictDelete(server.orig_commands, cmd->fullname) == DICT_OK);
+        sdsfree((sds)cmd->declared_name);
+        sdsfree(cmd->fullname);
+        zfree(cmd);
     }
     dictReleaseIterator(di);
 }
@@ -10523,7 +10566,7 @@ const char *RM_GetCurrentCommandName(RedisModuleCtx *ctx) {
     if (!ctx || !ctx->client || !ctx->client->cmd)
         return NULL;
 
-    return (const char*)ctx->client->cmd->name;
+    return (const char*)ctx->client->cmd->fullname;
 }
 
 /* --------------------------------------------------------------------------
diff --git a/src/networking.c b/src/networking.c
index 8ea5efd1b85..05001c56440 100644
--- a/src/networking.c
+++ b/src/networking.c
@@ -362,10 +362,9 @@ void _addReplyToBufferOrList(client *c, const char *s, size_t len) {
      * Note this is the simplest way to check a command added a response. Replication links are used to write data but
      * not for responses, so we should normally never get here on a replica client. */
     if (getClientType(c) == CLIENT_TYPE_SLAVE) {
-        sds cmdname = c->lastcmd ? getFullCommandName(c->lastcmd) : NULL;
-        logInvalidUseAndFreeClientAsync(c, "Replica generated a reply to command %s",
+        sds cmdname = c->lastcmd ? c->lastcmd->fullname : NULL;
+        logInvalidUseAndFreeClientAsync(c, "Replica generated a reply to command '%s'",
                                         cmdname ? cmdname : "<unknown>");
-        sdsfree(cmdname);
         return;
     }
 
@@ -484,10 +483,10 @@ void afterErrorReply(client *c, const char *s, size_t len) {
         }
 
         if (len > 4096) len = 4096;
-        const char *cmdname = c->lastcmd ? c->lastcmd->name : "<unknown>";
+        sds cmdname = c->lastcmd ? c->lastcmd->fullname : NULL;
         serverLog(LL_WARNING,"== CRITICAL == This %s is sending an error "
                              "to its %s: '%.*s' after processing the command "
-                             "'%s'", from, to, (int)len, s, cmdname);
+                             "'%s'", from, to, (int)len, s, cmdname ? cmdname : "<unknown>");
         if (ctype == CLIENT_TYPE_MASTER && server.repl_backlog &&
             server.repl_backlog->histlen > 0)
         {
@@ -550,6 +549,16 @@ void addReplyErrorFormat(client *c, const char *fmt, ...) {
     sdsfree(s);
 }
 
+void addReplyErrorArity(client *c) {
+    addReplyErrorFormat(c, "wrong number of arguments for '%s' command",
+                        c->cmd->fullname);
+}
+
+void addReplyErrorExpireTime(client *c) {
+    addReplyErrorFormat(c, "invalid expire time in '%s' command",
+                        c->cmd->fullname);
+}
+
 void addReplyStatusLength(client *c, const char *s, size_t len) {
     addReplyProto(c,"+",1);
     addReplyProto(c,s,len);
@@ -610,10 +619,9 @@ void *addReplyDeferredLen(client *c) {
      * Note this is the simplest way to check a command added a response. Replication links are used to write data but
      * not for responses, so we should normally never get here on a replica client. */
     if (getClientType(c) == CLIENT_TYPE_SLAVE) {
-        sds cmdname = c->lastcmd ? getFullCommandName(c->lastcmd) : NULL;
-        logInvalidUseAndFreeClientAsync(c, "Replica generated a reply to command %s",
+        sds cmdname = c->lastcmd ? c->lastcmd->fullname : NULL;
+        logInvalidUseAndFreeClientAsync(c, "Replica generated a reply to command '%s'",
                                         cmdname ? cmdname : "<unknown>");
-        sdsfree(cmdname);
         return NULL;
     }
 
@@ -2547,7 +2555,6 @@ sds catClientInfoString(sds s, client *client) {
         used_blocks_of_repl_buf = last->id - cur->id + 1;
     }
 
-    sds cmdname = client->lastcmd ? getFullCommandName(client->lastcmd) : NULL;
     sds ret = sdscatfmt(s,
         "id=%U addr=%s laddr=%s %s name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i multi=%i qbuf=%U qbuf-free=%U argv-mem=%U multi-mem=%U obl=%U oll=%U omem=%U tot-mem=%U events=%s cmd=%s user=%s redir=%I resp=%i",
         (unsigned long long) client->id,
@@ -2571,12 +2578,10 @@ sds catClientInfoString(sds s, client *client) {
         (unsigned long long) obufmem, /* should not include client->buf since we want to see 0 for static clients. */
         (unsigned long long) total_mem,
         events,
-        cmdname ? cmdname : "NULL",
+        client->lastcmd ? client->lastcmd->fullname : "NULL",
         client->user ? client->user->name : "(superuser)",
         (client->flags & CLIENT_TRACKING) ? (long long) client->client_tracking_redirection : -1,
         client->resp);
-    if (cmdname)
-        sdsfree(cmdname);
     return ret;
 }
 
diff --git a/src/sentinel.c b/src/sentinel.c
index 25bdb8cafb3..dbc3e98762f 100644
--- a/src/sentinel.c
+++ b/src/sentinel.c
@@ -4058,8 +4058,7 @@ NULL
     return;
 
 numargserr:
-    addReplyErrorFormat(c,"Wrong number of arguments for 'sentinel %s'",
-                          (char*)c->argv[1]->ptr);
+    addReplyErrorArity(c);
 }
 
 #define info_section_from_redis(section_name) do { \
diff --git a/src/server.c b/src/server.c
index 50fc7a9574f..f3802577252 100644
--- a/src/server.c
+++ b/src/server.c
@@ -2556,7 +2556,7 @@ void InitServerLast() {
  *
  * If this functions fails it means that the legacy (first,last,step)
  * spec used by COMMAND will show 0,0,0. This is not a dire situation
- * because anyway the legacy (first,last,step) spec is to be dperecated
+ * because anyway the legacy (first,last,step) spec is to be deprecated
  * and one should use the new key specs scheme.
  */
 void populateCommandLegacyRangeSpec(struct redisCommand *c) {
@@ -2607,21 +2607,23 @@ void populateCommandLegacyRangeSpec(struct redisCommand *c) {
     c->legacy_range_key_spec.fk.range.limit = 0;
 }
 
-void commandAddSubcommand(struct redisCommand *parent, struct redisCommand *subcommand) {
+sds catSubCommandFullname(const char *parent_name, const char *sub_name) {
+    return sdscatfmt(sdsempty(), "%s|%s", parent_name, sub_name);
+}
+
+void commandAddSubcommand(struct redisCommand *parent, struct redisCommand *subcommand, const char *declared_name) {
     if (!parent->subcommands_dict)
         parent->subcommands_dict = dictCreate(&commandTableDictType);
 
     subcommand->parent = parent; /* Assign the parent command */
-    sds fullname = getFullCommandName(subcommand);
-    subcommand->id = ACLGetCommandID(fullname); /* Assign the ID used for ACL. */
-    sdsfree(fullname);
+    subcommand->id = ACLGetCommandID(subcommand->fullname); /* Assign the ID used for ACL. */
 
-    serverAssert(dictAdd(parent->subcommands_dict, sdsnew(subcommand->name), subcommand) == DICT_OK);
+    serverAssert(dictAdd(parent->subcommands_dict, sdsnew(declared_name), subcommand) == DICT_OK);
 }
 
 /* Set implicit ACl categories (see comment above the definition of
  * struct redisCommand). */
-void setImplictACLCategories(struct redisCommand *c) {
+void setImplicitACLCategories(struct redisCommand *c) {
     if (c->flags & CMD_WRITE)
         c->acl_categories |= ACL_CATEGORY_WRITE;
     if (c->flags & CMD_READONLY)
@@ -2653,7 +2655,7 @@ int populateArgsStructure(struct redisCommandArg *args) {
     return count;
 }
 
-/* Recursively populate the command stracture. */
+/* Recursively populate the command structure. */
 void populateCommandStructure(struct redisCommand *c) {
     /* Redis commands don't need more args than STATIC_KEY_SPECS_NUM (Number of keys
      * specs can be greater than STATIC_KEY_SPECS_NUM only for module commands) */
@@ -2683,38 +2685,39 @@ void populateCommandStructure(struct redisCommand *c) {
     populateCommandMovableKeys(c);
 
     /* Assign the ID used for ACL. */
-    c->id = ACLGetCommandID(c->name);
+    c->id = ACLGetCommandID(c->fullname);
 
     /* Handle subcommands */
     if (c->subcommands) {
-        for (int j = 0; c->subcommands[j].name; j++) {
+        for (int j = 0; c->subcommands[j].declared_name; j++) {
             struct redisCommand *sub = c->subcommands+j;
 
             /* Translate the command string flags description into an actual
              * set of flags. */
-            setImplictACLCategories(sub);
+            setImplicitACLCategories(sub);
+            sub->fullname = catSubCommandFullname(c->declared_name, sub->declared_name);
             populateCommandStructure(sub);
-            commandAddSubcommand(c,sub);
+            commandAddSubcommand(c, sub, sub->declared_name);
         }
     }
 }
 
 extern struct redisCommand redisCommandTable[];
 
-/* Populates the Redis Command Table starting from the hard coded list
- * we have on top of server.c file. */
+/* Populates the Redis Command Table dict from from the static table in commands.c
+ * which is auto generated from the json files in the commands folder. */
 void populateCommandTable(void) {
     int j;
     struct redisCommand *c;
 
     for (j = 0;; j++) {
         c = redisCommandTable + j;
-        if (c->name == NULL)
+        if (c->declared_name == NULL)
             break;
 
         int retval1, retval2;
 
-        setImplictACLCategories(c);
+        setImplicitACLCategories(c);
 
         if (!(c->flags & CMD_SENTINEL) && server.sentinel_mode)
             continue;
@@ -2722,12 +2725,13 @@ void populateCommandTable(void) {
         if (c->flags & CMD_ONLY_SENTINEL && !server.sentinel_mode)
             continue;
 
+        c->fullname = sdsnew(c->declared_name);
         populateCommandStructure(c);
 
-        retval1 = dictAdd(server.commands, sdsnew(c->name), c);
+        retval1 = dictAdd(server.commands, sdsdup(c->fullname), c);
         /* Populate an additional dictionary that will be unaffected
          * by rename-command statements in redis.conf. */
-        retval2 = dictAdd(server.orig_commands, sdsnew(c->name), c);
+        retval2 = dictAdd(server.orig_commands, sdsdup(c->fullname), c);
         serverAssert(retval1 == DICT_OK && retval2 == DICT_OK);
     }
 }
@@ -2752,7 +2756,6 @@ void resetCommandTableStats(dict* commands) {
             resetCommandTableStats(c->subcommands_dict);
     }
     dictReleaseIterator(di);
-
 }
 
 void resetErrorTableStats(void) {
@@ -2812,6 +2815,10 @@ int isContainerCommandBySds(sds s) {
     return has_subcommands;
 }
 
+struct redisCommand *lookupSubcommand(struct redisCommand *container, sds sub_name) {
+    return dictFetchValue(container->subcommands_dict, sub_name);
+}
+
 /* Look up a command by argv and argc
  *
  * If `strict` is not 0 we expect argc to be exact (i.e. argc==2
@@ -2832,7 +2839,7 @@ struct redisCommand *lookupCommandLogic(dict *commands, robj **argv, int argc, i
         if (strict && argc != 2)
             return NULL;
         /* Note: Currently we support just one level of subcommands */
-        return dictFetchValue(base_cmd->subcommands_dict, argv[1]->ptr);
+        return lookupSubcommand(base_cmd, argv[1]->ptr);
     }
 }
 
@@ -3420,8 +3427,7 @@ int processCommand(client *c) {
     } else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
                (c->argc < -c->cmd->arity))
     {
-        rejectCommandFormat(c,"wrong number of arguments for '%s' command or subcommand",
-            c->cmd->name);
+        rejectCommandFormat(c,"wrong number of arguments for '%s' command", c->cmd->fullname);
         return C_OK;
     }
 
@@ -3478,11 +3484,9 @@ int processCommand(client *c) {
         switch (acl_retval) {
         case ACL_DENIED_CMD:
         {
-            sds cmdname = getFullCommandName(c->cmd);
             rejectCommandFormat(c,
                 "-NOPERM this user has no permissions to run "
-                "the '%s' command", cmdname);
-            sdsfree(cmdname);
+                "the '%s' command", c->cmd->fullname);
             break;
         }
         case ACL_DENIED_KEY:
@@ -3645,7 +3649,7 @@ int processCommand(client *c) {
         rejectCommandFormat(c,
             "Can't execute '%s': only (P|S)SUBSCRIBE / "
             "(P|S)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context",
-            c->cmd->name);
+            c->cmd->fullname);
         return C_OK;
     }
 
@@ -4045,8 +4049,7 @@ int writeCommandsDeniedByDiskError(void) {
 void pingCommand(client *c) {
     /* The command takes zero or one arguments. */
     if (c->argc > 2) {
-        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
-            c->cmd->name);
+        addReplyErrorArity(c);
         return;
     }
 
@@ -4376,7 +4379,7 @@ void addReplyCommandSubCommands(client *c, struct redisCommand *cmd, void (*repl
     while((de = dictNext(di)) != NULL) {
         struct redisCommand *sub = (struct redisCommand *)dictGetVal(de);
         if (use_map)
-            addReplyBulkSds(c, getFullCommandName(sub));
+            addReplyBulkCBuffer(c, sub->fullname, sdslen(sub->fullname));
         reply_function(c, sub);
     }
     dictReleaseIterator(di);
@@ -4419,10 +4422,7 @@ void addReplyCommandInfo(client *c, struct redisCommand *cmd) {
         }
 
         addReplyArrayLen(c, 10);
-        if (cmd->parent)
-            addReplyBulkSds(c, getFullCommandName(cmd));
-        else
-            addReplyBulkCString(c, cmd->name);
+        addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
         addReplyLongLong(c, cmd->arity);
         addReplyFlagsForCommand(c, cmd);
         addReplyLongLong(c, firstkey);
@@ -4489,7 +4489,7 @@ void addReplyCommandDocs(client *c, struct redisCommand *cmd) {
 
 /* Helper for COMMAND(S) command
  *
- * COMMAND(S) GETKEYS arg0 arg1 arg2 ... */
+ * COMMAND(S) GETKEYS cmd arg1 arg2 ... */
 void getKeysSubcommand(client *c) {
     struct redisCommand *cmd = lookupCommand(c->argv+2,c->argc-2);
     getKeysResult result = GETKEYS_RESULT_INIT;
@@ -4577,12 +4577,48 @@ int shouldFilterFromCommandList(struct redisCommand *cmd, commandListFilter *fil
             break;
         }
         case (COMMAND_LIST_FILTER_PATTERN):
-            return !stringmatchlen(filter->arg, sdslen(filter->arg), cmd->name, strlen(cmd->name), 1);
+            return !stringmatchlen(filter->arg, sdslen(filter->arg), cmd->fullname, sdslen(cmd->fullname), 1);
         default:
             serverPanic("Invalid filter type %d", filter->type);
     }
 }
 
+/* COMMAND LIST FILTERBY (MODULE <module-name>|ACLCAT <cat>|PATTERN <pattern>) */
+void commandListWithFilter(client *c, dict *commands, commandListFilter filter, int *numcmds) {
+    dictEntry *de;
+    dictIterator *di = dictGetIterator(commands);
+
+    while ((de = dictNext(di)) != NULL) {
+        struct redisCommand *cmd = dictGetVal(de);
+        if (!shouldFilterFromCommandList(cmd,&filter)) {
+            addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
+            (*numcmds)++;
+        }
+
+        if (cmd->subcommands_dict) {
+            commandListWithFilter(c, cmd->subcommands_dict, filter, numcmds);
+        }
+    }
+    dictReleaseIterator(di);
+}
+
+/* COMMAND LIST */
+void commandListWithoutFilter(client *c, dict *commands, int *numcmds) {
+    dictEntry *de;
+    dictIterator *di = dictGetIterator(commands);
+
+    while ((de = dictNext(di)) != NULL) {
+        struct redisCommand *cmd = dictGetVal(de);
+        addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
+        (*numcmds)++;
+
+        if (cmd->subcommands_dict) {
+            commandListWithoutFilter(c, cmd->subcommands_dict, numcmds);
+        }
+    }
+    dictReleaseIterator(di);
+}
+
 /* COMMAND LIST [FILTERBY (MODULE <module-name>|ACLCAT <cat>|PATTERN <pattern>)] */
 void commandListCommand(client *c) {
 
@@ -4613,29 +4649,16 @@ void commandListCommand(client *c) {
         }
     }
 
-    dictIterator *di;
-    dictEntry *de;
+    int numcmds = 0;
+    void *replylen = addReplyDeferredLen(c);
 
-    di = dictGetIterator(server.commands);
-    if (!got_filter) {
-        addReplySetLen(c, dictSize(server.commands));
-        while ((de = dictNext(di)) != NULL) {
-            struct redisCommand *cmd = dictGetVal(de);
-            addReplyBulkCString(c,cmd->name);
-        }
+    if (got_filter) {
+        commandListWithFilter(c, server.commands, filter, &numcmds);
     } else {
-        int numcmds = 0;
-        void *replylen = addReplyDeferredLen(c);
-        while ((de = dictNext(di)) != NULL) {
-            struct redisCommand *cmd = dictGetVal(de);
-            if (!shouldFilterFromCommandList(cmd,&filter)) {
-                addReplyBulkCString(c,cmd->name);
-                numcmds++;
-            }
-        }
-        setDeferredArrayLen(c,replylen,numcmds);
+        commandListWithoutFilter(c, server.commands, &numcmds);
     }
-    dictReleaseIterator(di);
+
+    setDeferredArrayLen(c,replylen,numcmds);
 }
 
 /* COMMAND INFO [<command-name> ...] */
@@ -4670,7 +4693,7 @@ void commandDocsCommand(client *c) {
         di = dictGetIterator(server.commands);
         while ((de = dictNext(di)) != NULL) {
             struct redisCommand *cmd = dictGetVal(de);
-            addReplyBulkCString(c, cmd->name);
+            addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
             addReplyCommandDocs(c, cmd);
         }
         dictReleaseIterator(di);
@@ -4682,10 +4705,7 @@ void commandDocsCommand(client *c) {
             struct redisCommand *cmd = lookupCommandBySds(c->argv[i]->ptr);
             if (!cmd)
                 continue;
-            if (cmd->parent)
-                addReplyBulkSds(c, getFullCommandName(cmd));
-            else
-                addReplyBulkCString(c, cmd->name);
+            addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
             addReplyCommandDocs(c, cmd);
             numcmds++;
         }
@@ -4768,14 +4788,6 @@ sds fillPercentileDistributionLatencies(sds info, const char* histogram_name, st
     return info;
 }
 
-sds getFullCommandName(struct redisCommand *cmd) {
-    if (!cmd->parent) {
-        return sdsnew(cmd->name);
-    } else {
-        return sdscatfmt(sdsempty(),"%s|%s",cmd->parent->name,cmd->name);
-    }
-}
-
 const char *replstateToString(int replstate) {
     switch (replstate) {
     case SLAVE_STATE_WAIT_BGSAVE_START:
@@ -4818,16 +4830,13 @@ sds genRedisInfoStringCommandStats(sds info, dict *commands) {
         char *tmpsafe;
         c = (struct redisCommand *) dictGetVal(de);
         if (c->calls || c->failed_calls || c->rejected_calls) {
-            sds cmdnamesds = getFullCommandName(c);
-
             info = sdscatprintf(info,
                 "cmdstat_%s:calls=%lld,usec=%lld,usec_per_call=%.2f"
                 ",rejected_calls=%lld,failed_calls=%lld\r\n",
-                getSafeInfoString(cmdnamesds, sdslen(cmdnamesds), &tmpsafe), c->calls, c->microseconds,
+                getSafeInfoString(c->fullname, sdslen(c->fullname), &tmpsafe), c->calls, c->microseconds,
                 (c->calls == 0) ? 0 : ((float)c->microseconds/c->calls),
                 c->rejected_calls, c->failed_calls);
             if (tmpsafe != NULL) zfree(tmpsafe);
-            sdsfree(cmdnamesds);
         }
         if (c->subcommands_dict) {
             info = genRedisInfoStringCommandStats(info, c->subcommands_dict);
@@ -4847,13 +4856,10 @@ sds genRedisInfoStringLatencyStats(sds info, dict *commands) {
         char *tmpsafe;
         c = (struct redisCommand *) dictGetVal(de);
         if (c->latency_histogram) {
-            sds cmdnamesds = getFullCommandName(c);
-
             info = fillPercentileDistributionLatencies(info,
-                getSafeInfoString(cmdnamesds, sdslen(cmdnamesds), &tmpsafe),
+                getSafeInfoString(c->fullname, sdslen(c->fullname), &tmpsafe),
                 c->latency_histogram);
             if (tmpsafe != NULL) zfree(tmpsafe);
-            sdsfree(cmdnamesds);
         }
         if (c->subcommands_dict) {
             info = genRedisInfoStringLatencyStats(info, c->subcommands_dict);
diff --git a/src/server.h b/src/server.h
index 084abdecdb6..cfd8d197503 100644
--- a/src/server.h
+++ b/src/server.h
@@ -2165,7 +2165,8 @@ typedef int redisGetKeysProc(struct redisCommand *cmd, robj **argv, int argc, ge
  */
 struct redisCommand {
     /* Declarative data */
-    const char *name; /* A string representing the command name. */
+    const char *declared_name; /* A string representing the command declared_name.
+                                * It is a const char * for native commands and SDS for module commands. */
     const char *summary; /* Summary of the command (optional). */
     const char *complexity; /* Complexity description (optional). */
     const char *since; /* Debut version of the command (optional). */
@@ -2196,6 +2197,7 @@ struct redisCommand {
                    ACLs. A connection is able to execute a given command if
                    the user associated to the connection has this command
                    bit set in the bitmap of allowed commands. */
+    sds fullname; /* A SDS string representing the command fullname. */
     struct hdr_histogram* latency_histogram; /*points to the command latency command histogram (unit of time nanosecond) */
     keySpec *key_specs;
     keySpec legacy_range_key_spec; /* The legacy (first,last,step) key spec is
@@ -2207,7 +2209,8 @@ struct redisCommand {
     int num_tips;
     int key_specs_num;
     int key_specs_max;
-    dict *subcommands_dict;
+    dict *subcommands_dict; /* A dictionary that holds the subcommands, the key is the subcommand sds name
+                             * (not the fullname), and the value is the redisCommand structure pointer. */
     struct redisCommand *parent;
 };
 
@@ -2403,6 +2406,8 @@ void addReplyErrorObject(client *c, robj *err);
 void addReplyOrErrorObject(client *c, robj *reply);
 void addReplyErrorSds(client *c, sds err);
 void addReplyError(client *c, const char *err);
+void addReplyErrorArity(client *c);
+void addReplyErrorExpireTime(client *c);
 void addReplyStatus(client *c, const char *status);
 void addReplyDouble(client *c, double d);
 void addReplyLongLongWithPrefix(client *c, long long ll, char prefix);
@@ -2705,7 +2710,7 @@ void ACLInit(void);
 
 int ACLCheckUserCredentials(robj *username, robj *password);
 int ACLAuthenticateUser(client *c, robj *username, robj *password);
-unsigned long ACLGetCommandID(const char *cmdname);
+unsigned long ACLGetCommandID(sds cmdname);
 void ACLClearCommandID(void);
 user *ACLGetUserByName(const char *name, size_t namelen);
 int ACLUserCheckKeyPerm(user *u, const char *key, int keylen, int flags);
@@ -2800,12 +2805,13 @@ void removeSignalHandlers(void);
 int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler);
 int changeListenPort(int port, socketFds *sfd, aeFileProc *accept_handler);
 int changeBindAddr(void);
-struct redisCommand *lookupCommand(robj **argv ,int argc);
+struct redisCommand *lookupSubcommand(struct redisCommand *container, sds sub_name);
+struct redisCommand *lookupCommand(robj **argv, int argc);
 struct redisCommand *lookupCommandBySdsLogic(dict *commands, sds s);
 struct redisCommand *lookupCommandBySds(sds s);
 struct redisCommand *lookupCommandByCStringLogic(dict *commands, const char *s);
 struct redisCommand *lookupCommandByCString(const char *s);
-struct redisCommand *lookupCommandOrOriginal(robj **argv ,int argc);
+struct redisCommand *lookupCommandOrOriginal(robj **argv, int argc);
 void call(client *c, int flags);
 void alsoPropagate(int dbid, robj **argv, int argc, int target);
 void propagatePendingCommands();
@@ -3379,7 +3385,6 @@ void _serverPanic(const char *file, int line, const char *msg, ...);
 #endif
 void serverLogObjectDebugInfo(const robj *o);
 void sigsegvHandler(int sig, siginfo_t *info, void *secret);
-sds getFullCommandName(struct redisCommand *cmd);
 const char *getSafeInfoString(const char *s, size_t len, char **tmp);
 sds genRedisInfoString(const char *section);
 sds genModulesInfoString(sds info);
@@ -3389,8 +3394,8 @@ void serverLogHexDump(int level, char *descr, void *value, size_t len);
 int memtest_preserving_test(unsigned long *m, size_t bytes, int passes);
 void mixDigest(unsigned char *digest, const void *ptr, size_t len);
 void xorDigest(unsigned char *digest, const void *ptr, size_t len);
-int populateSingleCommand(struct redisCommand *c, char *strflags);
-void commandAddSubcommand(struct redisCommand *parent, struct redisCommand *subcommand);
+sds catSubCommandFullname(const char *parent_name, const char *sub_name);
+void commandAddSubcommand(struct redisCommand *parent, struct redisCommand *subcommand, const char *declared_name);
 void populateCommandMovableKeys(struct redisCommand *cmd);
 void debugDelay(int usec);
 void killIOThreads(void);
diff --git a/src/t_hash.c b/src/t_hash.c
index 54d112d34f6..92f5cb2b004 100644
--- a/src/t_hash.c
+++ b/src/t_hash.c
@@ -609,7 +609,7 @@ void hsetCommand(client *c) {
     robj *o;
 
     if ((c->argc % 2) == 1) {
-        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",c->cmd->name);
+        addReplyErrorArity(c);
         return;
     }
 
diff --git a/src/t_list.c b/src/t_list.c
index 4d74a1665c2..1cd3fb1a522 100644
--- a/src/t_list.c
+++ b/src/t_list.c
@@ -492,8 +492,7 @@ void popGenericCommand(client *c, int where) {
     robj *value;
 
     if (c->argc > 3) {
-        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
-                            c->cmd->name);
+        addReplyErrorArity(c);
         return;
     } else if (hascount) {
         /* Parse the optional count argument. */
diff --git a/src/t_stream.c b/src/t_stream.c
index 8c7218e2804..e47194926bc 100644
--- a/src/t_stream.c
+++ b/src/t_stream.c
@@ -1810,7 +1810,7 @@ void xaddCommand(client *c) {
 
     /* Check arity. */
     if ((c->argc - field_pos) < 2 || ((c->argc-field_pos) % 2) == 1) {
-        addReplyError(c,"wrong number of arguments for XADD");
+        addReplyErrorArity(c);
         return;
     }
 
diff --git a/src/t_string.c b/src/t_string.c
index 7298fda6fca..2b43a570018 100644
--- a/src/t_string.c
+++ b/src/t_string.c
@@ -160,7 +160,7 @@ static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int
 
     if (*milliseconds <= 0 || (unit == UNIT_SECONDS && *milliseconds > LLONG_MAX / 1000)) {
         /* Negative value provided or multiplication is gonna overflow. */
-        addReplyErrorFormat(c, "invalid expire time in %s", c->cmd->name);
+        addReplyErrorExpireTime(c);
         return C_ERR;
     }
 
@@ -172,7 +172,7 @@ static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int
 
     if (*milliseconds <= 0) {
         /* Overflow detected. */
-        addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
+        addReplyErrorExpireTime(c);
         return C_ERR;
     }
 
@@ -557,8 +557,7 @@ void msetGenericCommand(client *c, int nx) {
     int j;
 
     if ((c->argc % 2) == 0) {
-        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
-                            c->cmd->name);
+        addReplyErrorArity(c);
         return;
     }
 
diff --git a/src/t_zset.c b/src/t_zset.c
index 7470f785d66..e09d4528b1f 100644
--- a/src/t_zset.c
+++ b/src/t_zset.c
@@ -2538,7 +2538,7 @@ void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIndex, in
 
     if (setnum < 1) {
         addReplyErrorFormat(c,
-            "at least 1 input key is needed for %s", c->cmd->name);
+            "at least 1 input key is needed for '%s' command", c->cmd->fullname);
         return;
     }
 


Test patch:
diff --git a/tests/modules/auth.c b/tests/modules/auth.c
index 959fb14f2c4..040a447ec5d 100644
--- a/tests/modules/auth.c
+++ b/tests/modules/auth.c
@@ -1,5 +1,7 @@
 #include "redismodule.h"
 
+#define UNUSED(V) ((void) V)
+
 // A simple global user
 static RedisModuleUser *global = NULL;
 static long long client_change_delta = 0;
@@ -87,3 +89,12 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
 
     return REDISMODULE_OK;
 }
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+    UNUSED(ctx);
+
+    if (global)
+        RedisModule_FreeModuleUser(global);
+
+    return REDISMODULE_OK;
+}
diff --git a/tests/modules/propagate.c b/tests/modules/propagate.c
index 7560e94d24c..d5132a5b4ec 100644
--- a/tests/modules/propagate.c
+++ b/tests/modules/propagate.c
@@ -41,6 +41,8 @@
 #include <pthread.h>
 #include <errno.h>
 
+#define UNUSED(V) ((void) V)
+
 RedisModuleCtx *detached_ctx = NULL;
 
 static int KeySpace_NotificationGeneric(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
@@ -390,3 +392,12 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
 
     return REDISMODULE_OK;
 }
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+    UNUSED(ctx);
+
+    if (detached_ctx)
+        RedisModule_FreeThreadSafeContext(detached_ctx);
+
+    return REDISMODULE_OK;
+}
diff --git a/tests/modules/subcommands.c b/tests/modules/subcommands.c
index 783414a363d..f0cc6ffafb3 100644
--- a/tests/modules/subcommands.c
+++ b/tests/modules/subcommands.c
@@ -16,6 +16,15 @@ int cmd_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
     return REDISMODULE_OK;
 }
 
+int cmd_get_fullname(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+    UNUSED(argv);
+    UNUSED(argc);
+
+    const char *command_name = RedisModule_GetCurrentCommandName(ctx);
+    RedisModule_ReplyWithSimpleString(ctx, command_name);
+    return REDISMODULE_OK;
+}
+
 int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
     REDISMODULE_NOT_USED(argv);
     REDISMODULE_NOT_USED(argc);
@@ -51,6 +60,18 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
     if (RedisModule_SetCommandKeySpecFindKeysRange(subcmd,spec_id,0,1,0) == REDISMODULE_ERR)
         return REDISMODULE_ERR;
 
+    /* Get the name of the command currently running. */
+    if (RedisModule_CreateCommand(ctx,"subcommands.parent_get_fullname",cmd_get_fullname,"",0,0,0) == REDISMODULE_ERR)
+        return REDISMODULE_ERR;
+
+    /* Get the name of the subcommand currently running. */
+    if (RedisModule_CreateCommand(ctx,"subcommands.sub",NULL,"",0,0,0) == REDISMODULE_ERR)
+        return REDISMODULE_ERR;
+
+    RedisModuleCommand *fullname_parent = RedisModule_GetCommand(ctx,"subcommands.sub");
+    if (RedisModule_CreateSubcommand(fullname_parent,"get_fullname",cmd_get_fullname,"",0,0,0) == REDISMODULE_ERR)
+        return REDISMODULE_ERR;
+
     /* Sanity */
 
     /* Trying to create the same subcommand fails */
diff --git a/tests/sentinel/tests/03-runtime-reconf.tcl b/tests/sentinel/tests/03-runtime-reconf.tcl
index 5e3a960395d..ad9284e41ce 100644
--- a/tests/sentinel/tests/03-runtime-reconf.tcl
+++ b/tests/sentinel/tests/03-runtime-reconf.tcl
@@ -40,7 +40,7 @@ test "Sentinel Set with other error situations" {
    assert_error "ERR Notification script seems non existing*" {S 0 SENTINEL SET mymaster notification-script test.txt}
 
    # wrong parameter number
-   assert_error "ERR wrong number of arguments for 'set' command or subcommand" {S 0 SENTINEL SET mymaster fakeoption}
+   assert_error "ERR wrong number of arguments for 'sentinel|set' command" {S 0 SENTINEL SET mymaster fakeoption}
 
    # unknown parameter option
    assert_error "ERR Unknown option or number of arguments for SENTINEL SET 'fakeoption'" {S 0 SENTINEL SET mymaster fakeoption fakevalue}
diff --git a/tests/support/test.tcl b/tests/support/test.tcl
index f3f7e132a09..34c7986dad6 100644
--- a/tests/support/test.tcl
+++ b/tests/support/test.tcl
@@ -40,6 +40,12 @@ proc assert_failed {expected_err detail} {
      error "assertion:$expected_err $detail"
 }
 
+proc assert_not_equal {value expected {detail ""}} {
+    if {!($expected ne $value)} {
+        assert_failed "Expected '$value' not equal to '$expected'" $detail
+    }
+}
+
 proc assert_equal {value expected {detail ""}} {
     if {$expected ne $value} {
         assert_failed "Expected '$value' to be equal to '$expected'" $detail
diff --git a/tests/unit/acl.tcl b/tests/unit/acl.tcl
index 5c559210859..24f06931360 100644
--- a/tests/unit/acl.tcl
+++ b/tests/unit/acl.tcl
@@ -476,6 +476,26 @@ start_server {tags {"acl external:skip"}} {
         }
     }
 
+    test "ACL CAT with illegal arguments" {
+        assert_error {*Unknown category 'NON_EXISTS'} {r ACL CAT NON_EXISTS}
+        assert_error {*Unknown subcommand or wrong number of arguments for 'CAT'*} {r ACL CAT NON_EXISTS NON_EXISTS2}
+    }
+
+    test "ACL CAT without category - list all categories" {
+        set categories [r acl cat]
+        assert_not_equal [lsearch $categories "keyspace"] -1
+        assert_not_equal [lsearch $categories "connection"] -1
+    }
+
+    test "ACL CAT category - list all commands/subcommands that belong to category" {
+        assert_not_equal [lsearch [r acl cat transaction] "multi"] -1
+        assert_not_equal [lsearch [r acl cat scripting] "function|list"] -1
+
+        # Negative check to make sure it doesn't actually return all commands.
+        assert_equal [lsearch [r acl cat keyspace] "set"] -1
+        assert_equal [lsearch [r acl cat stream] "get"] -1
+    }
+
     test {ACL #5998 regression: memory leaks adding / removing subcommands} {
         r AUTH default ""
         r ACL setuser newuser reset -debug +debug|a +debug|b +debug|c
@@ -632,7 +652,7 @@ start_server {tags {"acl external:skip"}} {
 
     test {ACL HELP should not have unexpected options} {
         catch {r ACL help xxx} e
-        assert_match "*wrong number of arguments*" $e
+        assert_match "*wrong number of arguments for 'acl|help' command" $e
     }
 
     test {Delete a user that the client doesn't use} {
diff --git a/tests/unit/expire.tcl b/tests/unit/expire.tcl
index b853df0350a..d85d59e7a91 100644
--- a/tests/unit/expire.tcl
+++ b/tests/unit/expire.tcl
@@ -244,35 +244,35 @@ start_server {tags {"expire"}} {
     test {SET with EX with big integer should report an error} {
         catch {r set foo bar EX 10000000000000000} e
         set e
-    } {ERR invalid expire time in set}
+    } {ERR invalid expire time in 'set' command}
 
     test {SET with EX with smallest integer should report an error} {
         catch {r SET foo bar EX -9999999999999999} e
         set e
-    } {ERR invalid expire time in set}
+    } {ERR invalid expire time in 'set' command}
 
     test {GETEX with big integer should report an error} {
         r set foo bar
         catch {r GETEX foo EX 10000000000000000} e
         set e
-    } {ERR invalid expire time in getex}
+    } {ERR invalid expire time in 'getex' command}
 
     test {GETEX with smallest integer should report an error} {
         r set foo bar
         catch {r GETEX foo EX -9999999999999999} e
         set e
-    } {ERR invalid expire time in getex}
+    } {ERR invalid expire time in 'getex' command}
 
     test {EXPIRE with big integer overflows when converted to milliseconds} {
         r set foo bar
 
         # Hit `when > LLONG_MAX - basetime`
-        assert_error "ERR invalid expire time in expire" {r EXPIRE foo 9223370399119966}
+        assert_error "ERR invalid expire time in 'expire' command" {r EXPIRE foo 9223370399119966}
 
         # Hit `when > LLONG_MAX / 1000`
-        assert_error "ERR invalid expire time in expire" {r EXPIRE foo 9223372036854776}
-        assert_error "ERR invalid expire time in expire" {r EXPIRE foo 10000000000000000}
-        assert_error "ERR invalid expire time in expire" {r EXPIRE foo 18446744073709561}
+        assert_error "ERR invalid expire time in 'expire' command" {r EXPIRE foo 9223372036854776}
+        assert_error "ERR invalid expire time in 'expire' command" {r EXPIRE foo 10000000000000000}
+        assert_error "ERR invalid expire time in 'expire' command" {r EXPIRE foo 18446744073709561}
 
         assert_equal {-1} [r ttl foo]
     }
@@ -281,14 +281,14 @@ start_server {tags {"expire"}} {
         r set foo bar
         catch {r PEXPIRE foo 9223372036854770000} e
         set e
-    } {ERR invalid expire time in pexpire}
+    } {ERR invalid expire time in 'pexpire' command}
 
     test {EXPIRE with big negative integer} {
         r set foo bar
 
         # Hit `when < LLONG_MIN / 1000`
-        assert_error "ERR invalid expire time in expire" {r EXPIRE foo -9223372036854776}
-        assert_error "ERR invalid expire time in expire" {r EXPIRE foo -9999999999999999}
+        assert_error "ERR invalid expire time in 'expire' command" {r EXPIRE foo -9223372036854776}
+        assert_error "ERR invalid expire time in 'expire' command" {r EXPIRE foo -9999999999999999}
 
         r ttl foo
     } {-1}
diff --git a/tests/unit/functions.tcl b/tests/unit/functions.tcl
index fe4ce5ad732..016bbb1e1c4 100644
--- a/tests/unit/functions.tcl
+++ b/tests/unit/functions.tcl
@@ -209,7 +209,7 @@ start_server {tags {"scripting"}} {
     test {FUNCTION - test function restore with wrong number of arguments} {
         catch {r function restore arg1 args2 arg3} e
         set _ $e
-    } {*wrong number of arguments*}
+    } {*Unknown subcommand or wrong number of arguments for 'restore'. Try FUNCTION HELP.}
 
     test {FUNCTION - test fcall_ro with write command} {
         r function load lua test REPLACE [get_no_writes_function_code test {return redis.call('set', 'x', '1')}]
@@ -302,7 +302,7 @@ start_server {tags {"scripting"}} {
         assert_match {*only supports SYNC|ASYNC*} $e
 
         catch {r function flush sync extra_arg} e
-        assert_match {*wrong number of arguments*} $e
+        assert_match {*Unknown subcommand or wrong number of arguments for 'flush'. Try FUNCTION HELP.} $e
     }
 }
 
diff --git a/tests/unit/info.tcl b/tests/unit/info.tcl
index 9439c0fc8af..b211e6c9180 100644
--- a/tests/unit/info.tcl
+++ b/tests/unit/info.tcl
@@ -197,7 +197,7 @@ start_server {tags {"info" "external:skip"}} {
             assert_match {} [errorstat ERR]
             r multi
             catch {r set} e
-            assert_match {ERR wrong number of arguments*} $e
+            assert_match {ERR wrong number of arguments for 'set' command} $e
             catch {r exec} e
             assert_match {EXECABORT*} $e
             assert_match {*count=1*} [errorstat ERR]
@@ -216,7 +216,7 @@ start_server {tags {"info" "external:skip"}} {
             assert_equal [s total_error_replies] 0
             assert_match {} [errorstat ERR]
             catch {r set k} e
-            assert_match {ERR wrong number of arguments*} $e
+            assert_match {ERR wrong number of arguments for 'set' command} $e
             assert_match {*count=1*} [errorstat ERR]
             assert_match {*calls=0,*,rejected_calls=1,failed_calls=0} [cmdstat set]
             # ensure that after a rejected command, valid ones are counted properly
diff --git a/tests/unit/introspection-2.tcl b/tests/unit/introspection-2.tcl
index 457ac6bc4a0..40124e035fb 100644
--- a/tests/unit/introspection-2.tcl
+++ b/tests/unit/introspection-2.tcl
@@ -101,14 +101,59 @@ start_server {tags {"introspection"}} {
         assert_equal {key1 key2} [r command getkeys lcs key1 key2]
     }
 
-    test "COMMAND LIST FILTERBY ACLCAT" {
-        set reply [r command list filterby aclcat hyperloglog]
-        assert_equal [lsort $reply] {pfadd pfcount pfdebug pfmerge pfselftest}
+    test "COMMAND LIST syntax error" {
+        assert_error "ERR syntax error*" {r command list bad_arg}
+        assert_error "ERR syntax error*" {r command list filterby bad_arg}
+        assert_error "ERR syntax error*" {r command list filterby bad_arg bad_arg2}
     }
 
-    test "COMMAND LIST FILTERBY PATTERN" {
-        set reply [r command list filterby pattern pf*]
-        assert_equal [lsort $reply] {pfadd pfcount pfdebug pfmerge pfselftest}
+    test "COMMAND LIST WITHOUT FILTERBY" {
+        set commands [r command list]
+        assert_not_equal [lsearch $commands "set"] -1
+        assert_not_equal [lsearch $commands "client|list"] -1
+    }
+
+    test "COMMAND LIST FILTERBY ACLCAT against non existing category" {
+        assert_equal {} [r command list filterby aclcat non_existing_category]
+    }
+
+    test "COMMAND LIST FILTERBY ACLCAT - list all commands/subcommands" {
+        set commands [r command list filterby aclcat scripting]
+        assert_not_equal [lsearch $commands "eval"] -1
+        assert_not_equal [lsearch $commands "script|kill"] -1
+
+        # Negative check, a command that should not be here
+        assert_equal [lsearch $commands "set"] -1
+    }
+
+    test "COMMAND LIST FILTERBY PATTERN - list all commands/subcommands" {
+        # Exact command match.
+        assert_equal {set} [r command list filterby pattern set]
+        assert_equal {get} [r command list filterby pattern get]
+
+        # Return the parent command and all the subcommands below it.
+        set commands [r command list filterby pattern config*]
+        assert_not_equal [lsearch $commands "config"] -1
+        assert_not_equal [lsearch $commands "config|get"] -1
+
+        # We can filter subcommands under a parent command.
+        set commands [r command list filterby pattern config|*re*]
+        assert_not_equal [lsearch $commands "config|resetstat"] -1
+        assert_not_equal [lsearch $commands "config|rewrite"] -1
+
+        # We can filter subcommands across parent commands.
+        set commands [r command list filterby pattern cl*help]
+        assert_not_equal [lsearch $commands "client|help"] -1
+        assert_not_equal [lsearch $commands "cluster|help"] -1
+
+        # Negative check, command that doesn't exist.
+        assert_equal {} [r command list filterby pattern non_exists]
+        assert_equal {} [r command list filterby pattern non_exists*]
+    }
+
+    test "COMMAND LIST FILTERBY MODULE against non existing module" {
+        # This should be empty, the real one is in subcommands.tcl
+        assert_equal {} [r command list filterby module non_existing_module]
     }
 
     test {COMMAND INFO of invalid subcommands} {
diff --git a/tests/unit/introspection.tcl b/tests/unit/introspection.tcl
index 29d881372bc..33a41ee50eb 100644
--- a/tests/unit/introspection.tcl
+++ b/tests/unit/introspection.tcl
@@ -1,20 +1,26 @@
 start_server {tags {"introspection"}} {
+    test "PING" {
+        assert_equal {PONG} [r ping]
+        assert_equal {redis} [r ping redis]
+        assert_error {*wrong number of arguments for 'ping' command} {r ping hello redis}
+    }
+
     test {CLIENT LIST} {
         r client list
-    } {*addr=*:* fd=* age=* idle=* flags=N db=* sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client*}
+    } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|list user=* redir=-1 resp=2*}
 
     test {CLIENT LIST with IDs} {
         set myid [r client id]
         set cl [split [r client list id $myid] "\r\n"]
-        assert_match "id=$myid*" [lindex $cl 0]
+        assert_match "id=$myid * cmd=client|list *" [lindex $cl 0]
     }
 
     test {CLIENT INFO} {
         r client info
-    } {*addr=*:* fd=* age=* idle=* flags=N db=* sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client*}
+    } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|info user=* redir=-1 resp=2*}
 
     test {CLIENT KILL with illegal arguments} {
-        assert_error "ERR wrong number*" {r client kill}
+        assert_error "ERR wrong number of arguments for 'client|kill' command" {r client kill}
         assert_error "ERR syntax error*" {r client kill id 10 wrong_arg}
 
         assert_error "ERR*greater than 0*" {r client kill id str}
diff --git a/tests/unit/latency-monitor.tcl b/tests/unit/latency-monitor.tcl
index 79de0883c75..39d404d5552 100644
--- a/tests/unit/latency-monitor.tcl
+++ b/tests/unit/latency-monitor.tcl
@@ -8,6 +8,7 @@ start_server {tags {"latency-monitor needs:latency"}} {
         set histo [dict create {*}[r latency histogram]]
         # Config resetstat is recorded
         assert_equal [dict size $histo] 1
+        assert_match {*config|resetstat*} $histo
     }
 
     test {LATENCY HISTOGRAM all commands} {
@@ -141,6 +142,6 @@ start_server {tags {"latency-monitor needs:latency"}} {
 
     test {LATENCY HELP should not have unexpected options} {
         catch {r LATENCY help xxx} e
-        assert_match "*wrong number of arguments*" $e
+        assert_match "*wrong number of arguments for 'latency|help' command" $e
     }
 }
diff --git a/tests/unit/moduleapi/aclcheck.tcl b/tests/unit/moduleapi/aclcheck.tcl
index 3fa3ed43f33..ad55eb160b0 100644
--- a/tests/unit/moduleapi/aclcheck.tcl
+++ b/tests/unit/moduleapi/aclcheck.tcl
@@ -72,4 +72,8 @@ start_server {tags {"modules acl"}} {
         assert {[dict get $entry context] eq {module}}
         assert {[dict get $entry object] eq {set}}
     }
+
+    test "Unload the module - aclcheck" {
+        assert_equal {OK} [r module unload aclcheck]
+    }
 }
diff --git a/tests/unit/moduleapi/auth.tcl b/tests/unit/moduleapi/auth.tcl
index 906ab0beb9a..6d8c3bd6a77 100644
--- a/tests/unit/moduleapi/auth.tcl
+++ b/tests/unit/moduleapi/auth.tcl
@@ -68,4 +68,7 @@ start_server {tags {"modules"}} {
         assert_equal [r acl whoami] "default"
     }
 
-}
\ No newline at end of file
+    test "Unload the module - testacl" {
+        assert_equal {OK} [r module unload testacl]
+    }
+}
diff --git a/tests/unit/moduleapi/basics.tcl b/tests/unit/moduleapi/basics.tcl
index be8b697f04f..b858c344a75 100644
--- a/tests/unit/moduleapi/basics.tcl
+++ b/tests/unit/moduleapi/basics.tcl
@@ -1,6 +1,5 @@
 set testmodule [file normalize tests/modules/basics.so]
 
-
 start_server {tags {"modules"}} {
     r module load $testmodule
 
@@ -30,7 +29,9 @@ start_server {tags {"modules"}} {
         assert_equal $reply 3
     }
 
-    r module unload test
+    test "Unload the module - test" {
+        assert_equal {OK} [r module unload test]
+    }
 }
 
 start_server {tags {"modules external:skip"} overrides {enable-module-command no}} {
diff --git a/tests/unit/moduleapi/blockedclient.tcl b/tests/unit/moduleapi/blockedclient.tcl
index c8651aabdb5..523d7ba69c0 100644
--- a/tests/unit/moduleapi/blockedclient.tcl
+++ b/tests/unit/moduleapi/blockedclient.tcl
@@ -1,5 +1,3 @@
-# source tests/support/util.tcl
-
 set testmodule [file normalize tests/modules/blockedclient.so]
 
 start_server {tags {"modules"}} {
@@ -181,4 +179,8 @@ start_server {tags {"modules"}} {
         set clients [r client list]
         assert_no_match "*name=myclient*" $clients
     }
+
+    test "Unload the module - blockedclient" {
+        assert_equal {OK} [r module unload blockedclient]
+    }
 }
diff --git a/tests/unit/moduleapi/commandfilter.tcl b/tests/unit/moduleapi/commandfilter.tcl
index 11284605866..723dd140937 100644
--- a/tests/unit/moduleapi/commandfilter.tcl
+++ b/tests/unit/moduleapi/commandfilter.tcl
@@ -91,4 +91,8 @@ start_server {tags {"modules"}} {
         r eval "redis.call('commandfilter.ping')" 0
         assert_equal {} [r lrange log-key 0 -1]
     }
+
+    test "Unload the module - commandfilter" {
+        assert_equal {OK} [r module unload commandfilter]
+    }
 } 
diff --git a/tests/unit/moduleapi/eventloop.tcl b/tests/unit/moduleapi/eventloop.tcl
index 44d28dfb94f..81e01cae025 100644
--- a/tests/unit/moduleapi/eventloop.tcl
+++ b/tests/unit/moduleapi/eventloop.tcl
@@ -21,4 +21,8 @@ start_server {tags {"modules"}} {
     test "Module eventloop oneshot" {
         r test.oneshot
     }
+
+    test "Unload the module - eventloop" {
+        assert_equal {OK} [r module unload eventloop]
+    }
 }
diff --git a/tests/unit/moduleapi/fork.tcl b/tests/unit/moduleapi/fork.tcl
index 64184d01eab..d6a2db9a944 100644
--- a/tests/unit/moduleapi/fork.tcl
+++ b/tests/unit/moduleapi/fork.tcl
@@ -39,4 +39,7 @@ start_server {tags {"modules"}} {
         assert {[count_log_message "Can't fork for module: File exists"] eq "1"}
     }
 
+    test "Unload the module - fork" {
+        assert_equal {OK} [r module unload fork]
+    }
 }
diff --git a/tests/unit/moduleapi/getkeys.tcl b/tests/unit/moduleapi/getkeys.tcl
index 8ceb2b3862b..6061fe8cf90 100644
--- a/tests/unit/moduleapi/getkeys.tcl
+++ b/tests/unit/moduleapi/getkeys.tcl
@@ -41,4 +41,8 @@ start_server {tags {"modules"}} {
         catch {r getkeys.introspect set key} e
         set _ $e
     } {*EINVAL*}
+
+    test "Unload the module - getkeys" {
+        assert_equal {OK} [r module unload getkeys]
+    }
 }
diff --git a/tests/unit/moduleapi/hash.tcl b/tests/unit/moduleapi/hash.tcl
index 89bb6c63ab6..116b1c5120f 100644
--- a/tests/unit/moduleapi/hash.tcl
+++ b/tests/unit/moduleapi/hash.tcl
@@ -20,4 +20,8 @@ start_server {tags {"modules"}} {
         assert_equal 1 [r hash.set k "" sushi :delete: none :delete:]
         r hgetall k
     } {squirrel ofcourse banana no what nothing something nice}
+
+    test "Unload the module - hash" {
+        assert_equal {OK} [r module unload hash]
+    }
 }
diff --git a/tests/unit/moduleapi/infotest.tcl b/tests/unit/moduleapi/infotest.tcl
index 1ad2ee6fc85..0d07aaa7e7a 100644
--- a/tests/unit/moduleapi/infotest.tcl
+++ b/tests/unit/moduleapi/infotest.tcl
@@ -90,5 +90,9 @@ start_server {tags {"modules"}} {
         assert_match {*infotest_unsafe_field:value=1*} $info
     }
 
+    test "Unload the module - infotest" {
+        assert_equal {OK} [r module unload infotest]
+    }
+
     # TODO: test crash report.
 } 
diff --git a/tests/unit/moduleapi/infra.tcl b/tests/unit/moduleapi/infra.tcl
index 07748745747..7bfa7d4b371 100644
--- a/tests/unit/moduleapi/infra.tcl
+++ b/tests/unit/moduleapi/infra.tcl
@@ -12,11 +12,11 @@ test {modules config rewrite} {
 
         assert_equal [lindex [lindex [r module list] 0] 1] infotest
 
-        r module unload infotest
+        assert_equal {OK} [r module unload infotest]
 
         r config rewrite
         restart_server 0 true false
 
         assert_equal [llength [r module list]] 0
     }
-} 
+}
diff --git a/tests/unit/moduleapi/keyspace_events.tcl b/tests/unit/moduleapi/keyspace_events.tcl
index 60800bbff70..39350e51853 100644
--- a/tests/unit/moduleapi/keyspace_events.tcl
+++ b/tests/unit/moduleapi/keyspace_events.tcl
@@ -82,5 +82,9 @@ tags "modules" {
             assert_equal {pmessage * __keyspace@9__:x notify} [$rd1 read]
             $rd1 close
         }
-	}
+
+        test "Unload the module - testkeyspace" {
+            assert_equal {OK} [r module unload testkeyspace]
+        }
+    }
 }
diff --git a/tests/unit/moduleapi/keyspecs.tcl b/tests/unit/moduleapi/keyspecs.tcl
index b1b6da9dbfc..cbd3aba8321 100644
--- a/tests/unit/moduleapi/keyspecs.tcl
+++ b/tests/unit/moduleapi/keyspecs.tcl
@@ -48,4 +48,8 @@ start_server {tags {"modules"}} {
         set reply [r command list filterby module keyspecs]
         assert_equal [lsort $reply] {kspec.complex1 kspec.complex2 kspec.legacy}
     }
+
+    test "Unload the module - keyspecs" {
+        assert_equal {OK} [r module unload keyspecs]
+    }
 }
diff --git a/tests/unit/moduleapi/list.tcl b/tests/unit/moduleapi/list.tcl
index 6094a0354b2..34a19767ca0 100644
--- a/tests/unit/moduleapi/list.tcl
+++ b/tests/unit/moduleapi/list.tcl
@@ -63,4 +63,8 @@ start_server {tags {"modules"}} {
         r list.edit k reverse rkr foo bar
         r list.getall k
     } {bar y foo}
+
+    test "Unload the module - list" {
+        assert_equal {OK} [r module unload list]
+    }
 }
diff --git a/tests/unit/moduleapi/misc.tcl b/tests/unit/moduleapi/misc.tcl
index 9aa191814b5..bc00b37d23d 100644
--- a/tests/unit/moduleapi/misc.tcl
+++ b/tests/unit/moduleapi/misc.tcl
@@ -1,6 +1,5 @@
 set testmodule [file normalize tests/modules/misc.so]
 
-
 start_server {tags {"modules"}} {
     r module load $testmodule
 
@@ -133,4 +132,8 @@ start_server {tags {"modules"}} {
         set x [r test.monotonic_time]
         assert { [r test.monotonic_time] >= $x }
     }
+
+    test "Unload the module - misc" {
+        assert_equal {OK} [r module unload misc]
+    }
 }
diff --git a/tests/unit/moduleapi/propagate.tcl b/tests/unit/moduleapi/propagate.tcl
index a95fbfd71b9..bbc9d3800fe 100644
--- a/tests/unit/moduleapi/propagate.tcl
+++ b/tests/unit/moduleapi/propagate.tcl
@@ -574,6 +574,11 @@ tags "modules" {
                     assert_equal [$replica ttl k1] -1
                 }
 
+                test "Unload the module - propagate-test/testkeyspace" {
+                    assert_equal {OK} [r module unload propagate-test]
+                    assert_equal {OK} [r module unload testkeyspace]
+                }
+
                 assert_equal [s -1 unexpected_error_replies] 0
             }
         }
@@ -598,6 +603,7 @@ tags "modules aof" {
             # Load the AOF
             r debug loadaof
 
+            assert_equal {OK} [r module unload propagate-test]
             assert_equal [s 0 unexpected_error_replies] 0
         }
     }
diff --git a/tests/unit/moduleapi/reply.tcl b/tests/unit/moduleapi/reply.tcl
index 43d44707cc6..7fe8c867898 100644
--- a/tests/unit/moduleapi/reply.tcl
+++ b/tests/unit/moduleapi/reply.tcl
@@ -94,4 +94,8 @@ start_server {tags {"modules"}} {
             assert_match "An error" $e
         }
     }
+
+    test "Unload the module - replywith" {
+        assert_equal {OK} [r module unload replywith]
+    }
 }
diff --git a/tests/unit/moduleapi/scan.tcl b/tests/unit/moduleapi/scan.tcl
index 43a0c4d8a84..66faf5eb872 100644
--- a/tests/unit/moduleapi/scan.tcl
+++ b/tests/unit/moduleapi/scan.tcl
@@ -49,4 +49,8 @@ start_server {tags {"modules"}} {
         r sadd ss 3
         lsort [r scan.scan_key ss]
     } {{1 {}} {2 {}} {3 {}}}
+
+    test "Unload the module - scan" {
+        assert_equal {OK} [r module unload scan]
+    }
 }
diff --git a/tests/unit/moduleapi/stream.tcl b/tests/unit/moduleapi/stream.tcl
index 15e97c18385..80c24ff6cd5 100644
--- a/tests/unit/moduleapi/stream.tcl
+++ b/tests/unit/moduleapi/stream.tcl
@@ -152,4 +152,8 @@ start_server {tags {"modules"}} {
         assert_equal 100   [r stream.trim mystream minid ~ +]
         assert_equal 0     [r xlen mystream]
     }
+
+    test "Unload the module - stream" {
+        assert_equal {OK} [r module unload stream]
+    }
 }
diff --git a/tests/unit/moduleapi/subcommands.tcl b/tests/unit/moduleapi/subcommands.tcl
index bc890b77e12..10c62d10d1c 100644
--- a/tests/unit/moduleapi/subcommands.tcl
+++ b/tests/unit/moduleapi/subcommands.tcl
@@ -21,9 +21,33 @@ start_server {tags {"modules"}} {
 
     test "Module pure-container command fails on arity error" {
         catch {r subcommands.bitarray} e
-        assert_match {*wrong number of arguments*} $e
+        assert_match {*wrong number of arguments for 'subcommands.bitarray' command} $e
 
         # Subcommands can be called
         assert_equal [r subcommands.bitarray get k1] {OK}
     }
+
+    test "Module get current command fullname" {
+        assert_equal [r subcommands.parent_get_fullname] {subcommands.parent_get_fullname}
+    }
+
+    test "Module get current subcommand fullname" {
+        assert_equal [r subcommands.sub get_fullname] {subcommands.sub|get_fullname}
+    }
+
+    test "COMMAND LIST FILTERBY MODULE" {
+        assert_equal {} [r command list filterby module non_existing]
+
+        set commands [r command list filterby module subcommands]
+        assert_not_equal [lsearch $commands "subcommands.bitarray"] -1
+        assert_not_equal [lsearch $commands "subcommands.bitarray|set"] -1
+        assert_not_equal [lsearch $commands "subcommands.parent_get_fullname"] -1
+        assert_not_equal [lsearch $commands "subcommands.sub|get_fullname"] -1
+
+        assert_equal [lsearch $commands "set"] -1
+    }
+
+    test "Unload the module - subcommands" {
+        assert_equal {OK} [r module unload subcommands]
+    }
 }
diff --git a/tests/unit/moduleapi/timer.tcl b/tests/unit/moduleapi/timer.tcl
index 10a90227670..c04f80b2342 100644
--- a/tests/unit/moduleapi/timer.tcl
+++ b/tests/unit/moduleapi/timer.tcl
@@ -53,5 +53,9 @@ start_server {tags {"modules"}} {
         # verify id does not exist
         assert_equal {} [r test.gettimer $id]
     }
+
+    test "Unload the module - timer" {
+        assert_equal {OK} [r module unload timer]
+    }
 }
 
diff --git a/tests/unit/moduleapi/zset.tcl b/tests/unit/moduleapi/zset.tcl
index 998d2076571..1c146eaf4fd 100644
--- a/tests/unit/moduleapi/zset.tcl
+++ b/tests/unit/moduleapi/zset.tcl
@@ -13,4 +13,8 @@ start_server {tags {"modules"}} {
         assert_equal 1 [r zset.rem k world]
         assert_equal 0 [r exists k]
     }
+
+    test "Unload the module - zset" {
+        assert_equal {OK} [r module unload zset]
+    }
 }
diff --git a/tests/unit/pause.tcl b/tests/unit/pause.tcl
index 0f9bb4ae402..99fc7214d2a 100644
--- a/tests/unit/pause.tcl
+++ b/tests/unit/pause.tcl
@@ -104,7 +104,7 @@ start_server {tags {"pause network"}} {
     test "Test clients with syntax errors will get responses immediately" {
         r client PAUSE 100000 WRITE
         catch {r set FOO} err
-        assert_match "ERR wrong number of arguments for *" $err
+        assert_match "ERR wrong number of arguments for 'set' command" $err
         r client unpause
     }
 
diff --git a/tests/unit/type/hash.tcl b/tests/unit/type/hash.tcl
index 6646ccc1857..75ba29f77ab 100644
--- a/tests/unit/type/hash.tcl
+++ b/tests/unit/type/hash.tcl
@@ -310,10 +310,10 @@ start_server {tags {"hash"}} {
         set _ $result
     } {foo}
 
-    test {HMSET wrong number of args} {
-        catch {r hmset smallhash key1 val1 key2} err
-        format $err
-    } {*wrong number*}
+    test {HSET/HMSET wrong number of args} {
+        assert_error {*wrong number of arguments for 'hset' command} {r hset smallhash key1 val1 key2}
+        assert_error {*wrong number of arguments for 'hmset' command} {r hmset smallhash key1 val1 key2}
+    }
 
     test {HMSET - small hash} {
         set args {}
diff --git a/tests/unit/type/list.tcl b/tests/unit/type/list.tcl
index 3981d3f8759..8cff56c6a5e 100644
--- a/tests/unit/type/list.tcl
+++ b/tests/unit/type/list.tcl
@@ -486,6 +486,11 @@ start_server {
         assert_equal c [r lpop mylist2]
     }
 
+    test "LPOP/RPOP with wrong number of arguments" {
+        assert_error {*wrong number of arguments for 'lpop' command} {r lpop key 1 1}
+        assert_error {*wrong number of arguments for 'rpop' command} {r rpop key 2 2}
+    }
+
     test {RPOP/LPOP with the optional count argument} {
         assert_equal 7 [r lpush listcount aa bb cc dd ee ff gg]
         assert_equal {gg} [r lpop listcount 1]
@@ -1600,9 +1605,9 @@ foreach {pop} {BLPOP BLMPOP_LEFT} {
     }
 
     test {LMPOP with illegal argument} {
-        assert_error "ERR wrong number of arguments*" {r lmpop}
-        assert_error "ERR wrong number of arguments*" {r lmpop 1}
-        assert_error "ERR wrong number of arguments*" {r lmpop 1 mylist{t}}
+        assert_error "ERR wrong number of arguments for 'lmpop' command" {r lmpop}
+        assert_error "ERR wrong number of arguments for 'lmpop' command" {r lmpop 1}
+        assert_error "ERR wrong number of arguments for 'lmpop' command" {r lmpop 1 mylist{t}}
 
         assert_error "ERR numkeys*" {r lmpop 0 mylist{t} LEFT}
         assert_error "ERR numkeys*" {r lmpop a mylist{t} LEFT}
diff --git a/tests/unit/type/set.tcl b/tests/unit/type/set.tcl
index 587bd58f8db..93f7311b131 100644
--- a/tests/unit/type/set.tcl
+++ b/tests/unit/type/set.tcl
@@ -144,8 +144,8 @@ start_server {
     } {3}
 
     test "SINTERCARD with illegal arguments" {
-        assert_error "ERR wrong number of arguments*" {r sintercard}
-        assert_error "ERR wrong number of arguments*" {r sintercard 1}
+        assert_error "ERR wrong number of arguments for 'sintercard' command" {r sintercard}
+        assert_error "ERR wrong number of arguments for 'sintercard' command" {r sintercard 1}
 
         assert_error "ERR numkeys*" {r sintercard 0 myset{t}}
         assert_error "ERR numkeys*" {r sintercard a myset{t}}
diff --git a/tests/unit/type/stream.tcl b/tests/unit/type/stream.tcl
index 97d498258dc..7ba3ed11616 100644
--- a/tests/unit/type/stream.tcl
+++ b/tests/unit/type/stream.tcl
@@ -52,6 +52,12 @@ set content {} ;# Will be populated with Tcl side copy of the stream content.
 start_server {
     tags {"stream"}
 } {
+    test "XADD wrong number of args" {
+        assert_error {*wrong number of arguments for 'xadd' command} {r XADD mystream}
+        assert_error {*wrong number of arguments for 'xadd' command} {r XADD mystream *}
+        assert_error {*wrong number of arguments for 'xadd' command} {r XADD mystream * field}
+    }
+
     test {XADD can add entries into a stream that XRANGE can fetch} {
         r XADD mystream * item 1 value a
         r XADD mystream * item 2 value b
@@ -797,11 +803,11 @@ start_server {tags {"stream needs:debug"} overrides {appendonly yes aof-use-rdb-
 start_server {tags {"stream"}} {
     test {XGROUP HELP should not have unexpected options} {
         catch {r XGROUP help xxx} e
-        assert_match "*wrong number of arguments*" $e
+        assert_match "*wrong number of arguments for 'xgroup|help' command" $e
     }
 
     test {XINFO HELP should not have unexpected options} {
         catch {r XINFO help xxx} e
-        assert_match "*wrong number of arguments*" $e
+        assert_match "*wrong number of arguments for 'xinfo|help' command" $e
     }
 }
diff --git a/tests/unit/type/string.tcl b/tests/unit/type/string.tcl
index a6b8fc1e9a8..d04bdff325f 100644
--- a/tests/unit/type/string.tcl
+++ b/tests/unit/type/string.tcl
@@ -155,7 +155,7 @@ start_server {tags {"string"}} {
          set ex {}
          catch {r getex} ex
          set ex
-     } {*wrong number of arguments*}
+     } {*wrong number of arguments for 'getex' command}
 
     test "GETDEL command" {
         r del foo
@@ -221,10 +221,10 @@ start_server {tags {"string"}} {
         r mget x{t} y{t} z{t}
     } [list 10 {foo bar} "x x x x x x x\n\n\r\n"]
 
-    test {MSET wrong number of args} {
-        catch {r mset x{t} 10 y{t} "foo bar" z{t}} err
-        format $err
-    } {*wrong number*}
+    test {MSET/MSETNX wrong number of args} {
+        assert_error {*wrong number of arguments for 'mset' command} {r mset x{t} 10 y{t} "foo bar" z{t}}
+        assert_error {*wrong number of arguments for 'msetnx' command} {r msetnx x{t} 20 y{t} "foo bar" z{t}}
+    }
 
     test {MSETNX with already existent key} {
         list [r msetnx x1{t} xxx y2{t} yyy x{t} 20] [r exists x1{t}] [r exists y2{t}]
diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl
index f9d0c3a2839..10945674e92 100644
--- a/tests/unit/type/zset.tcl
+++ b/tests/unit/type/zset.tcl
@@ -1157,9 +1157,9 @@ start_server {tags {"zset"}} {
     }
 
     test "ZMPOP with illegal argument" {
-        assert_error "ERR wrong number of arguments*" {r zmpop}
-        assert_error "ERR wrong number of arguments*" {r zmpop 1}
-        assert_error "ERR wrong number of arguments*" {r zmpop 1 myzset{t}}
+        assert_error "ERR wrong number of arguments for 'zmpop' command" {r zmpop}
+        assert_error "ERR wrong number of arguments for 'zmpop' command" {r zmpop 1}
+        assert_error "ERR wrong number of arguments for 'zmpop' command" {r zmpop 1 myzset{t}}
 
         assert_error "ERR numkeys*" {r zmpop 0 myzset{t} MIN}
         assert_error "ERR numkeys*" {r zmpop a myzset{t} MIN}
@@ -1475,6 +1475,16 @@ start_server {tags {"zset"}} {
         assert_error "*not*float*" {r zadd myzset "" abc}
     }
 
+    test "zunionInterDiffGenericCommand at least 1 input key" {
+        assert_error {*at least 1 input key * 'zunion' command} {r zunion 0 key{t}}
+        assert_error {*at least 1 input key * 'zunionstore' command} {r zunionstore dst_key{t} 0 key{t}}
+        assert_error {*at least 1 input key * 'zinter' command} {r zinter 0 key{t}}
+        assert_error {*at least 1 input key * 'zinterstore' command} {r zinterstore dst_key{t} 0 key{t}}
+        assert_error {*at least 1 input key * 'zdiff' command} {r zdiff 0 key{t}}
+        assert_error {*at least 1 input key * 'zdiffstore' command} {r zdiffstore dst_key{t} 0 key{t}}
+        assert_error {*at least 1 input key * 'zintercard' command} {r zintercard 0 key{t}}
+    }
+
     proc stressers {encoding} {
         set original_max_entries [lindex [r config get zset-max-ziplist-entries] 1]
         set original_max_value [lindex [r config get zset-max-ziplist-value] 1]
@@ -1950,9 +1960,9 @@ start_server {tags {"zset"}} {
     }
 
     test "BZMPOP with illegal argument" {
-        assert_error "ERR wrong number of arguments*" {r bzmpop}
-        assert_error "ERR wrong number of arguments*" {r bzmpop 0 1}
-        assert_error "ERR wrong number of arguments*" {r bzmpop 0 1 myzset{t}}
+        assert_error "ERR wrong number of arguments for 'bzmpop' command" {r bzmpop}
+        assert_error "ERR wrong number of arguments for 'bzmpop' command" {r bzmpop 0 1}
+        assert_error "ERR wrong number of arguments for 'bzmpop' command" {r bzmpop 0 1 myzset{t}}
 
         assert_error "ERR numkeys*" {r bzmpop 1 0 myzset{t} MIN}
         assert_error "ERR numkeys*" {r bzmpop 1 a myzset{t} MIN}