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

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

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

92263 次浏览

在纯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标志在手册页中描述为

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

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

命令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' ]]检查交互模式
  • 通过ttytty -s检查tty状态
  • 通过[[ "$(ps -o comm= -p $PPID)" =~ 'sshd' ]]检查ssh状态

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

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

Solaris上,德杰·克莱顿的建议大部分工作。-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"

我不知道为什么,但似乎文件描述符“;3”;以某种方式在Bash函数的标准输入管道中创建。