ntc*_*tc2 62 bash shell command-line io-redirection
是否可以在不使用临时文件的情况下在不同的变量中存储或捕获stdout和stderr ?现在我这样做是为了out在err运行时获取stdout 和stderr some_command,但是我想避开临时文件.
error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< error_file)
rm $error_file
Run Code Online (Sandbox Code Playgroud)
The*_*tor 35
好吧,它有点难看,但这是一个解决方案:
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) )"
Run Code Online (Sandbox Code Playgroud)
其中(echo std; echo err >&2)需要通过实际的命令来代替.的输出标准输出被保存到数组中$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) )"
Run Code Online (Sandbox Code Playgroud)
它几乎模仿了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 )"
Run Code Online (Sandbox Code Playgroud)
这里$?保存成$t_ret
使用GNU bash,版本4.2.37(1)-release(i486-pc-linux-gnu)在Debian wheezy上测试.
orm*_*aaj 15
乔纳森有答案.作为参考,这是ksh93技巧.(需要非古代版本).
function out {
echo stdout
echo stderr >&2
}
x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values
Run Code Online (Sandbox Code Playgroud)
产生
x=stderr
y=stdout
Run Code Online (Sandbox Code Playgroud)
该${ cmds;}语法只是一个命令替换不创建一个子shell.这些命令在当前的shell环境中执行.开头的空间很重要({是一个保留字).
内部命令组的Stderr被重定向到stdout(因此它适用于内部替换).接下来,stdout out被分配给y,并且重定向的stderr被捕获x,而没有通常丢失y到命令替换的子shell.
在其他shell中是不可能的,因为捕获输出的所有构造都需要将生成器放入子shell中,在这种情况下,它将包括赋值.
更新:现在也受mksh支持.
小智 14
此命令在当前运行的shell中设置stdout(stdval)和stderr(errval)值:
eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"
Run Code Online (Sandbox Code Playgroud)
如果已定义此功能:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }
Run Code Online (Sandbox Code Playgroud)
将execcommand更改为捕获的命令,无论是"ls","cp","df"等.
所有这些都是基于我们可以在函数setval的帮助下将所有捕获的值转换为文本行的想法,然后setval用于捕获此结构中的每个值:
execcommand 2> CaptureErr > CaptureOut
Run Code Online (Sandbox Code Playgroud)
将每个捕获值转换为setval调用:
execcommand 2> >(setval errval) > >(setval stdval)
Run Code Online (Sandbox Code Playgroud)
将所有内容包装在执行调用中并回显它:
echo "$( execcommand 2> >(setval errval) > >(setval stdval) )"
Run Code Online (Sandbox Code Playgroud)
您将获得每个setval创建的声明调用:
declare -- stdval="I'm std"
declare -- errval="I'm err"
Run Code Online (Sandbox Code Playgroud)
要执行该代码(并设置vars),请使用eval:
eval "$( execcommand 2> >(setval errval) > >(setval stdval) )"
Run Code Online (Sandbox Code Playgroud)
最后回应设置的变量:
echo "std out is : |$stdval| std err is : |$errval|
Run Code Online (Sandbox Code Playgroud)
还可以包括return(退出)值.
完整的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|"
Run Code Online (Sandbox Code Playgroud)
mad*_*phy 14
我想在说“你不能”做某事之前,人们至少应该亲自尝试一下……
eval或任何异国情调{
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(some_command)" 1>&2) 2>&1)
Run Code Online (Sandbox Code Playgroud)
要求: printf ,read
stdout和的虚拟脚本stderr:useless.sh#!/bin/bash
#
# useless.sh
#
echo "This is stderr" 1>&2
echo "This is stdout"
Run Code Online (Sandbox Code Playgroud)
stdout和的实际脚本stderr:capture.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
Run Code Online (Sandbox Code Playgroud)
capture.shHere is the captured stdout:
This is stdout
And here is the captured stderr:
This is stderr
Run Code Online (Sandbox Code Playgroud)
命令
Here is the captured stdout:
This is stdout
And here is the captured stderr:
This is stderr
Run Code Online (Sandbox Code Playgroud)
发送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,然后将其重定向回标准输出。
之后,命令
(printf '\0%s\0' "$(some_command)" 1>&2) 2>&1
Run Code Online (Sandbox Code Playgroud)
开始读取字符串${stderr}\n\0${stdout}\n\0直到第一个NUL字节并将内容保存到${CAPTURED_STDERR}. 然后命令
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
Run Code Online (Sandbox Code Playgroud)
继续读取相同的字符串直到下一个NUL字节并将内容保存到${CAPTURED_STDOUT}.
上面的解决方案依赖于一个NUL字节用于之间的分隔符stderr和stdout,因此它不会如果由于任何原因的工作stderr包含其他NUL字节。
尽管这永远不会发生,但可以通过在将两个输出传递给(清理)之前和之前剥离所有可能的NUL字节来使脚本完全牢不可破——字节无论如何都会丢失,因为不可能将它们存储到 shell 变量中:stdoutstderrreadNUL
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
Run Code Online (Sandbox Code Playgroud)
需要: printf , read,tr
我删除了另一个用于将退出状态传播到当前 shell 的示例,因为正如Andy在评论中指出的那样,它并不像预期的那样“牢不可破”(因为它没有printf用于缓冲其中之一)流)。为了记录,我在这里粘贴有问题的代码:
保持退出状态(仍然牢不可破)
以下变体还将 的退出状态传播
some_command到当前 shell:Run Code Online (Sandbox Code Playgroud){ 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,xargs
然后,安迪提交了以下“建议编辑”以捕获退出代码:
保存退出值的简单而干净的解决方案
我们可以在 末尾添加
stderr第三条信息,另外NUL加上exit命令的状态。它将在之后stderr但之前输出stdoutRun Code Online (Sandbox Code Playgroud){ 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-)
他的解决方案似乎有效,但有一个小问题,即退出状态应作为字符串的最后一个片段放置,以便我们能够exit "${CAPTURED_EXIT}"在圆括号内启动而不污染全局范围,正如我在删除的例子。另一个问题是,由于他最里面的输出printf立即附加到stderrof 中some_command,我们无法再清理 中可能的NUL字节stderr,因为在这些字节中现在也有我们的 NUL分隔符。
思考一些关于最终的办法后,我拿出一个解决方案,使用printf缓存都 stdout和退出代码为两个不同的参数,所以,他们从来没有干涉。
我做的第一件事是概述一种将退出状态传达给 的第三个参数的方法printf,这是很容易以最简单的形式(即没有清理)做的事情。
{
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)
Run Code Online (Sandbox Code Playgroud)
需要: exit , printf,read
但是,当我们尝试引入消毒时,事情会变得非常混乱。tr为清理流而启动实际上会覆盖我们之前的退出状态,因此显然唯一的解决方案是在后者丢失之前将其重定向到单独的描述符,将其保留在那里直到tr完成其工作两次,然后将其重定向回其位置.
在文件描述符之间进行了一些相当复杂的重定向之后,这就是我得出的结论。
下面的代码是对我已删除的示例的重写。它还可以清理NUL流中可能的字节,以便read始终正常工作。
{
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)
Run Code Online (Sandbox Code Playgroud)
需要: exit , printf, read,tr
这个解决方案非常强大。退出代码总是在不同的描述符中保持分离,直到它printf作为单独的参数直接到达。
我们还可以将上面的代码转换为通用函数。
{
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)
Run Code Online (Sandbox Code Playgroud)
需要: cat , exit, printf, read,tr
使用该catch函数,我们可以启动以下代码段,
# 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)
}
Run Code Online (Sandbox Code Playgroud)
并得到以下结果:
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
Run Code Online (Sandbox Code Playgroud)
下面是一个快速的示意图:
some_command启动:我们再有some_command'Sstdout的描述符1some_command的stderr对描述符2和some_command重定向到请求3的退出码stdout被输送到tr(消毒)stderr与stdout(临时使用描述符 4)交换并通过管道传输到tr(消毒)stderr(现在是描述符 1)交换并通过管道传送到exit $(cat)stderr (现在描述符 3)被重定向到描述符 1,结束扩展为第二个参数 printfexit $(cat)由第三个参数捕获printfprintf被重定向到描述符 2,那里stdout已经存在stdout和 的输出通过printf管道传输到read进程替换(< <()语法)不是 POSIX 标准的(尽管它们实际上是)。在不支持< <()语法的shell 中,获得相同结果的唯一方法是通过<<EOF … EOF语法。不幸的是,这不允许我们使用NUL字节作为分隔符,因为它们在到达read. 我们必须使用不同的分隔符。自然选择落在CTRL+Z字符上(ASCII 字符编号 26)。这是一个可破解的版本(输出不得包含该CTRL+Z字符,否则它们会混在一起)。
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
Run Code Online (Sandbox Code Playgroud)
需要: exit , printf,read
这是它的牢不可破的版本,直接以函数形式(如果其中一个stdout或stderr包含CTRL+Z字符,则流将被截断,但永远不会与另一个描述符交换)。
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
Run Code Online (Sandbox Code Playgroud)
需要: cat , cut, exit, printf, read,tr
Tin*_*ino 13
stderr解决方案这个版本确实使用子shell并且没有stdouts 运行.(对于bash没有子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 )";
}
Run Code Online (Sandbox Code Playgroud)
使用示例:
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"
Run Code Online (Sandbox Code Playgroud)
这打印
ret=3
stdout=$'\ndiffcult\n data '
stderr=$'\nother\n difficult \n data '
Run Code Online (Sandbox Code Playgroud)
所以它可以在没有深入思考的情况下使用.只要放在tempfile任何一个面前,tempfile你就完成了.
有些人catch VAR1 VAR2会成为command args...真的没什么复杂的.
问:它是如何运作的?
它只是将其他答案中的想法包含在一个函数中,以便可以轻松地重用它.
if cmd args..; then主要用于if catch VAR1 VAR2 cmd args..; then设置两个变量.这类似于/sf/answers/1266058391/
考虑一下catch():
让我们跳过了eval和catch out err dummy 1 2a 3b现在.我稍后会谈到这个.
eval "$({执行__2="$(并存储它__1="$("$("${@:3}")"; } 2>&1;以dummy 1 2 3供以后使用.因此stdout变得__1.它还重定向__1的2a到stderr,使得外键锁可以收集dummy
stdout 捕获退出代码,即 stdout
ret=$?;然后输出1到printf '%q=%q\n' "$1" "$__1" >&2;. out=2a在这里使用,因为目前stderr已接管的作用stderr的的stdout命令.
stderr然后将退出代码(dummy)转发到下一个阶段.
现在到外面exit $ret:
这捕获1以上,这是的__2="$( ... )"所述的stdout呼叫,为可变stderr.(我们可以dummy在这里重复使用,但过去常常__2让它不那么混乱.)因此__1变得__2
__2再次捕获(返回)返回代码3b(from ret="$?";)
1然后输出dummy到printf '%s=%q\n' "$2" "$__2" >&2;. err=3a再次使用,因为它已经用于输出另一个变量stderr.
stderrcatch`.
请注意,作为优化,我们可以将这些2编写out=2a成单个的,例如printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. I did not find a better way, as assignig it to a variable needs a variable name, which then cannot be used as first oder second argument to"$ __ 2""$ ret".
那么到目前为止我们有什么?
我们有以下写信给stderr:
out=2a
err=3b
( exit 1 )
Run Code Online (Sandbox Code Playgroud)
其中printf是从printf '%s=%q\n( exit %q ),out是从$1的2a,stdout是从dummy,err是从$2的3b,并且stderr是从返回代码dummy.
请注意,1在dummy引用引用的格式中,shell会看到正确的(单个)参数%q. printf并且eval非常简单,它们是字面上复制的.
现在到外面2a:
这将执行以上所有操作,输出2个变量,并3b捕获它(因此eval "$({ ... } 2>&1 )";)并使用它将其解析为当前shell exit.
这样就可以设置2个变量并返回代码.
问:它使用2>&1哪个是邪恶的.这样安全吗?
eval没有错误,它应该是安全的.但是你总是要非常小心,只要想想ShellShock.问:虫子?
没有明显的错误,除了以下:
像往常一样eval 吞下所有的换行,不仅仅是最后一行.这是POSIX要求.如果你需要保持LF不受伤害,只需在输出中添加一些尾随字符,然后将其删除,如下面的配方(查看printf %q允许读取指向以a结尾的文件的软链接的尾部$(echo $'\n\n\n\n')):
target="$(readlink -e "$file")x"
target="${target%x}"
Run Code Online (Sandbox Code Playgroud)Shell变量不能携带字节NUL(x).如果它们恰好发生在$'\n'或中,它们就会被忽略$'\0'.
给定的命令在子子shell中运行.所以它无法访问stdout,也无法改变shell变量.你可以stderr使用shell函数,甚至是内置函数,但是那些将无法改变shell变量(因为在其中运行的所有东西$PPID都不能这样做).因此,如果你需要在当前shell中运行一个函数并捕获它的stderr/stdout,你需要以通常的方式使用catchs.(有很多方法可以做到这一点,中断外壳通常不会留下碎片,但这很复杂,值得自己回答.)
问:Bash版本?
$( .. ))问:这仍然看起来很尴尬.
tempfile更清洁地完成它.但是我不习惯printf %q,所以我把它留给其他人来创建一个类似的易于重用的配方ksh.问:为什么不使用ksh呢?
ksh解决方案问:脚本可以改进
问:有一个错字. ksh应该阅读bash
: catch STDOUT STDERR cmd args..在# catch STDOUT STDERR cmd args..静默吞下评论时显示出来.因此,如果碰巧在函数定义中出现拼写错误,您可以看到解析器的位置.这是一个旧的调试技巧.但要注意一点,你可以轻松地在参数中创建一些干净的副作用:.编辑:添加了更多bash -x,以便更容易创建单线程:.并添加了它的工作原理.
Jac*_*din 11
该图显示了 @madmurphy非常简洁的解决方案如何工作。
还有单行的缩进版本:
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
)
}
Run Code Online (Sandbox Code Playgroud)
从技术上讲,命名管道不是临时文件,这里没有人提到它们.它们不会在文件系统中存储任何内容,您可以在连接它们后立即删除它们(因此您将永远不会看到它们):
#!/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
Run Code Online (Sandbox Code Playgroud)
您可以通过这种方式拥有多个后台进程,并在方便的时候异步收集他们的stdouts和stderrs等.
如果你只需要一个进程,你也可以使用硬编码的fd数字,如3和4,而不是{fdout}/{fderr}语法(为你找到一个免费的fd).
不喜欢 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 -
}
Run Code Online (Sandbox Code Playgroud)