news 2026/10/16 3:29:11

comm命令详解:Linux三列输出与集合比较实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
comm命令详解:Linux三列输出与集合比较实战

comm这个命令,我第一次在脚本里见到时愣了一下:参数就三个数字、输出还分三列、用之前必须先排序……乍看像个“半成品工具”。但真正用熟之后,我反而觉得它是 GNU coreutils 里被低估最严重的文本处理命令之一。这篇内容就围绕comm命令的完整用法、三列输出的设计逻辑,以及它在实际脚本里的组合玩法展开。适合刚接触 Linux 命令行的人,也适合那些“用过但没吃透边界”的老手——比如它和diff、join、sort -u的分工到底是什么,什么场景下用comm才是最优解。

1. comm命令能做什么:三列输出的设计逻辑

1.1 从需求场景理解comm的存在意义

日常写脚本、维护服务器的时候,经常要回答这类问题:两份配置文件里哪些行是两边都有的?旧版本服务器的软件包列表和新版本差了哪些?这次数据同步之后,A 清单里多出了哪几条记录?

comm就是为了这种“两个集合的比较”设计的。它同时读取两个已经排好序的文件,逐行做字典序比较,然后按三列输出:第一列是“只在第一个文件出现的行”,第二列是“只在第二个文件出现的行”,第三列是“两个文件都出现的行”。

这个设计听起来平平无奇,但非常符合 Unix 工具哲学:命令只做一件事,把比较结果原样交给你,怎么裁剪由参数控制,怎么组合交给管道。它不像diff那样给你展示“第几行改成了什么”,也不像grep -f那样只关心“某个集合里的行在另一个集合里是否存在”,它把集合之间的差集、交集、补集关系用一条命令全包了。

我实际用下来的体会是:只要你在处理“清单类”文本——IP 白名单、用户列表、软件包名、订单号、文件路径——comm几乎总能比手写循环和diff更快给出答案。它不需要把整个文件载入内存,读一行比一行,内存占用极低,处理大文件也不虚。

1.2 输出三列的底层约定

为什么叫“三列”?这里要解释一下comm输出格式的具体约定,否则很多人第一次跑会以为命令坏了。

默认情况下,第一列的行首没有任何前缀字符;第二列的行首有一个 Tab 制表符;第三列的行首有两个 Tab 制表符。所以它并不是真的帮你对齐成表格,而是用 Tab 的数量来标记行归属。举个例子:

# f1.txt apple banana cherry # f2.txt banana cherry date

执行comm f1.txt f2.txt,输出是:

apple banana cherry date

肉眼看上去很乱,但实际含义很明确:apple前没有空白,表示只在 f1;banana前有一个 Tab,表示只在 f2;cherry前有两个 Tab,表示两个文件都有;date前有一个 Tab,表示只在 f2。

这个约定一旦理解,再看它的参数就完全不绕了:-1是“不显示第一列”,-2是“不显示第二列”,-3是“不显示第三列”。所以comm -12只输出第三列,也就是交集;comm -23只输出第一列,就是文件1独有;comm -13只输出第二列,就是文件2独有。换句话说,所谓“三个数字参数”,本质就是三个列的显示开关。

2. 核心用法与参数解析

2.1 基本语法与一个关键前提

comm的基本语法很简单:

comm [选项] 文件1 文件2

两个文件参数缺一不可,如果其中一个想用标准输入,可以用-占位。例如:

cat f1.txt | comm -12 - f2.txt

这里-代表从管道读取的标准输入。这个写法在处理流式数据时非常常用。

真正需要注意的关键前提是:两个输入文件必须已经按字典序排好。comm本身不排序,它只是把两个文件当作已经排过序的序列,一路游标式地向前推进比较。如果输入无序,它的输出就是错的,而且不会报错——除非你开了--check-order让它检查。

我有一个习惯,凡是涉及comm的脚本,都会在管道里显式写sort,不赌“这个文件碰巧是排好序的”。曾有个线上脚本把两个 CSV 文件直接丢给comm,本地测试没问题,因为开发时的样例数据恰好有序;到生产环境跑出来结果完全不符合预期,排查半天才发现数据源里混入了乱序行。从那以后我给自己定了死规矩:comm前面必须有sort,除非是刚用sort生成的文件。

2.2 三个数字参数的含义与常用组合

把参数展开说清楚:

