NOTE

Unix philosophy

#software-engineering (40)#tooling (2)

Unix got famous for small programs and pipes. The design rules that stuck are narrower than “everything should be a shell script,” but they still explain why some CLIs feel inevitable and some platforms fight you at every step.

Write programs that do one thing and do it well. Write programs to work together. Write programs that handle text streams, because that is a universal interface.

Doug McIlroy summarized it that way on Bell Labs’ Unix history site after pipes made composition normal at the keyboard. Ken Thompson had put the mechanism in place; the philosophy followed once who | grep was easier than the pre-pipe redirection syntax everyone hated.

“Do one thing” is not “one public method.” It is a boundary on what failure mode you are willing to own. grep finds lines. sort sorts. wc counts. None of them try to be your entire analytics stack.

Modern CLIs that inherit the habit feel the same. jq only transforms JSON. curl only moves bytes over HTTP. kubectl get and kubectl apply split read paths from write paths on purpose. When a single binary grows fifteen subcommands, each with its own flags and side effects, you are often looking at a toolkit pretending to be one tool. That can be fine (git is a civilization), but you pay in memorization and in test surface.

Microservices rebroadcast the same argument with worse plumbing. A service that owns signup, billing, and email dispatch has three reasons to change and three outage blast radii. The Unix move is not “more repos.” It is narrower contracts so you can replace the sorting step without rewiring the grep step. What developers miss about the Single Responsibility Principle is the same idea at class scope: one reason to change, not one function per file.

McIlroy’s “universal interface” was line-oriented text on a tty. That choice made programs inspectable. You could pipe output through less, stash it in a file, or email it to a colleague without a schema registry.

JSON and protobuf are still streams. They are just streams you cannot read without tooling. That is a trade, not a betrayal. kubectl get pods -o json | jq '.items[].metadata.name' is the modern pipe. Logs shipped as JSON lines to an aggregator are the modern syslog. The discipline that matters is stable, documented shapes on stdout (or the equivalent) and errors on stderr, not whether the bytes are ASCII art.

Where teams drift is hiding the stream behind opaque SDKs. If the only way to list resources is a GUI or a vendor console, you lose the compose-at-the-keyboard loop Kernighan described on that same Bell Labs page. CLIs like gh, aws, and terraform stayed relevant because they print something scriptable even when the control plane is a giant hosted service.

Composition needs predictable edges. Pipes work because exit codes and stream semantics are boring. grep exits 1 when nothing matches; shell scripts branch on that without parsing English error text.

HTTP APIs are pipes with worse ergonomics and better reach. A service that does one job well exposes a small surface (clear resources, idempotent writes where it matters, errors that machines can classify). A platform that only works through a chain of bespoke wizards is the pre-pipe shell again: powerful, but you cannot glue it to the next tool without a integration project.

For internal platforms, treat composability as a product requirement. Document how to call your service from a shell one-liner. Publish OpenAPI or protobuf so the next team can wrap you without a meeting. Prefer events or webhooks with stable payloads over “log into the dashboard and click around.”

Fat CLIs that mix auth, config, and domain operations in one process are hard to test in isolation and hard to wrap safely in CI. Services that return HTML error pages to API clients break the stream contract. Pipelines that require six proprietary binaries in a row are composition theater: the syntax is Unix, the economics are a single vendor.

You do not need nostalgia to apply the philosophy. You need sharp boundaries, inspectable outputs, and permission for the next program in the chain to exist without your blessing. That is as useful for a 2020s SaaS control plane as it was for a 1970s research machine. Gall’s law and YAGNI are the product-schedule versions of the same discipline. Ship a small working core, then compose.