50

As most of you have done many times, it's convenient to view long text using less:

some_command | less

Now its stdin is connected to a pipe (FIFO). How can it still read commands like up/down/quit?

iBug
  • 3,508

2 Answers2

56

As mentioned by William Pursell, less reads the user’s keystrokes from the terminal. It explicitly opens /dev/tty, the controlling terminal; that gives it a file descriptor, separate from standard input, from which it can read the user’s interactive input. It can simultaneously read data to display from its standard input if necessary. (It could also write directly to the terminal if necessary.)

You can see this happen by running

some_command | strace -o less.trace -e open,read,write less

Move around the input, exit less, and look at the contents of less.trace: you’ll see it open /dev/tty, and read from both file descriptor 0 and whichever one was returned when it opened /dev/tty (likely 3).

This is common practice for programs wishing to ensure they’re reading from and writing to the terminal. One example is SSH, e.g. when it asks for a password or passphrase.

As explained by schily, if /dev/tty can’t be opened, less will read from its standard error (file descriptor 2). less’s use of /dev/tty was introduced in version 177, released on April 2, 1991.

If you try running cat /dev/tty | less, as suggested by Hagen von Eitzen, less will succeed in opening /dev/tty but won’t get any input from it until cat closes it. So you’ll see the screen blank, and nothing else until you press CtrlC to kill cat (or kill it in some other way); then less will show whatever you typed while cat was running, and allow you to control it.

Stephen Kitt
  • 434,908
  • 4
    @HagenvonEitzen Your computer will explode! It's like the way Kirk and Spock made Mudd's androids crash. – Barmar Jun 30 '18 at 14:03
  • 7
    @HagenvonEitzen Wow. A doubly-useless use of cat. I'm impressed. – Andrew Henle Jun 30 '18 at 14:53
  • @Andrew: I'm impressed that people are complaining about the use of pipes on Unix, which is perhaps the greatest feature of Unix. If anything, less should only be able to read from stdin and leave the task of reading from files to other tools, should it not? – u1686_grawity Jun 30 '18 at 15:08
  • 8
    @grawity I think Andrew’s point is that cat blah | can be replaced by < blah, and even that’s unnecessary in this case since less blah works too (well, less -f /dev/tty). But reading from /dev/tty is a bit of a special case, and all three variants (cat /dev/tty | less, less < /dev/tty and less -f /dev/tty) produce different results. – Stephen Kitt Jun 30 '18 at 16:51
  • 1
    Does /dev/tty always point to the right place somehow? I would think you'd need to use /dev/ptsX usually? – Weaver Jul 01 '18 at 08:51
  • 2
    @StarWeaver see this question about the difference between /dev/tty and /dev/pts/.... – Stephen Kitt Jul 01 '18 at 12:11
  • 1
    @grawity Standard input is a stream, which means you can't necessarily seek backwards. That would limit the usefulness (or at least efficiency, if less buffered its input) if less couldn't read from a file directly. – chepner Jul 01 '18 at 15:37
  • 1
    less behaves like cat if it's stdout isn't a terminal. So if you do echo foo | strace less | cat, it's just a transparent no-op filter. The first TTY system call it makes is ioctl(1, TCGETS, 0x7ffd7a4b7000) = -1 ENOTTY (Inappropriate ioctl for device). It does later make some ioctl system calls on stderr, but doesn't do anything with it. – Peter Cordes Jul 02 '18 at 08:18
  • @Peter the ioctl calls on stderr are used to determine the screen size. – Stephen Kitt Jul 02 '18 at 08:31
  • My point was that they still happen after it's detected that stdout is not a terminal, so it will run non-interactively without touching /dev/tty. – Peter Cordes Jul 02 '18 at 08:32
  • @Peter indeed, it queries the screen size in all cases apparently... – Stephen Kitt Jul 02 '18 at 08:33
  • Any idea why it uses stdout and stderr for the query, instead of /dev/tty? Some kind of sanity check? Is it possible for stderr and stdout to be open on a terminal other than the controlling TTY, if stdin (or some other FD) is still open on the CTTY? – Peter Cordes Jul 02 '18 at 08:44
  • 1
    @Peter it could be a historical artifact — part of the code which was overlooked when /dev/tty was added. I’ve tried finding the diff between 176 and 177 but no luck so far (I’m going to look through Usenet archives next). This post is a smoking gun. – Stephen Kitt Jul 02 '18 at 09:15
29

UNIX gives two methods to read users input while stdin has been redirected:

  • The original method is to read from stderr. Stderr is open for writing and reading and this is still mentioned in POSIX.

  • Later UNIX versions did (around 1979) add a /dev/tty driver interface that allows to open the controlling tty of a process. Since there are processes without a controlling tty, it is possible that an attempt to open /dev/tty fails. Friendly written software therefore has a fallback to the original method and then tries to read from stderr.

schily
  • 19,173
  • 12
    Read from stderr? Learned something new. – iBug Jun 30 '18 at 09:06
  • 1
    I'm glad somebody remembers the old ways. – Joshua Jun 30 '18 at 14:23
  • 3
    Is the reason that stderr is used for reading, because it is the least likely to have been redirected? I don't see any other difference between it and stdout (or for that mater stdin, before redirection). – ctrl-alt-delor Jun 30 '18 at 18:18
  • 4
    Yes, it is because this is the file descriptor that has the least chance for being redirected. – schily Jun 30 '18 at 18:31
  • @ctrl-alt-delor: It was / is typical for shells to be running with stdin, stdout, and stderr, all being dup()licates of the same file description, though, all opened on the tty. (Apparently POSIX still requires or suggest (this answer doesn't say) that stderr be a read/write FD, not opened with something like open("/dev/ttyS0", O_WRONLY). Reading stderr would fail in that case.) – Peter Cordes Jul 02 '18 at 08:41
  • @ctrl-alt-delor: and BTW, echo foo | less | cat just prints foo, because it behaves like cat if its stdout is not a terminal. Maybe so if you add | grep blah and forget to remove less, your pipeline still works? Not sure what the idea is there. – Peter Cordes Jul 02 '18 at 08:42