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

How to detect if my shell script is running through a pipe?

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

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


在纯POSIX外壳中,

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

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

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

返回"not a terminal",因为parenthetic的输出通过管道传输到cat

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

-t fd True if file descriptor fd is open and refers to a terminal.

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

1
2
3
0:     stdin  
1:     stdout  
2:     stderr


没有简单的方法来确定stdin、stdout或stderr是否正在通过管道与脚本进行连接,这主要是因为像ssh这样的程序。

"正常"工作的东西

例如,以下bash解决方案在交互式shell中正常工作:

1
2
3
4
5
6
7
8
[[ -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是因为它更容易:

1
2
3
4
5
6
7
8
9
10
11
# 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都可以确定它是否接收管道输入:

1
2
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的解决方案。


命令test(内置于bash中)具有检查文件描述符是否为tty的选项。

1
2
3
if [ -t 1 ]; then
    # stdout is a tty
fi

见"man test"或"man bash",搜索"-t"


您没有提到正在使用的shell,但是在bash中,您可以这样做:

1
2
3
4
5
6
7
#!/bin/bash

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

在Solaris上,Dejay Clayton的建议主要起作用。-p没有按需响应。

bash_redir_test.sh如下所示:

1
2
3
4
5
6
7
8
[[ -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上,它工作得很好:

1
2
3
4
5
6
7
8
9
10
11
:$ ./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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
:# ./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中测试)不应视为可移植的,也不建议使用,但为了完整起见,这里是:

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

我不知道为什么,但似乎文件描述符"3"是在bash函数有stdin管道时创建的。

希望它有帮助,