从 shell 逐行处理输入(带空行)

从 shell 逐行处理输入(带空行)

在 shell 脚本中,我需要逐行解析命令行的输出。输出可能包含空行,这些是相关的。我使用的是 ash,而不是 bash,所以不能诉诸进程替换。我正在尝试这个:

    OUT=`my_command`
    IFS=$'\n'              
    i=1
    for line in $OUT; do
        echo $line                                          
        eval VAL$i=$line             
        i=$((i+1))             
    done

然而,这会丢弃 $OUT 中的空行。如何解决此问题以便也处理空行?

答案1


一个可行的 shell 循环可能看起来像......

set -f -- "-$-"' -- "$@" '"
    ${IFS+IFS=\$2} ${out+out=\$3}" \
    "$IFS" "$out" "$@"
IFS='
';for out in $(my command|grep -n '.\|')
do  : something with "${out%%:*}" and "${out#*:}"
done
unset IFS out
eval "set +f $1"
shift 3

您只需对其进行安排,使其不存在任何空行即可。尽管我最初建议nl用于此目的,但转念一想,nl逻辑页面分隔符有可能出现在输入中并扭曲其输出(实际上,它最终会产生一个空行,并且会影响对哪一行进行编号 - 不过,对于其他目的来说,这是一个非常方便的功能)。以外不是解释逻辑分页符,grep -n '.\|'结果将是相同的。

使用这样的管道并进行一些参数替换,您不仅可以避免空行问题,而且每次迭代都会同时进行预先编号 -(当前迭代的编号现在将位于为您提供的每个值的开头,$out后跟一个:

这些set ... IFS=...行的作用是确保 shell 的状态恢复到更改之前的位置。如果它是脚本而不是函数,那么这些预防措施可能有点过头了。尽管如此,你还是应该至少 set -f在 shell 分割之前避免无意的输入错误。


但关于(d)ash<(过程替代)

话又说回来,在 Debian 中( dash)衍生的ash (例如busybox ash您可能会发现,它对文件描述符链接和此处文档的处理为您可能习惯于进行的<(进程替换提供了更好的替代方案)

考虑这个例子:

exec "$((i=3))"<<R "$((o=4))"<<W 3<>/dev/fd/3 4<>/dev/fd/4
R
W

sed -u 's/.*/here I am./' <&"$o" >&"$i" &
echo "hey...sed?" >&"$o"
head -n1          <&"$i"

因为dash和衍生品回到这里-带有匿名管道的文档而不是(和大多数其他 shell 一样)与常规文件,并且因为/dev/fd/[num]Linux 系统上的链接提供了引用文件描述符的后备文件的间接方式(即使它无法在文件系统中引用 - 例如匿名管道)上面的序列演示了一种非常简单的方法来设置某些 shell 可能称为协进程。例如,在Linux 系统中busybox ash或之上dash(我不会为其他人担保)上面将打印:

here I am.

...并将继续这样做,直到 shell 关闭它的$i$o文件描述符。它利用-uGNUsed提供的 nbuffered 开关来避免缓冲问题,但即使没有它,后台进程的输入也可以在必要时在管道中的字节conv=sync块上进行过滤和时间化。\0NULdd

sed以下是我通常在交互式 shell 中使用上述内容的方法:

: & SEDD=$$$!
sed -un "/^$SEDD$/!H;//!d;s///;x;/\n/!q;s///;s/%/&&/g;l" <&"$o" >&"$i" &

...其后台 ased将读取并存储输入,直到遇到唯一的分隔符,此时它将将旧缓冲区%中出现的任何情况加倍H,并在我的匿名管道上打印execprintf 格式友好的 C 转义字符串单行 - 或者,如果结果大于 80 个字符,则多行。最后一个 - 对于 GNU sed- 可以用 w/ 来处理,sed -l0这是一个指示sed永远不要在 上换行的开关\,或者像这样:

fmt=
while IFS= read -r r <&"$i" 
      case $r in (*$) 
      ! fmt=$fmt$r ;;esac
do    fmt=$fmt${r%?} 
done

不管怎样,我构建了它的缓冲区,如下所示:

echo something at sed >&"$o"
printf '%s\n' more '\lines%' at sed "$SEDD" >&"$o"

然后我把它拉进去就像...

IFS= read -r fmt <&"$i"

这就是$fmt之后的内容:

printf %s\\n "$fmt"
something at sed\nmore\n\\lines%%\nat\nsed$

sed还将对不可打印的字符执行 C 风格的八进制转义。

所以我可以像...一样使用它

printf "%d\n${fmt%$}\n" 1 2 3

...打印...

1
something at sed
more
\lines%
at
sed
2
something at sed
more
\lines%
at
sed
3
something at sed
more
\lines%
at
sed

我可以sed根据需要杀死并释放管道,例如......

printf %s\\n "$SEDD" "$SEDD" >&"$o"
exec "$i">&- "$o">&-

当你持有一个 fd 而不是只使用一次时,你可以做这种事情。只要您需要,您就可以维护后管 - 而且它比命名管道更安全,因为内核不会向除拥有它们的进程之外的任何进程提供这些链接(你的外壳),而可以找到命名管道(以及窃听/窃取)任何有权访问其参考文件的进程都可以在文件系统中进行此操作。

要在进行进程替换的 shell 中执行类似的操作,您可能可以这样做......

eval "exec [num]<>"<(:)

……但我从来没有尝试过。

答案2

这样做:

i=1
my_command | while read line; do
    echo $line
    eval VAL$i="$line"
    i=$((i+1))
done

由于命令的输出是逐行读取的,因此将单独处理这些行(包括空行),而不必先将这些行存储在变量中。这也节省了内存,因为输出不会在内存中两次结束,并且 bash 脚本可以在输出后立即开始处理这些行,而不仅仅是在命令完成后。

编辑:由于 VALx 变量是在上面的子 shell 中设置的,因此需要进行修改:

eval `i=1
my_command | while read line; do
    # echo $line
    echo "VAL$i=\"$line\""
    i=$((i+1))
done`

如果您确实也需要echo $line,则需要进行一些修改。

答案3

我已经用这里的文档实现了这个:

    i=1
    while read -r line; do
        eval VAL$i=\$line
        i=$((i+1))
    done <<EOF
$(my_command)
EOF

效果很好。

更新:纳入了 Gilles 和 mikeserv 的反馈。

相关内容