在Bash中管道输出和捕获退出状态

时间:2009-08-03 11:31:52

标签: bash shell error-handling pipe

我想在Bash中执行一个长时间运行的命令,并且都捕获其退出状态,并输出tee

所以我这样做:

command | tee out.txt
ST=$?

问题是变量ST捕获tee的退出状态而不是命令的退出状态。我该如何解决这个问题?

请注意,命令长时间运行并将输出重定向到文件以便以后查看它对我来说不是一个好的解决方案。

15 个答案:

答案 0 :(得分:470)

有一个名为$PIPESTATUS的内部Bash变量;它是一个数组,用于保存最后一个前台命令管道中每个命令的退出状态。

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

或者另一个也适用于其他shell(如zsh)的替代方法是启用pipefail:

set -o pipefail
...

由于语法略有不同,第一个选项zsh一起使用。

答案 1 :(得分:137)

使用bash set -o pipefail是有帮助的

  

pipefail:管道的返回值是状态      以非零状态退出的最后一个命令,      如果没有以非零状态退出命令,则为零

答案 2 :(得分:108)

哑解决方案:通过命名管道(mkfifo)连接它们。然后命令可以第二次运行。

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?

答案 3 :(得分:34)

有一个数组可以为您提供管道中每个命令的退出状态。

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1

答案 4 :(得分:21)

此解决方案无需使用bash特定功能或临时文件即可运行。奖励:最后退出状态实际上是退出状态,而不是文件中的某些字符串。

情况:

someprog | filter

您希望退出状态来自someprog,输出来自filter

这是我的解决方案:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

请参阅my answer for the same question on unix.stackexchange.com以获取详细说明,并提供一个没有子弹和一些注意事项的备选方案。

答案 5 :(得分:19)

通过在子shell中组合PIPESTATUS[0]和执行exit命令的结果,您可以直接访问初始命令的返回值:

command | tee ; ( exit ${PIPESTATUS[0]} )

以下是一个例子:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

会给你:

return value: 1

答案 6 :(得分:8)

所以我想提供像lesmana这样的答案,但我认为我的可能是一个更简单,更有利的纯Bourne-shell解决方案:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

我认为这最好从内到外解释 - command1将在stdout(文件描述符1)上执行并打印其常规输出,然后一旦完成,printf将执行并打印icommand1&#39; s退出其stdout上的代码,但该stdout被重定向到文件描述符3。

