Hi all,

a while ago I looked at tools that could be used too build a call graph. The simplest but most effective that I found was a small Perl program (called "egypt", which is rot13 for "rtlcg" aka RTL call graph) that used the GCC dumps to build the graph.

I have now rewritten it in Python and extended it with a lot of new functionality:

- consult compile_commands.json to find/build dumps automatically

- virtual (manually created) nodes and edges

- query call graph in addition to generating DOT file

- interactive mode with readline + completion

The name is unfortunately not rot13 anymore, it stands for visit RTL callgraph.

Here is an example (run vrc from the root build directory of QEMU):

        # load files
        load libblock.fa.p/*.c.o

        # introduce virtual edges corresponding to function pointers
        node BlockDriverState.bdrv_co_flush
        edge bdrv_co_flush BlockDriverState.bdrv_co_flush
        edge BlockDriverState.bdrv_co_flush blk_log_writes_co_do_file_flush
        edge BlockDriverState.bdrv_co_flush preallocate_co_flush
        edge BlockDriverState.bdrv_co_flush raw_co_invalidate_cache
        edge BlockDriverState.bdrv_co_flush cbw_co_flush
        edge BlockDriverState.bdrv_co_flush quorum_co_flush
        edge BlockDriverState.bdrv_co_flush throttle_co_flush
        edge BlockDriverState.bdrv_co_flush blkdebug_co_flush
        edge BlockDriverState.bdrv_co_flush blkverify_co_flush
        edge BlockDriverState.bdrv_co_flush bdrv_mirror_top_flush
        # apply filter
        only --callees bdrv_co_flush
        # draw graph
        dotty --files

The filtering functionality is a bit rough in the presence of mutual recursion, but hopefully this can be already useful to find the root calls of bdrv_*, which are the places where the graph lock has to be taken for read. Continuing the previous example:

        # apply another filter
        reset
        omit --callees bdrv_co_flush
        keep bdrv_co_flush
        # example of query
        callers bdrv_co_flush

already gives a reasonable answer (not entirely correct, but the actual analysis must be done on all callbacks at once):

        qed_co_request -> bdrv_co_flush
        qed_need_check_timer_entry -> bdrv_co_flush
        blk_log_writes_co_log -> bdrv_co_flush
        bdrv_co_flush_entry -> bdrv_co_flush
        bdrv_co_flush -> bdrv_co_flush
        blk_co_do_flush -> bdrv_co_flush
        bdrv_driver_pwritev -> bdrv_co_flush
        blk_co_flush -> bdrv_co_flush
        bdrv_flush -> bdrv_co_flush
        bdrv_co_do_pwrite_zeroes -> bdrv_co_flush
        blk_aio_flush_entry -> bdrv_co_flush

Paolo
#! /usr/bin/env python3
import argparse
from collections import defaultdict
import dataclasses
import glob
import io
import json
import os
import re
import readline
import shlex
import subprocess
import sys
import typing


@dataclasses.dataclass
class Node:
    name: str
    callers: set[str]
    callees: dict[str, str]
    username: typing.Optional[str] = None
    external: bool = True

    def __init__(self, name):
        super().__init__()
        self.name = name
        self.callers = set()
        self.callees = dict()

    def __getitem__(self, callee: str) -> str:
        return self.callees[callee]

    def __setitem__(self, callee: str, type: str):
        # A "ref" edge does not override a "call" edge
        if type == "call" or callee not in self.callees:
            self.callees[callee] = type


class Graph:
    nodes: dict[str, Node]
    nodes_by_username: dict[str, Node]
    files: dict[str, list[str]]
    keep: typing.Optional[set[str]]
    omit: set[str]
    filter_default: bool

    def __init__(self):
        self.nodes = {}
        self.nodes_by_username = {}
        self.files = defaultdict(lambda: list())

        self.reset_filter()

    def parse(self, fn: str, lines: typing.Iterator[str], verbose_print) -> 
None:
        RE_FUNC1 = re.compile(r"^;; Function (\S+)\s*$")
        RE_FUNC2 = re.compile(r"^;; Function (.*)\s+\((\S+)(,.*)?\).*$")
        RE_SYMBOL_REF = re.compile(r'\(symbol_ref [^(]* \( "([^"]*)"', 
flags=re.X)
        curfunc = None
        for line in lines:
            if line.startswith(";; Function "):
                m = RE_FUNC1.search(line)
                if m:
                    curfunc = m.group(1)
                    self.add_node(m.group(1), file=fn)
                    verbose_print(f"{fn}: found function {m.group(1)}")
                    continue
                m = RE_FUNC2.search(line)
                if m:
                    curfunc = m.group(2)
                    self.add_node(m.group(2), username=m.group(1), file=fn)
                    verbose_print(f"{fn}: found function {m.group(1)} 
({m.group(2)})")
                    continue
            elif curfunc:
                m = RE_SYMBOL_REF.search(line)
                if m:
                    type = "call" if "(call" in line else "ref"
                    verbose_print(f"{fn}: found {type} edge {curfunc} -> 
{m.group(1)}")
                    self.add_edge(curfunc, m.group(1), type)

    def add_external_node(self, name: str) -> None:
        if name not in self.nodes:
            self.nodes[name] = Node(name=name)

    def add_node(self, name: str, username: typing.Optional[str] = None,
                 file: typing.Optional[str] = None) -> None:
        self.add_external_node(name)
        if self.nodes[name].external:
            # This is now a defined node.  It might have a username and a file
            self.nodes[name].external = False
            if username:
                self.nodes[name].username = username
                self.nodes_by_username[username] = self.nodes[name]
            if file:
                self.files[file].append(name)

    def add_edge(self, caller: str, callee: str, type: str) -> None:
        # The caller must exist, but the callee could be external.
        self.add_external_node(callee)
        self.nodes[caller][callee] = type
        self.nodes[callee].callers.add(caller)

    def _get_node(self, name: str) -> typing.Optional[Node]:
        if name in self.nodes_by_username:
            return self.nodes_by_username[name]
        elif name in self.nodes:
            return self.nodes[name]
        else:
            return None

    def has_node(self, name: str) -> bool:
        return bool(self._get_node(name))

    def _visit(self, start: str, targets: typing.Callable[[Node], 
typing.Iterable[str]]) -> typing.Iterator[str]:
        visited = set()

        def visit(n: Node) -> typing.Iterator[str]:
            if n.name in visited:
                return
            visited.add(n.name)
            yield n.username or n.name
            for caller in targets(n):
                target = self._get_node(caller)
                if target:
                    yield from visit(target)

        n = self._get_node(start)
        if not n:
            return iter({})
        yield from visit(n)

    def all_callers(self, callee: str) -> typing.Iterator[str]:
        return self._visit(callee, lambda n: n.callers)

    def all_callees(self, caller: str) -> typing.Iterator[str]:
        return self._visit(caller, lambda n: n.callees.keys())

    def callers(self, callee: str, ref_ok: bool) -> typing.Iterator[str]:
        n = self._get_node(callee)
        if not n:
            return iter([])
        return (
            self.name(caller)
            for caller in n.callers
            if self.filter_edge(caller, callee, True, ref_ok))

    def callees(self, caller: str, external_ok: bool, ref_ok: bool) -> 
typing.Iterator[str]:
        n = self._get_node(caller)
        if not n:
            return iter([])
        return (self.name(callee)
                for callee in n.callees.keys()
                if self.filter_edge(caller, callee, external_ok, ref_ok))

    def all_nodes(self) -> typing.Iterator[str]:
        return (self.name(x)
                for x in self.nodes.keys()
                if self.filter_node(x, False))

    def all_nodes_for_file(self, file: str) -> typing.Iterator[str]:
        return (x if x in self.nodes_by_username else self.name(x)
                for x in self.files[file]
                if self.filter_node(x, False))

    def name(self, x: str) -> str:
        n = self.nodes[x]
        return n.username or x

    def _filter_node(self, n: Node, external_ok: bool) -> bool:
        if not external_ok and n.external:
            return False
        if self.keep is not None and n.name in self.keep:
            return True
        if n.name in self.omit:
            return False
        return self.filter_default

    def filter_node(self, x: str, external_ok: bool) -> bool:
        n = self._get_node(x)
        if not n:
            return False
        return self._filter_node(n, external_ok)

    def filter_edge(self, caller: str, callee: str, external_ok: bool, ref_ok: 
bool) -> bool:
        caller_node = self._get_node(caller)
        if not caller_node or not self._filter_node(caller_node, True):
            return False
        callee_node = self._get_node(callee)
        if not callee_node or not self._filter_node(callee_node, external_ok):
            return False
        return caller_node[callee] == "call" or (ref_ok and not 
callee_node.external)

    def omit_node(self, name: str) -> None:
        n = self._get_node(name)
        name = n.name if n else name

        self.omit.add(name)
        if self.keep is not None and name in self.keep:
            self.keep.remove(name)

    def keep_node(self, name: str) -> None:
        if self.keep is None:
            self.keep = set()

        n = self._get_node(name)
        name = n.name if n else name

        self.keep.add(name)
        if name in self.omit:
            self.omit.remove(name)

    def reset_filter(self) -> None:
        self.omit = set()
        self.keep = None
        self.filter_default = True


GRAPH = Graph()


class NoUsageFormatter(argparse.HelpFormatter):
    def add_usage(self, usage: typing.Optional[str], actions: 
typing.Iterable[argparse.Action],
                  groups: typing.Iterable[argparse._ArgumentGroup], prefix: 
typing.Optional[str] = ...) -> None:
        pass


class MyArgumentParser(argparse.ArgumentParser):
    def __init__(self, *args, **kwargs):
        super().__init__(exit_on_error=False, add_help=False, 
formatter_class=NoUsageFormatter)

    def format_usage(self):
        return ""

    def error(self, message: str):
        raise argparse.ArgumentError(None, f"{self.prog}: error: {message}" "")


PARSER = MyArgumentParser()


class VRCCommand:

    NAME: typing.Optional[tuple[str, ...]] = None

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        pass

    def run(self, args: argparse.Namespace):
        """Run command.
        args: parsed argument by argument parser.
        argv: remaining arguments from sys.argv.
        """
        pass


class ChdirCommand(VRCCommand):
    """Change current directory."""
    NAME = ("cd",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("dir", metavar="DIR",
                            help="New current directory")

    def run(self, args: argparse.Namespace):
        os.chdir(os.path.expanduser(args.dir))


class PwdCommand(VRCCommand):
    """Print current directory."""
    NAME = ("pwd",)

    def run(self, args: argparse.Namespace):
        print(os.getcwd())


class HistoryCommand(VRCCommand):
    """Print command history."""
    NAME = ("history",)

    def run(self, args: argparse.Namespace):
        # TODO: limit history to N entries
        for i in range(1, readline.get_current_history_length() + 1):
            print('{:7} {}'.format(i, readline.get_history_item(i)))


class CompdbCommand(VRCCommand):
    """Loads a compile_commands.json file."""
    NAME = ("compdb",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("file", metavar="FILE",
                            help="JSON file to be loaded")

    def run(self, args: argparse.Namespace):
        with open(args.file, 'r') as f:
            for entry in json.load(f):
                key = os.path.relpath(os.path.join(entry["directory"], 
entry["output"]))
                COMPDB[key] = entry["command"]

COMPDB: dict[str, str] = dict()


class LoadCommand(VRCCommand):
    """Loads a GCC RTL output (.expand, generated by -fdump-rtl-expand)."""
    NAME = ("load",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        def eat(*args: list[typing.Any]) -> None:
            pass

        def print_stderr(*args: list[typing.Any]) -> None:
            print(*args, file=sys.stderr)

        parser.add_argument("--verbose", action="store_const",
                            const=print_stderr, default=eat,
                            help="Report progress while parsing")
        parser.add_argument("files", metavar="FILE", nargs="+",
                            help="Dump or object file to be loaded")

    def run(self, args: argparse.Namespace):
        def build_gcc_S_command_line(cmd, outfile):
            args = shlex.split(cmd)
            out = []
            was_o = False
            for i in args:
                if was_o:
                    i = '/dev/null'
                    was_o = False
                elif i == '-c':
                    i = '-S'
                elif i == '-o':
                    was_o = True
                out.append(i)
            return out + ['-fdump-rtl-expand', '-dumpbase', outfile]

        def expand(files: typing.Iterator[str]) -> typing.Iterator[tuple[str, 
io.TextIOWrapper]]:
            cwd = os.getcwd()
            for pattern in files:
                for fn in glob.glob(os.path.expanduser(pattern), root_dir=cwd):
                    if fn.endswith(".o"):
                        objfile = os.path.relpath(fn)
                        if objfile not in COMPDB:
                            print(f"Could not find '{objfile}' in 
compile_commands.json", file=sys.stderr)
                            continue

                        dumps = glob.glob(objfile + ".*r.expand")
                        if len(dumps) > 1:
                            print(f"Found more than one dump file: {', 
'.join(dumps)}", file=sys.stderr)
                            continue

                        if not dumps:
                            cmdline = build_gcc_S_command_line(COMPDB[objfile], 
objfile)
                            args.verbose(f"Launching {shlex.join(cmdline)}")
                            try:
                                result = subprocess.run(cmdline,
                                                        
stdin=subprocess.DEVNULL)
                            except KeyboardInterrupt as e:
                                print("Interrupt", file=sys.stderr)
                                break
                            if result.returncode != 0:
                                print(f"Compiler exited with return code 
{result.returncode}", file=sys.stderr)
                                continue
                            dumps = glob.glob(objfile + ".*r.expand")
                            if len(dumps) > 1:
                                print(f"Found more than one dump file: {', 
'.join(dumps)}", file=sys.stderr)
                                continue

                        fn = dumps[0]
                        print(f"Reading {fn}", file=sys.stderr)
                    else:
                        args.verbose(f"Reading {fn}")

                    with open(fn, "r") as f:
                        yield fn, f

        for fn, f in expand(args.files):
            GRAPH.parse(fn, f, verbose_print=args.verbose)


class NodeCommand(VRCCommand):
    """Creates a new node for a non-external symbol."""
    NAME = ("node",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("name", metavar="NAME",
                            help="Name for the new node")
        parser.add_argument("file", metavar="FILE", nargs="?",
                            help="File in which the new node is defined")

    def run(self, args: argparse.Namespace):
        GRAPH.add_node(args.name, file=args.file)


class EdgeCommand(VRCCommand):
    """Creates a new edge.  The caller must exist already."""
    NAME = ("edge",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("caller", metavar="CALLER",
                            help="Source node for the new edge")
        parser.add_argument("callee", metavar="CALLEE",
                            help="Target node for the new edge")
        parser.add_argument("type", metavar="TYPE", nargs="?",
                            help="Type of the new edge (call or ref)",
                            choices=["call", "ref"], default="call")

    def run(self, args: argparse.Namespace):
        if not GRAPH.has_node(args.caller):
            raise argparse.ArgumentError(None, "caller not found in graph")
        GRAPH.add_edge(args.caller, args.callee, args.type)


class OmitCommand(VRCCommand):
    """Removes a node, and optionally its callers and/or callees, from
       the graph that is generated by "output" or "dotty"."""
    NAME = ("omit",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("--callers", action="store_true",
                            help="Omit all callers, recursively.")
        parser.add_argument("--callees", action="store_true",
                            help="Omit all callees, recursively.")
        parser.add_argument("funcs", metavar="FUNC", nargs="+",
                            help="The functions to be filtered")

    def run(self, args: argparse.Namespace):
        for f in args.funcs:
            GRAPH.omit_node(f)
            if args.callers:
                for caller in GRAPH.all_callers(f):
                    GRAPH.omit_node(caller)
            if args.callees:
                for callee in GRAPH.all_callees(f):
                    GRAPH.omit_node(callee)


class KeepCommand(VRCCommand):
    """Undoes the effect of "omit" on a node, and optionally
       its callers and/or callees."""
    NAME = ("keep",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("--callers", action="store_true",
                            help="Keep all callers, recursively.")
        parser.add_argument("--callees", action="store_true",
                            help="Keep all callees, recursively.")
        parser.add_argument("funcs", metavar="FUNC", nargs="+",
                            help="The functions to be filtered")

    def run(self, args: argparse.Namespace):
        for f in args.funcs:
            GRAPH.keep_node(f)
            if args.callers:
                for caller in GRAPH.all_callers(f):
                    GRAPH.keep_node(caller)
            if args.callees:
                for callee in GRAPH.all_callees(f):
                    GRAPH.keep_node(callee)


class OnlyCommand(VRCCommand):
    """Limits the graph that is generated by "output" or "dotty"
       to a node, and optionally its callers and/or callees.
       If invoked multiple times, the filters are ORed.  Nodes
       added by "keep" are included too."""
    NAME = ("only",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("--callers", action="store_true",
                            help="Keep all callers, recursively.")
        parser.add_argument("--callees", action="store_true",
                            help="Keep all callees, recursively.")
        parser.add_argument("funcs", metavar="FUNC", nargs="+",
                            help="The functions to be filtered")

    def run(self, args: argparse.Namespace):
        GRAPH.filter_default = False
        for f in args.funcs:
            GRAPH.keep_node(f)
            if args.callers:
                for caller in GRAPH.all_callers(f):
                    GRAPH.keep_node(caller)
            if args.callees:
                for callee in GRAPH.all_callees(f):
                    GRAPH.keep_node(callee)


class ResetCommand(VRCCommand):
    """Undoes any filtering done by the "keep" or "omit" commands."""
    NAME = ("reset",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        pass

    def run(self, args: argparse.Namespace):
        GRAPH.reset_filter()


class CallersCommand(VRCCommand):
    """Prints the caller of all the specified functions."""
    NAME = ("callers",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("--include-ref", action="store_true",
                            help="Include references to functions.")
        parser.add_argument("funcs", metavar="FUNC", nargs="+",
                            help="The functions to be filtered")

    def run(self, args: argparse.Namespace):
        result = defaultdict(lambda: list())
        for f in args.funcs:
            for i in GRAPH.callers(f, ref_ok=args.include_ref):
                result[i].append(f)

        for caller, callees in result.items():
            print(f"{caller} -> {', '.join(callees)}")


class CalleesCommand(VRCCommand):
    """Prints the callees of all the specified functions."""
    NAME = ("callees",)

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("--include-external", action="store_true",
                            help="Include external functions.")
        parser.add_argument("--include-ref", action="store_true",
                            help="Include references to functions.")
        parser.add_argument("funcs", metavar="FUNC", nargs="+",
                            help="The functions to be filtered")

    def run(self, args: argparse.Namespace):
        result = defaultdict(lambda: list())
        for f in args.funcs:
            for i in GRAPH.callees(f, external_ok=args.include_external, 
ref_ok=args.include_ref):
                result[i].append(f)

        for callee, callers in result.items():
            print(f"{', '.join(callers)} -> {callee}")


class OutputCommand(VRCCommand):
    """Creates a DOT file with the callgraph.  If invoked as "dotty" and
       with no arguments, the graph is laid out and showed in a graphical
       window."""
    NAME = ("output", "dotty")

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("--files", action="store_true",
                            help="Create box containers for source files.")
        parser.add_argument("--include-external", action="store_true",
                            help="Include external functions.")
        parser.add_argument("--include-ref", action="store_true",
                            help="Include references to functions.")
        parser.add_argument("file", metavar="FILE", nargs="?")

    def run(self, args: argparse.Namespace):
        def emit(f):
            print("digraph callgraph {", file=f)
            nodes = set()
            for func in GRAPH.all_nodes():
                nodes.add(func)

            if args.files:
                i = 0
                for file in GRAPH.files.keys():
                    file_nodes = list(GRAPH.all_nodes_for_file(file))
                    if not file_nodes:
                        continue
                    print(f"subgraph cluster_{i}", "{", file=f)
                    print(f'label = "{file}";', file=f)
                    for func in file_nodes:
                        print(f'"{func}";', file=f)
                    print("}", file=f)
                    i += 1

            connected = set()
            for func in nodes:
                has_edges = False
                for i in GRAPH.callees(func, external_ok=args.include_external, 
ref_ok=args.include_ref):
                    print(f'"{func}" -> "{i}";', file=f)
                    connected.add(i)
                    has_edges = True
                if has_edges:
                    connected.add(func)

            for func in nodes:
                if func not in connected:
                    print(f'"{func}";', file=f)

            print("}", file=f)

        if args.file:
            fn = os.path.expanduser(args.file)
            with open(fn, "w") as f:
                try:
                    emit(f)
                except Exception as e:
                    os.unlink(fn)
                    raise e
        elif args.cmd == "dotty":
            graph = io.StringIO()
            emit(graph)
            dotty = subprocess.Popen("dotty -", stdin=subprocess.PIPE, 
shell=True,
                                     errors="backslashreplace", 
encoding="ascii")
            dotty.communicate(graph.getvalue())
        else:
            emit(sys.stdout)


class QuitCommand(VRCCommand):
    """Exits VRC."""
    NAME = ("q", "quit")

    @classmethod
    def run(self, args: argparse.Namespace):
        sys.exit(0)


class HelpCommand(VRCCommand):
    """Prints the list of commands, or the syntax of a command."""
    NAME = ("help",)
    PARSERS: dict[str, argparse.ArgumentParser] = {}

    @classmethod
    def args(self, parser: argparse.ArgumentParser):
        """Setup argument parser"""
        parser.add_argument("command", metavar="COMMAND", nargs="?",
                            help="Show help for given command.")

    @classmethod
    def register(self, command: str, parser: argparse.ArgumentParser):
        self.PARSERS[command] = parser

    def run(self, args: argparse.Namespace):
        if args.command and args.command in self.PARSERS:
            self.PARSERS[args.command].print_help()
        else:
            PARSER.print_help()


class ReadlineInput:
    def __init__(self, prompt: str):
        self.prompt = prompt
        readline.parse_and_bind("tab: complete")
        readline.set_completer(self.complete)
        readline.set_completer_delims(' \t')
        readline.set_completion_display_matches_hook(self.display_matches)

    def __iter__(self):
        return self

    def __next__(self):
        try:
            return input(self.prompt)
        except EOFError:
            print()
            raise StopIteration

    def complete(self, text: str, state: int) -> typing.Optional[str]:
        if state == 0:
            self.matches = self.get_matches(text)
        if state >= len(self.matches):
            return None
        return self.matches[state]

    def get_matches(self, text: str):
        line = readline.get_line_buffer()
        words = line.strip().split()
        nwords = len(words) - (0 if not line or line[-1] in " \t" else 1)

            # Expand the text that is used for completion
        replacement = self.get_forced_replacement(words, nwords, text)
        if replacement:
            text = replacement

        completions = self.get_completions(words, nwords, text)
        completions = [x for x in completions if x.startswith(text)]
        if len(completions) == 1 \
            and (text != "" or not completions[0].startswith("-")) \
            and not completions[0].endswith("/"):
            return [completions[0] + " "]
        if len(completions) > 1 and replacement:
            return [replacement]
        return completions

    def get_forced_replacement(self, words: list[str], nwords: int, text: str) 
-> typing.Optional[str]:
        expanded = text
        if words and words[0] in ['load', 'cd', 'compdb', 'output']:
            if text.startswith('~'):
                expanded = os.path.expanduser(expanded)
            if not expanded.endswith("/") and os.path.isdir(expanded):
                expanded += "/"
        return expanded if expanded != text else None

    def get_completions(self, words: list[str], nwords: int, text: str) -> 
list[str]:
        if nwords == 0:
            return sorted(HelpCommand.PARSERS.keys())

        opts = []
        if text.startswith('--') or text == '' or text == '-':
            # ugly...
            opts = 
sorted(HelpCommand.PARSERS[words[0]]._option_string_actions.keys())

        args = []
        if words[0] in ['callers', 'callees', 'keep', 'omit', 'edge']:
            # complete by function name
            args = 
sorted(set(GRAPH.nodes_by_username.keys()).union(GRAPH.nodes.keys()))
        elif words[0] in ['pwd']:
            pass
        elif words[0] in ['cd']:
            # complete by directory only
            args = sorted(glob.glob(text + '*/'))
        elif words[0] in ['load']:
            # complete by RTL dump, object file or directory
            path = os.path.dirname(text)
            args = glob.glob(path + '/*r.expand')
            args += glob.glob(path + '/*.o')
            args += glob.glob(text + '*/')
            args = sorted(args)
        elif words[0] in ['compdb']:
            # complete by json or directory
            path = os.path.dirname(text)
            args = glob.glob(path + '/*.json')
            args += glob.glob(text + '*/')
            args = sorted(args)
        elif words[0] in ['output']:
            # complete by any file name
            args = sorted(glob.glob(text + '*'))
            args = [x + "/" if os.path.isdir(x) else x for x in args]

        return opts + args

    def display_matches(self, substitution: str, matches: typing.Sequence[str], 
longest_match_length: int):
        line_buffer = readline.get_line_buffer()
        columns = os.get_terminal_size()[0]

        print()

        length = longest_match_length * 6 // 5 + 2
        buffer = ""
        for match in matches:
            match += " " * (length - len(match))
            if len(buffer + match) > columns:
                print(buffer.rstrip())
                buffer = ""
            buffer += match

        if buffer:
            print(buffer)

        print(self.prompt, end="")
        print(line_buffer, end="")
        sys.stdout.flush()


def main():
    if os.path.exists("compile_commands.json"):
        print("Loading compile_commands.json", file=sys.stderr)
        args = PARSER.parse_args(["compdb", "compile_commands.json"])
        try:
            args.cmdclass().run(args)
        except OSError as e:
            print("Could not load compile_commands.json:", e, file=sys.stderr)

    if os.isatty(0):
        inf = ReadlineInput("(vrc) ")
    else:
        inf = sys.stdin

    while True:
        try:
            line = next(inf)
        except KeyboardInterrupt:
            break
        except StopIteration:
            break

        line = line.strip()
        if line.startswith('#'):
            continue

        argv = line.split()
        if not argv:
            continue
        try:
            args = PARSER.parse_args(argv)
            try:
                args.cmdclass().run(args)
            except OSError as e:
                print(e)
        except argparse.ArgumentError as e:
            print(str(e), file=sys.stderr)


def init_subparsers():
    subparsers = PARSER.add_subparsers(title="subcommands", help=None)
    for cls in VRCCommand.__subclasses__():
        for n in cls.NAME:  # type: ignore
            subp = subparsers.add_parser(n, help=cls.__doc__, 
exit_on_error=False, add_help=False)
            HelpCommand.register(n, subp)
            cls.args(subp)
            subp.set_defaults(cmd=n)
            subp.set_defaults(cmdclass=cls)


init_subparsers()
if __name__ == "__main__":
    main()

Reply via email to