How can I detect if my shell script is running through a pipe?
Asked Answered
C

6

349

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.

Cima answered 26/5, 2009 at 15:3 Comment(2)
Here are some more interesting test cases! <a href="serverfault.com/questions/156470/… for a script that is waiting on stdin</a>Slumber
@Slumber The correct link is serverfault.com/q/156470/197218Zonked
R
529

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:

Rondure answered 26/5, 2009 at 15:13 Comment(9)
@Kelvin The man page snippet there suggest that it should, but those file descriptors are not assigned by default.Rondure
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.htmlParlormaid
Works when running a script as ssh remote command as well. Best answer ever and very simple.Boonie
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).Zonked
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.Bast
can we find out path of the input file piped to stdin?Meld
@linux_newbie this approach doesn't work when invoking ssh -TStrawflower
@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.Odel
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 ;(Artemis
S
161

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.

Strawflower answered 29/5, 2015 at 2:48 Comment(5)
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?Fatherland
@Fatherland 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.Strawflower
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 catValley
And change -t 1 to -t 0 if you're worried about STDIN, but not STDOOUT. :)Bibliotheca
Has anything changed here? Is this answer the solution?Nucleonics
E
38

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

Ewall answered 26/5, 2009 at 15:14 Comment(3)
+1 for "man test" because /usr/bin/test will work even in a shell that doesn't implement -t in its build-in testStagemanage
As noted by FireFly in dmckee's answer, a shell which doesn't implement -t doesn't conform to POSIX.Collaboration
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.Impart
M
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
Moleskins answered 26/5, 2009 at 15:21 Comment(1)
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).Composition
A
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

:#
Agape answered 12/6, 2015 at 20:41 Comment(1)
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.Strawflower
M
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.

Misguide answered 13/2, 2019 at 11:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.