tsort: The POSIX Topological Sort That's Been Sitting in /usr/bin Since 1979

2026-07-22

You have a bag of things and a bunch of "A must come before B" edges. You want a total ordering. Every developer I've watched attempt this in the last decade reaches for Python, NetworkX, or writes a broken DFS on a napkin. Meanwhile tsort — shipped in Version 7 Unix in 1979, mandated by POSIX, sitting in coreutils on every Linux box you own — does exactly this in one pipeline stage.

Input is pairs of whitespace-separated tokens on stdin: A B means "A comes before B." Output is a topological order, one node per line. That's the whole interface.

$ tsort <<'EOF'
fetch    build
build    test
test     package
build    lint
lint     package
EOF
fetch
build
lint
test
package

The origin story tells you the real trick. tsort exists because Unix's link editor was single-pass: if foo.o referenced a symbol in bar.o, bar.o had to appear later on the ld command line. So Ian Darwin wrote lorder(1) to emit symbol-dependency pairs, and its output pipes straight into tsort:

$ ar cr libutil.a *.o
$ lorder *.o | tsort | xargs ld -r -o util.o

Forty-six years later, the same shape solves modern problems:

Debug a broken Makefile. Extract the dep graph and check for cycles:

$ make -Bnd 2>/dev/null | awk '/^Considering target/{t=$NF}
                                /^ *Considering.*prerequisite/{print $NF, t}' \
  | tsort >/dev/null
tsort: -: input contains a loop:
tsort: config.h
tsort: generated.c
tsort: config.h

When a cycle exists, tsort writes the offending nodes to stderr and still emits a best-effort ordering to stdout. Exit status is nonzero. That's your cycle detector in one line.

Order database migrations by foreign key dependencies:

$ psql -Atc "SELECT confrelid::regclass, conrelid::regclass
             FROM pg_constraint WHERE contype='f'" \
  | tr '|' ' ' | tsort
users
accounts
orders
line_items

Compute a valid systemctl start order from unit After= directives, or sort a list of Terraform modules by depends_on, or produce a shutdown sequence for a service mesh. Any DAG of "X before Y" facts fits.

A self-loop trick. A node with no dependencies has no lines in the graph, so it disappears from output. Force isolated nodes to show up by feeding tsort node node — a self-edge that tsort treats as a benign standalone:

$ printf 'a b\nc c\n' | tsort
a
c
b

That idiom means you can pipe every node through as N N, plus real edges as A B, and get a complete ordering with isolated nodes preserved.

Why not just write it? Because your DFS won't detect cycles correctly on the first try, won't handle disconnected components, won't be composable in a pipeline, won't be there on the sad little Alpine container you SSH'd into at 2 a.m., and won't have been battle-tested by every Unix vendor for four decades. tsort is 400 lines of C that solves the problem exactly once, correctly, forever.

It pairs beautifully with comm, join, and uniq — the whole "relational algebra on line-oriented text" toolkit that predates SQL and still outlives most of the tools built on top of it.

Key Takeaway: Any time you have "X must happen before Y" pairs — build steps, migrations, service startup, linker order — pipe them into tsort and stop reinventing Kahn's algorithm.

All newsletters