将stdout和stderr捕获到不同的变量中


78

是否可以在不使用临时文件的情况下以不同的变量存储或捕获stdout和stderr ?现在,我这样做是为了outerr运行时输入stdout和stderr some_command,但是我想避免使用temp文件。

error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< $error_file)
rm $error_file

这个问题包括我的问题,但没有回答。
ntc2 2012年

3
首先,这可能在ksh93中。ksh -c 'function f { echo out; echo err >&2; }; x=${ { y=$(f); } 2>&1;}; typeset -p x y'
ormaaj 2012年

1
@ormaaj:您能解释一下ksh93技术如何工作吗?您可能需要使其成为答案。
乔纳森·勒夫勒


3
@gniourf_gniourf嗯,这个问题在这个问题的后面,所以,如果有重复的话,应该是那个问题:以bash捕获stdout和stderr:-D

Answers:


45

好的,它有点难看,但这是一个解决方案:

unset t_std t_err
eval "$( (echo std; echo err >&2) \
        2> >(readarray -t t_err; typeset -p t_err) \
         > >(readarray -t t_std; typeset -p t_std) )"

其中(echo std; echo err >&2)需要通过实际的命令来代替。通过省略换行()和stderr到的行,将stdout的输出保存到数组$t_std行中。-t$t_err

如果您不喜欢数组,可以这样做

unset t_std t_err
eval "$( (echo std; echo err >&2 ) \
        2> >(t_err=$(cat); typeset -p t_err) \
         > >(t_std=$(cat); typeset -p t_std) )"

它几乎模仿了行为,var=$(cmd)除了其值$?使我们进行了最后修改:

unset t_std t_err t_ret
eval "$( (echo std; echo err >&2; exit 2 ) \
        2> >(t_err=$(cat); typeset -p t_err) \
         > >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"

这里$?保存成$t_ret

喘鸣使用上Debian测试GNU bash,版本4.2.37(1)-release(1486-PC-Linux的GNU)


2
这就是为什么我将以相同的方式处理退货的原因。试试eval "$( eval "$@" 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"; exit $t_ret
TheConstructor

1
谢谢你的概念。我在这里对其进行了扩展(蒸馏):stackoverflow.com/a/28796214/2350426

1
typeset -p t_outtypeset -p t_err可能是混合的,撕心裂肺的输出没用。
2015年

1
@ 4ae1e1我考虑了这种可能性,但无法确认是否会发生这种情况。
TheConstructor

1
@TheConstructor嗯,我想你是对的。我在Zsh中使用>>()而不是> >()。前者在巴什(Bash)不行。在Zsh中,它可以正确解析出进程替换部分,但有时会发出错误的输出。不知道为什么,但> >()似乎工作可靠。我仍然没有完全相信。typeset -p绝对不是原子的,不是吗?
4ae1e1

21

这是为了将stdout和stderr捕获到不同的变量中。 如果您只想按原样赶上stderrstdout那么有一个更好,更短的解决方案

总结的一切读者的利益,这里是一个

易于重用的bash解决方案

此版本确实使用子外壳,并且不带tempfiles运行。(对于tempfile没有子shell的版本,请参阅我的其他答案。)

: catch STDOUT STDERR cmd args..
catch()
{
eval "$({
__2="$(
  { __1="$("${@:3}")"; } 2>&1;
  ret=$?;
  printf '%q=%q\n' "$1" "$__1" >&2;
  exit $ret
  )"
ret="$?";
printf '%s=%q\n' "$2" "$__2" >&2;
printf '( exit %q )' "$ret" >&2;
} 2>&1 )";
}

使用示例:

dummy()
{
echo "$3" >&2
echo "$2" >&1
return "$1"
}

catch stdout stderr dummy 3 $'\ndiffcult\n data \n\n\n' $'\nother\n difficult \n  data  \n\n'

printf 'ret=%q\n' "$?"
printf 'stdout=%q\n' "$stdout"
printf 'stderr=%q\n' "$stderr"

此打印

ret=3
stdout=$'\ndiffcult\n data '
stderr=$'\nother\n difficult \n  data  '

因此,无需深入思考即可使用它。只要放在catch VAR1 VAR2任何一个前面,就command args..可以完成。

有些if cmd args..; then会成为if catch VAR1 VAR2 cmd args..; then。真的没什么复杂的。

讨论区

问:如何运作?

它只是将其他答案中的想法包装到一个函数中,以便可以轻松地重用它。

catch()基本上eval用来设置两个变量。这类似于https://stackoverflow.com/a/18086548

