bash中的间歇性管道故障

时间:2019-05-08 09:39:13

标签: bash

我有一个看起来像这样的代码段

    while grep "{{SECRETS}}" /tmp/kubernetes/$basefile | grep -v "#"; do
      grep -n "{{SECRETS}}" /tmp/kubernetes/$basefile | grep -v "#" | head -n1 | while read -r line ; do
        lineno=$(echo $line | cut -d':' -f1)
        spaces=$(sed "${lineno}!d" /tmp/kubernetes/$basefile | awk -F'[^ \t]' '{print length($1)}')
        spaces=$((spaces-1))
        # Delete line that had {{SECRETS}}
        sed -i -e "${lineno}d" /tmp/kubernetes/$basefile
        while IFS='' read -r secretline || [[ -n "$secretline" ]]; do
          newline=$(printf "%*s%s" $spaces "" "$secretline")
          sed -i "${lineno}i\ ${newline}" /tmp/kubernetes/$basefile
          lineno=$((lineno+1))
        done < "/tmp/secrets.yaml"
      done
    done

在/ tmp / kubernetes / $ basefile中,字符串{{SECRETS}}出现两次的百分比为100%。

几乎每次都可以完成。但是,很少有脚本在文件的第二个循环中出错。像这样,根据-x

...
IFS=
+ read -r secretline
+ [[ -n '' ]]
+ read -r line
exit code 1

工作时,设置-x如下所示,并继续正确处理文件。

...
+ IFS=
+ read -r secretline
+ [[ -n '' ]]
+ read -r line
+ grep '{{SECRETS}}' /tmp/kubernetes/deployment.yaml
+ grep -v '#'

对于只能偶尔发生这种情况我没有答案,所以我认为bash管道的并行性有些我不了解的东西。 grep -n "{{SECRETS}}" /tmp/kubernetes/$basefile | grep -v "#" | head -n1 | while read -r line ; do中是否有某种方式可能导致执行混乱?根据该错误,似乎正在尝试读取一行,但是由于先前的命令无效而无法执行。但是在set -x输出中没有任何指示。

1 个答案:

答案 0 :(得分:0)

该问题的可能原因是包含内部循环的管道同时读取和写入“基本文件”。参见How to make reading and writing the same file in the same pipeline always “fail”?

解决问题的一种方法是在尝试更新文件之前先对其进行完全读取。试试:

basepath=/tmp/kubernetes/$basefile
secretspath=/tmp/secrets.yaml

while
    line=$(grep -n "{{SECRETS}}" "$basepath" | grep -v "#" | head -n1)
    [[ -n $line ]]
do
    lineno=$(echo "$line" | cut -d':' -f1)
    spaces=$(sed "${lineno}!d" "$basepath" \
                | awk -F'[^ \t]' '{print length($1)}')
    spaces=$((spaces-1))
    # Delete line that had {{SECRETS}}
    sed -i -e "${lineno}d" "$basepath"
    while IFS='' read -r secretline || [[ -n "$secretline" ]]; do
        newline=$(printf "%*s%s" $spaces "" "$secretline")
        sed -i "${lineno}i\ ${newline}" "$basepath"
        lineno=$((lineno+1))
    done < "$secretspath"
done

(我引入了变量basepathsecretspath,以使代码更易于测试。)

顺便说一句,也可以使用纯Bash代码执行此操作:

basepath=/tmp/kubernetes/$basefile
secretspath=/tmp/secrets.yaml

updated_lines=()
is_updated=0
while IFS= read -r line || [[ -n $line ]] ; do
    if [[ $line == *'{{SECRETS}}'* && $line != *'#'* ]] ; then
        spaces=${line%%[^[:space:]]*}
        while IFS= read -r secretline || [[ -n $secretline ]]; do
            updated_lines+=( "${spaces}${secretline}" )
        done < "$secretspath"
        is_updated=1
    else
        updated_lines+=( "$line" )
    fi
done <"$basepath"

(( is_updated )) && printf '%s\n' "${updated_lines[@]}" >"$basepath"
  • 整个更新的文件都存储在内存中(update_lines数组中),但这不成问题,因为任何太大而无法存储在内存中的文件都肯定会太大而无法逐行处理与Bash联机。 Bash通常非常慢。
  • 在这段代码中,spaces保留了用于缩进的实际空格字符,而不是其数量。