做了这么多年Linux运维,有个现象我一直挺纳闷:身边不少同事给文件加行号,第一反应永远是cat -n。这个习惯我从入行第一天就看到,十几年了还是这样。可他们不知道的是,Linux 命令行里有一个专门干这件事的老家伙,叫nl,全称 number lines。它不是cat -n的替代品,而是把“给文本加行号”这一个小需求,从“能用”做到了“好用”。下面我把自己用nl的一些经验、踩过的坑和习惯用法整理出来,希望对刚上手 Linux 命令的朋友和做了几年运维的老同事都有点用。
1. 别再用cat -n糊弄自己:nl到底比它多做了什么
很多人不知道,cat -n本质上只是cat命令顺带提供的一个输出修饰功能,而nl是一个独立的标准 POSIX 命令,专门负责行号排版。它俩看似都在给文本编号,实际上从设计哲学到能力边界都不一样。
1.1 cat -n与nl的原始差异
先看一个最简单的对比。假设有个文件text.txt,内容只有四行:
hello world end用cat -n输出:
$ cat -n text.txt 1 hello 2 3 world 4 end用nl默认参数输出:
$ nl text.txt 1 hello 2 world 3 end注意看第二个空行。cat -n把空行也编上了号,显示为第 2 行;而nl默认直接跳过了空行,下一行非空内容编为第 2 行。
这个差异第一次接触时很容易让人误解,觉得nl是不是漏编号了。实际上这是它有意为之的默认行为。你想啊,我们平时给文件加行号,目的是什么?是方便定位代码、配置项、日志片段。空行通常只是视觉上的分隔留白,并不是真正要引用的“内容”。如果空行也占一个编号,你去跟同事说“看第 5 行”,对方数到的可能是一截空白,那就尴尬了。cat -n给所有行编号,属于无脑式操作;nl默认只给“有效内容”编号,属于思考型操作。
1.2 默认行为差异带来的第一个冲击
还有一点,cat -n的输出格式是写死的:行号宽度固定,右对齐,后面跟一个制表符。而nl的组合能力要强得多。同样输出编号,nl可以做到左对齐、右对齐、补零、自定义宽度、自定义分隔符,甚至可以通过正则表达式决定“哪些行值得拥有行号”。
所以我的建议是:如果你只是临时瞄一眼文件内容,cat -n完全够用;但如果你要拿行号去做后续处理,比如提取指定区间、生成带序号的清单、写脚本逻辑判断,那就上nl。两者的区别,就像手电筒和可调焦工作灯——都能照亮,但你真要干活的时候,就知道哪个顺手了。
顺便提一句,nl是 POSIX 标准的一部分,在绝大多数 Linux 发行版和 macOS 上都预装,不需要额外安装任何东西。这一点也是它容易被忽视的原因:太常用了,反而没人专门介绍。
2. 行号怎么算:编号规则的四种模式和三个控制旋钮
行号不是简单的 1、2、3 往后排。nl真正强大的地方,在于它允许你用规则来决定“编号落在哪一行”。这一节我会把它最核心的编号控制选项全部拆开讲清楚,配合示例看,你就能直观理解这些参数是干什么用的。
2.1 -b参数的四种编号风格
-b参数控制给哪种行编号,全称是--body-numbering,因为nl内部把普通内容区域称为 body(正文)。取值有四种:
| 取值 | 含义 | 典型场景 |
|---|---|---|
t | 只给非空行编号(默认) | 看代码、看配置,跳过空行分隔 |
a | 所有行都编号,包括空行 | 希望行号与物理行严格一一对应 |
n | 不编号任何行 | 相当于把 nl 当格式化工具用,只调对齐和分隔符 |
p正则 | 只给匹配正则表达式的行编号 | 按内容特征筛选编号,比如只给函数定义行标号 |
-b a是cat -n的行为模式,也是我刚接触nl时最常用的参数。因为很多时候,像我这种强迫症,希望文件里的每一行在物理位置上都有唯一编号,空行空着反而不知道中间丢了什么。尤其是在对比两个文件、或者把结果喂给sed做区间提取时,-b a能保证行号和文件物理行完全对应,绝对不会错位。
-b p的正则模式是重头戏。它接受的是基础正则表达式(BRE),匹配成功的行才编号,不匹配的行照常输出但前面不带行号。举个例子:
$ printf 'apple\nBanana\ncherry\nDate\n' | nl -b 'p^[A-Z]' apple 1 Banana cherry 2 Date这段输出里,只有大写字母开头的Banana和Date被编号了,小写开头的apple和cherry原样保留,前面干干净净。当时我看到这个能力时的第一反应是:以后完全可以这样快速圈出配置文件里的server块,比如:
$ nl -b 'pserver' /etc/nginx/nginx.conf它会只给包含server这个词的行编号,其它行原样输出。配合 grep 定位大文件里的关键节点,非常清爽。
2.2 用-v和-i控制行号的起点与步长
当默认从 1 开始、每次加 1 不够用的时候,-v(起始值)和-i(步长)就派上用场了。
-v指定起始编号,比如从第 100 行开始编号。-i指定编号递增的步长,默认是 1。组合起来很有意思:
$ printf 'a\nb\nc\nd\ne\n' | nl -v 100 -i 2 100 a 102 b 104 c 106 d 108 e你看,行号从 100 起跳,每次跳 2。这在实际工作中有什么用?有一个场景我经常遇到:一个日志文件里,每次错误输出的上下文固定是 3 行,我想给同一组事件编同一个区间号,方便后续按事件组筛选。虽然直接用-i不完全等价,但当你需要“每 N 行一组”的编号节奏时,-v配合-i可以临时拼出很多近似效果。
还有个小技巧:如果文件行数特别多,你希望每个 1000 行的块用不同区段编号,可以通过脚本循环处理,在每段文件上分别用nl -v,效果远好于手工数行号。
2.3 -l逻辑行:把多个物理行绑在一起编号
这个参数我重点说一下,因为它是最容易被忽略、但一旦用对就很惊艳的功能。-l N表示每 N 个物理行算一个逻辑行,编号只给逻辑行的第一行,剩下的行不编号但保持输出。
$ printf 'a\nb\nc\nd\ne\nf\n' | nl -l 2 1 a b 2 c d 3 e f每两行绑定为一组,只在组的首行给号。这个需求来自于文档排版逻辑:一个自然段落往往占据多个物理行,你希望的是“段落编号”,而不是“每行编号”。比如一些配置文件里,一个参数值被折行写成两行,或者日志里一次完整的事件记录横跨两行,-l就能精确控制。
要注意的是,nl还有-p(保持编号不重置)和-d(自定义页分隔符)这两个冷门参数,它们配合-h、-f、-b可以在同一份文档里给页眉、正文、页脚分别设置编号规则。但坦白讲,日常命令行操作里我几乎没用到分页标记,这一整套机制适合处理带页眉页脚的排版文档,普通运维和开发场景用不上,大家了解名字即可,真遇到了再查手册。
3. 让输出变好看:对齐方式、宽度和分隔符的组合
行号除了“有”和“没有”,还有一个维度是“输出长什么样”。默认情况下,nl的行号占 6 个字符宽度,右对齐,后面跟一个制表符。这就是为什么默认输出里行号前面会有那么多空格。
3.1 三种对齐格式:ln、rn、rz
-n参数控制编号格式,三种取值对应三种风格:
ln:左对齐,数字靠左,右边的空位用空格补齐;rn:右对齐,数字靠右,左边补空格(默认);rz:右对齐,左边补零,效果类似 0001。
看一个直观对比,同样处理两行内容,宽度设为 4:
$ printf 'hello\nworld\n' | nl -n ln -w 4 1 hello 2 world $ printf 'hello\nworld\n' | nl -n rn -w 4 1 hello 2 world $ printf 'hello\nworld\n' | nl -n rz -w 4 0001 hello 0002 worldln最省眼力,因为它跟内容之间几乎没有空隙;rz适合严格对齐的场景,比如后续要用cut -c按字符位置截取行号段;rn则是默认且最传统的排版方式,看起来工整但占地方。
3.2 宽度与分隔符对后续处理的影响
-w控制行号区域的最小宽度。注意是“最小”,如果文件超过 9999 行,哪怕你设了-w 2,它也会自动扩充到能放下实际编号,不会截断数字。这一点我测过很多次,放心用。
-s控制行号与正文之间的分隔符,默认是制表符,但你可以替换成任何字符串。这个参数的影响比想象中大,因为如果你想把结果喂给awk或cut,默认的制表符常常不好切。把分隔符改成一个明确的字符,后续处理就顺了。
$ nl -ba -w 1 -s ',' /etc/passwd 1,root:x:0:0:root:/root:/bin/bash 2,daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin看到没有,这已经是一份合法的 CSV 数据了。事情的性质就变了:nl不只是给人看的,还可以作为数据加工环节给文本补一列序号,供其它工具消费。
3.3 实用性优先的几个组合建议
我用得最多的三个组合:
nl -ba -w 2 -s ' ':所有行编号,宽度收窄到 2,分隔符换成两个空格。输出紧凑,人眼扫起来不累;nl -ba -w 1 -s ',' file:给文件生成带序号的数据,方便 awk/cut 后处理;nl -b a -n rz -w 4 -s ' ' file:四位零填充的行号,适合对齐要求极高的文件,比如代码评审截图。
排版这件事,最忌讳的是“默认就是对的”。我见过太多人因为默认输出了 6 个字符宽的行号,导致后续awk '{print $1}'拿到的字段是乱七八糟的空格加数字,最后还要再洗一遍数据。先想清楚输出要干什么用,再决定宽度和分隔符,能省很多事。
4. 真实战场:nl如何帮我快速干完这些活
前面讲的是参数,这一节聊聊我怎么在真实工作中把这些参数组合起来用。每个场景都是我自己踩过的需求,直接给可复用的命令。
4.1 日志定位:nl + grep 查看错误上下文
排查线上问题时,最原始的需求是:找到出错的日志行,同时看清它前后的上下文。以前我的做法是grep -n -A 10 -B 5 "ERROR" app.log,这样确实能得到带行号的上下文,但 grep 自带的行号格式很朴素,只有数字加冒号,而且上下文行的行号不会连续递增显示(它会把每个匹配块的行号都单独标一次)。
我的替代方案是:
$ tail -n 2000 app.log | nl -ba -w 4 -s ' ' | grep -A 10 -B 5 'ERROR'思路是:先用nl -ba给日志整体加好连续行号,再用 grep 按关键字筛选。区别在哪?grep 输出的每个匹配块,上下文里的行号都是从同一套编号体系里带出来的,你能一眼看出错误附近实际的日志总行数间隔,而不用自己心算。比如输出里1234后面紧跟着1244,中间隔了 10 行,这个信息在排查时非常有参考价值。
4.2 脚本清单:nl + ls 生成带序号的列表
我经常要给一堆配置文件或者服务器列表生成带序号的清单,然后发给同事确认。最土的办法是复制到 Excel 里再补序号,实际上一条命令就能生成:
$ ls -1 /etc/*.conf 2>/dev/null | nl -ba -w 2 -s ' ' 1 /etc/adduser.conf 2 /etc/apt/apt.conf.d/proxy.conf 3 /etc/auto.masterls -1保证每行一个文件,nl -ba保证每一行都有编号。几十上百个文件的清单几秒钟就出来了。如果你想把清单文件化和后续核对,配合重定向保存下来即可:
$ ls -1 /etc/*.conf 2>/dev/null | nl -ba -w 2 -s ' ' > conf_list.txt4.3 与其他命令协同的几个经典管道
nl最妙的用法在于它处在管道中间时,行号可以成为下游命令的数据来源。三个我常用的组合:
先编号再截取区间:
$ nl -ba server.log | sed -n '120,140p'当你事先知道某个编号区间时,先用nl -ba给全文编号,再交给 sed 取区间,比直接 sed '120,140p' 多了可以随时回看整体编号的余地,而且空行也被依次编号,行号与文件物理行完全一致,不会错位。
转 CSV 后按字段提取:
$ nl -ba -w 1 -s ',' /etc/passwd | awk -F',' '{print $1, $NF}'这里-s ','将分隔符设为逗号,awk 按逗号拆分后,第一列就是行号,最后一列是用户的登录 shell。这个写法在批量处理配置文件时很有用:把行号和内容拆成两列,逻辑一下子清晰了。
与 while 循环配合做行级处理:
$ nl -ba -w 1 -s '|' file.txt | while IFS='|' read -r num content; do echo "Line $num: $content" done这个玩法的价值在于:当你的脚本需要对文件逐行处理且需要知道行号时,用nl -ba -w 1 -s '|'先把行号作为显式字段附加到每一行,然后 read 拆分,处理逻辑会干净很多。比自己在循环里维护计数器变量要省心,也不容易出错。
5. 我踩过的坑和仍然在用的习惯
任何命令用得久了都会踩几个坑,nl也不例外。这一节我把最常见的误区和经验写出来,能帮你少走弯路。
5.1 最常见的三个误会
误会一:空行“被漏掉”是 bug。其实不是。默认-b t只给非空行编号,这是设计使然。如果你要的是物理行号一一对应,那么用-b a即可。记住这个对应关系,比每次报错都强。
误会二:-w 6是上限。不是。-w只是最小宽度。当文件超过 6 位数行时,行号自动扩展,输出依然正确。我自己见过一个大文件到 20 万行时输出完全正常,数字没有被截断。
误会三:-s只能设制表符。这是cat -n留下的惯性思维。-s可以设成任何字符串,包括,、|、空格甚至多个字符。充分利用这一点,可以极大方便下游解析。
5.2 多文件与管道输入时的编号陷阱
nl可以一次性接多个文件参数,但它的编号是连续累计的。什么意思?比如nl a.txt b.txt,它会先把两个文件作为同一个流来处理,行号从 1 一直递增到最后一个文件的最后一行,不会因为文件切换而重置编号。
如果你希望每个文件都从第 1 行开始编号,有两种做法:一种是把多个文件分开,逐个执行nl;另一种是每个文件单独通过管道输入给nl,让每个独立的进程都从默认的 1 开始。在脚本里写个循环最稳妥:
for f in *.conf; do nl -ba "$f" done还有管道输入时的坑。nl从 stdin 读取内容时,-b t依然默认生效。如果上游命令输出了大量空行,你会发现行号跳得很厉害。想要“输入多少行就编号多少行”,记得显式加-b a。
5.3 我的alias搭配与日常用法
用久了以后,我给自己配了一个固定的别名,写进~/.bashrc或~/.zshrc里:
alias nln="nl -ba -w 2 -s ' '"这样每次查看文件时,敲nln 文件名就能得到紧凑的逐行编号输出。它跟cat -n的区别一眼可见,行号占位少、信息密度高,看起来非常舒服。
还有一个习惯是对 Windows 传过来的文件先处理换行符。nl本身不在乎行尾是\n还是\r\n,但如果你把输出继续交给awk、cut或者while read处理,\r会残留在字段末尾,导致各种莫名的匹配失败。所以遇到来自 Windows 环境的文件,我会先dos2unix或sed -i 's/\r$//'再做行号处理。这一步虽然小,却能省下排错半小时。
最后再说一个版本差异的小提醒:nl是 POSIX 标准命令,在 GNU coreutils、macOS 自带版本上核心参数基本一致。但个别发行版的老版本可能在默认宽度、-w单位上有细微差别。跨平台跑脚本之前,最好先确认目标机器的nl --version或man nl输出,不要想当然。