为何Bash中定时命令触发shell退出时time输出重定向失败?
time捕获输出在被定时命令触发退出时失败的原因分析 核心需求与初始实现
需要通过多重重定向将Bash内置命令time的输出捕获到变量中,同时让被定时的命令能正常写入stdout和stderr。初始实现的简化命令如下:
$ v=$(exec 3>&2 2>&1; time TIMED_COMMAND 2>&3); echo "v=${v@Q}";
这套重定向逻辑在被定时命令不触发shell退出时可以正常工作:
$ v=$(exec 3>&2 2>&1; time { echo hello >&2; } 2>&3); echo "v=${v@Q}"; hello v=$'\n\treal\t0m0.002s\n\tuser\t0m0.000s\n\tsys\t0m0.000s'
可以看到,被定时命令的stderr输出(hello)正常显示,time的统计信息被成功捕获到变量v中。
问题复现:被定时命令触发退出时捕获失败
当被定时命令触发shell退出(比如执行exit)时,time的输出捕获会失败,统计信息直接打印到终端,变量v为空:
$ v=$(exec 3>&2 2>&1; time exit 2>&3); echo "v=${v@Q}"; real 0m0.001s user 0m0.000s sys 0m0.000s v=''
以下所有变体均出现相同的失败情况:
$ v=$(exec 3>&2 2>&1; time { exit; } 2>&3); echo "v=${v@Q}"; $ v=$(exec 3>&2 2>&1; time { exit 2>&3; }); echo "v=${v@Q}"; $ v=$(exec 3>&2 2>&1; time (exit 2>&3)); echo "v=${v@Q}";
注:若将第一次重定向3>&2替换为3>&1,或第三次重定向2>&3替换为2>&1,则time的输出可被正确捕获;若将exit替换为不触发退出的命令(如true),也能正常捕获。
成功捕获的变体
以下命令变体可以正确捕获time的输出:
$ v=$(exec 3>&2 2>&1; time eval "exit 2>&3"); echo "v=${v@Q}"; $ v=$(exec 3>&2 2>&1; time {(exit)} 2>&3); echo "v=${v@Q}"; $ v=$(exec 3>&2 2>&1; time {(exit) 2>&3; }); echo "v=${v@Q}"; $ v=$(exec 3>&2 2>&1; time {(exit 2>&3)}); echo "v=${v@Q}";
原因分析
基础逻辑:Bash处理time与重定向的流程
Bash执行time CMD REDIRECT时,会先执行重定向、运行命令CMD、恢复被修改的文件描述符,最后输出time的统计信息。但当CMD触发shell退出时,Bash会跳过文件描述符恢复步骤,导致time的统计信息输出到了被重定向后的文件描述符,而非预期的目标。
比如(time exit 2>/dev/null)中,原本应该在exit后恢复stderr,但Bash跳过了这一步,导致time的统计信息被输出到/dev/null而非终端,这属于Bash的一个bug。
子shell场景的特殊行为
对于(time (exit 2>/dev/null))这种被定时命令是子shell的情况,Bash会将计时逻辑下放到子shell中,等价于执行( (time exit 2>/dev/null) )。此时time的统计信息会被子shell的重定向影响,导致无法正常输出到父shell的目标位置。而使用{(exit 2>/dev/null)}这种花括号组时,计时逻辑保留在父shell,因此不受子shell重定向的影响,能正常捕获输出。
eval场景的生效原因
(time eval "exit 2>/dev/null")能正常捕获输出,是因为从eval中强制退出时,Bash会完整执行文件描述符的恢复流程,不会遗漏步骤。但如果重定向放在eval外部(如(time eval "exit" 2>/dev/null)),则依然会触发文件描述符未恢复的问题,导致统计信息丢失。
内容的提问来源于stack exchange,提问作者Philippe

