我如何从一个shell脚本中检测,如果它的标准输出被发送到终端或如果它被管道到另一个进程?

举个例子:我想添加转义码来着色输出,但只在交互运行时,而不是在管道运行时,类似于ls——color。


在纯POSIX shell中,

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

返回"terminal",因为输出被发送到您的终端,而

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

返回“非终端”,因为插入元素的输出被管道传输给cat。


-t标志在手册页中描述为

-t fd如果打开文件描述符fd并指向终端,则为True。

... fd可以是常见的文件描述符赋值之一:

0:标准输入 1:标准输出 2:标准误差

命令test(内置在Bash中)有一个选项来检查文件描述符是否是tty。

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

参见“man test”或“man bash”,搜索“-t”。

你没有提到你在使用哪个shell,但是在Bash中,你可以这样做:

#!/bin/bash

if [[ -t 1 ]]; then
    # stdout is a terminal
else
    # stdout is not a terminal
fi

没有一种万无一失的方法来确定STDIN、STDOUT或STDERR是否通过管道传输到您的脚本,这主要是因为像ssh这样的程序。

“正常”工作的东西

例如,以下bash解决方案在交互式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'

但它们并不总是有效

然而,当将此命令作为非tty的ssh命令执行时,STD流看起来总是像管道。为了演示这一点,使用STDIN,因为它更简单:

# 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 ${?}'

为什么这很重要

这是一个相当大的问题,因为它意味着bash脚本无法判断非tty ssh命令是否正在通过管道传输。请注意,这种不幸的行为是在ssh的最新版本开始为非tty STDIO使用管道时引入的。以前的版本使用套接字,可以在bash中使用[[-S]]进行区分。

重要的时候

当您希望编写行为类似于编译实用程序(如cat)的bash脚本时,这种限制通常会导致问题。例如,cat在同时处理各种输入源时允许以下灵活的行为,并且足够智能,可以确定它是否正在接收管道输入,而不管是否使用非tty或强制tty ssh:

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

只有当您能够可靠地确定是否涉及管道时,您才能这样做。否则,在没有管道或重定向输入时执行读取STDIN的命令将导致脚本挂起并等待STDIN输入。

其他不起作用的东西

为了解决这个问题,我研究了几种无法解决问题的技术,其中包括:

检查SSH环境变量 在/dev/stdin文件描述符上使用stat 通过[["${-}" =~ 'i']]检查交互模式 通过tty和tty -s检查tty状态 通过[["$(ps -o comm= -p $PPID)"检查ssh状态=~ 'sshd']]

注意,如果您使用的是支持/proc虚拟文件系统的操作系统,那么您可能会幸运地按照STDIO的符号链接来确定是否正在使用管道。但是,/proc不是一个跨平台的、与posix兼容的解决方案。

我对解决这个问题非常感兴趣,所以如果您想到任何其他可行的技术,最好是在Linux和BSD上都能工作的基于posix的解决方案,请告诉我。

在Solaris上,来自Dejay Clayton的建议最有效。-p没有按期望的方式响应。

bash_redir_test.sh文件如下:

[[ -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'

在Linux上,它工作得很好:

:$ ./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

在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

:#

下面的代码(仅在Linux Bash 4.4中测试)不应该被认为是可移植的,也不推荐,但为了完整性,这里是:

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

我不知道为什么,但当Bash函数具有标准输入管道时,似乎以某种方式创建了文件描述符“3”。