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

Posted

技术标签:

【中文标题】如何检测我的 shell 脚本是不是通过管道运行?【英文标题】:How can I detect if my shell script is running through a pipe?如何检测我的 shell 脚本是否通过管道运行? 【发布时间】:2010-10-29 00:16:05 【问题描述】:

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

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

【问题讨论】:

这里有一些更有趣的测试用例! serverfault.com/questions/156470/… 用于在标准输入上等待的脚本 @user940324 正确的链接是serverfault.com/q/156470/197218 【参考方案1】:

在纯 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 已打开并指向终端,则为真。

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

0: standard input 1:standard output 2:standard error

【讨论】:

@Kelvin 手册页 sn-p 建议应该这样做,但默认情况下未分配这些文件描述符。 澄清一下,-t 标志是在 POSIX 中指定的,因此应该适用于任何 POSIX 兼容的 shell(也就是说,它不是 bash 扩展)。 pubs.opengroup.org/onlinepubs/009695399/utilities/test.html 在将脚本作为 ssh 远程命令运行时也可以使用。有史以来最好的答案,非常简单。 我同意,在您的编辑(修订版 5)之后,答案比修订版 3 更清晰,而且事实上也是正确的(忽略“返回”的使用非常非正式,而“打印”会更精确) . 正在寻找fish shell 答案。使用test 很简洁,但我不能尝试括号中的示例,因为它不受支持。尝试将其包装在类似的 begin; ...; end 中,但这似乎不起作用,只是再次运行正代码块。以为我可能需要使用status,但这似乎并不能检查管道。我想我基本上想检查是否没有将前面命令/脚本的 STDOUT 设置为终端,这要归功于这些澄清的答案。【参考方案2】:

在Solaris、the suggestion from 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”文件系统,则有更可靠的解决方案,包括为 stdin、stdout 和 stderr 搜索“/proc”符号链接。【参考方案3】:

以下代码(仅在 Linux Bash 4.4 中测试)不应被视为可移植也不推荐,但为了完整起见,这里是:

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

我不知道为什么,但似乎文件描述符“3”是在 Bash 函数具有 standard input 管道时创建的。

【讨论】:

【参考方案4】:

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

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

查看“man test”或“man bash”并搜索“-t”。

【讨论】:

+1 表示“man test”,因为 /usr/bin/test 即使在未在其内置测试中实现 -t 的 shell 中也能工作 正如 FireFly 在 dmckee 的回答中指出的那样,不实现 -t 的外壳不符合 POSIX。 另请参阅 bash 的内置 help test(和 help help 了解更多信息),然后 info bash 了解更多深入信息。如果您最终脱机编写脚本,或者只是想获得更广泛的理解,这些命令非常有用。【参考方案5】:

没有万无一失的方法来确定是否将 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 中的套接字区分开来。

重要的时候

当您想要编写行为类似于已编译实用程序的 bash 脚本(例如 cat)时,此限制通常会导致问题。例如,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 的解决方案。

【讨论】:

清楚地检查环境变量或进程名称是非常不可靠的启发式方法。但是您能否扩展一下为什么其他启发式方法不适合此目的或它们的问题是什么?例如,我发现在 /dev/stdin 上的 stat 调用的输出没有区别。为什么"$-"tty -s 不起作用?我还查看了cat 的源代码,但看不到哪一部分正在发挥您在 POSIX shell 中无法做到的魔力。你能扩展一下吗? @josch 我希望我能!我目前没有时间对此进行更深入的研究。但是使用ssh -tssh -T 尝试任何你建议的方法 - 你会发现使用ssh -t 的方法在使用ssh -T 时不起作用。 cat..piped...substituted 示例似乎不会在输出中产生任何可观察到的差异,无论是通过ssh -[tT]bash -c 还是直接运行。而且我在man cat 中没有看到任何与 TTY 相关的注释【参考方案6】:

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

#!/bin/bash

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

【讨论】:

解释一下。例如,想法/要点是什么(如果没有其他内容,请链接到具体文档)?什么是特定于 Bash 的?其中一些依赖于 Bash 的版本吗?请通过editing (changing) your answer 回复,而不是在 cmets 中(without "Edit:"、"Update:" 或类似的 - 答案应该看起来像是今天写的)。

以上是关于如何检测我的 shell 脚本是不是通过管道运行?的主要内容,如果未能解决你的问题,请参考以下文章

如何检测我运行 shell 脚本的当前目录?

shell脚本0——一切皆文件与管道

Shell脚本根据Hash值判断web服务器页面是不是被更改

高效实用k8s运行状态自动检测shell脚本

高效实用k8s运行状态自动检测shell脚本

如何从 PySpark rdd.mapPartitions 运行内存密集型 shell 脚本