Bash循环遍历文件提前结束

3

我需要在一个大约有2万行的文本文件中使用Bash循环,但是遇到了问题。

这是我的(精简后的)代码:

LINE_NB=0
while IFS= read -r LINE; do
    LINE_NB=$((LINE_NB+1))
    CMD=$(sed "s/\([^ ]*\) .*/\1/" <<< ${LINE})
    echo "[${LINE_NB}] ${LINE}: CMD='${CMD}'"   
done <"${FILE}"

当我加入CMD=$(sed...)的时候,while循环在执行几百次后会提前结束。但是,如果我去掉CMD=$(sed...),while循环就能够正常工作。显然,有一些我没有发现的干扰。

根据我在这里所读到的,在尝试了以下方法之后:

LINE_NB=0
while IFS= read -r -u4 LINE; do
    LINE_NB=$((LINE_NB+1))
    CMD=$(sed "s/\([^ ]*\) .*/\1/" <<< ${LINE})
    echo "[${LINE_NB}] ${LINE}: CMD='${CMD}'"
done 4<"${FILE}"

但是没有任何变化。针对这种行为有什么解释和如何解决的帮助吗?

谢谢!

为了向用户1934428澄清情况(感谢您的关注!),我现在创建了一个最小脚本并添加了"set-x"。完整的脚本如下:

#!/usr/bin/env bash
set -x
FILE="$1"
LINE_NB=0

while IFS= read -u "$file_fd" -r LINE; do
  LINE_NB=$((LINE_NB+1))
  CMD=$(sed "s/\([^ ]*\) .*/\1/" <<< "${LINE}")
  echo "[${LINE_NB}] ${LINE}: CMD='${CMD}'" #, TIME='${TIME}' "

done {file_fd}<"${FILE}"

echo "Done."

输入文件是一个由大约20k行组成的列表,格式如下:
S1 0.018206
L1 0.018966
F1 0.006833
S2 0.004212
L2 0.008005
I8R190 18.3791
I4R349 18.5935
...

while循环在(看起来)随机的位置提前结束。可能的输出结果之一是:
+ FILE=20k/ir-collapsed.txt
+ LINE_NB=0
+ IFS=
+ read -u 10 -r LINE
+ LINE_NB=1
++ sed 's/\([^ ]*\) .*/\1/'
+ CMD=S1
+ echo '[1] S1 0.018206: CMD='\''S1'\'''
[1] S1 0.018206: CMD='S1'
+ echo '[6510] S1514 0.185504: CMD='\''S1514'\'''
...[snip]...
[6510] S1514 0.185504: CMD='S1514'
+ IFS=
+ read -u 10 -r LINE
+ echo Done.
Done.

如您所见,循环在第6510行之后提前结束,而输入文件有大约20k行。


2
对于给定的 FILE,它是否总是在相同的点中止,并且没有错误消息?使用 set -x 跟踪它是否能提供任何见解?这是完整的脚本吗,还是您之前进行了任何设置(例如 set -e)? - user1934428
你是否设置了程序的最大执行时间的 ulimit?否则,为了调试,我会用一些简单的程序(如 :)替换 sed,以查看是 sed 的问题还是其他原因。 - user1934428
3
在单独的行上运行 sed 是一个不好的反模式。你正在重新发明 awk '{printf "[%i] %s: CMD=\047%s\047\n", NR, $0, $1}' "$FILE",但引号使用是有问题的。 - tripleee
我发布了一个最小的脚本和输出。不幸的是,我对awk非常害怕,我知道有一天我应该学习它... ;) 我当然可以尝试使用awk,但实际上我的真正脚本需要对$0执行多个正则表达式匹配,并根据结果将某些内容附加到多个文件中的一个(现在我使用if ...=~ elif ... fi)。 - Guido
2
我想我找到了问题所在。首先,我用以下代码替换了单独行中的“sed”:IFS=' ' read -r -a TOK <<< "${LINE}"; CMD="${TOK[0]}"; TIME="${TOK[1]}"。这当然提高了效率(至少暂时省去了学习awk的麻烦;))。问题仍然存在。我在脚本末尾添加了“wc -l $ {FILE}”,发现“wc”也失败了。原因可能是${FILE}仍在增长(它被另一个正在运行的进程附加)。通过将“$ {FILE}”复制到新文件中(即,就脚本而言冻结它),循环可以正常工作。虽然这种行为很奇怪... - Guido
显示剩余4条评论
1个回答

2
是的,制作一个稳定的文件副本是一个很好的开始。
学习awk和/或perl仍然值得你花时间。这并不像看起来那么难。 :)
除此之外,有几个优化建议 - 尽可能避免在循环内运行任何程序。对于一个20k行的文件,这意味着20k个sed,这会增加不必要的负担。相反,你可以只使用参数解析。
# don't use all caps.
# cmd=$(sed "s/\([^ ]*\) .*/\1/" <<< "${line}") becomes
cmd="${cmd%% *}" # strip everything from the first space

使用read来处理它更好, 因为你已经在使用它了,但如果可以避免的话就不要再创建一个。尽管我很喜欢它,但是read效率相当低下;它必须进行大量的调整以处理所有选项。

while IFS= read -u "$file_fd" cmd timeval; do
  echo "[$((++line_nb))] CMD='${CMD}' TIME='${timeval}'"
done {file_fd}<"${file}"

或者

while IFS= read -u "$file_fd" -r -a tok; do
  echo "[$((++line_nb))] LINE='${tok[@]}' CMD='${tok[0]}' TIME='${tok[1]}'"
done {file_fd}<"${file}"

(这将对该行进行“排序”,但如果存在制表符或额外空格等,则只会使用$IFS的第一个字符进行填充,默认情况下为一个空格。在这里应该没有问题。)

awk可以轻松解决这个问题,而且速度更快,已经内置了更好的工具。

awk '{printf "NR=[%d] LINE=[%s] CMD=[%s] TIME=[%s]\n",NR,$0,$1,$2 }' 20k/ir-collapsed.txt

进行一些时间比较 - 使用sed和不使用sed,使用一个read和两个read,然后将每个与awk进行比较。:)

您需要对每行执行的任务越多,文件中的行数越多,它就会更加重要。养成尽可能整洁地完成即使是小事情的习惯 - 长期来看它会带来很好的回报。


太酷了!我不知道可以使用read -u "$file_fd" cmd timeval。至于awk,我看到我真的必须学习它!谢谢! - Guido

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接