情景复现
我在学习《纯粹的bash圣经》一文时,发现下面的代码统计的行数有问题。
1 lines_loop() {
2 # Usage: lines_loop "file"
3 count=0
4 while IFS= read -r _; do
5 ((count++))
6 done < "$1"
7 printf '%s\n' "$count"
8 }
9
10 lines_loop $1
从这里可以看到,这个 1.sh 文件一共有10行代码。
但是,实际上,我们可以从真实环境中看出,这段代码只统计出了9行,而wc -l 命令也只统计的9行。
![](https://fangjunyu.com/wp-content/uploads/2024/04/1-5.png)
上面的代码,可能不是很明显。下面给一个一行的代码:
![](https://fangjunyu.com/wp-content/uploads/2024/04/2-4.png)
这里,我们可以清楚的看到,一个一行的r.sh文件,没有被 wc -l 统计出来。
问题排查
经上述情景可以看到wc命令可能存在统计缺失的问题,然后我从网上找到了一个解释:
‘wc’ 计算每个给定 FILE 中的字节数、字符数、空格分隔的单词数和换行符数,如果没有给出或 FILE 为 ‘-‘,则计算标准输入数。
因此,如果文件中没有最后的换行符,则输出的“行”部分wc将比预期少 1。
但是,这个问题是通用的么?
答案不是,我发现使用 vi 命令创建的文件,他们的后缀会自动添加回车。而通过VS Code等工具编辑的文件就会存在这个问题。
![](https://fangjunyu.com/wp-content/uploads/2024/04/3-5.png)
我们可以在这里看到有a.txt和b.txt两个文件,其中a.txt是用vi编辑的,使用wc -l命令,会发现他们统计的行数不一致。
通过cat -e输出发现,b.txt后面是没有行尾标记的,我们通过shasum对比发现两个文件的哈希值不同。
当我执行 echo >> b.txt ,给b.txt文件添加回车后,两个文件的哈希值就一致了。
因此,这个统计不准确的问题,也可能是编辑工具没有在末尾添加回车导致的。
为了测试这一点:我重新用vi创建了a.txt和用VScode创建了b.txt,相同内容为“1 2 3”的文件,用wc -l命令发现他们的行数不同,然后我用vi命令打开b.txt后,使用 :wq 命令保存。这时可以发现,b.txt文件自动在行尾添加了回车,这就说明是编辑工具导致的这个问题。
如果是命令行下编辑的文件,可能不会存在wc命令统计缺少一行的情况。
![](https://fangjunyu.com/wp-content/uploads/2024/04/4-5.png)
问题找到了,但是为什么我的代码也跟wc命令一样识别最后一行呢?
还需要继续探索这个问题。
优化代码
lines_loop() {
# Usage: lines_loop "file"
count=0
while IFS= read -r line; do
echo $line
((count++))
done < "$1"
printf '%s\n' "$count"
}
lines_loop $1
我在代码中,把read -r 后面改成变量line,然后while循环时,用echo输出这个变量:
![](https://fangjunyu.com/wp-content/uploads/2024/04/5-4.png)
我们可以看到,没有输出最后一行代码。
接着,我们创建一个 a.txt,内容为:
fang
jun
yu
然后,我们给刚才的代码添加一个调试模式:
lines_loop() {
# Usage: lines_loop "file"
set -x # 开启调试模式
count=0
while IFS= read -r line; do
echo "$line"
((count++))
done < $1
echo $count
set +x # 关闭调试模式
}
lines_loop $1
执行后,显示为:
![](https://fangjunyu.com/wp-content/uploads/2024/04/6-3.png)
我们可以看到,经过调试发现,read -r line 最后也没有进入到最后一行,到最后一行时直接跳出了while循环并执行了 echo $count这个语句。
现在,问题基本可以定位到下面这个语句:
while IFS= read -r line; do
echo "$line"
((count++))
done < $1
原因是在while循环的过程中,不会获取到最后一行。
然后,从其他答案中了解到问题的原因为:
read 函数在读取文件的最后一行后返回非零退出码,所以 while 循环终止,不会再执行 echo $line。
解决方案为,修改代码以忽略read函数的退出码,让循环继续执行。
具体代码如下:
lines_loop() {
# Usage: lines_loop "file"
set -x # 开启调试模式
count=0
while IFS= read -r line || [[ -n $line ]]; do
echo "$line"
((count++))
done < "$1"
echo $count
set +x # 关闭调试模式
}
lines_loop "$1"
![](https://fangjunyu.com/wp-content/uploads/2024/04/7-2.png)
这时,再次执行,就会发现这段代码可以统计完整的命令了。
其原因是,添加了下面这个条件测试:
|| [[ -n $line ]]
所以,当循环到最后一个语句的时候,$line为:
yu(+文件结束符EOF)
因为不是空的,所以判定循环会根据 || [[ -n $line ]] 判定为真,而执行最后一遍循环。
最后,我们把调试功能和line变量删掉,用VScode工具将a.txt内容改为1:
lines_loop() {
# Usage: lines_loop "file"
count=0
while IFS= read -r line || [[ -n $line ]]; do
((count++))
done < "$1"
echo $count
}
lines_loop "$1"
再次执行该代码:
![](https://fangjunyu.com/wp-content/uploads/2024/04/8-2.png)
一行文本仍然可以正常统计,问题解决。
参考资料
1、Error in wc command to read number of lines in file:
https://unix.stackexchange.com/questions/553852/error-in-wc-command-to-read-number-of-lines-in-file
2、wc命令统计文本少一行:
https://segmentfault.com/a/1190000021434663
3、Shell脚本:while read line无法读取最后一行的问题: