
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前没有空白表示只在 f1banana前有一个 Tab表示只在 f2cherry前有两个 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_ALLC让排序规则稳定。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里有两行appleB.txt里有一行applecomm会把其中一对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 后还剩一个。稳妥的做法是去掉全部行首 Tabcomm -3 A.txt B.txt | sed s/^\t*// | grep ^keyword不过前面也说过行内容本身含 Tab 时这种方式有风险。所以更推荐的做法是不要用comm -3再清洗而是用comm -23和comm -13分别取两侧独有行再各自处理天然没有前缀问题。这个习惯能避开一大批后处理陷阱。我个人在实际操作中的体会是comm的难点从来不在命令本身而在三个地方排序前提、Tab 前缀、退出码语义。只要把这三件事想清楚它就是你工具箱里最锋利的集合比较工具。我的脚本里出现comm的频率远高于diff因为大多数时候我关心的是“多了什么、少了什么”而不是“改动在第几行”。如果你也经常和清单、白名单、快照打交道花点时间把comm的参数和输出约定吃透回报率会非常高。