如何将 bash 命令分组和管道状态结合起来?
这是一个命令组示例:
{ tar -cf - my_folder 2>&1 1>&3 | grep -v "Removing leading" 1>&2; } 3>&1 | gzip --rsyncable > my_file.tar.gz
这是与上述内容相匹配的管道状态读数示例:
[[ ${PIPESTATUS[*]} =~ [1-9] ]] && rm my_file.tar.gz
在此示例中,命令组使邮件池免受有关“从 tar 中删除前导 /”的警告,通过 cron 传递,因为它们落在 stderr 上(Unices 缺少 stdwarn,tar 缺少安静选项),同时让真正的错误通过。
管道状态读数可确保立即删除损坏的备份文件,以防止以后使用标准 FIFO 算法进行清理时删除旧的有效文件。
但这个例子不起作用。上面的管道状态包含[1 0],即grep和gzip的退出码,但不包含tar。
我尝试过的一种尝试是这样的:
{ tar -cf - my_folder 2>&1 1>&3 | grep -v "Removing leading" 1>&2; GROUPSTATUS=${PIPESTATUS[*]}; } 3>&1 | gzip --rsyncable > my_file.tar.gz
[[ $GROUPSTATUS =~ [1-9] ]] && rm my_file.tar.gz
但离开群组后,GROUPSTATUS 为空。
(请注意,通过将 GROUPSTATUS 设置为管道状态以外的任何内容,例如某种文字,可以验证该变量实际上在正常情况下逃脱了命令分组范围。)
我还尝试过是否return
可以从组内将第一个管道组件的退出代码传递到外部,但在命令组内返回只会从 bash 产生错误消息。
答案1
当您执行管道时,每个管道分隔的元素都在其自己的进程中执行。变量赋值只在自己的进程中生效。在ksh和zsh下,管道的最后一个元素在原始shell中执行;在其他 shell(例如 bash)下,每个管道元素都在其自己的子 shell 中执行,原始 shell 只是等待它们全部结束。
$ bash -c 'GROUPSTATUS=foo; echo GROUPSTATUS is $GROUPSTATUS'
GROUPSTATUS is foo
$ bash -c 'GROUPSTATUS=foo | :; echo GROUPSTATUS is $GROUPSTATUS'
GROUPSTATUS is
在您的情况下,由于您只关心所有成功的命令,因此您可以使状态代码向上流动。
{ tar -cf - my_folder 2>&1 1>&3 | grep -v "Removing leading" 1>&2;
! ((PIPESTATUS[0])); } 3>&1 |
gzip --rsyncable > my_file.tar.gz;
if ((PIPESTATUS[0] || PIPESTATUS[1])); then rm my_file.tar.gz; fi
如果您想从管道左侧获取超过 8 位的信息,您可以写入另一个文件描述符。这是一个原理验证示例:
{ { tar …; echo $? >&4; } | …; } | { gzip …; echo $? >&4; } \
4>&1 | ! grep -vxc '0'
一旦获得标准输出上的数据,您可以使用命令替换将其输入 shell 变量,即$(…)
.命令替换从命令的标准输出读取,因此如果您还打算将内容打印到脚本的标准输出,则它们需要暂时通过另一个文件描述符。以下代码片段使用 fd 3 来表示最终进入脚本 stdout 的内容,使用 fd 4 来表示捕获到$statuses
.
statuses=$({ { tar -v … >&3; echo tar $? >&4; } | …; } |
{ gzip …; echo gzip $? >&4; } 4>&1) 3>&1
如果您需要将不同命令的输出捕获到不同的变量中,我认为即使在“高级”shell(例如 bash、ksh 或 zsh)中也没有直接的方法。以下是一些解决方法:
- 使用临时文件。
- 使用单个输出流,例如每行都有一个前缀来指示其来源,并在顶层进行过滤。
- 使用更高级的语言,例如 Perl 或 Python。
答案2
一个简单的方法是绕过这个问题,即将 my_folder 放入 shell 变量中并从中删除前导斜杠:
tar -cf - ${my_folder##/} | gzip --rsyncable > my_file.tar.gz
否则,您可以检查块内的 ${PIPESTATUS[0]} ,如果它不为零,则使用 false 向外部进程发出错误信号:
{ tar -cf - my_folder 2>&1 1>&3 |
grep -v "Removing leading" 1>&2;
[ ${PIPESTATUS[0]} -eq 0 ] && true || false; } 3>&1 |
gzip --rsyncable > my_file.tar.gz