如何检测我的Shell脚本是否通过管道运行?

341

如何从 shell 脚本中检测其标准输出是被发送到终端还是被管道发送到另一个进程?

具体情况是: 我想添加转义码以着色输出,但仅在交互式运行时添加,而不在管道发送时添加,类似于ls --color所做的。


2
以下是一些更有趣的测试案例!<a href="http://serverfault.com/questions/156470/testing-for-a-script-that-is-waiting-on-stdin">测试等待标准输入的脚本</a> - user940324
2
@user940324 正确的链接是 http://serverfault.com/q/156470/197218。 - Palec
6个回答

514
在一个纯的POSIX shell环境下,
if [ -t 1 ] ; then echo terminal; else echo "not a terminal"; fi

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

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

当括号元素的输出被管道传递给cat时,返回“非终端”。


-t 标志在 man 页面中描述为:

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

... 其中 fd 可以是通常的文件描述符分配之一:


1
@Kelvin 那里的 man 页面片段建议应该这样做,但是这些文件描述符默认情况下不会被分配。 - dmckee --- ex-moderator kitten
50
为了澄清,-t 标志在 POSIX 中被指定,因此应该适用于任何符合 POSIX 标准的 shell(也就是说,它不是 bash 的扩展)。参考链接:http://pubs.opengroup.org/onlinepubs/009695399/utilities/test.html - FireFly
1
我同意在您的编辑(第5版)之后,该答案比第3版更清晰,并且在事实上也是正确的(忽略“返回”在非常非正式的情况下使用,“打印”可能更精确)。 - Palec
@dmckee---ex-moderatorkitten - 但是这些文件描述符不会默认分配。 哪些文件描述符?对于任何进程,012都会被默认分配。 - Shuzheng
我们能否使用环境变量来欺骗它?比如我想通过脚本捕获输出,我调用这个shell命令但它检测到我不在终端中,所以输出无法被捕获 ;( - Mathieu J.
显示剩余4条评论

158

没有绝对可靠的方法来确定 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 管道时引入的。之前的版本使用套接字,可以通过使用 [[ -S ]] 在 bash 中区分。

何时很重要

当您想编写一个具有类似编译实用程序(如 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)”=~'sshd']]检查ssh状态
请注意,如果您使用支持/proc虚拟文件系统的操作系统,则可以通过跟随STDIO的符号链接来确定是否使用了管道。但是,/proc不是跨平台的POSIX兼容解决方案。
我非常有兴趣解决这个问题,请让我知道您是否想到任何其他可能起作用的技术,最好是在Linux和BSD上都适用的基于POSIX的解决方案。

3
明显地,检查环境变量或进程名称是非常不可靠的启发式方法。但您能否稍微解释一下为什么其他启发式方法不适合此目的,或者它们的问题是什么?例如,我在使用“stat”调用/dev/stdin时看不出任何输出差异。为什么"${-}"或"tty -s"无法正常工作?我还查看了“cat”的源代码,但无法看出其中哪个部分正在进行魔术,以至于您无法在POSIX shell中完成此操作。您能详细说明一下吗? - josch
@josch,我真希望我能!但我现在没有时间深入研究这个问题。不过,你可以尝试使用任何你建议的方法,同时使用ssh -tssh -T,你会发现那些使用ssh -t可行的方法,在使用ssh -T时并不起作用。 - Dejay Clayton
“cat..piped...substituted” 的示例似乎无论是通过 ssh -[tT]bash -c 还是直接运行,都没有产生任何明显的输出差异。而且我在 man cat 中也没有看到任何关于 TTY 的说明。 - usretc
如果你担心标准输入(STDIN),但不担心标准输出(STDOUT),那么请将“-t 1”更改为“-t 0”。 :) - chicks
这里有什么变化吗?这个答案是解决方案吗? - Lockszmith

37
命令test(Bash内置)有一个选项,可以检查文件描述符是否为tty。
if [ -t 1 ]; then
    # Standard output is a tty
fi

请参考 "man test" 或 "man bash",并搜索 "-t",相关内容与IT技术有关。


6
因为即使在没有实现 -t 的内置测试的 shell 中,/usr/bin/test 仍然可以工作,所以对于 "man test" 给出的 +1。 - Neil Mayhew
4
正如FireFly在dmckee的回答中所指出的,不实现-t选项的shell不符合POSIX标准。 - scy
还可以参考bash的内置命令help test(以及更多的help help),然后查看info bash以获取更深入的信息。如果您需要脱机编写脚本或只是想获得更广泛的理解,这些命令非常有用。 - Joel Purra

14

您没有提及使用哪种shell,但在Bash中,您可以这样做:

#!/bin/bash

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

需要解释一下。例如,什么是主意/要点(如果没有其他内容,请链接到具体文档)?什么是Bash特定的?其中一些是否取决于Bash的版本?请通过编辑(更改)您的答案进行回复,而不是在此处进行评论(不带“编辑:”,“更新:”或类似内容 - 答案应该看起来像今天写的)。 - Peter Mortensen

8
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

:#

有趣,我希望我能够访问Solaris进行测试。如果你的Solaris实例使用"/proc"文件系统,那么有更可靠的解决方案,涉及搜索标准输入、标准输出和标准错误的"/proc"符号链接。 - Dejay Clayton

2
下面的代码(仅在Linux Bash 4.4中测试)不应被视为可移植或推荐,但为了完整起见,这里是它:
ls /proc/$$/fdinfo/* >/dev/null 2>&1 || grep -q 'flags:    00$' /proc/$$/fdinfo/0 && echo "pipe detected"

我不知道为什么,但似乎当Bash函数被标准输入管道时,文件描述符“3”会被创建。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接