Vivian Voss

The Forty-Four Switches on ls

lean software freebsd unix posix

I typed man ls this morning and did not reach the bottom. The page on this machine runs to 506 lines and 3,142 words, which is longer than the piece you are reading, and it describes a program whose job is to tell you what is in a directory. Somewhere past line 400 it explains the environment variable that controls the colour of a socket.

Then I ran ls with nothing after it, and it did the job in the time it took my finger to leave the key. That version of the program, the one with no switches at all, is the one nearly everybody uses nearly all the time. The other 43 switches are for the rest of us, on the days when we remember they exist.

The job it was given

The program is from 1971, older than C, and its job has not changed: list what is here. Doug McIlroy wrote down the rule that went with it seven years later, in the Bell Labs journal, and the first clause is the one people quote: make each program do one thing well. The second clause is the one they forget, which says that a program should expect its output to become the input of another, and that this is how you get the complicated things done without building a complicated program.

The standard that grew out of that world is more generous than its reputation. POSIX, in the 2024 edition, lists 26 switches for ls, seven for cp, two for mv, one for cat. Twenty-six is not a small number, and every one of them was argued over by people who had to make Unix systems from different vendors agree, so the count is already a treaty, negotiated line by line.

Here the concession comes before the arithmetic, because the switches were not an accident and were not laziness either. Each one arrived because somebody needed it. -t sorts by time, and without it you would be piping the listing through sort on a field that ls does not print by default. -h shows a size as 234M where the plain listing shows 245366784, which is the difference between reading a number and counting its digits. -G on this system turns the colours on, because terminals could show them and somebody wanted to see which entries were directories without squinting at a trailing slash.

Each of those was a good patch. Each was small and reviewed, and each saved somebody an afternoon. That is precisely how a program acquires 44 switches, one afternoon at a time, over fifty years, and nobody who added one did anything wrong.

The count

I took ten programs that anybody who has used a shell has typed this week and counted the documented switches on three implementations of each. The standard's figure comes from the POSIX specification. The FreeBSD figure is the base system on this machine, counted from the SYNOPSIS of its own manual page. The GNU figure is coreutils 9.9 installed from the ports tree beside it, counted from what --help prints. Same method both sides: one documented switch is one entry.

Documented switches, ten programs, three implementations POSIX.1-2024 FreeBSD 15.1 base GNU coreutils 9.9 ls 26 44 60 cat 1 8 12 cp 7 14 36 mv 2 5 18 rm 6 9 12 grep 12 41 49 sort 13 24 31 wc 4 6 9 head 2 4 7 tail 4 8 13 Totals across all ten: POSIX 77, FreeBSD 163, GNU 247. Five units per switch.

The order is the same on every line, without an exception in ten. The base system carries roughly twice what the standard asks for, and the GNU tools carry roughly three times. mv is the sharpest case: two switches in the standard, five here, eighteen there, for a program whose entire purpose is to change the name of a file.

I would like to be clear about what that table does and does not say. It does not say the base system is lean. cat with eight switches, against a standard that asks for one, is not lean by any definition I would defend in public. What the table says is that both trees walked down the same road, and one of them walked further.

The GNU side did it on purpose, and says so. The GNU Coding Standards, the document that governs how those tools are written, tell the contributor that POSIX prohibits many kinds of extension and then, in the next sentence, to feel free to make the extensions anyway. A little further down: additional useful features are welcome regardless of whether there is any precedent for them. Nobody was looking away when that ls reached 60 switches. The rules asked for them.

What a switch costs

None of this costs anything at runtime. A switch nobody passes is a branch nobody takes, and the binary does not care how many of those it carries. If the bill were in nanoseconds this piece would be two lines long. The bill is somewhere else.

It is in the manual, first. Those 506 lines are the price of admission for anybody who wants to know what the program can do, and the price rises with every switch whether or not you intend to use it, because you cannot know which ones you need until you have read about the ones you do not.

It is in the reading of other people's scripts. ls -lahtrG is six decisions in a row, and every one of them has to be unpacked in the head of whoever inherits the file. A pipeline of two commands with no switches reads like a sentence, and six switches in a row read like a number plate that somebody has to decode.

And it is in the drift between the two trees, which is where the real money goes. -G on this machine turns the colours on. -G on the GNU tool suppresses the group column in a long listing. Same letter, same program name, opposite corners of the manual. A script written on one and run on the other does not fail, which would be merciful. It does something slightly different and carries on. That switch was added twice, by two projects with two reasons, and the person who pays for the collision is the one who wrote a perfectly correct script on the wrong machine.

One letter, two manuals FreeBSD 15.1, ls(1) -G does enable colourised output same as CLICOLOR in the environment affects every listing GNU coreutils 9.9, ls -G does hide the group column same as --no-group affects long listings only A script written for one runs on the other without an error, and does something else.

Who profits, who pays

The profit went to the person who needed the switch, on the afternoon they needed it, and it was a real profit. It also went to everybody whose scripts kept working after the switch was added, because switches get added and hardly ever taken away. That is the part of the arrangement that deserves respect: the tools are unbreakable in the backward direction, and a script that worked a decade ago works this morning.

The bill lands on the reader. On the newcomer who opens the manual for the one thing they need and finds it on page four. On the reviewer who has to know what -p does on this side of the fence before approving a script that will run on the other. And on the standard itself, which now describes a program the common implementations outgrew long ago, so that writing to it means reading two other documents to find out what you are allowed to leave out.

There is a version of this where the bill is smaller, and it lives in the second half of McIlroy's rule, the one about output becoming input. ls | sort | head needs nothing that the reader does not already know, because each program in the chain is doing the one thing its name says. The chain is longer to type, it is understood at a glance, and it works on every machine that has a pipe, which is all of them.

The same listing, asked for two ways SIX DECISIONS IN ONE WORD ls -lahtrG -l -a -h -t -r -G long hidden too human sizes by time reversed colour, or not Each box is a decision the reader unpacks, and the last one means something else on the other tree. THREE PROGRAMS, ONE JOB EACH ls list what is here sort put it in order head keep the first few Longer to type, understood at a glance, and it works on every machine that has a pipe.

The limit

Four things, and the first one is the most important.

This is a bill in attention. Nothing here runs a single cycle slower for having switches it does not use, and anybody expecting a performance argument should close the tab now.

The count is only as fair as the method. A synopsis and a --help screen document switches in slightly different ways, and I have counted each documented entry once on both sides. Somebody with a different rule would get different totals; I would be surprised if they got a different order.

The base system is not the moral of this story. It sits closer to the standard than the GNU tree does, and that is worth noting, but the honest reading of the table is that both trees left the standard behind a long time ago and the question is only how far.

And ten programs is ten programs. There are 528 in /bin and /usr/bin alone, and I have not counted the rest, though I would not expect the direction to change.

What I do know is that ls with nothing after it still does the job in the time it takes a finger to leave a key, and that it has been doing so since before most of the switches existed. The switches are for the days we remember them, and the plain command is for the rest, which is nearly all of them.

POSIX asks ls for 26 switches, the FreeBSD base system gives it 44, GNU coreutils 60, and the order holds on every one of ten everyday programs. None of it costs a cycle. It costs the reader, in the manual, in other people's scripts, and in a letter that means one thing on one tree and something else on the other.