上下文:我有一个复制文件的 Bash 脚本
function log () {
read IN
if [ "$IN" == "" ]; then
:
else
echo "$datetime"$'\t'"$IN" | tee -a logfile
fi
}
function copy () {
command cp -L --parents $@
}
...
copy -R /etc . 2>&1 | log
...
问题:当cp -L --parents -R /etc 2>&1
手动运行时,我遇到了大约 10 次失败(符号链接损坏,这是预料之中的),并且整个 /etc 已被复制。
但是当脚本运行时,只报告 1 次失败,并且 /etc 只复制到 1 次失败发生的位置。
在尝试排除故障时,我所做的就是2>&1
从脚本中删除,然后复制按预期进行。
问题:是我的log
函数造成了麻烦,还是脚本编写方式存在语法问题(尽管不是破坏脚本)?
答案1
你的log
功能是罪魁祸首。它的作用是:读取一行,如果该行不为空,则打印时间戳,然后打印该行内容。这就是它所做的一切:一旦处理完一行,它就会返回。
当cp
发出第一条错误消息时,该log
函数会读取它并处理它。由于该log
函数随后返回,因此管道右侧的进程退出,这导致管道的读取端关闭。当cp
发出第二条错误消息时,它会尝试写入关闭的管道,这会导致它因SIGPIPE信号。标准错误是行缓冲的(默认情况下,并且cp
不会尝试更改它),因此缓冲不起作用。
要处理所有输入行,您需要read
循环。
log () {
while IFS= read -r IN; do
echo "$datetime"$'\t'"$IN"
done | tee -a logfile >&2
}
我还修复了read
呼叫IFS= read -r
实际读取一行。我删除了对空行的特殊处理,这是毫无意义的(输入中不会有空行)。我认为您将其放入是为了处理输入为空(零行输入)时的情况,但处理该问题的正确方法是检查命令的返回状态read
。我还修复了log
打印其标准错误,因为它用于处理错误消息。
看在命令的每一行输出前添加时间戳了解其他方法来执行此操作。
请注意,将命令放在管道的左侧有一个主要缺点:它导致退出状态被忽略。因此,如果cp
失败,您的脚本将愉快地继续。错误将记录在某处,但后续命令将正常执行,并且不会提醒您应该去阅读日志。在 bash、ksh 或 zsh 中,您可以设置pipefail
选项除此之外,set -e
一旦任何命令失败,即使在管道的左侧,您的脚本也会以错误状态退出。
set -o errexit -o pipefail
copy … |& log
或者,使用流程替代而不是通过另一个进程通过管道传输错误输出的管道。进程替代的注意事项与管道略有不同;中的错误log
会被有效地忽略,并且命令可能会在log
完成之前返回(在 bash 中,log
在后台运行)。
set -e
copy … 2> >(log)
几乎,您不需要IFS= read -r IN
阅读更多内容,也不可能破坏该行。