diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py index c12fadc38..220f6f0af 100644 --- a/pwndbg/commands/__init__.py +++ b/pwndbg/commands/__init__.py @@ -90,11 +90,13 @@ class Command: is_alias: bool = False, aliases: List[str] = [], category: CommandCategory = CommandCategory.MISC, + doc: str | None = None, ) -> None: self.is_alias = is_alias self.aliases = aliases self.category = category self.shell = shell + self.doc = doc if command_name is None: command_name = function.__name__ @@ -102,7 +104,7 @@ class Command: def _handler(_debugger, arguments, is_interactive): self.invoke(arguments, is_interactive) - self.handle = pwndbg.dbg.add_command(command_name, _handler) + self.handle = pwndbg.dbg.add_command(command_name, _handler, doc) self.function = function if command_name in command_names: @@ -547,7 +549,7 @@ class _ArgparsedCommand(Command): file = io.StringIO() self.parser.print_help(file) file.seek(0) - self.__doc__ = file.read() + doc = file.read() # Note: function.__doc__ is used in the `pwndbg [filter]` command display function.__doc__ = self.parser.description.strip() @@ -555,6 +557,7 @@ class _ArgparsedCommand(Command): super().__init__( # type: ignore[misc] function, command_name=command_name, + doc=doc, *a, **kw, ) diff --git a/pwndbg/dbg/__init__.py b/pwndbg/dbg/__init__.py index 86d8fd9db..942ef6117 100644 --- a/pwndbg/dbg/__init__.py +++ b/pwndbg/dbg/__init__.py @@ -352,7 +352,7 @@ class Debugger: raise NotImplementedError() def add_command( - self, name: str, handler: Callable[[Debugger, str, bool], None] + self, name: str, handler: Callable[[Debugger, str, bool], None], doc: str | None ) -> CommandHandle: """ Adds a command with the given name to the debugger, that invokes the diff --git a/pwndbg/dbg/gdb.py b/pwndbg/dbg/gdb.py index 06aaf533f..c1a2cd8fd 100644 --- a/pwndbg/dbg/gdb.py +++ b/pwndbg/dbg/gdb.py @@ -129,9 +129,11 @@ class GDBCommand(gdb.Command): debugger: GDB, name: str, handler: Callable[[pwndbg.dbg_mod.Debugger, str, bool], None], + doc: str | None, ): self.debugger = debugger self.handler = handler + self.__doc__ = doc super().__init__(name, gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION) def invoke(self, args: str, from_tty: bool) -> None: @@ -326,9 +328,12 @@ class GDB(pwndbg.dbg_mod.Debugger): @override def add_command( - self, name: str, handler: Callable[[pwndbg.dbg_mod.Debugger, str, bool], None] + self, + name: str, + handler: Callable[[pwndbg.dbg_mod.Debugger, str, bool], None], + doc: str | None, ) -> pwndbg.dbg_mod.CommandHandle: - command = GDBCommand(self, name, handler) + command = GDBCommand(self, name, handler, doc) return GDBCommandHandle(command) @override diff --git a/pwndbg/dbg/lldb.py b/pwndbg/dbg/lldb.py index c1fd0ef6e..42735709c 100644 --- a/pwndbg/dbg/lldb.py +++ b/pwndbg/dbg/lldb.py @@ -260,7 +260,10 @@ class LLDB(pwndbg.dbg_mod.Debugger): @override def add_command( - self, command_name: str, handler: Callable[[pwndbg.dbg_mod.Debugger, str, bool], None] + self, + command_name: str, + handler: Callable[[pwndbg.dbg_mod.Debugger, str, bool], None], + doc: str | None, ) -> pwndbg.dbg_mod.CommandHandle: debugger = self diff --git a/tests/gdb-tests/tests/test_help.py b/tests/gdb-tests/tests/test_help.py new file mode 100644 index 000000000..c2e75bb84 --- /dev/null +++ b/tests/gdb-tests/tests/test_help.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import gdb + +from pwndbg import commands + + +def test_command_help_strings(start_binary): + """ + Tests whether the `help` command works for Pwndbg commands. We go through + every command and check whether the value of `help ` matches the + help string we pass to the Debugger-agnostic API when it's being registered. + """ + + for command in commands.commands: + help_str = gdb.execute(f"help {command.__name__}", from_tty=False, to_string=True) + if command.doc is None: + assert help_str.strip() == "This command is not documented." + else: + truth = [line.strip() for line in command.doc.splitlines() if len(line.strip()) > 0] + gdb_out = [line.strip() for line in help_str.splitlines() if len(line.strip()) > 0] + + # We check both of these cases since for some commands GDB will + # output the list of aliases as the first line. + assert truth == gdb_out or truth == gdb_out[1:]