← 返回文章
技术笔记

同一份名单,为什么排出两种顺序

2026-09-11 · M 写

同一份名单,同一个 sort,只改一个环境变量,顺序就变了。输入是四行:

banana
Apple
apple
Banana

两次运行,两种结果:

LC_ALL=C            Apple   Banana  apple   banana
LC_ALL=en_US.UTF-8  apple   Apple   banana  Banana

两边都没有报错,也没有哪一边算「排错了」。差别在于用了哪一套比较规则。

规则不在数据里

排序要成立,先得能回答一件事:两个东西谁在前。这个判断不由数据给出,而由一套比较规则给出。GNU 的 sort 手册写得很直接:除非另有指定,所有比较都使用 LC_COLLATE 这个 locale 指定的字符排序序列;而一旦设置了 LC_ALL,它会盖过前者。

那套序列规定的是大小写、重音、标点各自的权重。权重一变,顺序就变,而数据一个字都没有动。

标点被降权以后

换一组输入,三行:a-baba b

LC_ALL=C            a b   a-b   ab
LC_ALL=en_US.UTF-8  a b   ab    a-b

按 C 规则,空格(0x20)最小,连字符(0x2D)居中,小写 b(0x62)最大,所以 ab 排在最后。到了 en_US.UTF-8,连字符在排序里的分量被降得很低,ab 反而跑到了 a-b 前面。同一行数据,在一套规则下位置不对,换一套规则就完全合理。

排在一起,不等于相同

再看两条只差大小写的输入。在 en_US.UTF-8 下它们相邻,而且小写在前:

LC_ALL=en_US.UTF-8 sort -u     apple   Apple
LC_ALL=en_US.UTF-8 sort -f -u  apple

第一行里 -u 把两条都留了下来,说明比较时它们并不相等,只是主序相同,再靠大小写分出先后。要真正合并它们,得另说,比如加 -f 忽略大小写,那时才只剩一行。把「排在一起」读成「是同一个」,是这类规则最容易误导人的地方。

不点名,就不会按数值排

三个文件名:file10、file9、file2。

sort      file10  file2  file9
sort -V   file2   file9   file10

默认是逐字符比字符串:12 小,所以 file10 排在最前。这不是缺陷,是字符串序的定义。要按数字读就得明说:-V 把每段连续数字当版本号处理,-n 则整体按数值比较。所以「已经排好序了」这句话其实不完整,没说清按什么序,它可能正是你不想要的那一种。

目录列表也一样。同样三个文件名放进一个目录,ls 默认给出的是 file10、file2、file9,和 sort 一样按字符比;要按数值列,得加 -v。这一条最容易在日常里撞上,因为文件名的编号几乎总是数字,而我们看的时候总是按数字读。

把规则写进代码

上面两种顺序,locale 都是我显式给的。如果什么都不给,规则由程序启动时的环境决定,而不是由你眼前的终端决定。同一个脚本换个启动方式,就可能得到另一种顺序,而且它不会报错,只是安静地给出另一种结果。顺序错了,通常没有异常跳出来提醒你。

所以凡是有排序的地方,我把规则写进脚本:要稳定、跨机器一致,就写 LC_ALL=C;要贴近人的字母直觉,就写清真正需要的那个 locale;要按数值排,就写 -V-n。把规则交给代码,而不是交给环境的默认值,因为默认值会随启动方式变,代码不会。

这里有个细节值得单独说:写 LC_ALL,而不是只写 LC_COLLATE。手册专门提醒过,单独设置 LC_COLLATE 有两个问题,其中之一是:如果环境里已经存在 LC_ALL,你设的那个会被它盖掉,等于没设。既然无法保证启动环境里有没有 LC_ALL,就直接用它,一步到位。

还有一点值得说明:规则的选择是取舍,不是「更严格」或「更正确」。C 规则快、跨机器一致,代价是它不认人类的字母表,它把所有大写字母排在小写字母前面。en_US.UTF-8 更接近人的预期,代价是它依赖一套可能不存在的 locale 数据。两种都能用,问题只在于你有没有说清用的是哪一种。

开头那两行输出,不是「正确与错误」,而是两套规则各自的结果。顺序从来不是名单自带的属性,它是「按什么规则读它」的属性。下次觉得顺序不对,先别急着翻数据,先问一句:这是按什么规则排的。

参考:GNU Coreutils 手册 · sortlocale(7)。文中的顺序都是我在本机实测的输出。

—— M-D