Install · First explanation · Search · Tutorial · Design · Caveats
The man page already knows.
expfinds the paragraph. Read status and caveats before relying on it.
exp explains a shell command line by matching every token to the exact
paragraph that documents it in the man pages installed on your machine.
Deterministic, offline, no model, no bundled database: the answer
describes the version of each tool you actually have.
Grouped shorts, an option argument, and a wrapper chain. Follow the tutorial or play the cast.
Type a command line, get it back with every span underlined and a
numbered footnote for each: the page, the section, and the paragraph
that documents that token. Grouped shorts split per letter, an option
that takes an argument shares its footnote with the argument, wrappers
like sudo and xargs hand off to the command they run, and shell
grammar (pipes, redirects, $( ), globs) is explained natively. Pages
are resolved the way man -w does it and parsed from their groff
source, so the answer describes the versions installed here, not a
snapshot from somewhere else.
cargo install --git https://github.com/0typos/exp --lockedOr from a checkout with cargo install --path .. A Rust toolchain is the
only requirement; SQLite is bundled. Pages compressed with xz, bzip2, or
zstd are decompressed through the matching command line tool, gzip
in-process.
exp 'tar -xzvf a.tgz'tar -xzvf a.tgz
1 2 3 5 5
4
┌───┬───────┬──────────────────────────────────────┬────────────────────────────────────────┐
│ # │ token │ source │ explanation │
├───┼───────┼──────────────────────────────────────┼────────────────────────────────────────┤
│ 1 │ tar │ tar(1) §NAME │ tar - an archiving utility │
├───┼───────┼──────────────────────────────────────┼────────────────────────────────────────┤
│ 2 │ -x │ tar(1) §DESCRIPTION / Operation mode │ -x, --extract, --get │
│ │ │ │ Extract files from an archive. … │
├───┼───────┼──────────────────────────────────────┼────────────────────────────────────────┤
│ 5 │ f │ tar(1) §OPTIONS / Device selection │ -f, --file=ARCHIVE │
│ │ a.tgz │ and switching │ Use archive file or device ARCHIVE. … │
└───┴───────┴──────────────────────────────────────┴────────────────────────────────────────┘
Every span is underlined and coloured on a terminal; the ruler puts its
footnote number under the span. Spans that point at the same paragraph
share a row (f and the archive it names).
Quote the whole line. Unquoted words also work, but exp's own flags
must then come first or after --:
exp 'git log --oneline | head -n 5 > out.txt'
exp --format text -- find . -name '*.log' -exec rm {} \;
fc -ln -1 | exp # the previous commandExplain the last command from your shell
fish:
function explain --description 'explain the last command'
exp (history --max 1)
end
funcsave explainbash or zsh:
alias explain='fc -ln -1 | exp'--format |
for |
|---|---|
pretty (default) |
the terminal: highlighted line, ruler, box table wrapped to the width |
text |
pagers and plain logs: ruler plus footnotes, blank line between them |
markdown |
issues and notes: the command in a code block and a table |
json (or --json) |
tooling: byte offsets, kinds, confidence, full provenance; the snapshot format |
exp --format markdown 'rsync -avz --delete src/ dst/'
exp --json 'ps aux' | jq '.footnotes[].source | select(.type=="page") | .tag'Explaining fills a SQLite cache with every paragraph it extracted, FTS5 indexed. Search it by tool, by category, or across everything:
exp search --tool rsync checksum
exp search --category archive exclude
exp search --category network --regex '^--proxy'
exp search --tool tar --fuzzy 'exclde vcs'
exp search --all --zone any 'follow symlinks'
exp index --all --report # index the whole manpath; list pages that extracted badly$ exp search --tool rsync checksum
rsync(1) §OPTIONS -c, --checksum — --[checksum], -c
rsync(1) §OPTIONS --checksum-seed — --[checksum]-seed=NUM
exp categories lists the shipped groups (archive, files, text,
network, processes, shell, git, containers, packages). A member ending
in * covers subcommand pages (git-*). Add your own in
~/.config/exp/categories.toml.
Real command lines are not all getopt. These are data in
knowledge/commands.toml, overridable per
command in ~/.config/exp/commands.toml:
| case | example | what happens |
|---|---|---|
| wrappers | sudo xargs -0 rm -f, env FOO=1 timeout 5s nice -n 10 ls |
the wrapped command gets its own page, recursively |
find -exec |
find . -name '*.log' -exec rm {} \; |
everything up to \; or + is a command |
| subcommands | git log, docker run, systemctl start |
routed to git-log(1), docker-run(1), or the COMMANDS section |
| legacy bundles | tar xzvf a.tgz, ps aux |
expanded per letter against the page's bare tags |
| key=value | dd bs=4M, ssh -o StrictHostKeyChecking=no, git -c core.fileMode=false |
bs= matched directly; keys inside arguments resolved from ssh_config(5) or git-config(1) |
| modes | chmod u+x,go-w, chmod 0644 |
operand patterns pick the paragraph that documents the syntax |
| builtins | cd -P, export FOO=1, source x |
from bash(1)'s SHELL BUILTIN COMMANDS |
| ambiguous arity | a page that omits an option's argument | fish completions fill it in when installed |
tree-sitter-bashparses the line. Byte ranges from the tree are the spans in the output.- Each command's page is resolved like
man -w:$MANPATHor$PATH-derived roots,$MANSECTorder,.soredirects followed. - The groff source is parsed directly. man(7):
.SH/.SSzones,.TP/.IP/.HPand docbook or asciidoctor tagged paragraphs. mdoc(7):.Sh/.Ssand.Bl -taglists with.It Flitems. Tags become option specs: spellings, argument policy, argument name. - Pages are cached in SQLite (
~/.cache/exp/cache.sqlite) with an FTS5 index, keyed by file, invalidated on mtime, hash, or extractor version change. - The matcher walks the argument words and assigns each span a paragraph, or the SYNOPSIS at low confidence when nothing matches.
DESIGN.md records each decision and why, including the measured extraction health across a 21,000-page manpath.
| guide | what it answers |
|---|---|
| Tutorial | What does each feature look like, with a recording per section? |
| Design | Why is extraction, matching, and caching shaped this way? |
| Demos | How are the casts and GIFs regenerated? |
| Brand | Where are the mark, hero, and palette? |
| Knowledge | Which commands are special, and how do I add one? |
cargo test # fast tier: unit, snapshots, golden corpus, properties
INSTA_UPDATE=always cargo test # accept reviewed snapshot changes
cargo mutants -f src/matcher.rs -f src/optspec.rs # full tier: do the matcher tests bite? (weekly and on-demand workflow, not part of push CI)
docs/demos/record # re-record the tutorial casts and GIFsTests never read the host manpath. tests/fixtures/man/ holds vendored
pages (GNU man(7) pages from coreutils, tar, rsync, grep, findutils, git,
procps, util-linux; mdoc(7) pages from OpenSSH, sudo, file). Each keeps
its upstream licence. The golden corpus in tests/golden/corpus.txt
lists command line → (span, page, zone, option) triples; a heuristic
that fails it becomes a rule or a TOML entry, never an inline hack.
This is a 0.1.0. It does what the sections above show, and the following is where it stops.
Limits of the approach
- Options that a page documents only in prose, with no tagged paragraph,
cannot be matched. Those tokens point at the SYNOPSIS and are marked
low confidence. On this development host that is about 150 of the
section 1 and 8 pages (attr, cups, devlink, clevis);
exp index --all --reportshows yours. - Pages with no NAME section or no structure at all (some Sphinx and pandoc output, stap stubs) explain the command word poorly. There is no rendered-text fallback yet; measurements in DESIGN.md say the tail is small, but it exists.
- Shell builtins are explained from their paragraph in bash(1) as a
whole; their individual options are not matched. Commands with a real
page (coreutils
echo,test) use that page, as explainshell does. - Unknown options are assumed to take no argument. That is the safer failure, and it means a wrongly unmatched option can shift the meaning of the word after it to low confidence.
- The man page you have is the truth. If your page is old, wrong, or from a different implementation than the binary on your PATH, the explanation follows the page.
Parity with explainshell.com
The golden corpus was checked by hand against the fixture page text,
not against explainshell.com, because the site blocks non-browser
clients from the build host. Conventions follow explainshell where
known (per-letter short groups, operands to the SYNOPSIS, wrapped
commands from their own pages). A side-by-side pass is still owed and
tracked in tests/golden/corpus.txt.
Platform coverage
- Linux is exercised daily. macOS runs in CI, but Apple's mdoc(7) page set has not been reviewed by hand beyond the vendored OpenSSH, sudo, and file pages; expect rough edges on BSD-specific macros until it has.
- No Windows.
Operational notes
- The cache fills lazily. Named tools are indexed on demand, but
--alland category members written asgit-*only see what is cached; runexp index --allonce (about 20 seconds for 21,000 pages) and the tool says so when the cache is small. - The recorded demos run against the vendored fixture pages so they are reproducible, which is why the archive-category search in the demo shows only tar.
- The crate is published as
exp-clibecauseexpis taken on crates.io; the binary and the library are both stillexp. - No release binaries yet; install from source.
Web UI, daemon mode, fetching pages over the network, LLM integration, Windows.
MIT



