Why there are 11 tabstops on a 80-column console?

Solution 1:

There are only 9 tabstops on an 80-column console.

Let's expand upon the comment first. You haven't actually tested any consoles. Those are all GUI terminal emulator programs. Fortunately, actual consoles (on Linux and FreeBSD at least) do also exhibit this behaviour.

But it's not the behaviour that you claim. It doesn't demonstrate 11 tabstops. It demonstrates 9.

To understand tabstops properly, you need to throw away the "They always expand to N spaces." idea. If you have access to a mechanical typewriter with tabstops, then go and look at how it works (with tabs being set by protruding pins on the carriage). Tabstops in terminals in the Unix and Linux worlds work this way.

(One can configure the line discipline to enact soft tabs, replacing tab characters in the output stream with space characters before they ever reach the terminal. We're discussing hard tabs, executed by the terminal itself rather than by the line discipline, here though. With hard tabs, a bone fide TAB character is sent to the terminal. With soft tabs on, conversely, the behaviour that you describe doesn't occur in the first place. Try setting stty tab3 and see.)

As initialized, a terminal has no tabstops set at all. Early on, getty, or the reset command or something akin (it varying as to system), outputs a series of space characters and escape sequences that sets the tabstops at particular columns. Sometimes the columns and even the escape sequences are hardwired. Most often, though, the command looks up the information in the termcap/terminfo database. The terminfo capability giving the escape sequence for clearing all tabs is tbc; the hts capability gives the escape sequence for setting a tab at the current column; and the it capability specifies the number of columns between tabstops by default. For termcap, the equivalent capabilities are it, ct, and st.

The command obtains the terminal size (from the kernel, via a system call, or from another part of the terminfo/termcap record), looks at the number of columns, and repeatedly outputs it spaces followed by a st sequence for as many columns as it can, finally outputting a carriage return to return to column #0. This is exactly the way that one sets a set of tabstops on many mechanical typewriters: space repeatedly to the next tabstop, hit the "set tabstop" lever to push a pin out, repeat until the right margin, push the carriage back to the left.

For an 80-column-wide terminal, with it given as 8 as it usually is, the command does this nine times, setting tabstops at columns 8, 16, 24, 32, 40, 48, 56, 64, and 72.

There's no tabstop (barring a program bug) at column 0. This is your first counting mistake. There's also no tabstop in column 80, your second counting mistake. "Why", you may ask, "is that?" Firstly, it is because it is not possible to be in column 80 to set a tabstop. The cursor goes from column 0 to column 79 on an 80-column terminal.

Secondly, one must think about the behaviour of the TAB character. It does not, contrary to popular belief, expand to enough spaces to move to the next multiple of 8 columns. Unix and Linux terminals work like mechanical typewriters, remember. In the world of mechanical typewriters, the tab key moves the carriage until it is stopped by a tabstop pin or by the right margin. (Some clever-clogs might mention margin release at this point. This is something that mechanical typewriters have that Unix and Linux terminals have not, for obvious reasons.)

The TAB character on Unix and Linux terminals actually works the very same way. In response to it, the terminal emits spaces until the next set tabstop or the rightmost column is reached. And that's what's happening here. When the cursor is in column 79, you can write as many TAB characters as you like, and nothing will happen. There's no tabbing beyond the rightmost column.

The termcap/terminfo system allows settable hard tabs to be optional. But the programs that you've tested with all provide an "xterm"-type terminal emulator. (The escape sequences that it understands are those given in the xterm entry/entries in the termcap/terminfo database.) So too, nowadays, does the terminal emulator in the FreeBSD virtual console. The terminal emulator in the Linux virtual console is in fact a slightly different terminal type, designated linux. (This used to be the case for the FreeBSD virtual console, too, which had a cons25 type. FreeBSD changed its kernel terminal emulator to be xterm-compatible in version 9.0, as part of a project to get UTF-8 support on virtual consoles.) All of these xterm-alikes, and the Linux kernel terminal emulator, have the full settable hard tabs mechanism.

(For those who enjoy reading the source: Look at the TabNext() and TabToNextStop() functions in tabs.c in the xterm source code.)

You can try it for yourself. Run these commands:

tbc=$(tput tbc) hts=$(tput hts)
printf '%s\r%s\r\n' "${tbc}" "a${hts}aaa${hts}aaaaaaa${hts}aaaaaaaaaaaaa${hts}aaaaaaaaaaaaaaaaaa${hts}"

Then run your command again. Exactly five tabstops have been set. Count the ${hts} in the command, or look at the line of a characters. Beyond the fifth, the TAB character causes that metaphorical typewriter carriage to whizz all of the rest of the way over to the right-hand margin.

The next (printing, non-TAB) character causes an automatic carriage return to the left margin, and linefeed to the next line. In termcap/terminfo terminology, the xterm terminal type has automatic margins. In theory, Unix and Linux can work with terminals that do not have automatic margins. Your printf command would get stuck at column 79, not matter how much more you output, until you wrote a carriage return. In practice, such a device would boggle the minds of most people nowadays.