参数作用常见场景
-1不输出仅存在于文件1的行配合其他参数组合使用
-2不输出仅存在于文件2的行配合其他参数组合使用
-3不输出两个文件都有的行只看两边差异
无参数完整三列输出快速肉眼分组检查
-12只输出两文件共有行求交集
-23只输出文件1独有行求A差集
-13只输出文件2独有行求B差集
-3输出文件1独有和文件2独有两侧差异汇总

-12、-23、-13这三个组合是我用得最多的。GNU 版本的comm允许把参数写在一起,也可以分开写,比如comm -1 -2和comm -12完全等价。shell 脚本里为了可读性,我通常用合并写法,很短,一眼能看懂。

-123这种全关的写法实际没意义,输出一定是空,不建议出现在脚本里,容易误导后人。

2.3 --check-order 与 --nocheck-order 的坑

GNUcomm提供了两个和排序检查相关的选项,很多人不看文档根本不知道:

  • --check-order:显式要求检查输入是否已排序,发现乱序直接报错退出
  • --nocheck-order:关闭排序检查

默认行为是:如果两个输入都是普通文件,comm会自动检查排序状态,发现乱序会提示类似comm: file 1 is not in sorted order的报错;如果其中一个输入是标准输入管道,由于无法预读整个数据流,检查逻辑可能不会生效,行为会退化为不检查。

这个差异坑了我一次:我在脚本里用进程替换<(...)给comm传数据,其中一个分支的输出没有正确排序,结果comm没有报错,而是静默地给出了一个乱七八糟的结果。排查到半夜才意识到默认检查在管道场景下不生效,后来我干脆在比较逻辑前强制加sort,把排序责任从“依赖默认检查”变成“在源头保证”。

另外,如果你用sort生成输入,默认排序规则和comm的字典序比较规则在大多数场景下是一致的,都基于 locale 的排序规则。但要注意,sort的排序行为受LC_ALL、LANG等环境变量影响,不同机器上的 locale 不同,可能导致排序结果不一致,进而影响comm比较。我遇到过开发机是en_US.UTF-8,服务器是C.UTF-8,同一个脚本在两台机器上输出不同的情况。规避方法很简单:脚本开头固话export LC_ALL=C,让排序规则稳定。

3. 实操案例:从入门到组合拳

3.1 基础案例:对比两个软件包列表

假设我有两台环境上的软件包清单,分别是pkg_old.txt和pkg_new.txt:

# pkg_old.txt curl-7.29.0 git-2.30.1 nginx-1.18.0 openssl-1.1.1k vim-8.2
# pkg_new.txt curl-7.29.0 git-2.39.2 nginx-1.18.0 nodejs-16.20.0 openssl-1.1.1k vim-8.2

先排版再比较:

sort pkg_old.txt -o pkg_old_sorted.txt sort pkg_new.txt -o pkg_new_sorted.txt comm pkg_old_sorted.txt pkg_new_sorted.txt

输出结果里能清楚看到:curl、nginx、openssl、vim这三个(实际上四个)是一样的,出现在第三列;git-2.30.1出现在第一列,git-2.39.2出现在第二列,因为版本号不同,它们被当作两个完全不同的行,而不是“同一个包升级了”;nodejs出现在第二列,是新增包。

这种场景特别适合做发布前检查。我写过一个升级脚本:升级前和升级后各导出一份依赖清单,用comm -13提取新增的依赖,用comm -23提取消失的依赖,然后人工 review 一遍。看起来不起眼,但它把“人眼对比两个几百行的清单”变成了“只看几条差异”,效率提升非常明显。

3.2 进阶案例:集合运算(交集、差集、并集)

如果只看两个文件的集合关系,comm值得被当作集合运算工具来用。

假设A.txt是允许访问的 IP 列表,B.txt是实际发生登录失败的 IP 列表,现在要看两拨人:

# A.txt(白名单) 192.168.1.10 192.168.1.20 192.168.1.30 10.0.0.5 # B.txt(失败列表) 192.168.1.30 10.0.0.5 172.16.3.9
  • 白名单内且登录失败(交集):comm -12 <(sort A.txt) <(sort B.txt)
  • 白名单外但登录失败(异常来源):comm -13 <(sort A.txt) <(sort B.txt),输出的正好是只存在于 B 的 IP,也就是“不在白名单却尝试访问”的家伙

如果只是想把两边的独有元素都拿出来,而不关心交集,可以用comm -3,输出会保留两列,需要后续处理。要注意的是,我在实践里不太用comm -3接tr -d '\t'这种粗暴方式去除列表前缀,因为如果行内容本身含有 Tab 字符,删除前缀会把内容里的 Tab 也毁了。更安全的做法是分别用comm -23和comm -13取两侧独有,想合成一个文件时就交错着 append,而不是依赖-3后做文本清洗。

