Repository: pwndbg/pwndbg

Issue:
Change how we display aliases in the command list
We have a bunch of commands that are just aliases for other commands. Instead of outputting all the commands and aliases in `pwndbg`, we can only output the primary command, and after the help text add (`aliases: ...`) with the list of aliases.


Discussion:

Resolving patch:
diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py
--- a/pwndbg/commands/__init__.py
+++ b/pwndbg/commands/__init__.py
@@ -52,7 +52,11 @@ class Command(gdb.Command):
     builtin_override_whitelist = {"up", "down", "search", "pwd", "start", "ignore"}
     history = {}  # type: Dict[int,str]
 
-    def __init__(self, function, prefix=False, command_name=None, shell=False):
+    def __init__(
+        self, function, prefix=False, command_name=None, shell=False, is_alias=False, aliases=[]
+    ):
+        self.is_alias = is_alias
+        self.aliases = aliases
         self.shell = shell
 
         if command_name is None:
@@ -417,7 +421,7 @@ def _OnlyWithResolvedHeapSyms(*a, **kw):
 
 
 class _ArgparsedCommand(Command):
-    def __init__(self, parser, function, command_name=None, *a, **kw):
+    def __init__(self, parser, function, command_name=None, is_alias=False, aliases=[], *a, **kw):
         self.parser = parser
         if command_name is None:
             self.parser.prog = function.__name__
@@ -431,7 +435,9 @@ def __init__(self, parser, function, command_name=None, *a, **kw):
         # Note: function.__doc__ is used in the `pwndbg [filter]` command display
         function.__doc__ = self.parser.description.strip()
 
-        super(_ArgparsedCommand, self).__init__(function, command_name=command_name, *a, **kw)
+        super(_ArgparsedCommand, self).__init__(
+            function, command_name=command_name, is_alias=is_alias, aliases=aliases, *a, **kw
+        )
 
     def split_args(self, argument):
         argv = gdb.string_to_argv(argument)
@@ -464,8 +470,10 @@ def __init__(self, parser_or_desc, aliases=[], command_name=None):
 
     def __call__(self, function):
         for alias in self.aliases:
-            _ArgparsedCommand(self.parser, function, command_name=alias)
-        return _ArgparsedCommand(self.parser, function, command_name=self._command_name)
+            _ArgparsedCommand(self.parser, function, command_name=alias, is_alias=True)
+        return _ArgparsedCommand(
+            self.parser, function, command_name=self._command_name, aliases=self.aliases
+        )
 
 
 # We use a 64-bit max value literal here instead of pwndbg.gdblib.arch.current
diff --git a/pwndbg/commands/misc.py b/pwndbg/commands/misc.py
--- a/pwndbg/commands/misc.py
+++ b/pwndbg/commands/misc.py
@@ -4,6 +4,7 @@
 import gdb
 
 import pwndbg.auxv
+import pwndbg.color as C
 import pwndbg.commands
 import pwndbg.gdblib.regs
 import pwndbg.gdblib.symbol
@@ -84,8 +85,22 @@ def pwndbg_(filter_pattern, shell, all_):
         shell_cmds = False
         pwndbg_cmds = True
 
-    for name, docs in list_and_filter_commands(filter_pattern, pwndbg_cmds, shell_cmds):
-        print("%-20s %s" % (name, docs))
+    from tabulate import tabulate
+
+    table_data = []
+    for name, aliases, docs in list_and_filter_commands(filter_pattern, pwndbg_cmds, shell_cmds):
+        alias_str = ""
+        aliases_len = 0
+        if aliases:
+            aliases = map(C.blue, aliases)
+            alias_str = f" [{', '.join(aliases)}]"
+
+        command_names = C.green(name) + alias_str
+        table_data.append((command_names, docs))
+
+    print(
+        tabulate(table_data, headers=[f"{C.green('Command')} [{C.blue('Aliases')}]", "Description"])
+    )
 
 
 parser = argparse.ArgumentParser(description="Print the distance between the two arguments.")
@@ -125,6 +140,10 @@ def list_and_filter_commands(filter_str, pwndbg_cmds=True, shell_cmds=False):
         if not c.shell and not pwndbg_cmds:
             continue
 
+        # Don't print aliases
+        if c.is_alias:
+            continue
+
         name = c.__name__
         docs = c.__doc__
 
@@ -134,6 +153,6 @@ def list_and_filter_commands(filter_str, pwndbg_cmds=True, shell_cmds=False):
             docs = docs.splitlines()[0]
 
         if not filter_str or filter_str in name.lower() or (docs and filter_str in docs.lower()):
