ASCII call tree generator for C, C++, Python, Rust, Go, Java, JavaScript, TypeScript, Ruby, Lua, PHP, Perl, C#, Kotlin, Scala, Swift and ~25 other languages β single file or entire project. Parses function definitions via universal-ctags and call edges via a small Perl backend, then renders them as a tree in the terminal. Supports cross-file call resolution, recursive directory scanning, whitelist/blacklist filtering, and exports to Mermaid, Graphviz DOT, and plain text.
src/sink/rntuple.hpp (depth=4)
RNTuples() -> void
ingest() -> void
βββ get_or_create() -> MetaWriters&
βββ bucket_key() -> std::string
β βββ year_month() -> std::string
β βββ bucket_week() -> uint8_t
βββ rotate() -> void
β βββ bucket_key() -> std::string [seen]
β βββ make_dir() -> std::string
β βββ make_writer() -> std::unique_ptr<ROOT::RNTupleWriter>
β βββ make_fields() -> void
βββ make_dir() -> std::string
βββ make_writer() -> std::unique_ptr<ROOT::RNTupleWriter> [seen]
| Dep | Notes |
|---|---|
bash |
>= 4.0 |
perl |
Standard on Linux and macOS; only JSON::PP is needed, which has been in Perl core since 5.14 |
universal-ctags |
With +json feature. Not exuberant-ctags. |
graphviz |
Optional β only needed to render .dot output (dot -Tsvg) |
Install universal-ctags:
# Debian / Ubuntu
sudo apt install universal-ctags
# Fedora
sudo dnf install ctags
# Arch
sudo pacman -S ctags
# macOS
brew install universal-ctags
# FreeBSD
pkg install universal-ctagsVerify:
ctags --version | head -1 # must say "Universal Ctags"
ctags --list-features | grep json # must list "json"git clone https://github.com/MoonFlowww/CallTree
cd CallTree
chmod +x calltree.shOr drop calltree.sh anywhere on your $PATH:
cp calltree.sh ~/.local/bin/calltreecalltree.sh PATH [PATH ...] [OPTIONS]
PATH may be a file or a directory. Multiple paths are accepted and can be freely mixed. Directories are scanned recursively for known source extensions.
# Single file
./calltree.sh src/main.cpp
# Multiple files
./calltree.sh src/main.cpp src/util.cpp lib/io.hpp
# Whole directory
./calltree.sh src/
# Mixing files and directories
./calltree.sh src/ include/ vendor/one_file.cpp
# Directory with filters
./calltree.sh src/ -I "*.cpp" -E "test_*"
# Paths that begin with a dash need the -- separator
./calltree.sh -- -weird-file.cpp| Flag | Argument | Default | Description |
|---|---|---|---|
-I |
PATTERN |
β | Include glob, basename match. Repeatable. Applied before -E. When absent, all files pass |
-E |
PATTERN |
β | Exclude glob, basename match. Repeatable. Takes precedence over -I matches |
-f |
FUNC |
auto | find function: start tree from it. Accepts a bare function name (auto-picks the first file that defines it) or a fully-qualified key filepath::::funcname to pin a specific file |
-d |
N |
4 |
Max recursion depth in the tree |
-out-T |
[FILE] |
<base>.txt |
Write plain-Text tree (no ANSI codes) |
-out-M |
[FILE] |
<base>.mmd |
Write Mermaid graph. Multi-file mode wraps each file's functions in a named subgraph |
-out-D |
[FILE] |
<base>.dot |
Write Graphviz DOT. Multi-file mode wraps each file's functions in a cluster |
-bg-d |
- | - | Define background as dark for Mermaid and Dot graphs |
-bg-w |
- | - | Define background as white for Mermaid and Dot graphs |
-c |
β | off | Colorize function names in terminal using 256-color ANSI |
-s |
β | off | See β always expand repeated subtrees (disable [seen] compression) |
-t |
β | off | No terminal output; only -out-* files are written. Does not affect the -out-* flags themselves |
-p |
β | off | Show performance footer: mapping/print/file timings plus line counters |
-v |
β | β | Print version and exit |
-w |
β | β | Print absolute path to this script (where) and exit |
-h |
β | β | Print help (with full language list) and exit |
-- |
β | β | End of options; everything after is treated as a path |
File arguments for -out-* flags are optional. When provided, the value must end with the matching extension (.txt, .mmd, .dot) so the parser doesn't mistake a positional input path for an output filename. When omitted, the output path is derived automatically from the input:
# Single file
./calltree.sh src/foo.cpp -out-M # β src/foo.mmd
./calltree.sh src/foo.cpp -out-M graph.mmd # β graph.mmd (explicit)
# Directory
./calltree.sh src/ -out-M # β src/calltree.mmd
./calltree.sh src/ -out-D # β src/calltree.dot
# Multiple positional files
./calltree.sh a.cpp b.cpp -out-D # β ./calltree.dotAnything universal-ctags can parse is a candidate; the backend has an explicit kind allow-list for the languages below and a permissive fallback for everything else. The complete list is also printed by calltree.sh -h.
| Language | Extensions | Return types |
|---|---|---|
| C / C++ | .c .h .cpp .hpp .cc .cxx .hxx |
yes |
| C# | .cs |
yes |
| Python | .py |
- (no annotations) |
| Go | .go |
yes |
| Rust | .rs |
yes (parsed from signature -> T) |
| Java | .java |
yes |
| JavaScript / TypeScript | .js .jsx .ts .tsx |
partial (TS yes) |
| Ruby | .rb |
- |
| Lua | .lua |
- |
| PHP | .php |
yes |
| Perl | .pl .pm |
- |
| Kotlin | .kt |
yes |
| Scala | .scala |
yes |
| Swift | .swift |
yes |
| Haskell, OCaml, F# | .hs .ml .fs |
best effort |
For languages without type annotations (Python, Ruby, Lua, Perl), the return type column shows -.
./calltree.sh src/sink/rntuple.hpp./calltree.sh src/sink/rntuple.hpp -d 2ingest() -> void
βββ get_or_create() -> MetaWriters&
βββ bucket_key() -> std::string
βββ rotate() -> void
βββ make_dir() -> std::string
βββ make_writer() -> std::unique_ptr<ROOT::RNTupleWriter>
./calltree.sh src/sink/rntuple.hpp -f rotaterotate() -> void
βββ bucket_key() -> std::string
β βββ year_month() -> std::string
β βββ bucket_week() -> uint8_t
βββ make_dir() -> std::string
βββ make_writer() -> std::unique_ptr<ROOT::RNTupleWriter>
βββ make_fields() -> void
./calltree.sh src/sink/rntuple.hpp -cEach function name is assigned a unique 256-color ANSI color.
Colors are derived from the sorted function list so they stay stable across runs.
The usable palette is clamped to indices 40β210 β near-black and near-white tones are excluded.
color index = 40 + round(170 * i / (N - 1))
Colors also apply in the summary table's calls column.
./calltree.sh src/sink/rntuple.hpp -t -out-T -out-M -out-D-t suppresses the terminal tree and summary table, but -out-* files are still written. Useful for scripts, CI pipelines, or when only the exports are needed.
-> plain text : src/sink/rntuple.txt
-> Mermaid : src/sink/rntuple.mmd
-> DOT : src/sink/rntuple.dot (render: dot -Tsvg -o graph.svg src/sink/rntuple.dot)
./calltree.sh src/sink/rntuple.hpp -pShows backend timing, render timing, and line counters for source and terminal output:
mapping 207 ms
print 43 ms
ββββββββββββββββββββββ
total 250 ms
read 127 lines (src)
write 70 lines (cli)
When combined with any -out-* flag, a file row is added to both the timings and the line counters:
mapping 207 ms
print 43 ms
file 112 ms
ββββββββββββββββββββββ
total 362 ms
read 127 lines (src)
write 70 lines (cli)
206 lines (file)
When combined with -t, the cli counter shows 0 lines (cli, suppressed by -t).
| Row | Meaning |
|---|---|
mapping |
ctags parse + perl call-edge analysis + bash array load |
print |
Tree traversal and summary table render for the terminal |
file |
All -out-* file writes combined (only shown when at least one is requested) |
total |
Wall time of the entire run |
read |
Raw source lines consumed |
write / cli |
Lines written to the terminal |
write / file |
Lines written across all -out-* files (only when requested) |
./calltree.sh src/sink/rntuple.hpp -out-MWrites src/sink/rntuple.mmd, fenced in ```mermaid ``` blocks so it renders directly when pasted into a GitHub README, GitLab wiki, or Notion page.
graph TD
RNTuples["void RNTuples()"]
ingest["void ingest()"]
bucket_week["uint8_t bucket_week()"]
year_month["std::string year_month()"]
bucket_key["std::string bucket_key()"]
make_dir["std::string make_dir()"]
get_or_create["MetaWriters& get_or_create()"]
rotate["void rotate()"]
make_fields["void make_fields()"]
make_writer["std::unique_ptr<ROOT::RNTupleWriter> make_writer()"]
ingest --> get_or_create
bucket_key --> year_month
bucket_key --> bucket_week
get_or_create --> bucket_key
get_or_create --> rotate
get_or_create --> make_dir
get_or_create --> make_writer
rotate --> bucket_key
rotate --> make_dir
rotate --> make_writer
make_writer --> make_fields
./calltree.sh src/sink/rntuple.hpp -out-DRender the .dot file to SVG or PNG:
dot -Tsvg -o graph.svg src/sink/rntuple.dot
dot -Tpng -o graph.png src/sink/rntuple.dotNode labels include the return type and call frequency.
./calltree.sh src/sink/rntuple.hpp -out-TIdentical layout to the terminal output, with no ANSI codes β safe to grep, diff, or commit.
src/sink/rntuple.hpp (depth=4)
rotate() -> void
βββ bucket_key() -> std::string
β βββ year_month() -> std::string
β βββ bucket_week() -> uint8_t
βββ make_dir() -> std::string
βββ make_writer() -> std::unique_ptr<ROOT::RNTupleWriter>
βββ make_fields() -> void
function called calls return type
ββββββββββββββββββββββββββββ ββββββ ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββ
bucket_week 1 ---- uint8_t
year_month 1 ---- std::string
bucket_key 2 year_month bucket_week std::string
make_dir 2 ---- std::string
rotate 1 bucket_key make_dir make_writer void
make_fields 1 ---- void
make_writer 2 make_fields std::unique_ptr<ROOT::RNTupleWriter>./calltree.sh src/sink/rntuple.hpp -c -s -p -out-T -out-M -out-DMulti-file mode is activated whenever more than one file is provided, either via multiple positional paths or via directory scanning. All flags continue to work identically; the only visual changes are the [basename] annotations in the tree and an extra file column in the summary table.
./calltree.sh src/core.cpp src/net.cpp -d 3 2 files (depth=3)
dispatch() [core.cpp] -> void
βββ make_key() [core.cpp] -> std::string
β βββ format() [net.cpp] -> int
βββ send() [net.cpp] -> void
βββ encode() [net.cpp] -> std::string
β βββ compress() [net.cpp] -> std::string
βββ flush() [net.cpp] -> void
βββ write_buf() [net.cpp] -> void
function file called calls return type
ββββββββββββββββββββββββββββ ββββββββββββββββββββββ ββββββ ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββ
make_key core.cpp 1 format std::string
dispatch core.cpp 0 make_key send void
send net.cpp 1 encode flush void
...
Cross-file calls are shown inline in the tree. The [basename] tag after each function name shows which file it lives in β it only appears in multi-file mode.
./calltree.sh src/ -d 4Scans src/ recursively for all supported source files (sorted, deduplicated), analyzes them as a single unit, and prints the unified call tree.
./calltree.sh src/ include/ tests/ -d 3Paths are merged into a single unit of analysis. Files found in any of the listed directories are combined before call resolution.
# Only implementation files, not headers
./calltree.sh src/ -I "*.cpp"
# Exclude generated and test files
./calltree.sh src/ -E "*.pb.cc" -E "test_*" -E "*_mock.*"
# Combined β only implementation, no tests
./calltree.sh src/ -I "*.cpp" -E "test_*"
# Mixed-language project: only Rust and Go
./calltree.sh src/ -I "*.rs" -I "*.go"-I and -E both match against the basename of each file using standard shell glob syntax. Processing order: -I is applied first (if any are specified); then -E is applied to the surviving set. Both flags are repeatable.
# Bare function name β auto-picks the first file that defines it
./calltree.sh src/ -f dispatch
# Fully-qualified key β pin to a specific file when the name is ambiguous
./calltree.sh src/ -f "src/core.cpp::::dispatch"The FILE::::FUNC key syntax uses four colons as a separator, safe because :::: cannot appear in typical source-code identifiers or paths.
./calltree.sh src/ -out-M
# β src/calltree.mmdEach file's functions are grouped in a named subgraph. Cross-file edges connect nodes across subgraphs automatically. Node IDs use SAFE_BASENAME_funcname to stay unique even when two files define a function with the same name.
flowchart TD
subgraph figure_hpp["figure.hpp"]
figure_hpp_Figure["void Figure()"]
figure_hpp_set_title["void set_title()"]
figure_hpp_render["void render()"]
figure_hpp_compute_plot_area["void compute_plot_area()"]
figure_hpp_render_grid["void render_grid()"]
figure_hpp_render_axes["void render_axes()"]
figure_hpp_render_data["void render_data()"]
end
subgraph text_hpp["text.hpp"]
text_hpp_get_glyph["const Glyph & get_glyph()"]
text_hpp_draw_text["void draw_text()"]
text_hpp_text_width["i32 text_width()"]
end
subgraph tick_engine_hpp["tick_engine.hpp"]
tick_engine_hpp_compute["std::vector<Tick> compute()"]
end
figure_hpp_render --> figure_hpp_compute_plot_area
figure_hpp_render --> figure_hpp_render_grid
figure_hpp_render --> figure_hpp_render_axes
figure_hpp_render --> figure_hpp_render_data
figure_hpp_render_grid --> tick_engine_hpp_compute
figure_hpp_render_axes --> text_hpp_text_width
figure_hpp_render_axes --> text_hpp_draw_text
text_hpp_draw_text --> text_hpp_get_glyph
./calltree.sh src/ -out-D
dot -Tsvg -o graph.svg src/calltree.dotEach file becomes a subgraph cluster_N with its own label and a light grey background. Cross-cluster edges are drawn between the full-path node IDs.
./calltree.sh src/ -t -out-T -out-M -out-D -pUseful in CI scripts: the -t flag suppresses the terminal tree, the three -out-* flags produce artifacts on disk, and -p still prints the performance footer so timings can be captured in logs.
The table is always printed below the tree. In multi-file mode it gains a file column.
Single-file:
function called calls return type
ββββββββββββββββββββββββββββ ββββββ ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββ
bucket_week 1 ---- uint8_t
year_month 1 ---- std::string
bucket_key 2 year_month bucket_week std::string
make_dir 2 ---- std::string
rotate 1 bucket_key make_dir make_writer void
make_fields 1 ---- void
make_writer 2 make_fields std::unique_ptr<ROOT::RNTupleWriter>
Multi-file:
function file called calls return type
ββββββββββββββββββββββββββββ ββββββββββββββββββββββ ββββββ ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββ
make_key core.cpp 1 format std::string
dispatch core.cpp 0 make_key send void
send net.cpp 1 encode flush void
...
| Column | Description |
|---|---|
function |
Function name as defined in the file |
file |
Basename of the file where the function is defined (multi-file mode only) |
called |
Total number of times this function is invoked across all callers in the analyzed set |
calls |
Space-separated list of functions this function calls (display names only, stripped of file path) |
return type |
Extracted from ctags typeref or parsed from the signature; - for untyped languages |
universal-ctags ββ(JSON tags)βββΆ perl backend ββ(CALLS/TYPES/FREQ)βββΆ bash renderer
- ctags parses every input file and emits one JSON line per tag (function/method/sub) with fields:
name,path,language,line,end,kind,typeref,signature. - perl consumes the stream, filters by a per-language kind allow-list, drops anonymous ctags-generated names (lambdas, anonymous structs/unions), builds a global
funcname β [files]registry, re-opens each source file, extracts the body rangeline..endfor each function, and scans it for callees matching known names (excluding method calls via a(?<![>.])lookbehind). - bash loads the emitted
CALLS/TYPES/FREQtables into associative arrays and renders the tree, summary table, and optional-out-*exports.
In single-file mode, function keys are bare names. In multi-file mode, the internal key is filepath::::funcname throughout β in the tree, the table, and the export files. The four-colon separator is chosen because it cannot appear in most language identifiers. Display always strips the path back to a bare function name; the file is shown separately as an annotation or table column.
ctags classifies each tag with a language-specific kind. The backend has an explicit allow-list per language:
| Language | Accepted kinds |
|---|---|
| C, C++ | function |
| C# | method |
| Python | function, member (class methods) |
| Go | func |
| Rust | function, method |
| Java, Kotlin | method |
| JavaScript, TypeScript | function, method, getter, setter, generator |
| Ruby | method, singletonMethod |
| Lua, PHP | function |
| Perl | subroutine |
| Scala, Swift | method, function |
For any other language, the default fallback accepts function, method, func, fn, subroutine.
Anonymous ctags-generated names (such as __anon0566b84d0102 for lambdas or anonymous structs) are filtered out automatically.
Three strategies, tried in order:
- ctags
typereffield β populated for C, C++, Go, Java, TypeScript, Kotlin, PHP, and others. Contains the raw type, prefixed withtypename:which is stripped. - Signature parsing β Rust and some other languages embed the return type inside the signature as
(args) -> Type. The backend extracts the-> ...tail whentyperefis missing. - Fallback β
voidfor C/C++ when nothing else matches;-for languages without static return types (Python, Ruby, Lua, Perl, untyped JS).
For each function, the backend reads lines line..end from the source file, strips comments and string literals (best-effort, not language-perfect), then scans for \bname\s*\( where name is in the global function registry. Identifiers preceded by . or -> (method calls) are excluded via a lookbehind β this works uniformly for C/C++/Rust/Go/Java/Python/JS.
The Perl pass runs once on all input files. Pass 1 builds a global funcname β [files that define it] registry. Pass 2 scans each function body and, for every callee found in the global registry, applies this rule:
- If the callee is defined in the same file as the caller, use that definition.
- Otherwise, use the first file in definition-order that defines the callee.
This matches compiler lookup semantics for non-overloaded free functions and ensures that same-file helper calls are never misattributed to a homonymous function in another file.
find is invoked with -print0 and the result piped through sort -z, so filenames with spaces and special characters are handled correctly. The scanner recognises these extensions out of the box:
.c .h .cpp .hpp .cc .cxx .hxx
.cs .py .rs .go .java
.js .jsx .ts .tsx
.rb .lua .php .pl .pm
.scala .kt .swift .hs .ml .fs
-I and -E patterns are applied in bash using case/glob matching against basenames only.
The tree emitter threads a colon-delimited VISITED string down the call stack. If a node appears in its own ancestor path, it is printed with [cycle] and recursion stops. Nodes reached via different paths are drawn in full β both call sites are real and belong in the documentation.
- Call detection is a name-in-body scan, not a true semantic analysis. Overloaded names in different files collapse to the first definition.
- Method calls (
obj.foo(),ptr->foo(),self.foo()) are intentionally excluded to keep the tree readable for free-function-heavy code. OO-heavy codebases will see incomplete graphs. - Template and generic specialisations (
process<T>vsprocess<U>,process[Int], etc.) map to the same base name. - Macro-defined pseudo-functions are not detected, since ctags does not preprocess.
- Cross-file resolution picks the first matching definition when a name is defined in multiple files. There is no namespace awareness or overload resolution.
- File extensions must match content. Renaming
foo.cpptofoo.pycauses ctags to parse C++ with the Python parser and produce zero tags. - File paths containing the literal string
::::are not supported β this sequence is reserved as the internal separator. - Python lambdas, nested inner functions, and heavily decorated definitions may be classified differently than expected. Top-level
defstatements and class methods always work.