339

How do I detect from within a shell script if its standard output is being sent to a terminal or if it's piped to another process?

The case in point: I'd like to add escape codes to colorize output, but only when run interactively, but not when piped, similar to what ls --color does.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131

6 Answers6

512

In a pure POSIX shell,

if [ -t 1 ] ; then echo terminal; else echo "not a terminal"; fi

returns "terminal", because the output is sent to your terminal, whereas

(if [ -t 1 ] ; then echo terminal; else echo "not a terminal"; fi) | cat

returns "not a terminal", because the output of the parenthetic element is piped to cat.


The -t flag is described in man pages as

-t fd True if file descriptor fd is open and refers to a terminal.

... where fd can be one of the usual file descriptor assignments:

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
dmckee --- ex-moderator kitten
  • 98,632
  • 24
  • 142
  • 234
  • 1
    @Kelvin The man page snippet there suggest that it should, but those file descriptors are not assigned by default. – dmckee --- ex-moderator kitten May 14 '12 at 19:07
  • 49
    To clarify, the `-t` flag is specified in POSIX, and thus should work for any POSIX-compatible shell (that is, it's not a bash extension). http://pubs.opengroup.org/onlinepubs/009695399/utilities/test.html – FireFly May 14 '13 at 14:12
  • Works when running a script as ssh remote command as well. Best answer ever and very simple. – one-liner Dec 06 '16 at 19:52
  • 1
    I agree that after your edit (revision 5), the answer is clearer than in revision 3 and also factually correct (ignoring that “returns” is used very informally where “prints” would be more precise). – Palec Jan 27 '17 at 10:26
  • Was looking for a `fish` shell answer. Using `test` is neat, but I cannot try the parenthesized example as that is not supported. Tried wrapping it in an analogous `begin; ...; end`, but that did not seem to work, and just ran the positive code block again. Thought I might need to use `status` but that doesn't seem to check for piping. I guess I essentially want to check if STDOUT of a preceding command/script is not set to the terminal, thanks to these clarifying answers. – Pysis Apr 24 '19 at 14:10
  • can we find out path of the input file piped to stdin? – Aamir Jun 06 '20 at 08:45
  • @linux_newbie this approach doesn't work when invoking `ssh -T` – Dejay Clayton Jul 29 '20 at 01:44
  • @dmckee---ex-moderatorkitten - **but those file descriptors are not assigned by default.**, which file descriptors? `0`, `1`, and `2` are assigned by default for any process. – Shuzheng Mar 13 '21 at 10:10
  • can we cheat this using an ENV var? say I want to capture the output via a script, I call this shell command but it detects I'm not in a terminal, so output to be captured ;( – Mathieu J. Jul 06 '21 at 12:22
156

There is no foolproof way to determine if STDIN, STDOUT, or STDERR are being piped to/from your script, primarily because of programs like ssh.

Things that "normally" work

For example, the following bash solution works correctly in an interactive shell:

[[ -t 1 ]] && \
    echo 'STDOUT is attached to TTY'

[[ -p /dev/stdout ]] && \
    echo 'STDOUT is attached to a pipe'

[[ ! -t 1 && ! -p /dev/stdout ]] && \
    echo 'STDOUT is attached to a redirection'

But they don't always work

However, when executing this command as a non-TTY ssh command, STD streams always looks like they are being piped. To demonstrate this, using STDIN because it's easier:

# CORRECT: Forced-tty mode correctly reports '1', which represents
# no pipe.
ssh -t localhost '[[ -p /dev/stdin ]]; echo ${?}'

# CORRECT: Issuing a piped command in forced-tty mode correctly
# reports '0', which represents a pipe.
ssh -t localhost 'echo hi | [[ -p /dev/stdin ]]; echo ${?}'

# INCORRECT: Non-tty mode reports '0', which represents a pipe,
# even though one isn't specified here.
ssh -T localhost '[[ -p /dev/stdin ]]; echo ${?}'

Why it matters

This is a pretty big deal, because it implies that there is no way for a bash script to tell whether a non-tty ssh command is being piped or not. Note that this unfortunate behavior was introduced when recent versions of ssh started using pipes for non-TTY STDIO. Prior versions used sockets, which COULD be differentiated from within bash by using [[ -S ]].

When it matters

This limitation normally causes problems when you want to write a bash script that has behavior similar to a compiled utility, such as cat. For example, cat allows the following flexible behavior in handling various input sources simultaneously, and is smart enough to determine whether it is receiving piped input regardless of whether non-TTY or forced-TTY ssh is being used:

ssh -t localhost 'echo piped | cat - <( echo substituted )'
ssh -T localhost 'echo piped | cat - <( echo substituted )'

You can only do something like that if you can reliably determine if pipes are involved or not. Otherwise, executing a command that reads STDIN when no input is available from either pipes or redirection will result in the script hanging and waiting for STDIN input.

Other things that don't work

In trying to solve this problem, I've looked at several techniques that fail to solve the problem, including ones that involve:

  • examining SSH environment variables
  • using stat on /dev/stdin file descriptors
  • examining interactive mode via [[ "${-}" =~ 'i' ]]
  • examining tty status via tty and tty -s
  • examining ssh status via [[ "$(ps -o comm= -p $PPID)" =~ 'sshd' ]]

Note that if you are using an OS that supports the /proc virtual filesystem, you might have luck following the symbolic links for STDIO to determine whether a pipe is being used or not. However, /proc is not a cross-platform, POSIX-compatible solution.

I'm extremely interesting in solving this problem, so please let me know if you think of any other technique that might work, preferably POSIX-based solutions that work on both Linux and BSD.

Dejay Clayton
  • 3,710
  • 2
  • 29
  • 20
  • 3
    Clearly inspecting environment variables or process names are very unreliable heuristics. But could you expand a bit why the other heuristics are unfit for this purpose or what their problem is? For example I see no difference in the output of a `stat` call on /dev/stdin. And why does `"${-}"` or `tty -s` not work? I also looked into the source code of `cat` but fail to see which part is doing the magic there that you cannot do in POSIX shell. Could you expand on that? – josch Aug 08 '19 at 11:31
  • @josch I wish I could! I don't presently have time to do a deeper dive into this. But try any of your suggested approaches with both `ssh -t` and `ssh -T` - you'll see that approaches that work using `ssh -t` don't work using `ssh -T`. – Dejay Clayton Jul 29 '20 at 01:47
  • the `cat..piped...substituted` examples don't seem to produce any observable differences in output, whether run via `ssh -[tT]`, `bash -c`, or directly. And I don't see any TTY-related notes in `man cat` – usretc Apr 10 '21 at 09:15
  • And change `-t 1` to `-t 0` if you're worried about STDIN, but not STDOOUT. :) – chicks Dec 23 '22 at 19:35
  • Has anything changed here? Is [this](https://stackoverflow.com/a/54668834/799379) answer the solution? – Lockszmith Jun 26 '23 at 22:24
37

The command test (builtin in Bash), has an option to check if a file descriptor is a tty.

if [ -t 1 ]; then
    # Standard output is a tty
fi

See "man test" or "man bash" and search for "-t".

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Beano
  • 7,551
  • 3
  • 24
  • 27
  • 6
    +1 for "man test" because /usr/bin/test will work even in a shell that doesn't implement -t in its build-in test – Neil Mayhew Aug 14 '13 at 20:45
  • 4
    As noted by FireFly in dmckee's answer, a shell which doesn't implement -t doesn't conform to POSIX. – scy Oct 08 '13 at 15:38
  • See also bash's builtin `help test` (and `help help` for more), then `info bash` for more in-depth information. These commands are great if you ever end up scripting offline, or just want to get a broader understanding. – Joel Purra Nov 25 '15 at 07:27
14

You don't mention which shell you are using, but in Bash, you can do this:

#!/bin/bash

if [[ -t 1 ]]; then
    # stdout is a terminal
else
    # stdout is not a terminal
fi
Dan Moulding
  • 211,373
  • 23
  • 97
  • 98
  • An explanation would be in order. E.g., what is the idea/gist (if nothing else, link to ***specific*** documentation)? What is Bash-specific? Is some of it dependent on the version of Bash? Please respond by [editing (changing) your answer](https://stackoverflow.com/posts/911267/edit), not here in comments (***without*** "Edit:", "Update:", or similar - the answer should appear as if it was written today). – Peter Mortensen Nov 25 '21 at 15:16
8

On Solaris, the suggestion from Dejay Clayton works mostly. The -p does not respond as desired.

File bash_redir_test.sh looks like:

[[ -t 1 ]] && \
    echo 'STDOUT is attached to TTY'

[[ -p /dev/stdout ]] && \
    echo 'STDOUT is attached to a pipe'

[[ ! -t 1 && ! -p /dev/stdout ]] && \
    echo 'STDOUT is attached to a redirection'

On Linux, it works great:

:$ ./bash_redir_test.sh
STDOUT is attached to TTY

:$ ./bash_redir_test.sh | xargs echo
STDOUT is attached to a pipe

:$ rm bash_redir_test.log
:$ ./bash_redir_test.sh >> bash_redir_test.log

:$ tail bash_redir_test.log
STDOUT is attached to a redirection

On Solaris:

:# ./bash_redir_test.sh
STDOUT is attached to TTY

:# ./bash_redir_test.sh | xargs echo
STDOUT is attached to a redirection

:# rm bash_redir_test.log
bash_redir_test.log: No such file or directory

:# ./bash_redir_test.sh >> bash_redir_test.log
:# tail bash_redir_test.log
STDOUT is attached to a redirection

:#
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
sbj3
  • 217
  • 2
  • 10
  • Interesting, I wish I had access to Solaris to test. If your Solaris instance uses the "/proc" filesystem, there are more reliable solutions that involve searching for "/proc" symbolic links for stdin, stdout, and stderr. – Dejay Clayton Jun 15 '15 at 20:36
2

The following code (tested only in Linux Bash 4.4) should not be considered portable nor recommended, but for the sake of completeness here it is:

ls /proc/$$/fdinfo/* >/dev/null 2>&1 || grep -q 'flags:    00$' /proc/$$/fdinfo/0 && echo "pipe detected"

I don't know why, but it seems that file descriptor "3" is somehow created when a Bash function has standard input piped.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
ATorras
  • 4,073
  • 2
  • 32
  • 39