在 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
文件描述符。它利用-u
GNUsed
提供的 nbuffered 开关来避免缓冲问题,但即使没有它,后台进程的输入也可以在必要时在管道中的字节conv=sync
块上进行过滤和时间化。\0NUL
dd
sed
以下是我通常在交互式 shell 中使用上述内容的方法:
: & SEDD=$$$!
sed -un "/^$SEDD$/!H;//!d;s///;x;/\n/!q;s///;s/%/&&/g;l" <&"$o" >&"$i" &
...其后台 ased
将读取并存储输入,直到遇到唯一的分隔符,此时它将将旧缓冲区%
中出现的任何情况加倍H
,并在我的匿名管道上打印exec
printf 格式友好的 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 的反馈。