Telling if a file descriptor points to a terminal device
A program can tell if a file descriptor is associated with a tty device by using the isatty()
standard C function (which generally underneath does an innocuous tty-specific ioctl()
system call that would return with an error when the fd doesn't point to a tty device).
The [
/test
utility can do it with its -t
operator.
if [ -t 1 ]; then
echo stdout is open to a terminal
fi
Tracing libc function calls on a GNU/Linux system:
$ ltrace [ -t 1 ] | cat
[...]
isatty(1) = 0
[...]
Tracing system calls:
$ strace [ -t 1 ] | cat
[...]
ioctl(1, TCGETS, 0x7fffd9fb3010) = -1 ENOTTY (Inappropriate ioctl for device)
[...]
Telling if it points to a pipe
To determine whether a fd is associated with a pipe/fifo, one can use the fstat()
system call, which returns a structure whose st_mode
field contains the type and permissions of the file opened on that fd. The S_ISFIFO()
standard C macro can be used on that st_mode
field to determine if the fd is a pipe/fifo.
There is no standard utility that can do a fstat()
, but there are several incompatible implementations of a stat
command that can do it. zsh
's stat
builtin with stat -sf "$fd" +mode
which returns the mode as a string representation whose first character represents the type (p
for pipe). GNU stat
can do the same with stat -c %A - <&"$fd"
, but also has stat -c %F - <&"$fd"
to report the type alone. With BSD stat
: stat -f %St <&"$fd"
or stat -f %HT <&"$fd"
.
Telling if it's seekable
Applications generally do not care if stdout is a pipe though. They may care that it's seekable (though generally not to decide whether to buffer or not).
To test whether a fd is seekable (pipes, sockets, tty devices are not seekable, regular files and most block devices generally are), one can attempt a relative lseek()
system call with an offset of 0 (so innocuous). dd
is a standard utility that's an interface to lseek()
but it can't be used for that test, as implementations would not call lseek()
at all if you ask for an offset of 0.
The zsh
and ksh93
shells have builtin seeking operators though:
$ strace -e lseek ksh -c ': 1>#((CUR))' | cat
lseek(1, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
ksh: 1: not seekable
$ strace -e lseek zsh -c 'zmodload zsh/system; sysseek -w current -u 1 0 || syserror'
lseek(1, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
Illegal seek
Disabling the buffering
The script
command uses a pseudo-terminal pair to capture the output of a program, so the program's stdout (and stdin and stderr) will be a pseudo-terminal device.
When the stdout is to a terminal device, there is still generally some buffering, but it is line based. printf
/puts
and co will not write anything until a newline character is to be output. For other types of files, the buffering is by blocks (of a few kilo bytes).
There are several options to disable the buffering which are discussed in a number of Q&As here (search for unbuffer or stdbuf, Can't redirect cut output gives a few approaches) either by using a pseudo-terminal as can be done by socat
/script
/expect
/unbuffer
(an expect
script)/zsh
's zpty
or by injecting code in the executable to disable the buffering as done by GNU's or FreeBSD's stdbuf
.