29

I'm having trouble debugging a segfaulting program because the ouput right before the segfault is what I need, but this is lost if I'm piping the output to a file. According to this answer: https://unix.stackexchange.com/a/17339/22615, this is because the output buffer of the program flushes immediately when connected to a terminal but only at certain points when connected to a pipe. A few questions here:

  • How does a program determine what its stdout is connected to?

  • How does the "script" command produce the same behavior as when the program writes to a terminal?

  • Can this be achieved without the script command?

1

1 Answer 1

46

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.

7
  • 2
    Awesome answer, thank you very much for this!
    – mowwwalker
    Apr 26, 2019 at 21:20
  • Another, Linux-specific approach is to traverse /proc directory and for each /proc/<integer>/ directory look into /proc/<integer>/fd/ and find file descriptor that has same inode number in pipefs serverfault.com/q/48330/363611 However, that is only useful in scripts when one cannot use the syscalls described in Stephane's answer, and is more of a workaround than proper solution IMHO Apr 27, 2019 at 15:49
  • On BSD, lseek will succeed on terminals and other character devices, and simply re/set a counter which is increased on each successful read(). I don't know whether this makes them "seekable".
    – user313992
    May 8, 2019 at 5:32
  • @mowwwalker If this answer solved your issue, please take a moment and accept it by clicking on the check mark to the left. That will mark the question as answered and is the way thanks are expressed on the Stack Exchange sites.
    – dessert
    Aug 13, 2019 at 3:56
  • This Perl bit works to tell if stdin is seekable: $ date | perl -e 'binmode STDIN ; seek(STDIN,0,0) or exit 1' || echo Perl bad seek gives output Perl bad seek because of the pipe, but is silent if you redirect stdin from a file. Nov 6, 2021 at 16:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .