360

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.

2
  • 2
    Here are some more interesting test cases! <a href="serverfault.com/questions/156470/… for a script that is waiting on stdin</a> Commented Sep 12, 2011 at 10:20
  • 2
    @user940324 The correct link is serverfault.com/q/156470/197218 Commented Feb 19, 2014 at 1:40

7 Answers 7

543

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:

Sign up to request clarification or add additional context in comments.

9 Comments

@Kelvin The man page snippet there suggest that it should, but those file descriptors are not assigned by default.
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). pubs.opengroup.org/onlinepubs/009695399/utilities/test.html
Works when running a script as ssh remote command as well. Best answer ever and very simple.
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).
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.
|
172

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.

5 Comments

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 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.
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
And change -t 1 to -t 0 if you're worried about STDIN, but not STDOOUT. :)
Has anything changed here? Is this answer the solution?
40

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".

3 Comments

+1 for "man test" because /usr/bin/test will work even in a shell that doesn't implement -t in its build-in test
As noted by FireFly in dmckee's answer, a shell which doesn't implement -t doesn't conform to POSIX.
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.
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 

2 Comments

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, not here in comments (without "Edit:", "Update:", or similar - the answer should appear as if it was written today).
@PeterMortensen The conditional command [[ syntax is bash specific. For other shells, just [ can be used, but I'm not sure if it's worth to edit the answer, because that's what's already used in the accepted answer.
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 :# 

1 Comment

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.
3

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.

1 Comment

On Mac, the semi-equivalent lsof -p $$ -a -d 0 | grep -q 'PIPE' && echo 'pipe detected' worked for me.
0

One way to sidestep the SSH issue by ignoring sshd-sess:

pipetest.sh

#!/bin/bash pipeFile="/tmp/${0}_$(date '+%Y-%m-%d_%H-%M-%S')" lsof -p $$ +E -lnPT 2>/dev/null | grep "FIFO" | grep -v sshd-sess > $pipeFile isPiped=$([[ (( $(stat -c%s "$pipeFile" 2>/dev/null) > 0 )) ]] && echo true || echo false) echo "isPiped=$isPiped" 

results:

$ ./pipetest.sh | cat isPiped=true $ ./pipetest.sh isPiped=false $ ssh -T localhost "./pipetest.sh | cat" isPiped=true $ ssh -T localhost "./pipetest.sh" isPiped=false 

Comments