侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 141 篇文章
  • 累计收到 7 条评论

数行数别只看 wc -l,末尾缺一个换行符就少一行

2026-9-28 / 0 评论 / 9 阅读

数行数别只看 wc -l,末尾缺一个换行符就少一行

要对文件按行处理,行数算出来不对的时候,第一反应往往是怀疑文件内容。多数情况下内容没错,是「行」这个字在两个地方的含义不一样:一种指换行符的个数,一种指肉眼看到的行。

准备了三个文件,两个都是三行内容,区别只在最后一个字节:

== 1. 两个文件都是三行内容,只有一个末尾带换行符 ==
  文件         字节    wc-l    wc-c    wc-m  grep-c ''   awk NR     sed $=
  with-nl.txt         6       3       6       6          3        3          3
  no-nl.txt           5       2       5       5          3        3          3

六个命令摆在一起看,前五个数字分成两派:wc -l 说 no-nl.txt 是 2 行,wc -c、wc -m、grep -c ''、awk 'END{print NR}' 和 sed -n '$=' 都说 3 行。同一个文件、同一台机器,答案差 1。

wc -l 数的是换行符

wc -l 数的从来不是「行」,是文件里 0x0a 的个数。POSIX 对文本文件的定义就是每一行以换行符结束,所以一个末尾没有换行符的文件严格来说不算完整的文本文件,wc -l 报 2 是照定义算的,没有 bug。

== 2. 末尾那个字节到底是什么 ==
  with-nl.txt  末尾 1 字节的十六进制:0a
  no-nl.txt    末尾 1 字节的十六进制:63
  0x0a 是换行符,0x63 是字符 c

两个文件末尾的字节一个 0x0a,一个 0x63。后者就是字符 c,文件到那里直接结束了。

这一类文件在真实项目里出现的原因通常很朴素:日志的最后一行是进程被 kill 时写了一半的,导出脚本用了 printf 而不是 echo,或者有人手工 echo -n 追加了一句。上游给的文件里混进这种结尾,按 wc -l 分配的任务数就会少一个,最后一行永远轮不到处理。

grep -c 数的是行,不是次数

第二个容易混的数字是出现次数。grep -c 的名字看着像 count 命中,实际数的是命中的行数:

== 4. grep -c 数的是行,不是命中次数 ==
    aaa
    aaa aaa
    bbb
  grep -c aaa          -> 2 行(文件里有 3 处 aaa)
  grep -o aaa | wc -l  -> 3 处
  命中所在行号         -> 1,2

那个文件里 aaa 出现在 1、2 两行,第 2 行有两处。grep -c aaa 给的是 2,grep -o aaa 才把三处拆成三行,接一个 wc -l 得到 3。

要统计的到底是「有多少行涉及」还是「总共出现几次」,在写统计脚本的时候就得定下来。少定这一步,报表上的数字会在数据分布变化时慢慢飘走:命中均匀分布时两种算法结果一样,一旦某一行挤进多处,两个数字就分叉了。

-c 和 -m 之间隔着编码

wc -c 数字节,wc -m 数字符。全 ASCII 的文件上两者永远相等,中文一进来就分开:

== 3. wc -m 数的是字符,wc -c 数的是字节 ==
  文件     字节    wc-c    wc-m   LC_ALL=C wc -m

第三个数字是加 LC_ALL=C 之后 wc -m 的结果,它退化成了字节数。wc -m 要按字符数得先知道当前 locale 认不认多字节字符,C 这个 locale 只认单字节,于是它把每个字节当一个字符。写中文内容的脚本,wc -m 前面不加 locale 就直接用,很可能得到一个既不是字节数也不是字符数的东西。

追加内容时最容易造出半行

半行是写出来的,不是天生的。用不带换行符的输出追加上去,两段内容会粘成一行:

== 5. 追加一行之后,两种数法差多少 ==
  printf 不带换行符追加一个 d 之后:
    文件内容:a|b|cd
    wc -l            -> 2
    awk END{print NR}-> 3
    按换行符切出来的段数 -> 3

追加一个 d 之后,文件内容变成 a、b、cd 三行,wc -l 报 2。这种文件用 while read 逐行读时,cd 会作为一个整体变量进来,本该是两个值的地方变成一个。

要避免也简单:追加之前先确认上一个文件以换行符结尾,或者干脆用 printf '%s\n' 这种自带换行的写法。真要在一个非空文件后面接内容,先跟一个 echo >> file 把行补齐。

每个数字该配哪个命令

把上面几件事收成一张对照:

想要的东西 命令 注意
换行符个数 wc -l 末尾无换行符时比可见行少 1
可见行数 awk 'END{print NR}' 或 grep -c '' 最后那半行也算一行
命中行数 grep -c PATTERN 一行里多处只算一次
命中次数 grep -o PATTERN \| wc -l 会输出每一处,文件大时慢
字节数 wc -c 跟编码有关
字符数 LC_ALL=zh_CN.UTF-8 wc -m 不指定 locale 会退化成字节数

表格里那几条里只有第一条和第二条是要长期分清的:给程序分配任务用哪个都行,只要前后一致;给人看的报表、给下游的行数校验,一旦和 wc -l 混用就会差一个数。

我给批量脚本定的一条规矩是按「可见行」算,统一用 awk 'END{print NR}',因为它跟编辑器里看到的一致,少一个需要解释的例外。代价是空文件上它和 wc -l 都给 0,这也符合预期。

最后一条经验:核对行数的时候别只对一个命令,至少跑两个。两个数字不一致时先看最后一个字节,十次里有九次问题就在那里。

    🤞 分享