The Quiet Multiplication of Command-Line Flags

Doug McIlroy's UNIX philosophy is often boiled down to "do one thing and do it well." The fuller version of that dictum is: "Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new 'features.'" His canonical example was that UNIX compilers produced no listings—printing was better handled by a separate program.

A glance at a modern ls manpage suggests that ideal didn't survive contact with decades of real-world use. On macOS, the single-character flags to ls include every lowercase letter except {jvyz}, 14 uppercase letters, plus @ and 1—38 one-letter options before counting long options. On Ubuntu 17, the coreutils ls manpage lists 58 options, including --help and --version.

But is ls an outlier, or the norm?

command1979199620152017
ls11425858
rm371112
mkdir0467
mv091314
cp0183032
cat1121212
pwd0244
chmod0699
echo1455
man5163940
which011
sudo02325
tar1253134139
touch191111
clear000
find14578282
ln0111516
ps4228585
ping121229
kill1333
ifconfig162525
chown061515
grep11224545
tail171213
df0101718
top61214

The table above counts command-line options for common commands across v7 Unix (1979), Slackware 3.1 (1996), Ubuntu 12 (2015), and Ubuntu 17 (2017). The trend is unambiguous: every entry either grew darker—more options—or stayed the same; none shrank. McIlroy has long lamented this direction, pointing to binaries that grew from 8K to a megabyte and manpages that expanded from a single page to "a small volume with a thousand options." His prescription was to ask why a new option was needed at all, on the theory that an option usually papers over a deficiency in the basic design.

There are, however, structural reasons the option count climbed. One is another McIlroy dictum—that programs should handle text streams as a universal interface. Text intermingles content with formatting in a way structured objects don't. When the final command in a pipeline needs specific formatting, it's far easier to add a flag (ls's formatting switches, tail -z for null delimiters) than to build a separate formatting tool or expect users to hand-roll parsing with cut, awk, or sed—each of which has inconsistent behaviors of its own. That pushes complexity from the tool to the user, and tools accrue convenience flags instead.

Convenience options go back to v7 Unix itself: even then, ls had a reverse-sort flag that could have been done by piping to a reverse tool had one existed. Since then, programs that began with no options at all gained them. mv now has three backup-related options plus flags to never overwrite or only overwrite newer files. mkdir added permission-setting and parent-making convenience flags. tail, originally accepting only -number, now supports -f (follow), -s (sleep interval), and --retry, among others.

Whether this growth warrants McIlroy's claim that "we're not better off" is debatable. Unlike a GUI, additional command-line flags don't clutter the visible interface; the cost falls on manpage length and maintainer burden. For widely used tools, that maintenance cost has an extreme ratio of benefit to burden—far more users benefit than maintainers pay. And in practice many users search for answers rather than reading manpages anyway.

The deeper problem isn't option count; it's that a system built on "do one thing well" remains consistent only when a small group shares a cultural understanding. Once anyone can add a tool or a flag, "simplicity" becomes subjective, and inconsistency blooms. The result is that a common shell plus standard *nix tools, taken as a whole, is a more incoherent standard library than PHP or JavaScript—less consistent even within a single distro, and far more so when switching across Linux, BSDs, or commercial Unixes.

McIlroy's implication is that more care, more thought, would have kept things small and coherent. But that philosophy only scales when designers can sit in the same room. The UNIX model's weak separation of interface and implementation—fine for a 1970s terminal, a known obstacle for modern terminal builders—is a legacy of that same assumption. The approach guaranteed today's mess rather than merely failing to prevent it.

That isn't a claim that the original designers could have done better with 1970s knowledge. What's questionable are modern assertions that simplicity was abandoned and we'd be fine if we just returned to those practices—that five decades of collective experience have little to teach. A design philosophy that couldn't scale past a single room seems an odd thing to insist was lost, not merely outgrown.

Appendix: Memory

McIlroy's complaint about binary size deserves context. In 2017, a $300 Chromebook shipped with 16GB of RAM. In 1979, a standard Apple II cost $1,298 (about $4,612 in 2020 dollars) and had 4KB of memory. A modern cheap machine costs a fifteenth as much and has four million times the RAM. A thousand-fold growth in binary size is a different problem when you have that headroom.

That isn't an argument against slimmer software—it can be a fine aesthetic and a fun optimization—but the bottleneck for command-line tools is rarely memory. Optimizing a megabyte tool's footprint is a hobby, not a prescription.

Methodology for the table

The frequency rankings come from public shell history files on GitHub and are not necessarily representative. The table excluded shell builtins like cd and "complex" commands such as curl, git, gcc (with over 1,000 options), and wget. What counts as simple is admittedly arbitrary.

Counting rules: repeated options count once (so git blame -C -C -C is one option). Sub-options count once (ls --format with its seven WORD values is a single option). No-op compatibility flags like ls -g count. Short/long pairs (-A/--almost-all) count as one. Documented-but-nonexistent options were excluded—the v7 mv manpage notes it should have -f, but since it doesn't exist, it isn't counted.