那么并集呢?comm本身不直接提供“去重后的并集”功能,因为它严格区分三列,没有“合并去重”这种输出模式。取并集的土办法是:

cat A.txt B.txt | sort -u

这个组合比comm更快,也更直观。所以我总结出的经验是:comm的强项在于“需要区分来源的差集运算”,而“并集去重”这种需求,用sort -u解决才是正确的工具选择。

3.3 组合案例:进程替换、管道与其他工具联动

真正把comm用活,靠的是和 Bash 的特性配合。最经典的是进程替换:

comm -12 <(sort a.txt) <(sort b.txt)

这里的<(...)语法让进程替换直接在参数位置生成一个“虚拟文件”,comm读取它时就像读一个普通文件一样。好处是省去了创建临时文件的步骤。我后来写对比脚本几乎不生成中间文件,全用这种写法,脚本短、可读性好,也不会留下垃圾临时文件。

再看一个和管道结合的典型场景:从一个大的访问日志里过滤出某个网段的 IP,再和封禁列表对比:

grep '^10\.' access.log | awk '{print $1}' | sort -u | comm -12 - block_list.txt

-在这里代表标准输入,前面管道传来的内容必须已经排序,否则就失去意义。

还有一个我常做的“快照对比”:记录某进程列表前后的差异。伪代码是这样的:

ps -eo comm --no-headers | sort -u > /tmp/procs_now.txt # /tmp/procs_before.txt 是之前存下的快照 comm -13 /tmp/procs_before.txt /tmp/procs_now.txt # 新增的进程 comm -23 /tmp/procs_before.txt /tmp/procs_now.txt # 消失的进程

这个思路可以迁移到很多地方:文件系统快照、依赖清单快照、任务队列快照。本质上都是“两个时间点的集合差”,comm是这类对比最顺手的实现方式。

还有个大杀器组合是comm配合awk对结果做二次加工。比如交集结果想带上行号:

comm -12 A.txt B.txt | awk '{print NR, $0}'

所有能从标准输入读取数据的工具,几乎都能和comm搭在一起用。关键是记住输出列前缀的 Tab 规则,后处理时才能避开那些隐形的坑,这一点我后面会详细讲。

4. 常见问题与排查技巧实录

4.1 输出看起来乱码?先查排序和分隔符

很多人第一次跑comm都会觉得输出“对不齐”“像乱码”,其实不是命令坏了,而是 Tab 在终端里显示得不够直观。想确认分隔符,可以用cat -A把 Tab 显性化:

$ comm A.txt B.txt | cat -A apple$ banana$ ^Icherry$

^I就是 Tab 的转义显示。看到这个,就能确认第二列、第三列的前缀到底存不存在。

排在分隔符之前的排查事项永远是排序。如果两个输入文件没有排序,什么结果都可能是错的。我遇到“输出结果和预期完全不符”的案例,十有八九是排序的问题,而不是comm本身的问题。建议把第一反应固定成:先sort再comm。

4.2 大文件跑不动?瓶颈往往不在comm本身

comm是流式逐行比较的,内存占用很低,所以“跑不动”这件事,责任通常在sort而不是comm。如果你要对 GB 级别的文件做排序,可以用sort -S控制内存上限,让它在内存不足时自动走外部归并排序。

sort -S 1G bigfile.txt -o bigfile_sorted.txt

排好序后再跑comm,它的速度是极快的,因为只是线性扫描两个文件各一遍。这个特性让comm非常适合做大文件集合比对,反而是一些脚本语言里“把所有行读进内存再集合求交”的做法在大数据下会内存爆炸。

另外提醒一点:如果同一个大文件要做多次比较,请先把排序结果落盘保存,不要每次比较都重新 sort 一遍。一次排序、多次使用,这算是处理文本数据时最基本的 IO 成本意识。

4.3 comm、diff、join、sort -u 到底怎么选

这几个工具经常被搞混,因为表面上都跟“比较”“去重”有关。我按自己的理解整理一张表:

工具核心功能输入要求输出特点
comm两个有序文件的逐行集合比较两个文件均已排序三列输出,列可以分别裁剪
diff两个文件的行级差异展示不需要排序带行号、上下文和变更标记
sort -u单个文件的排序与去重单一文件或流每行输出一个唯一元素
join按指定字段连接两个文件两个文件按连接键排序横向拼接字段
grep -f用文件B的每一行作为模式去匹配文件A无需排序,按模式匹配只输出匹配到的行

