You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pwndbg/scripts/_docs/extract_command_docs.py

182 lines
5.6 KiB
Python

#!/usr/bin/env python
"""
If the PWNDBG_DOCGEN_VERIFY environment variable
is set, then : Exit with non-zero exit status if the docs/commands/ files
aren't up to date with the sources. Don't modify anything.
If it isn't, this fixes up the docs/commands/ files to be up
to date with the (argparse) information from the sources.
"""
from __future__ import annotations
import os
# We need to patch shutil.get_terminal_size() because otherwise argparse will output
# .format_usage() based on terminal width which may be different for different users.
# I tried every other solution, it doesn't work :).
import shutil
from dataclasses import asdict
shutil.get_terminal_size = lambda fallback=(80, 24): os.terminal_size((80, 24))
import json
import sys
from typing import Tuple
import pwndbg.commands
from pwndbg.commands import CommandObj
from scripts._docs.command_docs_common import BASE_PATH
from scripts._docs.command_docs_common import ExtractedCommand
from scripts._docs.command_docs_common import category_to_folder_name
from scripts._docs.command_docs_common import extracted_filename
from scripts._docs.gen_docs_generic import get_debugger
def extract_commands() -> list[CommandObj]:
"""
Extract the commands.
Returns:
A list of all CommandObj objects that this debugger can see.
"""
commandobjs: list[CommandObj] = []
# This depends on pwndbg.commands.load_commands()
# `obj` iterates over all modules in pwndbg.commands (among other stuff).
for obj_name in dir(pwndbg.commands):
# Get the (potential) module by name.
mod = getattr(pwndbg.commands, obj_name)
# Iterate over everything in the module, which includes the command functions.
for thing_name in dir(mod):
cmdobj = getattr(mod, thing_name)
if not isinstance(cmdobj, pwndbg.commands.CommandObj):
continue
# This object is a command!
commandobjs.append(cmdobj)
assert commandobjs
return commandobjs
def distill_sources(commandobjs: list[CommandObj]) -> list[ExtractedCommand]:
extracted: list[ExtractedCommand] = []
for cmdobj in commandobjs:
name = cmdobj.command_name
category = cmdobj.category
cat_folder = category_to_folder_name(category)
filename = os.path.join(BASE_PATH, cat_folder, f"{name}.md")
description = cmdobj.description
if not description:
print(f"ERROR: Command {name} ({filename}) does not have a description.")
sys.exit(5)
aliases = cmdobj.aliases
examples = cmdobj.examples
notes = cmdobj.notes
pure_epilog = cmdobj.pure_epilog
# Extract data from the parser
parser = cmdobj.parser
formatter = parser._get_formatter()
usage = parser.format_usage()
used_actions = {}
# positional arguments
# [(argument name, argument help)]
positionals: list[Tuple[str, str]] = []
if parser._positionals._group_actions:
for action in parser._positionals._group_actions:
this_id = id(action)
if this_id in used_actions:
continue
# The formatter decides if the default should be shown.
param_help = formatter._expand_help(action)
positionals.append((action.dest, param_help))
used_actions[this_id] = True
# option arguments
# [(short name, long name, argument help)]
optionals: list[Tuple[str, str, str]] = []
if parser._option_string_actions:
for k in parser._option_string_actions:
action = parser._option_string_actions[k]
this_id = id(action)
if this_id in used_actions:
continue
short_name = ""
long_name = ""
for opt in action.option_strings:
# --, long option
if len(opt) > 1 and opt[1] in parser.prefix_chars:
long_name = opt
# short opt
elif len(opt) > 0 and opt[0] in parser.prefix_chars:
short_name = opt
# The formatter decides if the default should be shown.
param_help = formatter._expand_help(action)
optionals.append((short_name, long_name, param_help))
used_actions[this_id] = True
# Construct and append the final result
extracted.append(
ExtractedCommand(
name,
category,
filename,
description,
aliases,
examples,
notes,
pure_epilog,
usage,
positionals,
optionals,
)
)
return extracted
def main():
print("\n== Extracting Commands ==")
debugger = get_debugger()
commandobjs = extract_commands()
extracted = distill_sources(commandobjs)
result = {}
for c in extracted:
# We do a mapping instead of a simple
# list of objects so we can construct the index
# later easily. TODO: can we just do list?
result[c.filename] = asdict(c)
# Write to file.
out_path = extracted_filename(debugger)
with open(out_path, "w") as file:
json.dump(result, file)
print("== Finished Extracting Commands ==")
# Since lldb's `command script import ...` doesn't
# actually run the file like gdb's `source ...`, we can't
# use the __name__ == "__main__" guard.
main()