Linux檔案排序工具 sort 命令詳解
本文目錄:
1.1 選項說明
1.2 sort範例
1.3 深入研究sort
sort是排序工具,它完美貫徹了Unix哲學:"只做一件事,並做到完美"。它的排序功能極強、極完整,只要檔案中的資料足夠規則,它幾乎可以排出所有想要的排序結果,是一個非常優質的工具。
雖然sort很強大,但它的選項很少,使用方法也很簡單。更讓人覺得它成功的地方在於:即使想要實現複雜、完整的sort功能,所使用的選項和一般使用時的選項沒什麼不同。只不過要實現複雜功能時,必須得理解sort是如何工作的。
也就是說,沒搞懂sort工作機制時,它也能完成任務,指哪就能打哪,但沒被指到的地方難免會有所偏差和疑惑。只有搞懂了sort機制,才能真正的指哪打哪,結果中一絲偏差也沒有,即使出現了偏差也知道是為什麼。
本文先解釋sort命令的常用選項,再給出sort的簡單使用範例,用於初步解釋sort各選項,最後對sort深入說明。更完整的選項說明可參考info sort的譯文:sort命令中文手冊(info sort翻譯)。
1.1 選項說明
sort讀取每一行輸入,並按照指定的分隔符將每一行劃分成多個欄位,這些欄位就是sort排序的物件。同時,sort可以指定按照何種排序規則進行排序,如按照當前字元集排序規則(這是預設排序規則)、按照字典排序規則、按照數值排序規則、按照月份排序規則、按照檔案大小格式(k<M<G)。還可以去除重複行,指定降序或升序(預設)的排序方式。
預設的排序規則為字元集排序規則,通常幾種常見字元的順序為:"空字串<空白字元<數值<a<A<b<B<...<z<Z",字典排序規則也如此。
語法格式:
sort [OPTION]... [FILE]... 選項說明: -c:檢測給定的檔案是否已經已經排序。如未排序,則會輸出診斷資訊,提示從哪一行開始亂序。 -C:類似於"-c",只不過不輸出任何診斷資訊。可以通過退出狀態碼1判斷出檔案未排序。 -m:對給定的多個已排序檔案進行合併。在合併過程中不做任何排序動作。 -b:忽略欄位的前導空白字元。空格數量不固定時,該選項幾乎是必須要使用的。"-n"選項隱含該選項。 -d:按照字典順序排序,只支援字母、數值、空白。除了特殊字元,一般情況下基本等同於預設排序規則。 --debug:將顯示排序的過程以及每次排序所使用的欄位、字元。同時還會在最前幾行顯示額外的資訊。 -f:將所有小寫字母當成大寫字母。例如,"b"和"B"是相同的。 :在和"-u"選項一起使用時,如果排序欄位的比較結果相等,則丟棄小寫字母行。 -k:指定要排序的key,key由欄位組成。key格式為"POS1[,POS2]",POS1為key起始位置,POS2為key結束位置。 -n:按數值排序。空字串""或" "被當作空。該選項除了能識別負號"-",其他所有非數位字元都不識別。 :當按數值排序時,遇到不識別的字元時將立即結束該key的排序。 -M:按字串格式的月份排序。會自動轉換成大寫,並取縮寫值。規則:unknown<JAN<FEB<...<NOV<DEC。 -o:將結果輸出到指定檔案中。 -r:預設是升序排序,使用該選項將得到降序排序的結果。 :注意:"-r"不參與排序動作,只是操作排序完成後的結果。 -s:禁止sort做"最後的排序"。 -t:指定欄位分隔符。 :對於特殊符號(如製表符),可使用類似於-t$'t'或-t'ctrl+v,tab'(先按ctrl+v,然後按tab鍵)的方法實現。 -u:只輸出重複行的第一行。結合"-f"使用時,重複的小寫行被丟棄。
1.2 sort範例
此小節為sort的簡單用法範例,也是平時最可能用上的範例。如果只是為了使用sort,而不是為了刨根問題,本小節已經足夠。
假設當前已有檔案system.txt,內容如下:其中空白部分為單個製表符。
[root@linuxidc tmp]# cat system.txt 1 mac 2000 500 2 winxp 4000 300 3 bsd 1000 600 4 linux 1000 200 5 SUSE 4000 300 6 Debian 600 200
(1).不加任何選項時,將對整行從第一個字元開始依次向後直到行尾按照預設的字元集排序規則做升序排序。
[root@linuxidc tmp]# sort system.txt 1 mac 2000 500 2 winxp 4000 300 3 bsd 1000 600 4 linux 1000 200 5 SUSE 4000 300 6 Debian 600 200
由於每行的第一個字元1<2<3<4<5<6,所以結果如上。
(2).以第三列為排序列進行排序。由於要劃分欄位,所以指定欄位分隔符。指客製化表符這種無法直接輸入的特殊字元的方式是$'t'。
[root@linuxidc tmp]# sort -t $'t' -k3 system.txt 4 linux 1000 200 3 bsd 1000 600 1 mac 2000 500 2 winxp 4000 300 5 SUSE 4000 300 6 Debian 600 200
結果中雖然1000<2000<4000的順序是對了,但600卻排在最後面,因為這是按照預設字元集排序規則進行排序的,字元6大於4,所以排最後一行。
(3).對第三列按數值排序規則進行排序。
[root@linuxidc tmp]# sort -t $'t' -k3 -n system.txt 6 Debian 600 200 3 bsd 1000 600 4 linux 1000 200 1 mac 2000 500 2 winxp 4000 300 5 SUSE 4000 300
結果中600已經排在第一行。結果中第2行、第3行的第三列值均為1000,如何決定這兩行的順序?
(4).在對第3列按數值排序規則排序的基礎上,使用第四列作為決勝屬性,且是以數值排序規則對第四列排序。
[root@linuxidc tmp]# sort -t $'t' -k3 -k4 -n system.txt 6 Debian 600 200 4 linux 1000 200 3 bsd 1000 600 1 mac 2000 500 2 winxp 4000 300 5 SUSE 4000 300
如果想在第3列按數值排序後,以第2列作為決勝列呢?由於第2列為字母而非??值,所以下面的語句是錯誤的,雖然得到了期望的結果。
[root@linuxidc tmp]# sort -t $'t' -k3 -k2 -n system.txt 6 Debian 600 200 3 bsd 1000 600 4 linux 1000 200 1 mac 2000 500 2 winxp 4000 300 5 SUSE 4000 300
之所以最終得到了正確的結果,是因為預設情況下,在命令列中指定的排序行為結束後,sort還會做最後一次排序,這最後一次排序是對整行按照完全預設規則進行排序的,也就是按字元集、升序排序。由於1000所在的兩行中的第一個字元3小於4,所以3排在前面。
之所以說上面的語句是錯誤的,是因為第2列第一個字元是字母而不是數值,在按數值排序時,字母是不可識別字元,一遇到不可識別字元就會立即結束該欄位的排序行為。可以使用"--debug"選項來檢視排序的過程和排序時所使用的列。注意,該選項只有CentOS 7上的sort才有。
[root@linuxidc tmp]# sort --debug -t $'t' -k3 -k2 -n system.txt sort: using ‘en_US.UTF-8’ sorting rules sort: key 1 is numeric and spans multiple fields sort: key 2 is numeric and spans multiple fields 6>Debian>600>200 ___ # 第1次排序行為,即對"-k3"排序,此次用於排序的欄位為第3列 ^ no match for key # 第2次排序行為,即對"-k2"排序,但顯示無法匹配排序key ________________ # 預設sort總會進行最後一次排序,排序物件為整行 3>bsd>1000>600 ____ ^ no match for key ______________ 4>linux>1000>200 ____ ^ no match for key ________________ 1>mac>2000>500 ____ ^ no match for key ______________ 2>winxp>4000>300 ____ ^ no match for key ________________ 5>SUSE>4000>300 ____ ^ no match for key _______________
(5).在對第3列按數值排序規則排序的基礎上,使用第2列作為決勝屬性,且以預設排序規則對此列降序排序。
[root@linuxidc tmp]# sort -t $'t' -k3n -k2r system.txt 6 Debian 600 200 4 linux 1000 200 3 bsd 1000 600 1 mac 2000 500 2 winxp 4000 300 5 SUSE 4000 300
由於既要對第3列按數值升序排序,又要對第2列按預設規則降序排序,因此只能對每個欄位單獨分配選項。注意,雖然"r"選項是降序結果,但它不影響排序過程,只影響最終排序結果。也就是說,在按照升序排序結束得到最終結果後,再反轉第2列順序,也就是得到了降序的結果。同樣也說明,sort在排序的時候,一定且只能按照升序排序,只有排序動作結束了"r"選項才開始工作。
緊跟在欄位後的選項(如"-k3n"的"n"和"-k2r"的"r")稱為私有選項,使用短橫線寫在欄位外的選項(如"-n"、"-r")為全域性選項。當沒有為欄位分配私有選項時,該排序欄位將繼承全域性選項。當然,只有像"-n"、"-r"這樣的排序性的選項才能繼承和分配給欄位,"-t"這樣的選項則無法分配。
因此,"-n -k3 -k4"、"-n -k3n -k4"和"-k3n -k4n"是等價的,"-r -k3n -k4"和"-k3nr -k4r"是等價的。
實際上,上面的命令寫法並不嚴謹。更標準的寫法應該如下:
sort -t $'t' -k3n -k2,2r system.txt
"-k2,2"表示排序物件從第2個欄位開始到第2個欄位結束,也就是限定了只對第二個欄位排序。它的格式為"POS1,POS2",如果省略POS2,將自動擴充套件到行尾,即"-k2"等價於"-k2,4",也就是說,對整個第2列到第4列進行排序。
需要注意,由於上面的"-k2"繼承了全域性預設的排序規則,即按字元排序而非按數值排序,此時它能夠等價於"-k2,4",但如果是"-k2n"按照數值排序的話,它不等價於"-k2,4n"或"-k2n,4n"或"-k2n,4"(這3者為等價寫法),之所以不等價,是因為按數值排序時只能識別數位和負號"-",當排序時遇到其他所有字元,都將立即結束此次排序。所以"-k2n"等價於"-k2,2n"或"-k2n,2"或"-k2n,2n"。
這些理論性的知識點,請參照下一小節sort的理論內容。後文也不再解釋理論性的內容,只是介紹命令使用方法。
(6).在對第3列按數值排序規則排序的基礎上,使用第2列的第2個字元作為決勝屬性,且以預設排序規則對此列升序排序。
[root@linuxidc tmp]# sort -t $'t' -k3n -k2.2,2.2 system.txt 6 Debian 600 200 4 linux 1000 200 3 bsd 1000 600 1 mac 2000 500 2 winxp 4000 300 5 SUSE 4000 300
其中"-k2.2,2.2"表示從第2個欄位的第2個字元開始,到第2個欄位的第2個字元結束,即嚴格限定為第2個欄位第2個字元。如果需要對此字元降序排序,則"-k2.2,2.2r"。
(7).使用"-u"去除重複欄位所在的行。例如第3列有兩行1000,兩行4000,去除欄位重複的行時,將只保留排在前面的第一行。
[root@linuxidc tmp]# sort -t $'t' -k3n -u system.txt 6 Debian 600 200 3 bsd 1000 600 1 mac 2000 500 2 winxp 4000 300
由於需要去除重複欄位的行,因此使用"-u"時將禁止sort做"最後一次排序"。至於欄位重複的行中,如何判斷哪一行是排在最前面的行,需要搞懂sort的整個工作機制,請通讀本文。
"sort -u"和"sort | uniq"是等價的,但是如果多指定幾個選項,它們將不等價。例如,"sort -n -u"只會檢查排序欄位數值部分的唯一性,但"sort -n | uniq"在sort對行中欄位按數值排序後,uniq將檢查整個行的唯一性。
(8).將排序結果儲存到檔案中。即可以使用重定向,也可以使用"-o"選項,但使用重定向不可儲存到原檔案,因為在sort開始執行前,原檔案先被重定向截斷。而使用"-o"則沒有這樣的問題,因為sort在開啟檔案前先完成資料的讀取。但"-o"和"-m"一起使用時,同樣不安全。
[root@linuxidc tmp]# sort -t $'t' -k3n -o system1.txt system.txt
(9).使用"-c"或"-C"檢測檔案是否排過序。如果已排序,則不返回任何資訊,退出狀態碼為0。如果未排序,退出狀態碼為1,但"-c"會給出診斷資訊,並指明從哪一行開始亂序,而"-C"不返回任何資訊。
[root@linuxidc tmp]# sort -c -k3n system.txt ;echo $? sort: system.txt:3: disorder: 3 bsd 1000 600 1
說明system.txt中的第3行開始出現亂序,且退出狀態碼為1。
[root@linuxidc tmp]# sort -C -k3n system.txt ;echo $? 1
1.3 深入研究sort
咋一看上去,sort的使用方法很簡單,不就是"sort -t DELIMITER -k POS1,POS2 file"嗎,確實如此,它的man文件也才100來行,連info文件加上一堆廢話也才500多行。但事實上,sort命令很難,也可以說很簡單,簡單是因為不管是複雜功能還是簡單功能,用來用去就那麼幾個選項,難是因為沒搞懂它的工作機制和細節時,有些時候的結果會比較出人意料,也不知道為什麼會如此。
本小節主要講理論和工作機制的細節,偶爾給出幾個範例,所以遇到疑惑時請自行測試,當然也歡迎在部落格下方留言。另外,"--debug"(CentOS7才支援該選項)選項對排疑解惑有極大幫助,所以應該善用該選項。
(1).sort命令預設按照字元集的排序規則進行排序,可以指定"-d"選項按照字典順序排序,指定"-n"按照數值排序,指定"-M"按照字元格式的月份規則排序,指定"-h"按照檔案容量大小規則排序。
字元集排序規則和字典排序規則對能識別的字元來說,順序一般是一致的,幾種常見字元的順序為:"空字串<空白字元<數值<a<A<b<B<...<z<Z"。
指定不同的排序規則,不僅改變排序時的依據,還間接影響排序時的行為,因為不同排序規則能夠識別的字元型別不同。至於如何影響,見下面的(4)。
(2).sort使用"-t"選項指定的分隔符對每行進行分割,得到多個欄位,分隔符不作為欄位的內容。預設的分隔符為空白字元和非空白字元之間的空字元,並非網上眾多文章所說的空格或製表符(原文:By default, fields are separated by the empty string between a non-blank character and a blank character.)。
例如," foo bar"預設將分隔為兩個欄位" foo"和" bar",而使用空格作為分隔符時將分隔為三個欄位:第一個欄位為空,第二個欄位和第三個欄位為"foo"和"bar"。使用下面三個sort語句可以驗證預設的分隔符並非空格。
[root@linuxidc ~]# echo -e " 234 barn 123 car" | sort -t ' ' -b -k3 234 bar 123 car [root@linuxidc ~]# echo -e " 234 barn 123 car" | sort -b -k2 234 bar 123 car [root@linuxidc ~]# echo -e " 234 barn 123 car" | sort -b -k3 # -k3指定的欄位超出了範圍,所以key為空 123 car 234 bar
(3).使用"-k"選項指定排序的key。不指定排序key時,整行將成為排序key,即對整行進行排序。
- key由欄位組成,格式為"POS1,[POS2]",表示每行排序的起始和終止位置。也就是說,key才是排序的物件。
- POS的格式為"F[.C][OPTS]",其中F表示欄位的序號,C表示該欄位中字元的序號。欄位和字元的位置都從1開始計算。如果POS2的字元位置指定為0,則表示POS2欄位中的最後一個字元。如果POS1中省略".C",則預設值為1(欄位的起始字元),如果POS2中省略".C",預設值為0(欄位的終止字元)。使用"-b"選項忽略前導空白字元時,C從第一個非空白字元開始計算。如果F或C超出了有效範圍,則該key為空,例如一行只有3個欄位,卻指定了"-k4",或者第2欄位只有3個字元,卻指定了"-k2.5"。
- 如果省略POS2,則key將自動擴充套件到行尾,即等價於"POS1,line_end"。如果不省略POS2,則該key可能會跨越多個欄位。無論那種情況,跨越多個欄位時,key中會保留欄位間的分隔符。
- OPTS指定的是該key的選項,包括但不限於"bfnrhM",它們的作用和全域性選項"-b"、"-f"、"-n"、"-r"、"-h"、"-M"相同。預設情況下,如果key中沒有指定任何OPTS,則該key會繼承全域性選項。當key中單獨指定了選項時,這些選項是該key的私有排序選項,將覆蓋全域性選項。除了"b"選項外,其餘選項無論是指定在POS1還是POS2中都是等價的,對於"b"選項,指定在POS1則作用於POS1,指定在POS2則作用於POS2。如果繼承了全域性選項"-b",則作用於POS1和POS2。
- 欄位前數量不固定的前導空白字元,將使得欄位混亂,因此強烈建議總是忽略前導空白字元。數值排序時(即"n"選項)隱含"b"選項。
- 可以使用多個"-k"選項指定多個key,排序時將按照key的順序進行排序。第一個key通常稱為主排序key(primary key)。第二個key將在第一個key排序的基礎上排序,同理,第三個key將在第二個key的排序基礎上進行排序。
以下是幾個例子:例子中出現了選項"n"的,描述暫不嚴謹,但目前只能如此描述,在稍後的(4)中解釋。
"-k 2": 因為沒有指定POS2,所以key擴充套件到了行尾。因此該key從第2欄位第一個字元開始,到行尾結束。
"-k 2,3" :該key從第2欄位第一個字元開始到第3欄位最後一個字元結束。
"-k 2,2": 該key僅擁有第2欄位。
"-k 2,3n"和"-k 2n,3"和"-k 2n,3n" :這三者等價,因為除了"b"選項,OPTS指定在POS1或POS2的結果是一樣的。
"-k 2,3b"和"-k 2b,3"和"-k 2b,3b" :這三者互不等價。
"-k 2n": 該key從第2欄位開始直到行尾,都按數值排序。
"-k 2.2b,3.2n": 該key從第2欄位的第2個非空白字元開始,到第3欄位第2字元(可能包含空白字元)結束,且該key按照數值排序。其實此處的b選項是多餘的,因為n隱含了b選項。
"-k 5b,5 -k 3,3n": 定義了兩個排序key,主排序key為第5欄位不包含空白字元的部分,副key為第三個欄位。主key按照預設規則排序,副key按照數值排序。副key在主key排序後的基礎上再排序。
"-k 5,5n -k 3b,6b": 主key為第5欄位,按照數值排序,副key從第3欄位到第六欄位,忽略前導空白字元,但是按照預設規則排序。副key在主key排序後的基礎上再排序。
(4).當排序規則選項(例如"n"、"d"、"M"、"h")發現不識別的符號時,將立即結束當前key的排序。預設排序規則是字元集的排序規則,通常能識別所有字元,所以總會對整個key進行完整的排序。這是"何時跨欄位、跨key比較?"的問題。
例如,指定n選項按數值排序時,由於"n"選項只能識別數位和負號"-",當排序時遇到無法識別字元時,將導致該key的排序立即結束。也就是說,對於"abc 123 456 abc"這樣的輸入,分隔符為空格,當指定"-k 2,3n"時,雖然排序key包括"123 456",但由於中間的空白字元無法被n識別,使得在排完第2欄位"123"時就立即結束該key的排序。
正因如此,使得n選項絕對不會跨欄位、跨key進行比較。因此,"-k 2,3n"和"-k 2n"、"-k 2,2n"、"-k 2,4n"的結果是等價的,都只對第2欄位按照數值進行排序。但預設的排序規則不會有這樣的問題,因為預設排序規則能識別所有字元,也就是說"-k 2,3"、"-k 2"、"-k 2,2"、"-k 2,4"是互不等價的。
同理,"-d"的字典排序規則只能識別字母、數位和空白字元,所以遇到非這3類字元時也將立即結束當前key的排序。"-h"和"-M"也都有字元的識別限制,處理方式也一樣。關於"-h"和"-M"選項的說明,見info sort。
需要特意說明的是:n同樣不識別空字串,發現空字串時也結束排序。這可能會適得按數值排序的結果出人意料。例如:
[root@linuxidc ~]# echo -e "b 100:200 200na 110 300" | tr ':' '