当command1正在运行时,它的stdout被传送到command2(printf&#39; s输出永远不会使它成为command2,因为我们将它发送到文件描述符3而不是1,这是管道读取的内容)。然后我们将command2的输出重定向到文件描述符4,这样它也不会出现在文件描述符1之外 - 因为我们希望稍后释放文件描述符1,因为我们将把文件描述符3上的printf输出带回来下到文件描述符1 - 因为命令替换(反引号)将捕获什么,以及将被置于变量中的内容。

魔术的最后一点是我们作为单独命令执行的第一个exec 4>&1 - 它打开文件描述符4作为外部shell的标准输出的副本。命令替换将从其中的命令的角度捕获在标准输出上写入的任何内容 - 但是由于命令替换涉及命令替换不会导致命令2的输出转到文件描述符4,因此命令替换不会抓住它 - 然而一旦它得到&#34; out&#34;对于命令替换,它实际上仍然会转到脚本的整个文件描述符1。

exec 4>&1必须是一个单独的命令,因为当你尝试写入命令替换中的文件描述符时,许多常见的shell都不喜欢它,这是在&#34;使用替换的外部&#34;命令。所以这是最简单的可移植方式。)

您可以用技术性较低且更有趣的方式查看它,就像命令的输出相互跳跃一样:command1管道到命令2,然后printf的输出跳过命令2,这样命令2就不会#39; t捕获它,然后命令2的输出跳过命令替换,就像printf及时降落以被替换捕获一样,以便它最终在变量中,并命令2&#39 ; s输出以正常的方式写入标准输出,就像在普通管道中一样。

另外,据我所知,$?仍将包含管道中第二个命令的返回码,因为变量赋值,命令替换和复合命令对命令的返回码都是有效透明的在它们内部,所以command2的返回状态应该传播出来 - 这个,而不必定义一个额外的函数,这就是为什么我认为这可能是一个比lesmana提出的更好的解决方案。

根据lesmana提到的警告,command1可能会在某些时候最终使用文件描述符3或4,所以为了更加健壮,你会这样做:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

请注意,我在我的示例中使用了复合命令,但子shell(使用( )而不是{ }也可以使用,但可能效率较低。)

命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符4,复合命令后跟3>&1将继承文件描述符3。因此4>&-确保内部复合命令不会继承文件描述符四,而3>&-将不会继承文件描述符三,因此command1获得了更清晰的标准,更标准环境。您也可以移动4>&-旁边的内部3>&-,但我想为什么不尽可能地限制其范围。

我不确定直接使用文件描述符三和四的频率 - 我认为大多数时候程序都使用系统调用来返回当前未使用的文件描述符,但有时代码写入文件直接描述符3,我想(我可以想象一个程序检查一个文件描述符,看看它是否打开,如果是,则使用它,如果不是则表现不同)。所以后者可能最好记住并用于通用案例。

答案 7 :(得分:5)

在Ubuntu和Debian中,你可以apt-get install moreutils。它包含一个名为mispipe的实用程序,它返回管道中第一个命令的退出状态。

答案 8 :(得分:3)

管道命令返回后,必须立即将PIPESTATUS [@]复制到数组中。 任何读取PIPESTATUS [@]都会删除内容。 如果计划检查所有管道命令的状态,请将其复制到另一个阵列。 &#34; $&#34?;与&#34; $ {PIPESTATUS [@]}&#34;的最后一个元素的值相同, 阅读它似乎会破坏&#34; $ {PIPESTATUS [@]}&#34;,但我还没有完全证实这一点。

declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=( "${PIPESTATUS[@]}" )

如果管道位于子shell中,则无效。为了解决这个问题,
bash pipestatus in backticked command?

答案 9 :(得分:3)

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.AuthenticationScheme;
import org.springframework.stereotype.Service;

@Service
public class KeycloakClientCredentialsConfig {

    @Value("${keycloak.realm}")
    private String realm;

    @Value("${keycloak.auth-server-url}")
    private String authServerUrl;

    @Value("${keycloak.resource}")
    private String clientId;

    @Value("${keycloak.credentials.secret}")
    private String clientSecret;

    @Bean
    public KeycloakClientCredentialsRestTemplate createRestTemplate() {
        return new KeycloakClientCredentialsRestTemplate(getClientCredentialsResourceDetails(),
                new DefaultOAuth2ClientContext());
    }

    private ClientCredentialsResourceDetails getClientCredentialsResourceDetails() {
        String accessTokenUri = String.format("%s/realms/%s/protocol/openid-connect/token",
            authServerUrl, realm);
        List<String> scopes = new ArrayList<String>(0); // TODO introduce scopes

        ClientCredentialsResourceDetails clientCredentialsResourceDetails = 
                new ClientCredentialsResourceDetails();

        clientCredentialsResourceDetails.setAccessTokenUri(accessTokenUri);
        clientCredentialsResourceDetails.setAuthenticationScheme(AuthenticationScheme.header);
        clientCredentialsResourceDetails.setClientId(clientId);
        clientCredentialsResourceDetails.setClientSecret(clientSecret);
        clientCredentialsResourceDetails.setScope(scopes);

        return clientCredentialsResourceDetails;
    }

}

与@ cODAR的答案不同,它返回第一个命令的原始退出代码,不仅为0表示成功,127表示失败。但正如@Chaoran指出的那样,你可以打电话给(command | tee out.txt; exit ${PIPESTATUS[0]}) 。然而,重要的是将所有内容放入括号中。

答案 10 :(得分:2)

在bash之外,你可以这样做:

bash -o pipefail  -c "command1 | tee output"

这在例如预期shell为/bin/sh的忍者脚本中很有用。

答案 11 :(得分:2)

在普通bash中执行此操作的最简单方法是使用process substitution而不是管道。有几点不同,但它们对您的用例可能并不重要:

  • 运行管道时,bash会等待所有进程完成。
  • 将Ctrl-C发送到bash会使其终止管道的所有进程,而不仅仅是主要进程。
  • pipefail选项和PIPESTATUS变量与流程替换无关。
  • 可能更多

通过流程替换,bash只是启动流程并忘记它,它在jobs中甚至都不可见。

除了提到的差异外,consumer < <(producer)producer | consumer基本上是等价的。

如果你想翻转哪一个是&#34;主要&#34;过程中,您只需将命令和替换方向翻转到producer > >(consumer)即可。在你的情况下:

command > >(tee out.txt)

示例:

$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world

$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world

正如我所说,与管道表达存在差异。该过程可能永远不会停止运行,除非它对管道关闭敏感。特别是,它可能会继续向你的标准写入内容,这可能会令人困惑。

答案 12 :(得分:1)

纯壳解决方案:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
hello world

现在将第二个cat替换为false

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141

请注意第一只猫也失败了,因为它的标准输出已关闭。在此示例中,日志中失败命令的顺序是正确的,但不要依赖它。

此方法允许捕获单个命令的stdout和stderr,这样您就可以在发生错误时将其转储到日志文件中,或者只是在没有错误时删除它(如dd的输出)。

答案 13 :(得分:1)

基于@ brian-s-wilson的回答;这个bash帮助函数:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n "$*"
  then test "$*" = "${S[*]}"
  else ! [[ "${S[@]}" =~ [^0\ ] ]]
  fi
}

因此使用:

1:get_bad_things必须成功,但它不应该产生输出;但我们希望看到它产生的输出

get_bad_things | grep '^'
pipeinfo 0 1 || return

2:所有管道必须成功

thing | something -q | thingy
pipeinfo || return

答案 14 :(得分:1)

使用外部命令有时可能更简单,更清晰,而不是深入研究bash的细节。 pipeline,来自最小进程脚本语言execline,使用第二个命令*的返回代码退出,就像sh管道一样,但与sh不同,它允许反转管道的方向,以便我们可以捕获生产者进程的返回代码(下面全部在sh命令行,但安装了execline):

$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world

$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt 
hello world

$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world

$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1

$ pipeline -w tee out.txt "" true; echo $?
0

$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world

使用pipeline与本机bash管道具有相同的差异,就像回答#43972501中使用的bash流程替换一样。

*除非出现错误,否则pipeline根本不会退出。它执行到第二个命令,因此它是执行返回的第二个命令。