-            results.append((name, docs))
+            results.append((name, c.aliases, docs))
 
     return results
diff --git a/pwndbg/gdblib/prompt.py b/pwndbg/gdblib/prompt.py
--- a/pwndbg/gdblib/prompt.py
+++ b/pwndbg/gdblib/prompt.py
@@ -14,7 +14,9 @@
 
 funcs_list_str = ", ".join(message.notice("$" + f.name) for f in pwndbg.gdblib.functions.functions)
 
-num_pwndbg_cmds = sum(1 for _ in filter(lambda c: not c.shell, pwndbg.commands.commands))
+num_pwndbg_cmds = sum(
+    1 for _ in filter(lambda c: not (c.shell or c.is_alias), pwndbg.commands.commands)
+)
 num_shell_cmds = sum(1 for _ in filter(lambda c: c.shell, pwndbg.commands.commands))
 hint_lines = (
     "loaded %i pwndbg commands and %i shell commands. Type %s for a list."


Test patch:
diff --git a/tests/gdb-tests/tests/test_misc.py b/tests/gdb-tests/tests/test_misc.py
--- a/tests/gdb-tests/tests/test_misc.py
+++ b/tests/gdb-tests/tests/test_misc.py
@@ -1,13 +1,15 @@
+import pytest
+
 import pwndbg.commands
 from pwndbg.commands.misc import list_and_filter_commands
 
 STACK_COMMANDS = [
-    ("canary", "Print out the current stack canary."),
-    ("context", "Print out the current register, instruction, and stack context."),
-    ("down", "Select and print stack frame called by this one."),
-    ("retaddr", "Print out the stack addresses that contain return addresses."),
-    ("stack", "Dereferences on stack data with specified count and offset."),
-    ("up", "Select and print stack frame that called this one."),
+    ("canary", [], "Print out the current stack canary."),
+    ("context", ["ctx"], "Print out the current register, instruction, and stack context."),
+    ("down", [], "Select and print stack frame called by this one."),
+    ("retaddr", [], "Print out the stack addresses that contain return addresses."),
+    ("stack", [], "Dereferences on stack data with specified count and offset."),
+    ("up", [], "Select and print stack frame that called this one."),
 ]
 
 
@@ -16,37 +18,20 @@ def test_list_and_filter_commands_filter():
         assert cmd in list_and_filter_commands("stack")
 
 
-def test_list_and_filter_commands_full_list():
-    all_commands = list_and_filter_commands("", pwndbg_cmds=True, shell_cmds=True)
-
-    def get_doc(c):
-        return c.__doc__.strip().splitlines()[0] if c.__doc__ else None
-
-    cmd_name_docs = [(c.__name__, get_doc(c)) for c in pwndbg.commands.commands]
-    cmd_name_docs.sort()
-
-    assert all_commands == cmd_name_docs
-
-
-def test_list_and_filter_commands_shell():
-    all_commands = list_and_filter_commands("", pwndbg_cmds=False, shell_cmds=True)
+@pytest.mark.parametrize("pwndbg_cmds,shell_cmds", [(True, True), (False, True), (True, False)])
+def test_list_and_filter_commands_full_list(pwndbg_cmds, shell_cmds):
+    all_commands = list_and_filter_commands("", pwndbg_cmds=pwndbg_cmds, shell_cmds=shell_cmds)
 
     def get_doc(c):
         return c.__doc__.strip().splitlines()[0] if c.__doc__ else None
 
-    cmd_name_docs = [(c.__name__, get_doc(c)) for c in pwndbg.commands.commands if c.shell]
-    cmd_name_docs.sort()
-
-    assert all_commands == cmd_name_docs
-
-
-def test_list_and_filter_commands_pwndbg_cmds():
-    all_commands = list_and_filter_commands("", pwndbg_cmds=True, shell_cmds=False)
-
-    def get_doc(c):
-        return c.__doc__.strip().splitlines()[0] if c.__doc__ else None
+    commands = []
+    if pwndbg_cmds:
+        commands.extend([c for c in pwndbg.commands.commands if not c.is_alias and not c.shell])
+    if shell_cmds:
+        commands.extend([c for c in pwndbg.commands.commands if not c.is_alias and c.shell])
 
-    cmd_name_docs = [(c.__name__, get_doc(c)) for c in pwndbg.commands.commands if not c.shell]
+    cmd_name_docs = [(c.__name__, c.aliases, get_doc(c)) for c in commands]
     cmd_name_docs.sort()
 
     assert all_commands == cmd_name_docs