wc命令统计缺少一行问题
wc命令统计缺少一行问题

wc命令统计缺少一行问题

情景复现

我在学习《纯粹的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行。

上面的代码,可能不是很明显。下面给一个一行的代码:

这里,我们可以清楚的看到,一个一行的r.sh文件,没有被 wc -l 统计出来。

问题排查

经上述情景可以看到wc命令可能存在统计缺失的问题,然后我从网上找到了一个解释:

‘wc’ 计算每个给定 FILE 中的字节数、字符数、空格分隔的单词数和换行符数,如果没有给出或 FILE 为 ‘-‘,则计算标准输入数。

因此,如果文件中没有最后的换行符,则输出的“行”部分wc将比预期少 1。

但是,这个问题是通用的么?

答案不是,我发现使用 vi 命令创建的文件,他们的后缀会自动添加回车。而通过VS Code等工具编辑的文件就会存在这个问题。

我们可以在这里看到有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命令统计缺少一行的情况。

问题找到了,但是为什么我的代码也跟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输出这个变量:

我们可以看到,没有输出最后一行代码。

接着,我们创建一个 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

执行后,显示为:

我们可以看到,经过调试发现,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"

这时,再次执行,就会发现这段代码可以统计完整的命令了。

其原因是,添加了下面这个条件测试:

|| [[ -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"

再次执行该代码:

一行文本仍然可以正常统计,问题解决。

参考资料

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无法读取最后一行的问题:

https://www.cnblogs.com/Braveliu/p/10573389.html

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注