一 场景,适合坐电梯 电梯,蹲坑

   二 特点,执行快,效率高

  python+源文件,

  三 说明,这里不要bash,性能偶尔会翻车。

 博主有时间把它集成到个人APP 「已开源」

 

 #!/usr/bin/env python3

# -*- coding: utf-8 -*-

"""

纯 Python · C 源码调用图分析器

依赖:universal-ctags cflow (PATH 中即可)

用法:python3 cflow_analyser.py

"""

import json

import logging

import subprocess

import re

from pathlib import Path

 

logging.basicConfig(level=logging.INFO, format="%(levelname)s | %(message)s")

log = logging.getLogger("analyser")

 

# ---------- 工具调用 ----------

def run(cmd: list, cwd=None, check=True) -> str:

    """统一 subprocess 封装,失败时自动打印 stderr"""

    log.debug("RUN: %s", " ".join(cmd))

    try:

        cp = subprocess.run(

            cmd, cwd=cwd, capture_output=True, text=True, encoding="utf-8", check=check

        )

        return cp.stdout

    except subprocess.CalledProcessError as e:

        log.error("命令失败:%s\nstderr=%s", " ".join(cmd), e.stderr)

        if check:

            raise

        return "" # 允许跳过

 

# ---------- 1. cflow 文本格式 ----------

def cflow_posix(c_files: list[Path]) -> str:

    """返回 cflow --format=posix --brief 原始文本"""

    if not c_files:

        return ""

    cmd = ["cflow", "--format=posix", "--brief", "--level=2"] + [str(f) for f in c_files]

    # GNU 才支持 --level,旧版报错就砍掉

    try:

        return run(cmd)

    except subprocess.CalledProcessError:

        cmd.remove("--level=2")

        return run(cmd)

 

# ---------- 2. 解析 posix ----------

RE_POSIX = re.compile(

    r"^(?P<caller>\w+)\(\).*? at .*?:\d+.*?>:$"

) # 匹配 caller 行

 

def parse_posix(text: str) -> dict[str, list[str]]:

    """把 posix 文本转成 caller->[callee...]"""

    call_map: dict[str, list[str]] = {}

    current = None

    for line in text.splitlines():

        line = line.strip()

        if not line:

            continue

        m = RE_POSIX.match(line)

        if m: # 新的 caller

            current = m.group("caller")

            continue

        # 缩进行就是 callees

        if current and line.startswith("HAL_"):

            continue # 过滤 HAL 库噪音

        callee = line.split("()")[0].strip()

        if callee and callee != current:

            call_map.setdefault(current, []).append(callee)

    return call_map

 

# ---------- 3. ctags 拿符号 ----------

def ctags_symbols(c_file: Path) -> list[dict]:

    cmd = [

        "ctags",

        "--output-format=json",

        "--fields=+KnzS",

        "--kinds-c=+f",

        str(c_file),

    ]

    out = run(cmd, check=False)

    return [json.loads(l) for l in out.splitlines() if l.strip()]

 

# ---------- 4. 主流程 ----------

def main():

    root = Path(input("📂 源码目录: ")).expanduser().resolve()

    if not root.is_dir():

        log.error("目录不存在"); return

    report = root / "callgraph.md"

 

    c_files = sorted(root.rglob("*.c"))

    h_files = sorted(root.rglob("*.h"))

 

    # ---- 4.1 调用图 ----

    log.info("正在生成调用图...")

    posix_txt = cflow_posix(c_files)

    call_map = parse_posix(posix_txt)

 

    # ---- 4.2 符号表 ----

    log.info("正在提取符号...")

    symbols: dict[Path, list[dict]] = {}

    for f in c_files + h_files:

        symbols[f] = ctags_symbols(f)

 

    # ---- 4.3 写报告 ----

    with report.open("w", encoding="utf-8") as md:

        md.write("# C 源码分析\n\n")

        md.write(f"目录:`{root}` \n")

        md.write(f"生成时间:{subprocess.check_output(['date']).decode().strip()}\n\n")

        md.write("## 目录树\n```\n")

        md.write(run(["tree", "-L", "2"], cwd=root))

        md.write("```\n\n")

 

        md.write("## 函数列表\n")

        idx = 0

        for f, syms in symbols.items():

            for sym in syms:

                if sym.get("kind") != "function":

                    continue

                idx += 1

                name = sym["name"]

                line = sym.get("line", 0)

                md.write(f"{idx}. `{name}` *{f.relative_to(root)}:{line}*\n")

 

        md.write("\n## 调用关系\n")

        for caller, callees in call_map.items():

            md.write(f"\n`{caller}()` \n")

            for c in callees:

                md.write(f"├─▶ `{c}()` \n")

 

    log.info("✅ 报告已生成 → %s", report)

 

if __name__ == "__main__":

    main()

 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