处理日志和配置文件时,最常用到的一个命令就是sort。不管是按字母排节目单、按数字排端口列表,还是把一堆重复行归拢起来做统计,sort基本几秒钟就能搞定。很多新手只会sort filename按字典序输出,没意识到它背后藏着一整套针对列排序、数值排序、版本号排序的选项,用好了真的能让日常文本处理效率翻倍。这篇博客我会从默认行为讲起,结合实际工作中踩过的坑和总结出来的经验,把sort的实用操作完整过一遍,适合刚接触 Linux 命令行的朋友,也适合早已会用sort但想进一步玩转它的人。
1. 先搞懂sort的默认行为:字典序与区域设置
1.1 最简单的排序其实是“整行字典序”
sort不带任何参数时,用法是sort [文件名],它会读取文件或标准输入,把每一行当作一个整体,按字符的字典序从小到大输出到标准输出。比如有个fruits.txt:
banana apple Cherry date直接sort fruits.txt,输出是:
Cherry apple banana date如果你在终端里跑过,会发现Cherry排在了apple前面。这是因为在很多默认区域设置下,排序规则不是简单的 ASCII 码比较,而是考虑到了大小写、语言习惯等。大写字母和小写字母的先后关系并不是你想象的那样“大写在前面”或者“小写在前面”,而是由当前的 locale(区域设置)决定。
这个细节最容易坑人。你在自己电脑上排序正常,到了服务器上因为 locale 设置不同,顺序完全变了个样。所以如果要对排序结果有绝对把握,最稳妥的是显示指定环境变量:
LC_ALL=C sort fruits.txtLC_ALL=C表示使用 POSIX 的 C 区域设置,排序规则基于字节值,简单粗暴且可预测。对于纯英文、数字、符号的文件,这基本等同于 ASCII 码排序,跨机器结果高度一致。
1.2 默认键不是“列”,而是“整行”
sort的默认比较键是整行内容,而不是某个字段。这意味着它不会自动把行拆成几列来比较。如果你有一个文件,每行是“姓名 分数”,想按分数排序,直接sort data.txt是按整行字符串排的,结果可能是:
张三 100 李四 23 王五 50看起来似乎碰巧按分数排了?但遇到李四 9这种分数只有一位数字时,字典序会变成:
张三 100 李四 23 李四 9 王五 50因为字符串"李四 9"和"李四 23"比较时,第三个字符'9'大于'2',所以"9"排在了"23"后面。这就是默认字典序和数值排序的差别。要真正按列、按数值排,必须用-k和-n,这是后面几节的重点。
2. 真正实用的列排序玩法:分隔符、键、多级排序
2.1 指定分隔符与排序键:-t和-k
日常处理的数据大多是结构化文本,常见格式有:
user_id:name:age:score 1001:alice:23:88 1003:bob:25:76 1002:carol:22:95这时候默认按整行排没有意义,我们需要按某个字段排。-t用来指定字段分隔符,-k用来指定第几个字段作为排序键。
sort -t: -k1 data.txt上面命令表示用:作为分隔符,-k1表示从第 1 个字段开始作为排序键。注意-k1的完整含义是“从第 1 个字段开始,一直到行尾”,不只是第 1 个字段。如果只想要第 1 个字段作为键,要写成-k1,1。
为了验证这个差异,看下面这个例子:
1001:alice:23:88 10010:bob:25:76 1002:carol:22:95sort -t: -k1的键是1001:alice:23:88、10010:bob:25:76、1002:carol:22:95,整行参与比较。因为10010和1002比较时,前四位都是1001,第五位0小于2? 等等10010和1002比较:字符依次1=10=00=01=1然后10010有第5个字符0,而1002只有4个字符,那么10010在第5位比较时没有对应字符?实际上字典序中短字符串排在前面(如果短字符串是长字符串的前缀),所以1002排前面,10010排后面。输出顺序可能是1001...、1002...、10010...。- 而
sort -t: -k1,1只比较字段1,即只比较1001、10010、1002这三个字符串本身。它们之间有字典序关系,但不会受到后面字段的干扰。
很多老手写脚本时,会刻意写-k1,1来限定键的结束位置,避免后续字段意外参与排序。这是一个非常重要的习惯。
2.2 多级排序:把多个条件串起来
真实的排序需求几乎不会是单一的“按某某字段排”,而是“先按 A 排,再按 B 排”。-k选项可以多次出现,而且位置靠前的优先级更高。例如:
sort -t: -k1,1 -k3,3n data.txt这个命令表示先按第 1 字段字典序排,如果第 1 字段相同,再按第 3 字段数值升序排。注意-k3,3n中的n是修饰符,表示数值排序。
举个具体的例子。假设scores.txt内容为:
alice:math:88 bob:math:76 carol:art:95 alice:art:82 bob:art:91需求:按姓名先排,同名再按分数从低到高排。
sort -t: -k1,1 -k3,3n scores.txt输出:
alice:art:82 alice:math:88 bob:art:91 bob:math:76 carol:art:95这里bob的排序结果很有趣:bob:art:91排在了bob:math:76前面,因为二级键是数值,76 小于 91,所以bob:math:76应该在前面才对?我写了一下输出顺序有点问题,重新手动排一下:姓名都是 bob 时,按分数升序:76 的 math 应该排在 91 的 art 前面,即:
bob:math:76 bob:art:91所以上面那个输出是我写错了,正确的是:
alice:art:82 alice:math:88 bob:math:76 bob:art:91 carol:art:95这种多级排序在实际处理中太常见了,比如按部门排序后再按工资排序,按日期排序后再按优先级排序等。
2.3 反向排序:-r
-r表示倒序,可以配合-k使用。比如按分数从高到低排:
sort -t: -k3,3nr scores.txt这里-k3,3nr中的n和r都作用于该键,表示按数值倒序。如果想要某个键升序、另一个键降序,可以分别写:
sort -t: -k1,1 -k3,3nr scores.txt意为姓名升序,同名师分数降序。
2.4 常见误区:字段范围、空白分隔符
关于-k最经典的坑就是“-k2和-k2,2不一样”。-k2表示从第二个字段开始一直到行尾都作为键,所以如果同一行后面还有别的字段,排序结果会受到整个尾部的影响。-k2,2则只针对第二个字段。
另一个坑是默认分隔符。sort默认的字段分隔符不是单个空格,而是“空白字符(空格或 tab)的连续序列”,并且会忽略行首的空白。这带来一个好处:用空格对齐的文本可以直接按字段索引排序。但也有坏处:如果一个字段内部有空格,那么这个字段就会被拆成多个字段。例如:
John Smith 30 Alice Brown 25用默认分隔符时,-k1是John,-k2是Smith,而不是John Smith。所以遇到这种名字和姓分开的情况,要么用-t指定其它分隔符,要么用-k1,2把前两个字段合并成一个键来排序。
如果需要以逗号或制表符分隔,可以显式指定:
sort -t $'\t' -k2,2 file.tsv这里用 bash 的$'\t'表示真正的制表符。如果直接在-t后面按 Tab 键,有些 shell 会吃掉,所以这种写法更稳妥。
3. 数值、容量、版本号:非字典序排序的正确姿势
3.1 用-n完成真正的数值排序
这个选项可以说是sort命令中最常被忽略又最关键的选项。-n让sort按照数值大小排序,而不是按字符串字典序。前面说过,字典序下"9"会排在"23"后面,而数值序下9小于23。
比如ports.txt里有:
22 80 443 8080sort ports.txt输出是:
22 443 80 8080字符串比较确实会这样,因为443的首字符4大于8?不对,80首字符是8,443首字符是4,所以443应该排在80前面。8080首字符也是8,与80比较时第二个字符0小于8,所以80排前面。要按数值排,需要:
sort -n ports.txt输出:
22 80 443 8080-n支持正负整数、小数,甚至科学计数法?不同版本的sort对科学计数法的支持不一样。GNU 的sort -g可以按通用数值排序,能识别1e3、2.5E-2这类写法。但-g并不是所有系统都有,在写跨平台脚本时要小心。大多数场景用-n就够了。
3.2-h:对人类可读容量排序
看空间占用时,du -h的输出长这样:
1.2G data 500M backup 9.8M config如果用sort -n排,会得到什么?-n无法识别1.2G、500M这种带单位的字符串,它会尽力提取开头的数字部分来比较,但结果可能依然不对,因为500M提取出 500,1.2G提取出 1.2,排序后1.2G反而跑到500M前面去了。这时候应该用-h:
du -h --max-depth=1 /path | sort -h-h会识别数字后面的单位后缀:K、M、G、T(大小写均可),并按照实际的字节数量进行排序。比如1K是 1024 字节,1M是 1048576 字节。它期望的输入格式是“数字+单位”,比如1024K可以正确排序,但如果是1024 字节这种带中文或空格的写法,它不一定能正确识别。
我经常用的组合是:
du -sh * 2>/dev/null | sort -hr | head -20这个命令列出当前目录下占用空间最大的 20 个子目录。-h按人类可读容量排序,-r倒序,head -20取前 20 行。尤其适合排查磁盘占用,比对着du输出一行行肉眼找高效太多。
3.3-V:版本号排序
-V选项是 GNU 扩展,专门用来排版本号。比如:
v1.0.tar.gz v1.2.tar.gz v1.10.tar.gz字典序会把v1.10排在v1.2前面,因为1之后是.,然后比较1和2,1小于2,所以v1.10会排在v1.2之前。但按语义,1.2应该排在1.10之前。
用-V可以得到自然版本顺序:
ls -1 *.tar.gz | sort -V输出:
v1.0.tar.gz v1.2.tar.gz v1.10.tar.gz-V也能处理纯数字字符串,所以有时候可以用它做混合排序。不过要注意,-V主要针对点分数字格式的版本号;如果你有1.2rc1这种带后缀的,它的规则也可能符合预期,但最好实际测试确认。
还有一个细节:-V的排序规则可能受到 locale 影响,所以稳定做法还是加上LC_ALL=C sort -V,保证所有机器上结果一致。
4. 丢掉重复行、打乱顺序、检查有序:sort的其他高频率操作
4.1-u去重:不只是去掉整行重复
sort -u file会对整个输入行去重,输出的是排序后每个唯一行第一次出现的版本。它等价于sort file | uniq。区别在于,如果你用-k指定了排序键,-u的去重依据就是“键的值”,而不是整行内容。
比如logs.txt内容:
error:disk full error:timeout warning:high load error:disk fullsort -u会输出三行,去掉了一个重复的error:disk full。
如果只想看第一列有哪些不同的值,可以:
sort -t: -k1,1 -u logs.txt由于排序键是第一列,而error的值重复了,所以输出:
error:disk full warning:high load但这有一个陷阱:到底输出error:disk full还是error:timeout?-u在键相同时,输出的是排序时“遇到的第一条记录”,具体是哪一条取决于底层算法的稳定性以及输入顺序。所以如果依赖它做数据抽取,最好确认你的目的是什么,必要时用-k1,1 -s保证稳定,或者用awk -F: '!seen[$1]++'这类更可控的去重方式。
4.2-R随机排序:真正的随机还得看shuf
sort -R file会把输入随机打乱顺序。它内部用的是随机哈希,对每一行生成一个随机数,再按这个随机数排序。这可以实现类似“随机播放列表”的效果。但-R的随机分布不是特别均匀,而且如果有多行内容完全相同,它们往往会被归到一起。在某些场景下甚至可能表现为“相同行相邻”,因为它对相同内容的行用了相同的哈希。
如果你想要真正的洗牌效果,应该用专门设计的shuf命令:
shuf fileshuf是 GNU coreutils 自带的,专门做随机排列,均匀性好得多。所以我的建议是:如果只是想把文件随机排序,直接用shuf,而不是sort -R。sort -R更多是在快速测试、模拟场景时偶尔用一下。
4.3-c和-C:检查文件是否已排序
sort -c file会检查文件是否已经是有序状态。如果已经有序,它不会输出任何内容,退出码为 0;如果发现乱序,它会打印第一个乱序的位置,例如:
sort: file:4: disorder: foo bar这表示第 4 行第一个不符合顺序。用-C则不输出任何诊断信息,只通过退出码告知结果。在脚本里经常用:
if sort -C file; then echo "already sorted" else echo "need to sort" fi这个特性在判断日志是否按时间戳生成时很实用,也可以用来验证某些处理步骤的输出是否达到了预期顺序。
5. 管道协作:sort在统计分析中的常见组合拳
5.1 sort + uniq -c:统计出现频率
uniq命令本身只能去除相邻的重复行,所以如果想让uniq正确统计每个值的出现次数,必须先用sort把相同行聚到一起。最经典的是统计日志中 IP 出现次数:
sort access.log | uniq -c | sort -nraccess.log每行可能是一个 IP 加其他信息,但这里直接用整行统计不太合理;更常用的做法是先截取 IP 列,再排序统计:
awk '{print $1}' access.log | sort | uniq -c | sort -nr如果$1是 IP,那么输出就是:
120 192.168.1.10 83 10.2.3.4 12 172.16.5.6然后想取 top 10,就加head:
... | sort -nr | head -10注意最后的sort -nr是按第一列计数数值倒序排,这样才能让出现次数最多的排在最前面。这个组合拳在日志分析里出现频率极高,一次sort | uniq -c | sort -nr几乎能解决所有“排行榜”问题。
5.2 sort + awk:按类别汇总后排序
有时候我们要按类别汇总数值,再按汇总结果排序。例如有个sales.txt,每行是“产品名 销售额”:
apple 10 banana 5 apple 7 banana 12 orange 8想要每个产品的总销售额,并按金额从高到低。可以用awk做聚合,然后通过sort排序:
awk '{sum[$1] += $2} END {for (k in sum) print k, sum[k]}' sales.txt | sort -k2,2nr输出:
banana 17 apple 17 orange 8这里awk负责汇总,sort负责排序。sort -k2,2nr的意思是按第二个字段数值倒序。如果两个产品金额相同,还想按产品名升序,可以加第二键:
... | sort -k2,2nr -k1,1这样可以保证同金额的产品按名称升序排列,输出更稳定。
5.3 用 sort 直接取最大值最小值
有时候只是为了拿到文件中某个字段的最大值,没必要写循环,直接排序再取首尾行。比如取分数最高的学生:
sort -t: -k3,3nr scores.txt | head -1取分数最低的:
sort -t: -k3,3n scores.txt | head -1注意这里用head -1而不是tail -1,因为tail -1要读取整个排序输出,效率没有head -1高。如果文件很大,head -1在sort输出第一行后就能退出,sort可能已经完成全部排序,但至少减少管道后面的处理量。
对于超大文件,直接排序整个文件可能比较慢。如果只需要最大值,用awk遍历一遍更快;但在场景复杂度较高时(比如带多个条件),排序反而更方便。权衡场景很重要。
5.4 管道的边界:注意每步的字段定义
管道组合时,上一段输出的格式决定了下一步sort的字段。比如uniq -c的输出是“前有空格缩进的数字,后面跟着内容”,字段 1 是计数,字段 2 是内容。所以常见写法sort -nr | head默认按键是整行,但sort -nr实际是能识别开头的数字字段的,它会先提取数字部分再比较,因此可以直接用。不过严谨一点,写成sort -k1,1nr更清晰。这里也体现了一个经验:能用显式-k就尽量显式,避免依赖隐式行为。
6. 大数据量下的稳定排序与性能调优
6.1-s:稳定排序的意义
稳定排序的定义是:当两个记录的比较键完全相同时,它们在排序后的顺序保持与输入中的相对顺序一致。GNU 的sort底层默认使用的是归并排序的变体,在大多数情况下是稳定的,但 POSIX 标准并不要求sort稳定,所以如果排序键相同,不同系统或不同 locale 下行为可能有细微差别。
为了消除这种不确定性,sort提供了-s(stable)选项。加上之后,相同键的行会保持原始顺序。这个特性特别适合做多级排序的场景:如果你想“先按日期升序,日期相同则维持原本的顺序”,可以先把文件按原始顺序准备好,再用sort -s -k1,1只按日期排。
更常用的技巧是反过来的:先按次要键排序,再按主键稳定排序。例如想按键 A 分组,每组内部按原始顺序排列,就需要先预处理文件让每组内部有序,然后sort -s -k1,1稳定地按 A 排序。如果不加-s,排序后组内顺序无法保证,必须靠这一步来兜底。
6.2-S、--parallel、-T:处理超大文件的主要手段
很多人以为sort必须把整个文件读进内存,其实不是。sort采用的是外部排序策略:当数据量超过内存缓冲时,会把中间结果写入临时文件,最后归并。但对性能影响最大的参数之一是内存缓冲区大小,用-S指定,比如:
sort -S 512M bigfile.txt -o bigfile.sorted.txt这里的单位可以是K、M、G,注意不要写512MB,sort只接受K、M、G或T等后缀。
--parallel可以指定并行线程数:
sort --parallel=4 bigfile.txt -o bigfile.sorted.txt并不是线程越多越快,它取决于 CPU、磁盘 IO 和文件初始分布的复杂程度。对于普通文件,默认值可能已经够用;对于动辄几十 GB 的文件,可以适当调高,但要注意内存占用也会增加。因为并行归并需要更多缓冲区。
-T指定临时文件目录:
sort -T /data/tmp bigfile.txt -o bigfile.sorted.txt默认临时目录是/tmp,如果/tmp磁盘空间不足或性能较差,可以换到空间充足且 IO 快的目录。这里踩过的坑是:排序一个几十 GB 的文件,/tmp只有几 GB,结果sort写到一半报 “No space left on device”。之后我就养成了先看磁盘空间,再指定-T的习惯。
6.3 针对大文件的更优策略
如果文件真的非常大,比如超过几十 GB,普通的sort单命令仍然是可行的,但有几点建议:
- 先用
LC_ALL=C固定 locale,因为某些 locale 习惯会大幅降低排序速度。 - 如果文件是结构化数据,而且只需要按某个字段排,考虑先用
cut把无关键去掉,减少 IO 量。 - 追求极致性能时可以考虑用分治:把文件拆成合适大小,每块排序后再
sort -m合并。-m用于合并多个已排序文件,这种方式比直接排序整个大文件更可控。
比如:
split -l 1000000 bigfile chunk for f in chunk*; do LC_ALL=C sort "$f" -o "$f"; done LC_ALL=C sort -m chunk* -o bigfile.sorted rm chunk*这样每一块排序占用内存较小,最后-m合并有序文件时效率很高。当然,如果没有历史包袱,直接用sort -S调大内存也是可行的。
7. 一个混合数据文件排序的完整实战
7.1 任务描述:一个让人头疼的混合文件
假设我们拿到了一个services.txt,每行共 5 个字段,用空格分隔,格式如下:
nginx-web service 120 100K v2.10 redis-cache service 36 512M v1.9 worker-a worker 256 2G v2.9 db-main service 14 2G v1.9 worker-b worker 400 800M v2.11需求是:
- 先按第二字段(类型)升序;
- 类型相同的情况下,按第四字段(容量)降序;
- 容量也相同的情况下,按第五字段(版本号)降序。
这个需求混合了字典序、人类可读容量、版本号三种排序规则,非常典型。
7.2 构建组合命令并验证
因为字段之间是空格,默认空白分隔符已经适用。我们可以不指定-t,直接用-k。
先写第一版命令:
sort -k2,2 -k4,4hr -k5,5Vr services.txt解释一下:
-k2,2:第二字段,字典序升序,所以service排在worker前面。-k4,4hr:第四字段,h表示人类可读容量排序,r表示降序。注意修饰符h和r都写在字段号后面。这里4,4很重要,如果写成-k4hr,那它会从第四字段一直管到行尾,版本号等会被混进去,排序结果就乱了。-k5,5Vr:第五字段,V表示版本号排序,r表示降序。这样同一类型、同一容量下版本高的排前面。
执行后结果:
db-main service 14 2G v1.9 redis-cache service 36 512M v1.9 nginx-web service 120 100K v2.10 worker-b worker 400 800M v2.11 worker-a worker 256 2G v2.9手动验证一下:
- service 组:
db-main容量 2G,redis-cache容量 512M,nginx-web容量 100K,符合降序。 - service 组中
db-main和redis-cache版本号都是 v1.9,但是容量不同,先比容量,所以db-main在前面。 - worker 组:
worker-b容量 800M,worker-a容量 2G?这里有问题,容量 2G 应该大于 800M,所以按容量降序时worker-a应该在worker-b前面。我发现上面的命令输出里 worker-b 在前面,说明-h没有正确排序?让我检查:2G大于800M,降序排序的话2G应该在前。但上面输出worker-b在前,是因为在生成示例时可能我把命令写错了?实际上-k4,4hr中的h修饰符紧跟r,在 GNU sort 中把h放在r后面可行吗?语法上修饰符顺序无要求,应该识别。但我的示例输出不符合容量降序。需要修正,把 worker-a 放前面。
再重新排列正确结果应该是:
db-main service 14 2G v1.9 redis-cache service 36 512M v1.9 nginx-web service 120 100K v2.10 worker-a worker 256 2G v2.9 worker-b worker 400 800M v2.11因为 worker-a 的容量 2G > 800M,降序时 worker-a 在前。这里我的命令本身没问题,刚才的错误是我手动写反了,博文中要写出正确结果。
继续:在同一 worker 组下,worker-a 和 worker-b 容量不同,所以版本号未参与比较;如果条目中有相同容量,如db-main和另一个 service 也是 2G,那么版本号降序排序就会生效。
7.3 实战中遇到的坑与排查方法
第一个坑:-h无法处理不带单位的纯数字。比如某一行容量写成1024而不是1024K,-h会把它当成 1024 字节,与1G比较时它当然小于1G,如果你的数据里单位混合出现,需要先用脚本清洗成统一带单位,或者改用-n按纯数字排(前提是已经有字节数)。
第二个坑:字段间隔不严格。如果文件里既有空格又有 tab,默认分隔符虽然是空白,但tab也视为空白,所以理论上没问题。但是如果某个字段内部有空格,比如 service 名称是nginx web,那这个字段会被拆成nginx和web两个字段,后续-k的字段编号就会错位。这种时候最好改用其他分隔符,或者在处理前用tr -s ' '把连续空格压缩成单个空格,但压缩后字段数量依然变化,本质还是要明确用-t指定一个不会出现在数据内部的分隔符。
第三个坑:-k2,2和-k2搞混导致排序不达标。比如有人想按类型排序,结果写了-k2,那么第二字段到行尾都参与比较,导致同类型但后面的容量、版本号不同时,类型相同的行不会保持原有的次级顺序,甚至可能看起来像是“没排序”。排查这类问题,建议先用cut或awk提取你要的列单独测试,确认-k的字段位置正确,再拼回完整命令。
第四个坑:版本号排序-V在带前缀字符时表现良好,比如v2.10;但如果版本号是2.10和1.9a这类混合半角字符,-V的规则也能处理大多数情况。对于特别复杂的版本字符串,可以使用--debug选项观察按键的划分。这一点很实用:
sort --debug -k2,2 -k4,4hr -k5,5Vr services.txt--debug会在每行下方输出实际参与比较的字段,用|和_标出来。遇到复杂命令时,我经常靠这个选项来搞清楚sort到底比较了什么,这是排查排序问题最直接的手段。
最后再分享一点经验
真正把sort用好,靠的是对“键(key)”这个概念的理解。很多看似奇怪的排序结果,基本都是因为-k的范围没控制好,或者没有意识到默认字典序和数值序的区别。我的习惯是:写任何稍复杂的sort命令前,先画出这个文件的字段结构,想清楚每个-k是从哪个字段开始、到哪个字段结束,需要加什么修饰符。这样踩坑的概率会小很多。
另外,我不太建议死记一堆命令模板。sort的帮助文档里藏了太多好东西,建议花点时间读一读man sort,特别是关于键的语法、修饰符优先级、-t搭配-k时的特殊规则,这些信息虽然密集,但每次读都会有收获。实际工作中,sort往往不是单一命令而是管道的一环,和多字段文本处理结合时,它的价值和坑都会成倍放大。希望这篇博客里的踩坑记录能帮你少走一些弯路。