考虑调用catch out err dummy 1 2a 3b

  • 让我们暂时跳过eval "$({__2="$(。稍后我会谈到。

  • __1="$("$("${@:3}")"; } 2>&1;执行dummy 1 2a 3b并将其stdout变成__1供以后使用。如此__1成为2a。它还重定向stderrdummystdout,使得外键锁可以收集stdout

  • ret=$?; 捕获退出代码,即 1

  • printf '%q=%q\n' "$1" "$__1" >&2;然后输出out=2astderrstderr在这里使用,因为目前stdout已接管的作用stderr的的dummy命令。

  • exit $ret然后将退出代码(1)转发到下一个阶段。

现在到外面__2="$( ... )"

  • stdout将上面的内容stderr(即dummy调用的内容)捕获到variable中__2。(我们可以__1在这里重复使用,但是我过去常常__2减少它的混乱。)。所以__2变成3b

  • ret="$?";再次捕获(返回的)返回代码1(来自dummy

  • printf '%s=%q\n' "$2" "$__2" >&2;然后输出err=3astderrstderr再次使用,因为它已经用于输出另一个变量out=2a

  • printf '( exit %q )' "$ret" >&2;然后输出代码以设置正确的返回值。我没有找到更好的方法,因为将其分配给变量需要一个变量名,然后该变量名不能用作的第一个或第二个参数catch

请注意,作为一种优化,我们也可以将这2个代码写成printf一个像printf '%s=%q\n( exit %q )“ $ __ 2”,“ $ ret”`之类的代码。

那么到目前为止我们有什么呢?

我们已经写信给stderr:

out=2a
err=3b
( exit 1 )

其中out是从$12a是从stdoutdummyerr是从$23b是从stderrdummy,并且1是从返回代码dummy

请注意,%q采用格式时要printf注意引用,以使shell在使用时可以看到正确的(单个)参数eval2a而且3b非常简单,因此可以照原样复制它们。

现在到外面eval "$({ ... } 2>&1 )";

这将执行以上所有操作,输出2个变量和exit,并捕获它(因此2>&1),然后使用将其解析到当前shell中eval

这样,将设置两个变量,并返回代码。

问:它使用eval哪个是邪恶的。这样安全吗?

  • 只要printf %q没有错误,就应该安全。但是,您始终必须非常小心,只需考虑一下ShellShock。

问:虫子?

  • 除以下情况外,没有已知的错误:

    • 捕获大输出需要大内存和CPU,因为所有内容都变成变量,并且需要由Shell进行反向解析。因此,请明智地使用它。

    • 照常$(echo $'\n\n\n\n') 吞下所有换行符,而不仅仅是最后一个。这是POSIX要求。如果需要使LF不受损害,则只需在输出中添加一些尾随字符,然后像下面的配方中一样将其删除(请参见尾部x,该尾部允许读取指向以结尾的文件的软链接$'\n'):

          target="$(readlink -e "$file")x"
          target="${target%x}"
      
    • Shell变量不能携带字节NUL($'\0')。如果它们恰好发生在stdout或中,它们将被忽略stderr

  • 给定的命令在子子外壳中运行。因此,它无权访问$PPID,也无法更改shell变量。您可以catch使用shell函数,甚至可以是内置函数,但是这些函数将无法更改shell变量(因为其中运行的所有内容$( .. )都无法做到这一点)。因此,如果您需要在当前shell中运行一个函数并捕获其stderr / stdout,则需要使用tempfiles的常规方法来执行此操作。(这样做的方法很多,使得打断外壳通常不会留下碎片,但这很复杂,应该自己解决。)

问:Bash版本?

  • 我认为您需要Bash 4及更高版本(由于printf %q

问:这看起来仍然很尴尬。

  • 对。 这里的另一个答案显示了如何ksh更干净地完成它。但是我并不习惯ksh,所以我将它留给别人创建一个类似的易于重用的配方ksh

问:为什么不使用ksh呢?

  • 因为这是一个bash解决方案

问:脚本可以改进吗

  • 当然,您可以挤出一些字节并创建更小或更难以理解的解决方案。就去吧;)

问:有错别字。 : catch STDOUT STDERR cmd args..应阅读# catch STDOUT STDERR cmd args..

  • 实际上这是有意的。 :bash -x默默地吞下评论时出现。因此,如果您在函数定义中碰到错字,就可以看到解析器在哪里。这是一个古老的调试技巧。但请注意,您可以在的参数内轻松创建一些整洁的副作用:

编辑:添加了更多内容;,使从中创建单线更加容易catch()。并添加了部分工作原理。


考虑到它使这种用法更易于使用,这是一个非常有趣的解决方案。但是,考虑到它不遵循其他建议解决方案的一般模式,您应该提供更多有关它如何工作的详细信息。
jwatkins

如何做这catch对于重定向数据流的一个或者管道命令?尝试捕获两个输出(其中一个是空的)似乎是有问题的(因为命令本身还是将其重定向)。但是,即使在某些情况下注定其中一个变量为空,它也可以使任何命令一遍又一遍地使用相同的模式(特别是如果该命令是外部提供的,并且您不知道它是否重定向)。
亚当·巴杜拉

到目前为止,我发现了一个简单的解决方法。只需定义一个简单的函数即可function echo_to_file { echo -n "$1" >"$2" ; },然后catch与该函数一起使用。可以正常工作。但是,仍然可以拥有它catch本身会很好。(可以执行类似的“技巧”来在命令中添加管道。)
Adam Badura

@AdamBadura的问题是并行捕获2个不同的变量。如果只想捕获一个变量,则catch在这里不需要它!直接在外壳程序中捕获单个变量:stdout + stderr : var="$(command 2>&1)"; echo "command gives $? and outputs '$var'"; 捕获stderr并重定向stdout :(var="$(command 2>&1 >FILE)">FILE 2>&1,这会将stderr重定向到FILE!);仅限stdout:var="$(command)"; echo "command gives $? and has stdout '$var'"stderr或其他FD看到另一个答案
Tino

在函数中catch,最终的printf语句不应该printf 'return %q\n' "$ret" >&2吗?想要函数catch返回cmd的退出代码,而不是退出程序。
吉姆·菲舍尔

15

此命令在当前正在运行的shell中设置stdout(stdval)和stderr(errval)值:

eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"

只要已定义此功能:

function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }

将execcommand更改为捕获的命令,例如“ ls”,“ cp”,“ df”等。


所有这些都是基于这样的想法,我们可以借助setval函数将所有捕获的值转换为文本行,然后使用setval捕获此结构中的每个值:

execcommand 2> CaptureErr > CaptureOut

将每个捕获值转换为setval调用:

execcommand 2> >(setval errval) > >(setval stdval)

将所有内容包装在execute调用中并回显它:

echo "$( execcommand 2> >(setval errval) > >(setval stdval) )"

您将获得每个setval创建的声明调用:

declare -- stdval="I'm std"
declare -- errval="I'm err"

要执行该代码(并获得vars集),请使用eval:

eval "$( execcommand 2> >(setval errval) > >(setval stdval) )"

最后回显集合变量:

echo "std out is : |$stdval| std err is : |$errval|

也可以包含返回(退出)值。
一个完整的bash脚本示例如下所示:

#!/bin/bash --

# The only function to declare:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }

# a dummy function with some example values:
function dummy { echo "I'm std"; echo "I'm err" >&2; return 34; }

# Running a command to capture all values
#      change execcommand to dummy or any other command to test.
eval "$( dummy 2> >(setval errval) > >(setval stdval); <<<"$?" setval retval; )"

echo "std out is : |$stdval| std err is : |$errval| return val is : |$retval|"

2
之所以存在竞争条件是因为declare,当整个输出超过1008个字节时,原子不会写(Ubuntu 16.04,bash 4.3.46(1))。在两个setvalstdout和stderr调用之间存在隐式同步(for stderrcat中的setvalfor无法在setvalfor stdout关闭stderr之前完成)。但是,没有同步setval retval,因此它可以介于两者之间。在这种情况下,retval被其他两个变量之一吞没。因此,该retval案例无法可靠运行。
Tino

我想我喜欢这种方法。有没有办法将该评估移到一个单独的函数并将命令传递给它?当我尝试这样做时,它不会声明errval或stdval。
贾斯汀

我制作了capturable(){...}(setval书面)和capture(){ eval "$( $@ 2> >(capturable stderr) > >(capturable stdout); )"; test -z "$stderr" }capture make ... && echo "$stdout" || less <<<"$stderr"页面stderr或如果没有则输出标准输出。这对您有用吗,如果可以,对您有帮助吗?
约翰P,

14

乔纳森有答案。供参考,这是ksh93的技巧。(需要非古代版本)。

function out {
    echo stdout
    echo stderr >&2
}

x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values

产生

x=stderr
y=stdout

${ cmds;}语法只是一个命令替换不创建一个子shell。这些命令在当前的shell环境中执行。开头的空格很重要({保留字)。

内部命令组的Stderr重定向到stdout(以便应用于内部替换)。接下来,将的stdoutout分配给y,并通过捕获重定向的stderr x,而不会通常丢失y命令替换子外壳程序。

在其他shell中是不可能的,因为捕获输出的所有构造都需要将生产者放入子shell,在这种情况下,该子shell将包括分配。

更新: mksh现在也支持。


2
谢谢。关键在于${ ... }它不是子外壳,其余部分易于解释。整洁的技巧,只要您有ksh必要使用。
乔纳森·莱夫勒2012年

10
这不是问题的答案。问题是关于bash,而您的答案在ksh上有效。
mshamma

1
@mshamma显然。阅读最后一段。
ormaaj 2012年

13

从技术上讲,命名管道不是临时文件,此处没有人提及它们。它们在文件系统中不存储任何内容,您可以在连接它们后立即将它们删除(这样就永远不会看到它们):

#!/bin/bash -e

foo () {
    echo stdout1
    echo stderr1 >&2
    sleep 1
    echo stdout2
    echo stderr2 >&2
}

rm -f stdout stderr
mkfifo stdout stderr
foo >stdout 2>stderr &             # blocks until reader is connected
exec {fdout}<stdout {fderr}<stderr # unblocks `foo &`
rm stdout stderr                   # filesystem objects are no longer needed

stdout=$(cat <&$fdout)
stderr=$(cat <&$fderr)

echo $stdout
echo $stderr

exec {fdout}<&- {fderr}<&- # free file descriptors, optional

您可以通过这种方式具有多个后台进程,并在方便的时间异步收集其stdout和stderr等。

如果只需要一个进程使用此代码,则最好也使用3和4之类的硬编码fd数字,而不要使用{fdout}/{fderr}语法(为您找到一个免费的fd)。


11

我认为在说“你做不到”之前,人们至少应该亲身尝试一下……

简单,干净的解决方案,无需使用eval任何异国情调的东西

1.最低版本

{
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(some_command)" 1>&2) 2>&1)

要求: printfread

2.一个简单的测试

用于生成stdout和的虚拟脚本stderruseless.sh

#!/bin/bash
#
# useless.sh
#

echo "This is stderr" 1>&2
echo "This is stdout" 

实际的脚本将捕获stdoutstderrcapture.sh

#!/bin/bash
#
# capture.sh
#

{
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(./useless.sh)" 1>&2) 2>&1)

echo 'Here is the captured stdout:'
echo "${CAPTURED_STDOUT}"
echo

echo 'And here is the captured stderr:'
echo "${CAPTURED_STDERR}"
echo

输出 capture.sh

Here is the captured stdout:
This is stdout

And here is the captured stderr:
This is stderr

3.工作原理

命令

(printf '\0%s\0' "$(some_command)" 1>&2) 2>&1

发送some_commandto的标准输出printf '\0%s\0',从而创建字符串\0${stdout}\n\0(其中\0是一个NUL字节,并且\n是一个新行字符);\0${stdout}\n\0然后将字符串重定向到标准错误,其中some_command已经存在的标准错误,从而组成了字符串${stderr}\n\0${stdout}\n\0,然后将其重定向回标准输出。

之后,命令

IFS=$'\n' read -r -d '' CAPTURED_STDERR;

开始读取字符串${stderr}\n\0${stdout}\n\0直到第一个NUL字节,然后将内容保存到中${CAPTURED_STDERR}。然后命令

IFS=$'\n' read -r -d '' CAPTURED_STDOUT;

继续读取同一字符串直到下一个NUL字节,并将内容保存到中${CAPTURED_STDOUT}

4.使其坚不可摧

上面的解决方案依赖于一个NUL字节用于之间的分隔符stderrstdout,因此它不会如果由于任何原因的工作stderr包含其他NUL字节。

尽管永远不会发生,但是可以通过将两个可能的NUL字节从中删除stdoutstderr然后再将两个输出都传递给read(清除),从而使脚本完全牢不可破-NUL字节总会丢失,因为无法将它们存储到shell变量中

{
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
} < <((printf '\0%s\0' "$((some_command | tr -d '\0') 3>&1- 1>&2- 2>&3- | tr -d '\0')" 1>&2) 2>&1)

要求: printfreadtr

编辑

我删除了另一个将退出状态传播到当前shell的示例,因为正如安迪(Andy)在评论中指出的那样,它不像预期的那样“牢不可破”(因为它不曾用于printf缓冲其中之一)。流)。为了记录,我将有问题的代码粘贴到此处:

保留退出状态(仍然坚不可摧)

以下变体还将的退出状态传播some_command到当前shell:

{
  IFS= read -r -d '' CAPTURED_STDOUT;
  IFS= read -r -d '' CAPTURED_STDERR;
  (IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}");
} < <((({ { some_command ; echo "${?}" 1>&3; } | tr -d '\0'; printf '\0'; } 2>&1- 1>&4- | tr -d '\0' 1>&4-) 3>&1- | xargs printf '\0%s\0' 1>&4-) 4>&1-)

要求: printfreadtrxargs

然后,安迪提交了以下“建议的编辑”以捕获退出代码:

简单干净的解决方案,节省退出价值

我们可以在的末尾添加stderr第三条信息,再NUL添加另一条信息以及exit命令的状态。将在之后stderr但之前输出stdout

{
  IFS= read -r -d '' CAPTURED_STDERR;
  IFS= read -r -d '' CAPTURED_EXIT;
  IFS= read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\n\0' "$(some_command; printf '\0%d' "${?}" 1>&2)" 1>&2) 2>&1)

他的解决方案似乎有效,但是存在一个小问题,即退出状态应放置在字符串的最后一个片段中,以便我们能够exit "${CAPTURED_EXIT}"在圆括号内启动而不污染全局范围,就像我在尝试中所做的那样。删除的示例。另一个问题是,随着他最内层的输出printf立即附加到stderrof some_command,我们再也无法清理NULin中的可能字节stderr,因为在这些字节中现在也有我们的 NUL定界符。

思考一些关于最终的办法后,我拿出一个解决方案,使用printf缓存 stdout和退出代码为两个不同的参数,所以,他们从来没有干涉。

我要做的第一件事是概述一种将退出状态传达给的第三个参数的方法printf,这是最简单的形式(即无需消毒),很容易做到。

5.保留退出状态–蓝图(不进行消毒)

{
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
    (IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(some_command)" "${?}" 1>&2) 2>&1)

要求: exitprintfread

但是,当我们尝试引入消毒时,事情变得非常混乱。tr实际上,启动清理流确实会覆盖我们之前的退出状态,因此,显然唯一的解决方案是将后者丢失之前将其重定向到一个单独的描述符,将其保留直到tr其工作两次,然后将其重定向回到其位置。

在文件描述符之间进行了一些非常杂技的重定向之后,这就是我想到的。

6.通过消毒保持出口状态–牢不可破(重写)

下面的代码是对我删除的示例的重写。它还清除NUL了流中可能的字节,因此read始终可以正常工作。

{
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    (IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(((({ some_command; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)

要求: exitprintfreadtr

这个解决方案确实很健壮。退出代码始终保持在不同的描述符中,直到printf直接作为单独的参数到达为止。

7.最终解决方案–具有退出状态的通用功能

我们还可以将上面的代码转换为通用函数。

# SYNTAX:
#   catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch() {
    {
        IFS=$'\n' read -r -d '' "${1}";
        IFS=$'\n' read -r -d '' "${2}";
        (IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
    } < <((printf '\0%s\0%d\0' "$(((({ ${3}; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
}

要求: catexitprintfreadtr

使用该catch功能,我们可以启动以下代码段,

catch MY_STDOUT MY_STDERR './useless.sh'

echo "The \`./useless.sh\` program exited with code ${?}"
echo

echo 'Here is the captured stdout:'
echo "${MY_STDOUT}"
echo

echo 'And here is the captured stderr:'
echo "${MY_STDERR}"
echo

并得到以下结果:

The `./useless.sh` program exited with code 0

Here is the captured stdout:
This is stderr 1
This is stderr 2

And here is the captured stderr:
This is stdout 1
This is stdout 2

8.在最后的例子中会发生什么

以下是快速模式化:

  1. some_command启动:我们再有some_command'Sstdout的描述符1some_commandstderr对描述符2和some_command重定向到请求3的退出码
  2. stdout通过管道输送tr(消毒)
  3. stderrstdout(临时使用描述符4)交换并通过管道传递给tr(清理)
  4. 退出代码(描述符3)与stderr(现在是描述符1)交换并通过管道传递给exit $(cat)
  5. stderr (现在为描述符3)重定向到描述符1,最后扩展为的第二个参数 printf
  6. 的退出代码exit $(cat)由的第三个参数捕获printf
  7. 的输出printf重定向到stdout已经存在的描述符2
  8. 的串联stdout和输出通过printf管道传递到read

9.符合POSIX的版本#1(易碎)

进程替换< <()语法)不是POSIX标准的(尽管实际上是)。在不支持< <()语法的shell中,达到相同结果的唯一方法是通过<<EOF … EOF语法。不幸的是,这不允许我们使用NUL字节作为分隔符,因为这些字节在到达之前会自动剥离read。我们必须使用其他定界符。自然选择落在CTRL+Z字符上(ASCII字符编号26)。这是一个易碎的版本(输出不得包含CTRL+Z字符,否则输出将变得混乱)。

_CTRL_Z_=$'\cZ'

{
    IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDERR;
    IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDOUT;
    (IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; exit ${_ERRNO_});
} <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(some_command)" "${?}" 1>&2) 2>&1)
EOF

要求: exitprintfread

10.兼容POSIX的版本#2(不可破坏,但不如非POSIX更好)

这里是它的牢不可破的版本,直接在函数形式(如果任一stdoutstderr包含CTRL+Z的字符,该流将被截断,但绝不会与其他的描述符交换)。

_CTRL_Z_=$'\cZ'

# SYNTAX:
#     catch_posix STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch_posix() {
    {
        IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${1}";
        IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${2}";
        (IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; return ${_ERRNO_});
    } <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(((({ ${3}; echo "${?}" 1>&3-; } | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 4>&2- 2>&1- | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
EOF
}

要求: catcutexitprintfreadtr


这是非常聪明的。
安迪

最新版本无法正常工作。find /proc以非超级用户身份尝试命令。以前的版本效果很好,因为您正在使用printf的“ buffer” stdout参数,从而确保在命令完成并且流式传输并刷新了100%的stderr之后才输出stdout。但是,最后一个版本没有使用printf来抛光其中一个流,而只是退出代码。Stderr和stdout是交错的,并且stderr仅包含一次冲洗的值。如果您修复了问题,那么对它的解释将不胜感激,因为在引入FD 4之后我迷路了
Andy

嗨,安迪!感谢您的评论和修改建议。capture.sh用第三个版本修补后,您的计算机上的输出是什么?
madmurphy

好的,我现在看到了。最后一个版本会截断stderr,对不对?
madmurphy

1
好点子。您的解决方案可以工作,但是有一个小问题:如果我们希望能够exit "${CAPTURED_EXIT}"在圆括号内执行操作而不退出全局范围,那么退出状态应该代表字符串的最后一部分,就像我在上一个示例中尝试做的那样。另一个问题是,由于您最里面的输出printf会立即附加到stderrof的后面some_command,因此我们无法再清理NULin中可能的字节stderr,因为在这些字节中也有我们的 NUL定界符。我会在接下来的几天考虑一下。
madmurphy

4

为了读者的利益,这里是使用tempfiles的解决方案。

问题是不使用tempfiles。但是,这可能是由于/tmp/外壳死了而对tempfile造成了不必要的污染。在kill -9某些情况下trap 'rm "$tmpfile1" "$tmpfile2"' 0不点火。

如果您处在可以使用tempfile的情况下,但又不想遗留碎屑,请按照以下说明操作。

再次调用它catch()(作为我的其他答案),并且具有相同的调用语法:

catch stdout stderr command args..

# Wrappers to avoid polluting the current shell's environment with variables

: catch_read returncode FD variable
catch_read()
{
eval "$3=\"\`cat <&$2\`\"";
# You can use read instead to skip some fork()s.
# However read stops at the first NUL byte,
# also does no \n removal and needs bash 3 or above:
#IFS='' read -ru$2 -d '' "$3";
return $1;
}
: catch_1 tempfile variable comand args..
catch_1()
{
{
rm -f "$1";
"${@:3}" 66<&-;
catch_read $? 66 "$2";
} 2>&1 >"$1" 66<"$1";
}

: catch stdout stderr command args..
catch()
{
catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}";
}

它能做什么:

  • tempfilestdout和创建两个stderr。但是,它几乎立即删除了这些内容,因此它们仅存在很短的时间。

  • catch_1()卡子stdout(FD 1)代入变量并移动stderrstdout,使得下一个(“左”)catch_1可以赶上。

  • 处理catch从右到左完成,因此左catch_1执行最后并catchs stderr

可能发生的最坏情况是,某些临时文件显示在上/tmp/,但在这种情况下它们始终为空。(它们在填充之前已被移除。)。通常这不是问题,因为在Linux下,tmpfs每GB主内存支持大约128K文件。

  • 给定的命令也可以访问和更改所有本地shell变量。因此,您可以调用具有副作用的shell函数!

  • 这只会拨叉两次tempfile

错误:

  • 如果tempfile失败,将丢失良好的错误处理。

  • 这会照常\n移除外壳。请参阅中的评论catch_read()

  • 您不能使用文件描述符66将数据传递到命令。如果需要,请使用另一个描述符进行重定向,例如42(请注意,非常老的外壳程序最多只能提供9个FD)。

  • 这不能处理NUL字节($'\0')中stdoutstderr。(NUL仅被忽略。对于read变体,NUL后面的所有内容均被忽略。)

仅供参考:

  • Unix允许我们访问已删除的文件,只要您对它们有所保留(例如打开的文件句柄)即可。这样,我们可以打开然后将其删除。

4

不喜欢eval,因此这是一个使用一些重定向技巧将程序输出捕获到变量,然后解析该变量以提取不同组件的解决方案。-w标志设置块大小,并影响中间格式的std-out / err消息的顺序。1以开销为代价提供潜在的高分辨率。

#######                                                                                                                                                                                                                          
# runs "$@" and outputs both stdout and stderr on stdin, both in a prefixed format allowing both std in and out to be separately stored in variables later.                                                                  
# limitations: Bash does not allow null to be returned from subshells, limiting the usefullness of applying this function to commands with null in the output.                                                                   
# example:                                                                                                                                                                                                                       
#  var=$(keepBoth ls . notHere)                                                                                                                                                                                                  
#  echo ls had the exit code "$(extractOne r "$var")"                                                                                                                                                                            
#  echo ls had the stdErr of "$(extractOne e "$var")"                                                                                                                                                                            
#  echo ls had the stdOut of "$(extractOne o "$var")"                                                                                                                                                                            
keepBoth() {                                                                                                                                                                                                                     
  (                                                                                                                                                                                                                              
    prefix(){                                                                                                                                                                                                                    
      ( set -o pipefail                                                                                                                                                                                                          
        base64 -w 1 - | (                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
          while read c                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
          do echo -E "$1" "$c"                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
          done                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
        )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
      )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
    }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    ( (                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
        "$@" | prefix o >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
        echo  ${PIPESTATUS[0]} | prefix r >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                           
      ) 2>&1 | prefix e >&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
    ) 3>&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
  )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

extractOne() { # extract                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
  echo "$2" | grep "^$1" | cut --delimiter=' ' --fields=2 | base64 --decode -                                                                                                                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

2

简而言之,我相信答案是“否”。捕获$( ... )只捕获标准输出到变量;没有办法将捕获的标准错误捕获到单独的变量中。因此,您所拥有的几乎是整洁的。


1
@ormaaj:从基于的答案来看eval,似乎确实可行,但是,正如您所指出的,它基本上可以归结为“使用更好的shell或语言”。这不是直接回答这个问题,但我来到这里同样的问题,我认为,长期的,我根据要切换到一个shell函数式语言,例如哈斯克尔
James Haigh 2015年

2

那... D

GET_STDERR=""
GET_STDOUT=""
get_stderr_stdout() {
    GET_STDERR=""
    GET_STDOUT=""
    unset t_std t_err
    eval "$( (eval $1) 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std) )"
    GET_STDERR=$t_err
    GET_STDOUT=$t_std
}

get_stderr_stdout "command"
echo "$GET_STDERR"
echo "$GET_STDOUT"

3
这似乎是第一个答案的包装,没有添加任何新功能。这有何不同/更有用?
ntc2

0

如果命令1)没有状态副作用,而2)在计算上便宜,那么最简单的解决方案是只运行两次。我主要将其用于在引导序列期间运行的代码,当您尚不知道磁盘是否可以正常工作时。在我的情况下,它很小,some_command因此两次运行都不会降低性能,并且该命令没有副作用。

主要好处是,这是干净且易于阅读的。这里的解决方案非常聪明,但是我讨厌成为必须维护包含更复杂解决方案的脚本的解决方案。如果您的方案可以解决问题,我建议您采用两次运行的简单方法,因为它更清洁,更易于维护。

例:

output=$(getopt -o '' -l test: -- "$@")
errout=$(getopt -o '' -l test: -- "$@" 2>&1 >/dev/null)
if [[ -n "$errout" ]]; then
        echo "Option Error: $errout"
fi

同样,这也是可以的,因为getopt没有副作用。我知道这是性能安全的,因为我的父代码在整个程序中调用此事件的次数少于100次,并且用户永远不会注意到100个getopt调用与200个getopt调用。


你能举个例子吗?我猜像out=$(some_command)err=$(some_command 2>&1 1>/dev/null)
ntc2 2014年

@eicto-那么您将不得不使用上述解决方案之一-仅当您的命令没有副作用且计算上便宜时
这才

1
我怀疑是否存在许多需要单独处理stdoutstderr没有副作用的用例-即使命令在正常情况下是确定性的,错误也不是正常情况。这种方法也很可能容易出现比赛条件。
James Haigh,2015年

0

这是一个较简单的变体,它不是OP想要的,但不同于其他任何选项。您可以通过重新排列文件描述符来获得所需的任何内容。

测试命令:

%> cat xx.sh  
#!/bin/bash
echo stdout
>&2 echo stderr

它本身会:

%> ./xx.sh
stdout
stderr

现在,打印stdout,将stderr捕获到变量,并将stdout记录到文件

%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out")
stdout
%> cat out    
stdout
%> echo
$err 
stderr

或将stdout记录并捕获stderr到变量中:

export err=$(./xx.sh 3>&1 1>out 2>&3 )
%> cat out
stdout
%> echo $err
stderr

你明白了。


0

一种变通办法是比较棘手的,但比该页面上的某些建议更直观,它是标记输出流,合并它们,然后基于这些标记进行拆分。例如,我们可以用“ STDOUT”前缀标记stdout:

function someCmd {
    echo "I am stdout"
    echo "I am stderr" 1>&2
}

ALL=$({ someCmd | sed -e 's/^/STDOUT/g'; } 2>&1)
OUT=$(echo "$ALL" | grep    "^STDOUT" | sed -e 's/^STDOUT//g')
ERR=$(echo "$ALL" | grep -v "^STDOUT")

```

如果您知道stdout和/或stderr的格式受限制,则可以拿出一个与其允许的内容不冲突的标签。


有没有更通用的方法可以对所有输出都适用,请参阅我对这个问题的回答。
mncl

难道是这种sed解释的风险someCmd?潜在的有害代码执行?
adrelanos

以上示例中的@adrelanos AFAIKsed将仅解释字符串参数,即s/^/STDOUT/gs/^STDOUT//g。由于这些是固定的,已知的字符串,因此没有注入/不需要的执行向量。的stdout和stderrsomeCmd将流过;的stdin和stdout sed;它们将被编辑但不会执行。对的呼叫也是如此grep
Warbo

@adrelanos请注意,我假设的stdout和stderrsomeCmd永远不会包含以“ sentinel” text开头的行STDOUT。如果这不成立,我们可以选择其他哨兵。但是,如果输出是任意的(例如,用户定义的),则无法使用此方法,因为无法从数据中区分任何前哨文本。
Warbo

0

警告:还没有(工作?)!

以下似乎是导致它不创建任何临时文件而仅在POSIX sh上运行的可能原因;它需要base64,但是由于编码/解码的效率可能不高,因此还使用“更大”的内存。

  • 即使在简单的情况下,当最后一个stderr行没有换行符时,它也已经失败了。至少在某些情况下,可以通过用“ {exe; echo>&2;}”替换exe(即添加换行符)来解决此问题。
  • 然而,主要的问题是,一切似乎都风趣。尝试使用以下exe文件:

    exe(){cat /usr/share/hunspell/de_DE.dic cat /usr/share/hunspell/en_GB.dic>&2}

并且您会看到,例如base64编码行的某些部分在文件的顶部,部分在末尾,而未解码的stderr内容在中间。

好吧,即使以下想法无法实现(我认为是可行的),对于那些错误地认为自己可以像这样工作的人来说,它也可以作为反例。

想法(或反例):

#!/bin/sh

exe()
{
        echo out1
        echo err1 >&2
        echo out2
        echo out3
        echo err2 >&2
        echo out4
        echo err3 >&2
        echo -n err4 >&2
}


r="$(  { exe  |  base64 -w 0 ; }  2>&1 )"

echo RAW
printf '%s' "$r"
echo RAW

o="$( printf '%s' "$r" | tail -n 1 | base64 -d )"
e="$( printf '%s' "$r" | head -n -1  )"
unset r    

echo
echo OUT
printf '%s' "$o"
echo OUT
echo
echo ERR
printf '%s' "$e"
echo ERR

提供(使用stderr-newline修复程序):

$ ./ggg 
RAW
err1
err2
err3
err4

b3V0MQpvdXQyCm91dDMKb3V0NAo=RAW

OUT
out1
out2
out3
out4OUT

ERR
err1
err2
err3
err4ERR

(至少在Debian的破折号上)


0

这是@madmurphy解决方案的一种变体,应适用于任意大的stdout / stderr流,维护出口返回值,并处理流中的null(将它们转换为换行符)

function buffer_plus_null()
{
  local buf
  IFS= read -r -d '' buf || :
  echo -n "${buf}"
  printf '\0'
}

{
    IFS= time read -r -d '' CAPTURED_STDOUT;
    IFS= time read -r -d '' CAPTURED_STDERR;
    (IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}");
} < <((({ { some_command ; echo "${?}" 1>&3; } | tr '\0' '\n' | buffer_plus_null; } 2>&1 1>&4 | tr '\0' '\n' | buffer_plus_null 1>&4 ) 3>&1 | xargs printf '%s\0' 1>&4) 4>&1 )

缺点:

  • read命令是操作中最昂贵的部分。例如:find /proc在运行500个进程的计算机上,花费20秒(而该命令仅为0.5秒)。第一次读取需要10秒,第二次读取需要10秒,使总时间增加了一倍。

缓冲区说明

最初的解决方案是printf缓冲缓冲区的参数,但是由于需要退出代码排在最后,一种解决方案是缓冲stdout和stderr。我尝试过,xargs -0 printf但是随后您很快就开始达到“最大参数长度限制”。因此,我决定一个解决方案是编写一个快速缓冲功能:

  1. 使用read到流存储在一个变量
  2. read当流结束,或收到空将终止。由于我们已经删除了null,因此它在流关闭时结束,并返回非零值。由于这是预期的行为,因此我们添加了|| :“或true”的含义,以便该行始终求值为true(0)
  3. 现在我知道流已结束,可以安全地开始将其回显。
  4. echo -n "${buf}" 是内置命令,因此不限于参数长度限制
  5. 最后,在末尾添加一个空分隔符。

0

实时输出并写入文件:

#!/usr/bin/env bash

# File where store the output
log_file=/tmp/out.log

# Empty file
echo > ${log_file}

outToLog() {
  # File where write (first parameter)
  local f="$1"
  # Start file output watcher in background
  tail -f "${f}" &
  # Capture background process PID
  local pid=$!
  # Write "stdin" to file
  cat /dev/stdin >> "${f}"
  # Kill background task
  kill -9 ${pid}
}

(
  # Long execution script example
  echo a
  sleep 1
  echo b >&2
  sleep 1
  echo c >&2
  sleep 1
  echo d
) 2>&1 | outToLog "${log_file}"

# File result
echo '==========='
cat "${log_file}"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.