Bash:“tee”被下一个管道中的“no-op”阻止

Bash:“tee”被下一个管道中的“no-op”阻止
  • 当通过管道tee将其 stdout 发送到 no-op ( :) 命令时,则不会打印任何内容,并且文件大小为零。
  • 当通过管道tee将其标准输出发送到 a 时cat,所有内容都会正确打印,并且文件大小大于零。

这是一个显示它的代码示例(以脚本的第一个输入参数为条件):

#!/usr/bin/env bash

log_filepath="./log.txt"
[ -f "$log_filepath" ] && { rm "$log_filepath" || exit 1 ; }

fail_tee="$1"

while IFS= read -r -d $'\n' line ; do
    printf "%s%s\n" "prefix: " "$line"  | \
    tee -a "$log_filepath"              | \
    {
        if [ -n "$fail_tee" ]; then
            # Nothing is printed to stdout/terminal
            # $log_filepath size is ZERO.
            : # do nothing. 
        else
            # Each line in the input is prefixed w/ "prefix: " and sent to stdout
            # $log_filepath size is 46 bytes
            cat
        fi
    }
done <<'EOF'
1
23
456
7890
EOF

希望得到其背后的解释。
我对 no-op:命令的期望是它不应该阻止tee将输出发送到文件。

答案1

in仍然是一个进程:tee ... | :持有由 shell 设置的管道的读取端,其另一端正tee在写入。只是:立即退出,这会阻止它从管道中读取数据。 (为了使管道同时执行操作,shell 必须为管道的每个部分生成一个新进程,即使它只是为了处理 no-op :。在您的示例中,该进程将运行if最后一个中的语句管道的一部分,然后在“运行”:内置函数后最终退出。)

通常的行为是,当管道的读取器退出(读取端文件描述符关闭)时,写入器在下一次写入时收到 SIGPIPE 信号,这会导致其退出。

这通常是您想要的,因为这意味着如果管道的右侧退出,左侧也会退出,并且不会无用地继续可能很长的任务。或者(更糟糕的是)无助地试图写入阻塞的管道,该管道不允许任何写入,因为数据无处可去。

因为,tee看起来没有任何例外POSIX 规范;最接近的部分是提到文件操作数的写入错误:

如果对任何成功打开的文件操作数的写入失败,则对其他成功打开的文件操作数和标准输出的写入应继续,但退出状态应为非零。

如果 SIGPIPE 被忽略,我测试的实现将继续执行EPIPE然后从调用返回的错误write()

GNU coreutils 版本tee具有-p--output-error选项来控制写入失败时执行的操作:

未指定时的默认操作--output-error是在写入管道时发生错误时立即退出,并诊断写入非管道输出的错误。

虽然它退出的方式是通过 SIGPIPE,所以tee从忽略信号开始,它不会退出。

默认的-p模式是warn-nopipe“诊断写入任何非管道输出的错误”,而不是其他使其退出的选项。在幕后,它还会忽略 SIGPIPE 信号,然后停止尝试写入管道。

因此,至少对于 GNU 版本,您可以使用tee -p ... | ...它来防止管道读取器退出时退出。或者,您可以将右侧程序安排为模仿黑洞,例如cat > /dev/null(仍然显示写入它得到的所有内容,但内核最终会忽略写入的数据/dev/null)。

答案2

:不是一个 nop 进程。这并非cat毫无争议。不读取 stdin,并传递到 stdout(因为这不会是 nop。我看到它可以被视为 nop 管道阶段,但这是另一个想法)。这不是一个过程。

您正试图通过管道传递任何内容。我不知道外壳是做什么的,但如果管道没有连接到任何东西,我也不会感到惊讶。 (或者可能附加到外壳,并被忽略)。无论如何,我不希望发生任何好事。

正如@Kusalananda 所说。管道将等待其输出处的进程读取[或至少关闭管道],但没有进程读取或关闭管道。您可以通过管道连接到cat >/dev/null[(多态)。或者,如果您可以重定向 > /dev/null(静态)]。

其中之一(如果可能的话最好不要有猫)

| cat >/dev/null
>/dev/null

另一种多态解决方案(避免面包屑多余的猫):

if ...
then
  out=/dev/stdout
else
  out=/dev/null
fi

command >"$out"

答案3

由于 a:永远不会从 stdin 读取,因此它会产生 PIPE 信号。收到 PIPE 信号后,tee(默认情况下)退出。使用a:将会停止一切。

--output-error=warn如果使用 GNU tee(我假设您使用 bash 后已经有了)(不是 tee POSIX 选项),您可以通过添加到 tee 来看到这一点。

您可以使用以下方法进行测试:

$ echo "hello" | tee --output-error=warn | :
tee: 'standard output': Broken pipe

更改:cat >/dev/null将避免 PIPE 导致tee退出。

但:为什么使用 shell 循环处理文本被认为是不好的做法?

逐行读取文本文件(此处的文档)然后将其发送到管道并不是最好的选择。

考虑:

sed 's/^/preffix: /' <<'EOF' | tee -a "$log_filepath"
1
23
456
7890
EOF

您还可以根据变量的值在数组内设置 tee 选项。

并且还将输出设置在变量而不是此处文档中:

#!/bin/bash --

log_filepath="./log.txt"
[ -f "$log_filepath" ] && { rm "$log_filepath" || exit 1 ; }

enable_stdout="${1:+"yes"}"
enable_log="yes" # comment this line to avoid logging.

out=$'1\n23\n456\n7890\n'

unset teeoptions; teeoptions=()
[ -n "$enable_stdout" ] && teeoptions+=(-a "/dev/tty")
[ -n "$enable_log" ]    && teeoptions+=(-a "$log_filepath")

printf '%s' "$out" | 
    sed 's/^/preffix: /' | 
    tee "${teeoptions[@]}" >/dev/null

相关内容