选型逻辑其实很清晰:如果你关心的是“哪些行只有左边有、哪些只有右边有、哪些两边都有”,选comm;如果你想知道“文件A第几行变成了文件B的哪一行”,选diff;如果你的文本是有列结构的(比如 TSV、CSV),要按某一列做关联,选join;如果只是单纯给一个文件去重,选sort -u。grep -f适合“模式匹配”语义,不要求另一边的文件内容精确等于行,而是作为正则或字符串模式去扫。

举一个典型对比:两个文件内容基本一致,只有一处不同。用diff可以秒定位到那一行,用comm -3也能看到差异行,但看不到行号,还得自己数。反过来,如果两个文件都很长、重复少、只是想知道集合层面的差异,diff的输出会非常啰嗦,comm -12和comm -23这类用法反而清爽。项目里遇到比较需求,先想语义,再选命令。

4.4 同一个文件里重复行的问题

comm是按“行出现的次数”来配对的,不是先做去重再比较。举例说明:A.txt里有两行apple,B.txt里有一行apple,comm会把其中一对apple放到第三列(共有),剩下一个多余的apple放到第一列(A 独有)。

如果你没有提前sort -u,这个行为可能误导判断。比如一个订单号在日志里出现三次,另一个文件里出现两次,你看到的结果是:交集列里有一行order123,差集列里也有一行order123。这并不表示“这个订单号既共有又独有”,只能说明两个文件里该行出现的次数不一致。

所以我的建议是:在用comm做集合语义比较之前,先想清楚你要的是“集合”语义还是“多重集”语义。如果只关心“这个元素在不在”,请先sort -u再去重;如果关心“出现次数差异”,sort | uniq -c再比较才是正确工具。千万别拿comm硬算次数差,它能做到,但结果很容易被误读。

5. 我的几个独家使用心得

5.1 用函数封装comm的前提条件

comm最大的使用门槛是“输入必须排序”。为了根治这个隐患,我在 shell 配置里放了一组小函数:

comm_both() { comm -12 <(sort -u "$1") <(sort -u "$2") } comm_only_left() { comm -23 <(sort -u "$1") <(sort -u "$2") } comm_only_right() { comm -13 <(sort -u "$1") <(sort -u "$2") }

这样每次调用不用惦记排序和进程替换,函数名本身就把语义写清楚了。脚本里写comm_only_left old_list new_list,比裸写一行comm -23 <(sort old) <(sort new)可读性好得多,也不容易把参数顺序搞反。

这个习惯看着简单,实际帮我省了很多心。函数内部显式加sort -u,等于把comm的前置条件固化在入口处,后续谁调用都不会踩排序的坑。

5.2 用 --output-delimiter 适配数据导出

GNUcomm支持--output-delimiter参数,用来自定义列与列之间的分隔符。默认的 Tab 在终端里可读性尚可,但结果要导入 Excel 或 CSV 就要二次处理。这个参数可以直接改:

comm --output-delimiter=, -3 A.txt B.txt

这样输出就变成逗号分隔的两列,不需要再用sed去替换 Tab。不过要小心,如果行内容本身包含逗号,这种导出方案同样会被干扰。对于文本比较的最终输出,我通常还是用 Tab 分隔,因为它在通用性上最稳。

5.3 别依赖comm的退出码做判断

这点我觉得值得单独强调:comm的退出码不像diff那样携带“内容是否有差异”的信息。diff在两个文件不同时会返回 1,而comm无论比较结果如何,只要命令正常执行完都返回 0。

所以在 CI 或巡检脚本里想表达“两份清单是否完全一致”,不能直接判断comm的退出码,而是要看它的输出是否为空:

if [ -n "$(comm -3 <(sort cfg_a) <(sort cfg_b))" ]; then echo "配置有差异" else echo "配置一致" fi

这里comm -3只输出两侧不同的行,只要这段字符串非空,就说明两份配置不完全一致。这种写法比比较文件哈希更灵活,因为你能拿到具体差异,而不是只知道“不一样”。

5.4 用 awk 做后处理时的隐藏坑

最后分享一个我踩过好几次的坑:comm输出里的行首 Tab,会影响你后续的文本匹配。

例如:

comm -3 A.txt B.txt | grep '^keyword'

