script-chain-walk 追蹤跨檔案腳本呼叫,檢查 ShellCheck 看不到的斷鏈與循環
ShellCheck Passed Every File. The Pipeline Was Still Broken.
作者 hao li 開發 script-chain-walk,這款零依賴 Python CLI(Python ≥ 3.9)可追蹤跨檔案腳本呼叫,並找出不存在的呼叫目標和呼叫循環。
ShellCheck Passed Every File. The Pipeline Was Still Broken.
ShellCheck is great at what it does. So is Pylint. So is every linter you've
ever run in CI. They all share one blind spot: they look at one file at a
time.
Here's the failure mode that keeps biting me. A deploy script calls
./cleanup.sh. The linter passes. The pipeline runs. And it fails — because
cleanup.sh was renamed two sprints ago and nobody updated the caller. No
linter on earth flags a call to a file that doesn't exist, because no linter
is looking at the relationships between files.
The same blind spot shows up with agents. An AI agent writes a tidy little
setup.sh, then a run.py that calls it via subprocess, then a wrapper
that calls run.py… and somewhere in the middle there's a loop, or a dead
reference, and you only find out at 2 AM. What you want in that moment isn't
a per-file report. It's a map: which script calls which, where the chain
dead-ends, and where it loops back on itself.
I built that map.
What I built
script-chain-walk — a zero-dependency Python CLI (stdlib only, Python ≥
3.9) that traces cross-file script call chains. Point it at a directory and
it builds the full dependency graph, then flags two things linters can't
see: broken chains (calls to files that don't exist) and call cycles.
pip install script-chain-walk
walk ./scripts
Example output on a messy deploy directory:
deploy.sh
|-- migrate.py (line 3, exec)
| `-- seed.sh (line 12, exec)
`-- notify.sh (line 5, exec)
========================================
scripts: 4 edges: 3 broken: 1 cycles: 0
BROKEN CHAINS (call target not found):
notify.sh:8 -> ./cleanup.sh (exec, missing)
There's also --format json for machine-readable output, --entry to trace one chain, and
deploy.sh--check, which exits 1 if anything is
broken or cyclic — made for CI pipelines and agent guardrails.
How it finds calls
Two extractors, no regex soup on the Python side:
-
Shell: quote-aware tokenizing per line (comments stripped, line
continuations joined). Detects
bash foo.sh,./foo.sh,sh -x foo.sh,source lib.sh,. lib.sh,python3 bar.py,exec ./foo.sh. -
Python: parsed with the real
astmodule. Detectssubprocess.run(...),os.system(...)/os.popen(...),os.execv(...)and friends,exec(open("other.py").read()), andrunpy.run_path("other.py").
Every target is resolved against the directory tree — relative to the
caller first, then by unique basename match elsewhere in the tree, with
absolute paths honored. Anything unresolvable is reported as broken (or
ambiguous, when several files share the name). Cycles come from a DFS over
the graph; entry points are scripts nothing calls.
Why not just use a static analyzer?
Because that's a different job. ShellCheck and Pylint answer "is this file
correct?" script-chain-walk answers "does this system of scripts hold
together?" A single-file tool will never tell you that a.sh calls b.py
calls c.sh calls a.sh. If you run coding agents that generate scripts,
walk --check on their output directory is a thirty-second audit of what
the agent actually wired up — dead scripts it keeps "calling" and accidental
recursion included.
Links
- GitHub: https://github.com/hahahahahahahahah6/script-chain-walk
- PyPI: https://pypi.org/project/script-chain-walk/
pip install script-chain-walk — stdlib only, no dependencies, MIT licensed.
If it finds a broken chain in your repo, I'd genuinely like to hear about it.
來源:DEV Community · dev.to