tl;dr: We look at how Zsh and Fish is able to indicate a missing terminating linefeed in program output when the Unix programming model precludes examining the output itself.
Most shells, including bash, ksh, dash, and ash, will show a prompt wherever the previous command left the cursor when it exited.
The fact that the prompt (almost) always shows up on the familiar left-most column of the next line is because Unix programs universally cooperate to park the cursor there when they exit.
This is done by always making sure to output a terminating linefeed \n
(aka newline):
vidar@vidarholen-vm2 ~ $ whoami
vidar
vidar@vidarholen-vm2 ~ $ whoami | hexdump -c
0000000 v i d a r \n
If a program fails to follow this convention, the prompt will end up in the wrong place:
vidar@vidarholen-vm2 ~ $ echo -n "hello world"
hello worldvidar@vidarholen-vm2 ~ $
However, I recently noticed that zsh
and fish
will instead show a character indicating a missing linefeed, and still start the prompt where you’d expect to find it:
vidarholen-vm2% echo -n "hello zsh"
hello zsh%
vidarholen-vm2%
vidar@vidarholen-vm2 ~> echo -n "hello fish"
hello fish⏎
vidar@vidarholen-vm2 ~>
If you’re disappointed that this is what there’s an entire blog post about, you probably haven’t tried to write a shell. This is one of those problems where the more you know, the harder it seems (obligatory XKCD).
If you have a trivial solution in mind, maybe along the lines of if (!output.ends_with("\n")) printf("%\n");
, consider the following restrictions*:
- Contrary to popular belief, the shell does not sit between programs and the terminal. The shell has no ability to intercept or examine the terminal output of programs.
- The terminal programming model is based on teletypes (aka TTYs), electromechanical typewriters from the early 1900s. They printed letter by letter onto paper, so there is no memory or screen buffer that can be programmatically read back.
Given this, here are some flawed ways to make it happen:
-
The shell could use pipes to intercept all output, and relay it onto the terminal. While it works in trivial cases like
whoami
, some programs check whether stdout is a terminal and change their behavior, others go over your head and talk to the TTY directly (e.g.ssh
‘s password prompt), and some use TTY specificioctl
s that fail if the output is not a TTY, such as querying window size or disabling local echo for password input. -
The shell can
ptrace
the process to see what it writes where. This has a huge overhead and breakssudo
,ping
, and other commands that rely on suid. -
The shell can create a pseudo-tty (pty), run commands in that, and relay information back and forth much like
ssh
orscript
does. This is an annoying and heavy-handed approach, which in its ultimate form would require re-implementing an entire terminal emulator. -
The shell can use ECMA-48 cursor position reporting features:
printf '\e[6n'
on a supported terminal will cause the terminal to simulate user input on the form^[[y;xR
wherey
andx
is the row and column. The shell could then read this to figure out where the cursor is. These kinds of round trips are feasible, but somewhat slow and annoying to implement for such a simple feature.
Zsh and Fish instead have a much simpler and far more clever way of doing it:
- They always output the missing linefeed indicator, whether or not it’s needed.
- They then pad out the line with
$COLUMN-1
spaces - This is followed by a carriage return to move to the first column
- Finally, they show the prompt.
This solution is very simple because it only requires printing a fixed string before every prompt, but it’s highly effective on all terminals§.
Why?
Let’s pretend our terminal is 10 columns wide and 3 rows tall, and a canonical program just wrote a short string with a trailing linefeed:
[vidar ]
[| ]
[ ]
The cursor, indicated by |
, is at the start of the line. This is what would happen in step 1 and 2:
[vidar ]
[% |]
[ ]
The indicator is shown, and since we have written exactly $COLUMN
characters, the cursor is after the last column. Step 3, a carriage return, now moves it back to the start:
[vidar ]
[|% ]
[ ]
The prompt now draws over the indicator, and is shown on the same line:
[vidar ]
[~ $ | ]
[ ]
The final result is exactly the same as if we had simply written out the prompt wherever the cursor was.
Now, let’s look at what happens when a program does not output a terminating linefeed:
[vidar| ]
[ ]
[ ]
The indicator is shown, but this time the spaces in step 2 causes the line to wrap all the way around to the next line:
[vidar% ]
[ | ]
[ ]
The carriage return moves the cursor back to the start of the next line:
[vidar% ]
[| ]
[ ]
The prompt is now shown on that line, and therefore doesn’t overwrite the indicator:
[vidar% ]
[~ $ | ]
[ ]
And there you have it. A seemingly simple problem turned out harder than expected, but a clever use of line wrapping made it easy again.
Now that we know the secret sauce, we can of course do the same thing in Bash:
PROMPT_COMMAND='printf "%%%$((COLUMNS-1))s\\r"'
* These same restrictions are reflected in several other aspects of Unix:
- While useful and often requested, there is no robust way to get the output of the previously executed command.
- It’s surprisingly tricky to take screenshots/dumps of terminals, and it only works on specific terminals.
- The phenomenon of background process output cosmetically trashing foreground processes is well known, and yet there’s no solution
§ Fish developer and Hacker News reader ComputerGuru explains that there are many caveats related to various terminals’ line wrapping that make this trickier than shown here.