如果keyword正好是第二列的内容,行首会有一个 Tab,^keyword根本匹配不上。类似地,用sed -n '/^keyword/p'也会漏掉第二列和第三列的行。

解决方法有几种。最简单的是在需要精确匹配时,先把列前缀去掉再交给其他命令:

comm -3 A.txt B.txt | sed 's/^\t//' | grep '^keyword'

但注意,这个sed会把第三列开头的两个 Tab 都删掉吗?不会,^\t只匹配行开头的一个 Tab,第三列的行删掉一个 Tab 后还剩一个。稳妥的做法是去掉全部行首 Tab:

comm -3 A.txt B.txt | sed 's/^\t*//' | grep '^keyword'

不过前面也说过,行内容本身含 Tab 时这种方式有风险。所以更推荐的做法是:不要用comm -3再清洗,而是用comm -23和comm -13分别取两侧独有行,再各自处理,天然没有前缀问题。这个习惯能避开一大批后处理陷阱。

我个人在实际操作中的体会是,comm的难点从来不在命令本身,而在三个地方:排序前提、Tab 前缀、退出码语义。只要把这三件事想清楚,它就是你工具箱里最锋利的集合比较工具。我的脚本里出现comm的频率远高于diff,因为大多数时候我关心的是“多了什么、少了什么”,而不是“改动在第几行”。如果你也经常和清单、白名单、快照打交道,花点时间把comm的参数和输出约定吃透,回报率会非常高。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 3:29:07

n8n二进制文件流管理与复杂上下文传递全指南

做自动化工作流做到一定阶段&#xff0c;你会发现一个规律&#xff1a;凡是涉及二进制文件流的流程&#xff0c;翻车率永远比纯 JSON 流程高一个量级。文件下载下来打不开、邮件附件变成 0KB、多节点上下文传递时二进制数据“神秘消失”——这些坑几乎每个 n8n 玩家都踩过。这篇…

作者头像 李华
网站建设 2026/10/16 3:28:47

OpenCV双目标定与校正核心原理:从极线约束到视差图可用性

简介&#xff1a;本资源是一套基于OpenCV 3.0与Visual Studio 2013实现的双目视觉立体标定与校正完整工程&#xff0c;面向计算机视觉初学者、机器人感知方向学习者及三维重建实践者&#xff0c;解决双目相机系统参数标定、极线校正等核心问题&#xff0c;为后续立体匹配与深度…

作者头像 李华
网站建设 2026/10/16 3:28:46

Android端部署TensorFlow Lite int8量化模型实战指南

简介&#xff1a;本资源是一个面向Android开发者与移动端AI初学者的TensorFlow Lite图像分类实战Demo&#xff0c;帮助用户快速掌握在移动设备端部署轻量级深度学习模型的核心流程。压缩包共110个文件&#xff0c;包含64个XML布局与配置文件、10个PNG图标资源、9个BIN缓存及构建…

作者头像 李华
网站建设 2026/10/16 3:27:57

Flutter for OpenHarmony实战:逆向思维训练App与学习日历开发全解析

最近两个月&#xff0c;我完整做完了一个逆向思维训练App&#xff0c;并且把学习日历功能扎实落地&#xff0c;整个项目跑在 Flutter for OpenHarmony 这套生态上。这篇文章从头到尾拆一遍&#xff1a;为什么做这个方向、核心功能怎么设计、日历模块怎么实现、在鸿蒙设备上适配…

作者头像 李华
网站建设 2026/10/16 3:27:25

SpringBoot+微信小程序毕设实战:乐器宣传平台搭建全攻略

“微信小程序毕设项目推荐”这个标签&#xff0c;几乎是每个计算机类专业学生在大三下学期都要面对的关键词。刷了无数个帖子之后你会发现&#xff0c;乐器商城、校园助手、点餐系统这类题目出现的频率极高&#xff0c;但真正能让人眼前一亮、拿到答辩桌上不怕老师追问的并不多…

作者头像 李华
网站建设 2026/10/16 3:27:24

BIM模型大文件上传:前端目录结构分片传输方案详解

很多做工程信息化的人都有一个共同痛点&#xff1a;BIM模型传不上去。我在多个项目的运维现场见过同样的场景——一个综合模型动辄几十GB&#xff0c;Revit中心模型加一堆链接文件能上百GB&#xff0c;网络明明是千兆内网&#xff0c;带宽根本不缺&#xff0c;但Web端上传愣是能…

作者头像 李华