92

是否可以在不使用临时文件的情况下在不同的变量中存储或捕获标准输出和标准错误?现在我这样做是为了在运行时获取 stdout inout和 stderr in ,但我想避免使用临时文件。errsome_command

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

18 回答 18

48

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

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) )"

where(echo std; echo err >&2)需要替换为实际的命令。stdout的输出$t_std逐行保存到数组中,省略换行符 (the -t) 和stderr into $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)except 的行为,它的值$?将我们带到最后一个修改:

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

使用GNU 在 Debian wheezy 上测试bash,版本4.2.37(1)-release (i486-pc-linux-gnu)

于 2013-08-06T17:20:41.030 回答
28

这是为了在不同的变量中捕获标准输出和标准错误。 如果你只想抓住stderr,保持stdout原样,有一个更好更短的解决方案

为了读者的利益总结一切这里有一个

易于重复使用的bash解决方案

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

: 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在严格模式下同样适用于我。唯一需要注意的是,上面的示例返回错误代码 3,在严格模式下,它调用 ERR 陷阱。因此,如果您运行一些set -e预期会返回任意错误代码(不仅仅是 0)的命令,您需要将返回代码捕获到某个变量 && ret=$? || ret=$?中,如下所示:

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

catch stdout stderr dummy 3 $'\ndifficult\n data \n\n\n' $'\nother\n difficult \n  data  \n\n' && ret=$? || ret=$?

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

讨论

问:它是如何工作的?

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

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

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

  • 让我们暂时跳过 theeval "$({和 the __2="$(。我稍后会谈到这个。

  • __1="$("$("${@:3}")"; } 2>&1;执行dummy 1 2a 3b并将其存储stdout__1以供以后使用。就这样__1变成了2a。它还重定向to stderr,以便外部捕获可以聚集dummystdoutstdout

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

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

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

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

  • stdout这将上述(即stderr调用的)捕获dummy到变量__2中。(我们可以在这里重复使用__1,但我曾经__2让它不那么混乱。)。所以__2变成3b

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

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

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

请注意,作为一种优化,我们也可以将这两个printf写成一个单独的,比如printf '%s=%q\n( exit %q )"$__2" "$ret"`。

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

我们已将以下内容写入 stderr:

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

where outis from $1, 2ais from stdoutof dummy, erris from $2, 3bis from stderrof dummy, the1来自返回码 from dummy

请注意,%q在格式中printf注意引用,这样 shell 在涉及到eval. 2a并且3b是如此简单,以至于它们被逐字复制。

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

这将执行以上所有输出 2 个变量和 的exit,捕获它(因此2>&1)并使用eval.

这样 2 个变量和返回码都被设置了。

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

  • 只要printf %q没有错误,它应该是安全的。但是你总是要非常小心,想想 ShellShock。

问:虫子?

  • 没有明显的错误已知,除了以下:

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

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

          target="$(readlink -e "$file")x"
          target="${target%x}"
      
    • Shell 变量不能携带字节 NUL ( $'\0')。stdout如果它们碰巧出现在or中,它们就会被忽略stderr

  • 给定的命令在子子 shell 中运行。所以它无法访问$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(). 并添加了它是如何工作的部分。

于 2016-12-09T22:08:40.443 回答
25

我认为在说“你不能”做某事之前,人们至少应该亲手尝试一下……</p>

简单而干净的解决方案,无需使用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)

要求: printf ,read

2. 一个简单的测试

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

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

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

将捕获stdout和的实际脚本stderrcapture.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_command发送to的标准输出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. 使其牢不可破

上面的解决方案依赖于一个字节作为andNUL之间的分隔符,因此如果出于任何原因包含其他字节,它将不起作用。stderrstdoutstderrNUL

尽管这永远不会发生,但可以通过在将两个输出传递给(清理)之前和之前剥离所有可能的NUL字节来使脚本完全牢不可破——字节无论如何都会丢失,因为不可能将它们存储到 shell 变量中stdoutstderrreadNUL

{
    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)

要求: printf , read,tr

编辑

我已经删除了另一个将退出状态传播到当前 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-)

要求: printf , read, tr,xargs

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

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

我们可以在末尾添加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,我们无法再清理 中的可能NUL字节stderr,因为其中现在还有我们的 NUL分隔符。

5. 保留退出状态 - 蓝图(未经清理)

在考虑了一些最终方法之后,我提出了一个解决方案,用于将退出代码和退出代码printf缓存为两个 stdout不同的参数,这样它们就不会干扰。

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

{
    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)

要求: exit , printf,read

6. 通过清理保持退出状态——牢不可破(重写)

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

在文件描述符之间进行了一些相当杂技的重定向之后,这就是我想出来的。

下面的代码是对我已删除的示例的重写。它还清理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)

要求: exit , printf, read,tr

这个解决方案非常强大。退出代码始终保持在不同的描述符中,直到它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)
}

要求: cat , exit, printf, read,tr

使用该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启动:然后我们在描述符 1 上有some_command' ,在描述符 2 上有' ,并且' 退出代码重定向到描述符 3stdoutsome_commandstderrsome_command
  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

要求: exit , printf,read

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
}

要求: cat , cut, exit, printf, read,tr

于 2020-01-04T16:56:41.583 回答
17

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

#!/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

您可以通过这种方式拥有多个后台进程,并在方便的时候异步收集它们的标准输出和标准错误等。

如果你只需要一个进程,你也可以使用硬编码的 fd 数字,比如 3 和 4,而不是{fdout}/{fderr}语法(它会为你找到一个空闲的 fd)。

于 2015-10-16T09:06:33.760 回答
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)

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

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|"
于 2015-03-01T16:47:40.537 回答
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 环境中执行。开头的空格很重要({是保留字)。

内部命令组的标准错误被重定向到标准输出(以便它适用于内部替换)。接下来,将 的 stdoutout分配给y,并由 捕获重定向的 stderr ,而不会像命令替换的子 shellx一样丢失。y

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

更新:现在 mksh 也支持。

于 2012-06-23T20:09:38.933 回答
4

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

问题是不要使用tempfiles。然而,这可能是由于/tmp/在 shell 死机的情况下对 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}";
}

它能做什么:

  • 它为和创建两个tempfiles 。然而,它几乎立即删除了这些,因此它们只存在很短的时间。stdoutstderr

  • catch_1()将 (FD 1)捕获stdout到变量中并移动stderrstdout,以便下一个(“左”)catch_1可以捕获它。

  • 处理catch是从右到左完成的,所以左边catch_1最后执行并捕获stderr

可能发生的最糟糕的情况是,一些临时文件出现在 上/tmp/,但在这种情况下它们总是空的。(它们在填充之前被移除。)。通常这应该不是问题,因为在 Linux 下,tmpfs 支持每 GB 主内存大约 128K 文件。

  • 给定的命令也可以访问和更改所有本地 shell 变量。所以你可以调用一个有副作用的shell函数!

  • tempfile这只会为调用分叉两次。

错误:

  • tempfile在失败的情况下缺少良好的错误处理。

  • 这通常会\n移除外壳。见评论catch_read()

  • 您不能使用文件描述符66将数据通过管道传输到您的命令。如果需要,请使用另一个描述符进行重定向,例如42(请注意,非常旧的 shell 只提供最多 9 个 FD)。

  • 这无法处理和中的 NUL 字节 ( $'\0') 。(NUL 只是被忽略。对于变体,NUL 后面的所有内容都被忽略。)stdoutstderrread

供参考:

  • Unix 允许我们访问已删除的文件,只要您保留对它们的一些引用(例如打开的文件句柄)。这样我们就可以打开然后删除它们。
于 2017-01-14T17:07:37.730 回答
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 -                                                                                                                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
于 2018-01-08T09:23:25.413 回答
3

这是一张显示@madmurphy非常简洁的解决方案如何工作的图表。

@madmurhy 的解决方案示意图

以及单线的缩进版本:

catch() {
  {
      IFS=$'\n' read -r -d '' "$out_var";
      IFS=$'\n' read -r -d '' "$err_var";
      (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
  )
}
于 2022-01-17T02:36:10.130 回答
2

简而言之,我相信答案是“不”。捕获$( ... )仅将标准输出捕获到变量;没有办法将标准错误捕获到单独的变量中。所以,你所拥有的就是尽可能的整洁。

于 2012-06-14T06:33:42.503 回答
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"
于 2015-12-10T17:04:52.260 回答
2

一种解决方法是标记输出流,合并它们,然后根据标记进行拆分,这很hacky,但可能比此页面上的一些建议更直观。例如,我们可以用“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 是受限制的形式,您可以想出一个与其允许的内容不冲突的标签。

于 2016-06-02T21:19:54.853 回答
1

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

以下似乎可能导致它在不创建任何临时文件且仅在 POSIX sh 上工作的情况下工作;但是它需要base64,并且由于编码/解码可能不是那么有效并且还使用“更大”的内存。

  • 即使在简单的情况下,当最后一个 stderr 行没有换行符时,它也会失败。至少在某些情况下,这可以通过将 exe 替换为“{ exe ; echo >&2 ; }”来解决,即添加一个换行符。
  • 然而,主要问题是一切似乎都很活泼。尝试使用类似的exe:

    exe() { 猫 /usr/share/hunspell/de_DE.dic 猫 /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 的 dash 和 bash 上)

于 2016-07-27T18:13:42.713 回答
1

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

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 时终止。由于我们已经删除了空值,因此它会在流关闭时结束,并返回非零值。由于这是预期的行为,我们添加|| :了“或真”的含义,以便该行始终评估为真 (0)
  3. 现在我知道流已经结束,可以安全地开始回显它了。
  4. echo -n "${buf}"是内置命令,因此不受参数长度限制
  5. 最后,在末尾添加一个空分隔符。
于 2020-01-22T18:10:21.250 回答
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 调用。

于 2014-06-18T21:53:16.613 回答
0

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

测试命令:

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

它本身就是:

%> ./xx.sh
stdout
stderr

现在,打印标准输出,将标准错误捕获到一个变量,并将标准输出记录到一个文件中

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

或将标准输出和捕获标准错误记录到变量中:

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

你明白了。

于 2016-03-05T01:28:25.197 回答
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}"
于 2020-12-17T18:14:04.060 回答
0

我已经在这里发布了我对这个问题的解决方案。它确实使用进程替换并且需要 Bash > v4,但也将 stdout、stderr 和返回代码捕获到您在当前范围内命名的变量中:

https://gist.github.com/pmarreck/5eacc6482bc19b55b7c2f48b4f1db4e8

这个练习的重点是让我可以在测试套件中断言这些东西。事实上,我整个下午都在弄清楚这个听起来很简单的事情......我希望这些解决方案中的一个可以帮助其他人!

于 2021-10-25T22:28:14.617 回答