资讯动态

comm命令详解:从sort到diff,玩转Linux文件集合比较

发布时间:2026/10/16 9:47:11 来源:尧图企业网站定制
1. 先从一次真实的文件比较说起上个月帮朋友排查两个服务器上的软件包差异一个跑着生产业务一个准备做灾备切换两边按理说应该是同一套环境但总让人不放心。最简单的办法是把两边已安装的软件包列表导出来然后对比。大家第一个想到的是diff但diff看的是“逐行差异带上下文”输出结果在纯粹“找出两边各自独有的东西”这种需求下显得特别啰嗦。这时候comm就派上了用场。凡是老 Unix 兵应该都见过这个命令但说实话现代 Linux 环境下知道diff、grep的人很多真正把comm用熟的人反而不多。原因不外乎它有一个容易劝退的前提条件输入文件必须按字典序排序。很多人第一次试的时候直接拿两个没排序的文件跑输出结果完全没法看于是用了一次就再也不碰它。其实comm非常优雅尤其是和sort、find、cut等命令组合使用后专门解决“两个集合到底差在哪”这类问题比diff更适合脚本处理比grep -v -f更适合超大文件。这篇我就把这十几年前老命令的用法、原理、坑以及在实际场景里怎么跟其他命令组合一次性讲透。2. comm 的基本玩法与参数拆解2.1 输出三列格式究竟在表达什么comm的名字来自common意思是找两个文件里共有的行。它的标准输出是“三列”的排版第一列只出现在第一个文件的行第二列只出现在第二个文件的行第三列两个文件都有的行用文字描述总是绕直接跑一遍最直观。先造两个小文件printf apple\nbanana\ngrape\norange\n left.txt printf banana\ngrape\nkiwi\nmelon\n right.txt然后执行comm left.txt right.txt输出如图用字符表示 tab 分隔banana grape apple kiwi melon orange是不是一头雾水这就是comm被吐槽最多的地方它的“列”不是用空格对齐的而是用制表符tab占位。第一列的行前面没有任何分隔符第二列的行前面带有 1 个 tab第三列的行前面带有 2 个 tab。所以上面的输出实际意思是banana、grape是两边共有输出在第三列apple、orange只属于 left.txt输出在第一列kiwi、melon只属于 right.txt输出在第二列。但人眼直接看这个结果很吃力因为它没有表头、没有对齐、tab 在不同终端下显示宽度不一致。这也是comm劝退新手的另一个原因。想看清楚 tab 去哪了可以用cat -Acomm left.txt right.txt | cat -A^I就是 tab三列区分得清清楚楚。2.2 三个数字参数-1、-2、-3 的控制逻辑comm最核心的功能全在这三个参数上-1不输出第一列只在文件1里出现的行-2不输出第二列只在文件2里出现的行-3不输出第三列共有的行也就是说-1对应“第一个文件独有”-2对应“第二个文件独有”-3对应“两边共有”。需要哪种结果就删掉哪一列。我最常用的几个组合值得背下来# 只输出两边共有的行屏蔽第一列和第二列 comm -12 left.txt right.txt # 只输出左边独有、右边没有的行屏蔽第二列和第三列 comm -23 left.txt right.txt # 只输出右边独有、左边没有的行屏蔽第一列和第三列 comm -13 left.txt right.txt注意-12是-1 -2的简写形式三个参数可以任意叠加同时传-123就什么都不剩了相当于把两边的行都屏蔽掉。这个特性我用过很多次用来卡在管道里做“有副作用但无输出”的中间校验比如只想要comm的退出逻辑而不需要它的输出文本。2.3 第一次实战找出两个列表的差集回到开头那个服务器对比场景。假设已经从两台机器上导出了包列表格式是每行一个软件包名不含版本号文件分别叫pkg-prod.txt和pkg-dr.txt。现在要看生产环境有、灾备环境没有的包命令就一句话comm -23 (sort -u pkg-prod.txt) (sort -u pkg-dr.txt)这个写法用了进程替换(...)两个文件的排序结果直接作为comm的输入不用生成临时文件。如果测试下来结果为空说明灾备没有缺少生产环境任意一个包反过来查灾备多出来的包comm -13 (sort -u pkg-prod.txt) (sort -u pkg-dr.txt)这种能力用diff也能做但diff的输出夹杂、、---这类标记脚本里二次处理还得再解析。comm干净多了输出的每一行都对应真实存在的行直接喂给xargs或while read做后续操作都非常顺手。3. 为什么必须先排序原理层面的耐心解释3.1 comm 的比较逻辑是线性逐行扫描很多人只记住“要排序”但没搞明白“为什么必须排序”。其实comm内部工作原理非常朴素它同时对两个文件逐行读取维护两个“当前行”指针每次比较指针指向的两行。伪代码大致是read line1 from file1 read line2 from file2 while both files have more lines: if line1 line2: output line1 as only in file1 read next line1 else if line1 line2: output line2 as only in file2 read next line2 else: output line1 as common read next line1 and line2这种算法的时间复杂度是 O(m n)即两个文件的总行数相加并且几乎不占额外内存。相比之下grep -v -f的做法是把一个文件全部加载进内存构建模式集合文件一大内存压力急剧上升diff的 Myers 算法虽然也是线性复杂度但它要维护编辑路径输出中包含大量上下文标记对机器解析不友好。所以comm在“两个有序文本集合求差集”这个问题上是目前命令行工具中最经济的选择。它不排序只比较把排序这个职责甩给了sort自己保持轻薄小巧。3.2 排序的坑sort 和 comm 的排序规则必须一致这里我要单独拎出来强调一个很多老手都翻过车的细节sort -n数值排序和comm默认的字典序比较是不兼容的。comm的内部行比较是逐字符按字典序进行的也就是和sort默认行为一致。但如果你先执行了sort -n比如对进程 PID 或者端口号排序文件里出现10、9、100这样的顺序comm会拿这些行做字典序比较而不是按数值大小去对齐结果就会产生一堆“看似不该出现”的独有行。遇到数字开头的行我的建议是非必要不用sort -n直接用默认字典序排序后再交给comm。如果实在必须按数值排序那就需要重新审视这个方案是否应该改用join或awk来做关联逻辑。命令行工具各自有各自的长处不要老想用一个命令硬拗所有场景。另外comm不像sort那样支持-k指定排序字段它只能比较整行。如果只想按某个列作为比较键就得先用awk {print $N}把对应列抽出来或者用sort -k排好序后再cut出需要比的部分避免整行内容不一致导致误判。3.3 重复行comm 不帮你去除别指望它输出唯一值再多说一句重复行的问题。comm只是机械地把输入文件各自的行拿来做比较它不会去重。如果某个文件里同一行出现了两次而另一个文件里只出现一次结果可能出乎意料left.txt: A A B right.txt: A B执行comm -23 left.txt right.txt输出会是A因为 left 里第一个A与 right 里的A配对了第二个A没有配对对象就被输出为“left 独有”。这就是为什么前面实战里我在sort后面必须要加-u的原因。sort -u和comm -23是天生一对没有-u的排序结果交给comm等于亲手埋雷。4. 实际场景里的组合用法与效率观察4.1 场景一找两个软件源里版本不同的包名我有一次要从两个内网软件仓库的索引文件里找出“哪些包的版本两边不一致”。列表格式是xxx-core 2.3.1 yyy-lib 1.0.2 zzz-agent 7.5.0两边行顺序不同、包含的包也有差异。如果直接用comm会把这些行当成“整行不同”把版本号差异的行当成独有项这是符合逻辑的。但我想找的是“同名但版本不同的包”怎么办思路是先按包名抽列再求交集里的差异。步骤拆成两条# 先抽出包名去重排序 cut -d -f1 repo-old.txt | sort -u old.names cut -d -f1 repo-new.txt | sort -u new.names # 找到两边都有的包名 comm -12 old.names new.names both.names # 从原文件中筛出这些共同包名的版本进行对比 grep -F -f both.names repo-old.txt old.entries grep -F -f both.names repo-new.txt new.entries之后可以用paste、diff或者干脆写个小awk把两个同名包的版本对齐展示。这套流程的价值在于comm负责“求集合交集”这步脏活后面的版本差异就可以交给更擅长的工具。4.2 场景二大文件下的性能表现曾对比过两个百万行级别的白名单文件一个约 38MB一个约 41MB。用grep -v -F -f做差集跑了差不多 40 秒内存峰值接近 1GB换成sort -u加comm -23的组合整个过程只用了不到 5 秒内存占用不到 200MB。差距来源很直接grep -f的模式加载是内存密集型操作模式文件一大构建匹配串的开销很可观comm是流式逐行比较没有哈希表没有模式编译时间复杂度是令人安心的线性增长。所以只要排序能接受、行内容不包含特殊换行符comm在百万行级别的集合差运算里就是命令行首选。需要强调一下comm对大文件的另一个好处是稳定性——它不会因为输入中的某一行特别长导致性能突然劣化处理过程的时间预算基本可控。4.3 场景三配合 find 对比目录文件清单再分享一个日常高频用法对比两个目录下的文件列表。比如某个发布包在测试环境和生产环境的目录结构应该一致但可能有遗漏上传的文件。# 生成两个目录下所有文件相对路径列表 (cd /opt/test find . -type f | sort -u) test.files (cd /opt/prod find . -type f | sort -u) prod.files # 找出测试环境有、生产环境没有的文件 comm -23 test.files prod.files这个场景需要注意find输出如果后面要配合xargs或tar路径中有空格的文件名会被拆断。稳妥的做法是在生产端先确认是否有这类文件用find ... -print0检查如果有建议直接用rsync -n这类工具代替。comm对比文本行天然假设“每行一个文件名”空格问题没法绕过。4.4 场景四把输出列做标记化处理comm的三列输出不适合人读但很适合机器二次处理。有一个小技巧把三列变成带前缀的单列输出comm left.txt right.txt \ | sed -e s/^\t\t/COMMON\t/ -e s/^\t/RIGHT\t/ -e s/^/LEFT\t/执行后每行会带上标记前缀归类起来一目了然。这里利用了 tab 转义的顺序注意sed里最后的s/^/LEFT\t/不要写在前面否则会把所有行都标记成LEFT因为每行开头都可能匹配空串。实际经验是前两条替换规则执行后第一列的行模式变成了LEFT\t行内容第二列的行变成了RIGHT\t行内容第三列的行变成了COMMON\t行内容后面再接awk {print $1}就能统计三类行各自的数量。5. 容易踩的坑与排查方案5.1 排序不一致导致错误差集这个前面提过原理这里是实际的排查体验。曾经有人找我帮忙定位一个数据差异脚本他用sort -n排了端口号再用comm对比两个端口列表。结果发现一个文件里有1000、900、90另一个文件里有90、1000、900排序后第一种是数值顺序90、900、1000第二种是字典序1000、90、900两边同一个值不在同一位置comm当然找不出共同行。排查思路很简单先用od -c或head查看两边文件的开头几行确定它们是字典序一致的状态不规范的地方全部通过sort -u重新处理。还有一个检查手段是用join做个交叉验证如果join的结果行数和comm -12的行数对不上那大概率是排序规则出了问题。5.2 换行符不统一CRLF 惹的祸从 Windows 环境生成的文件再传到 Linux 上行尾经常是\r\n。在终端里看内容每行末尾都会带一个^M用cat -A能看到。comm把\r当成行内容的一部分导致一个明明是nginx的行和一个nginx\r的行被判定为不同内容完全匹配不上。处理办法一是用dos2unix转成 Linux 换行二是用sed -i s/\r$//清理三是如果不想动原文件可以在管道里顺手处理comm -23 (sed s/\r$// left.txt | sort -u) (sed s/\r$// right.txt | sort -u)这个坑在自动化流水线里特别容易复发因为文件源头可能来自异构系统。凡是comm结果明显不对劲而文件肉眼看着内容都对得上低概率怀疑排序问题剩下的高概率就是换行符和不可见字符问题。5.3 输出没反应先检查文件是否为空comm面对空文件不会报错只是安静地输出很容易让人以为比较结果为空。实际上如果一侧是全空另一侧有内容正确结果是整个非空文件的所有行都属于“独有”。如果用了comm -23 left.txt right.txt而 left.txt 是空的输出自然为空但这可能不是真实结果而是数据提取步骤没跑成功。我在脚本里通常加一个文件行数前置检查[ -s left.txt ] || { echo left.txt is empty; exit 1; }避免空文件混进流程导致后续决策基于一个假的“无差异”结论。这个问题容易忽略但一旦发生影响面比排序错误更可怕因为空结果看起来太正常了。5.4 性能定位卡在 sort 而不是 comm有时候整体执行很慢第一反应是comm效率低实际卡点往往在管道上游的sort。千万行级别的文件排序本来就是个重活comm自己是线性扫描几乎不会成为瓶颈。想要确认到底卡在哪可以用time分别测sort和comm或者把sort的输出先落到临时文件再手动跑comm就能拆出各环节耗时。另一个经验是把LC_ALLC设上再跑sort和comm可以避免 locale 带来的字符比较性能损失对大文件有明显提升。为什么有效因为 C locale 按字节直接比较不用处理 UTF-8 的多字节规则省掉大量计算开销。6. 和其他文本比较工具的横向选择6.1 diff 与 comm场景完全不一样很多初学者分不清diff和comm总觉得都在比较文件。我提供一个不太严格但很好用的理解方式diff输出的是“两个文件从文本 A 变成文本 B 需要做哪些修改”comm输出的是“两个文件在集合意义下各自独有哪些、共有哪些”。diff适合人工查看两个版本的代码或配置差异comm适合脚本判断某个元素是否落入特定集合。比如你要判断某个用户是否在允许列表里comm -12输出不为空就知道匹配了。如果用diff来做这个事还得解析它的和---标记多此一举。comm还有一个实用优势是天然适合做“多集合归并判断”比如三个文件分别求交集语法上不需要额外标记。而diff的嵌套解析会非常痛苦。6.2 grep -v -f 与 comm正确率与性能的取舍grep -v -F -f right.txt left.txt也可以找出“左边有右边没有”的行且不要求排序。但问题在于grep -F固定字符串匹配时一个文件里的每一行都作为模式set 构建内存开销大行内容里包含正则特殊字符时用-F能挡住一部分意外但仍然可能因为模式行中的空字符串或超长字符串产生意外匹配最关键的是grep -f没有集合语义它做的是“模式匹配”不是“行相等比较”。如果 right.txt 里有nginx而 left.txt 里有nginx.confgrep会认为后者“出现过”但comm会正确地把它当作不同行。所以结论是判断两个集合是否完全相等用comm判断一段文本是否包含另一个文件的某些关键内容用grep -f。两者目标不同不能互相替代。6.3 join 与 comm同门师兄弟但侧重不同join也是老牌排序文件处理命令它按公共字段把两个文件的行拼接起来类似于 SQL 里的 INNER JOIN。comm更接近集合运算join更接近关系表连接。比如两个文件都有用户 ID 和用户名你想把两边 ID 相同的行合并成一条记录这是join的活儿你想知道两边 ID 列表的并集、交集、差集这是comm的活儿。实际项目里经常三件套连用先sort -u规范化再comm做差集判断最后join做字段关联。老 Unix 工具的哲学是单一职责、管道协作组合起来的威力比任何单独一个都大。6.4 与 awk 实现差集比较的取舍近年写脚本的人越来越偏爱awk用NRFNR模式也能实现差集运算awk NRFNR { seen[$0]1 } NRFNR !seen[$0] right.txt left.txt这个写法的好处是不要求排序、记忆体里建哈希代码很短。但问题是一旦行数到几十万级内存占用远高于comm而且awk的哈希键在行内容过长时哈希冲突和内存碎片会让速度下滑。comm的地板内存就是两个缓冲区加常量光这一点就值得在大文件场景优先考虑。不过awk的灵活性在于可以按字段比较、支持复杂条件这是comm做不到的。日常小规模文件、字段语义复杂时我选awk数据量大、行语义纯粹时我选comm。7. 一点经验收尾经常有人问我怎么判断自己是不是真的掌握了一个命令。我的标准是能不能在不看手册的情况下准确说出它和三个相似命令的边界并且知道哪个场景应该用哪一个。comm给我最大的启发是排序不是它的缺点而是它的协议。它把复杂性和职责都推给了上游自己只做一件简单的事却在集合运算这个维度上无懈可击。那些觉得它不好用的人往往是在拿其他工具的语义去强套它。最后分享一个个人习惯我所有涉及comm的脚本第一行一定是set -euo pipefail并且在执行前先对输入文件做wc -l日志输出。这能帮助判断是数据源的问题还是比较逻辑的问题。这个习惯已经替我挡掉了好几次空文件的“无差异”假象。comm不像awk、sed那么“编程感”十足也不像grep那么花哨但它就是那种每个老系统管理员简历里一定会出现的命令——安静、可靠、招招到肉。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