Licencja Creative Commons

Autorem poniższego opracowania jest dr Piotr A. Dybczyński z Instytutu Obserwatorium Astronomiczne UAM w Poznaniu.
Inspiracją był wpis na blogu Alana Skorkina


Polecenie SORT służy do sortowania happy smiley

Sortowanie jest jednym z ważniejszych elementów programowania. Znajomość różnych algorytmów i technik sortowania to ważna część wiedzy programisty.

Tutaj jednak zajmiemy się czymś znacznie prostszym: wykorzystaniem unixowego polecenia sort do porządkowania jednego lub więcej plików, wiersz po wierszu, wedle ustalonych przez nas reguł. A przy okazji rozejrzymy się po środowisku w jakim pracujemy.

Wielu użytkowników kończy swoją znajomość polecenia sort na poleceniach typu:


$ sort some_file.txt
 

Domyślne zachowanie tego polecenia dla wielu prostych zastosowań będzie wystarczające - wyświetli w terminalu posortowane wiersze wskazanego pliku. Przyjrzymy się jednak działaniu tego polecenia nieco dokładniej. Można oczywiście przestudiować manual (polecenie man sort) ale łatwiej jest poznać specyfikę niektórych opcji i możliwości na przykładach.

Stwórzmy (polecenie touch) plik literki.txt zawierający po jednej literze w wierszu. Polecenie cat pozwoli nam wyświetlić jego zawartość (w tym i pozostałych przykładach znak dolara ($) zastępuje całe zgłoszenie systemowe (ang.: Prompt):


$ cat literki.txt
b
D
c
A
C
B
d
a
 

Podstawowe użycie polecenia sort widzimy poniżej:


$ sort literki.txt
a
A
b
B
c
C
d
D
 

a gdybyśmy chcieli posortować w odwrotnej kolejności:


sort -r letters.txt
D
d
C
c
B
b
A
a
 

W zasadzie wszystko dobrze, ale trochę dziwnie zachowuje się kolejność małych i dużych liter. W manualu do polecenia sort czytamy, że opcja: -f (lub --ignore-case) powoduje nierozróżnianie małych i dużych liter przy porównywaniu. Powinno to być zachowanie domyślne, ale sprawdźmy:

$ sort -f literki.txt
a
A
b
B
c
C
d
D

$ sort -rf literki.txt
D
d
C
c
B
b
A
a

Oszustwo! Widać gołym okiem, że nierozróżnianie małych i dużych liter nie jest prawdą. Co gorsza polecenie sort zachowuje się niestabilnie, to znaczy zmienia kolejność wierszy również tam, gdzie powinno traktować je jako "równe" z punktu widzenia reguł sortowania. Dopiero dodanie opcji -s (--stable) zmienia sytuację:


$ sort -rfs literki.txt
D
d
c
C
b
B
A
a

$ sort -fs literki.txt
A
a
b
B
c
C
D
d

Dlaczego tak jest? Ano, po szczegółowej lekturze opisu działania polecenia sort można się dowiedzieć, że jeśli sortowane wiersze nie różnią się z punktu widzenia przyjętych reguł to jako ostateczne kryterium stosowane jest jeszcze porównywanie kodów znaków! Dopiero opcja -s wyłącza to ostatnie porównywanie.

Zmodyfikujmy plik literki.txt, tak by otrzymać:


$ cat literki.txt
b
D
c
ą
A
C
B
d
ć
a
 

Kody "polskich literek" 'ą' oraz 'ć' powinny umieszczać je daleko od 'a' czy 'c' . Sprawdźmy:


$ sort -f literki.txt
a
A
ą
b
B
c
C
ć
d
D

$ sort -fs literki.txt
A
a
ą
b
B
c
C
ć
D
d
 

Nadal coś nie tak! Duże i małe litery reagują na opcję -s (to znaczy duże i małe te same litery występuja teraz w takiej samej kolejności jak w pliku nieposortowanym) ale system skądś wie, że 'ą' jest w polskim alfabecie przed 'b' i kody najwyraźniej nie mają tu nic do rzeczy.

Jak przeczytamy manual polecenia sort do samego końca to znajdziemy jeszcze ostrzeżenie:

*** WARNING *** The locale specified by the environment affects sort
order. Set LC_ALL=C to get the traditional sort order that uses native
byte values.

Cóż to oznacza? Sprawdźmy ustawienia językowe:


$ locale
LANG=pl_PL
LC_CTYPE="pl_PL"
LC_NUMERIC="pl_PL"
LC_TIME="pl_PL"
LC_COLLATE="pl_PL"
LC_MONETARY="pl_PL"
LC_MESSAGES="pl_PL"
LC_PAPER="pl_PL"
LC_NAME="pl_PL"
LC_ADDRESS="pl_PL"
LC_TELEPHONE="pl_PL"
LC_MEASUREMENT="pl_PL"
LC_IDENTIFICATION="pl_PL"
LC_ALL=pl_PL
 

Widać, że system wie, iż posługujemy się językiem polskim i stąd jego "nadinteligencja" przy sortowaniu. Ale rada zawarta w powyższym ostrzeżeniu nie jest najlepsza - ustawienie LC_ALL="C" spowoduje przestawienie całego naszego środowiska na język angielski i siedmiobitowy kod ASCII. Polecenie man locale wyjaśnia, do czego służą poszczególne zmienne oraz, ze ustawienie zmiennej LC_ALL ma najwyższy priorytet. O kolejności sortowania decyduje ustawienie zmiennej LC_COLLATE. Aby jednak zadziałała musimy usunąć ustawienie zmiennej LC_ALL. Spróbujmy:


$ LC_ALL=

$ locale
LANG=pl_PL
LC_CTYPE="pl_PL"
LC_NUMERIC="pl_PL"
LC_TIME="pl_PL"
LC_COLLATE="pl_PL"
LC_MONETARY="pl_PL"
LC_MESSAGES="pl_PL"
LC_PAPER="pl_PL"
LC_NAME="pl_PL"
LC_ADDRESS="pl_PL"
LC_TELEPHONE="pl_PL"
LC_MEASUREMENT="pl_PL"
LC_IDENTIFICATION="pl_PL"
LC_ALL=                       <---------------------

$ export LC_COLLATE=C

$ locale
LANG=pl_PL
LC_CTYPE="pl_PL"
LC_NUMERIC="pl_PL"
LC_TIME="pl_PL"
LC_COLLATE=C                  <----------------------
LC_MONETARY="pl_PL"
LC_MESSAGES="pl_PL"
LC_PAPER="pl_PL"
LC_NAME="pl_PL"
LC_ADDRESS="pl_PL"
LC_TELEPHONE="pl_PL"
LC_MEASUREMENT="pl_PL"
LC_IDENTIFICATION="pl_PL"
LC_ALL=

$ sort literki.txt
A
B
C
D
a
b
c
d
ą
ć

$ sort -f literki.txt
A
a
B
b
C
c
D
d
ą
ć
 

Uff. Umiemy sortować litery. A co z liczbami? Stwórzmy plik numerki.txt:


$ cat numerki.txt
5
4
12
1
3
56
 

i spróbujmy posortować jego wiersze:


$ sort numerki.txt
1
12
3
4
5
56
 

Wiersze nie są uporządkowane numerycznie ale łatwo zgadnąć co się stało: polecenie sort nic nie wie o naszych zamiarach i nadal sortuje według kolejności leksykograficznej. Czyli najpierw według pierwszego znaku, potem drugiego (jeśli jest) itd. lekarstwem jest opcja -n:


$ sort -n numerki.txt
1
3
4
5
12
56
 

Działa! Ale zmodyfikujmy plik numerki.txt następująco:


$ cat numerki.txt
+5
-4
+0.12
-0.1
+0.3
-0.56
 

Wynik sortowania będzie zaskakujący:


$ sort -n numerki.txt
-4
+0.12
+0.3
+5
-0.1
-0.56
 

Jeśli ktoś sądzi, że użycie opcji -g zamiast -n pomoże (a tak sugeruje dokumentacja) to jest w błędzie:


$ sort -g numerki.txt
-4
+0.12
+0.3
-0.1
-0.56
+5

Jest jeszcze dziwniej!. Powodem znowu są ustawienia językowe! Przypomnijmy je sobie:


$ locale
LANG=pl_PL
LC_CTYPE="pl_PL"
LC_NUMERIC="pl_PL"
LC_TIME="pl_PL"
LC_COLLATE=C
LC_MONETARY="pl_PL"
LC_MESSAGES="pl_PL"
LC_PAPER="pl_PL"
LC_NAME="pl_PL"
LC_ADDRESS="pl_PL"
LC_TELEPHONE="pl_PL"
LC_MEASUREMENT="pl_PL"
LC_IDENTIFICATION="pl_PL"
LC_ALL=

Teraz wszystko psuje ustawienie: LC_NUMERIC="pl_PL". Czemu?!?!? Bo w języku polskim separatorem części dziesiętnej jest... przecinek. Polecenie sort posłusznie sortuje według kolejności liczbowej, pomijając te części wierszy, które nie są liczbami (czyli od kropki w prawo!). Tak więc najpierw mamy -4, potem cztery razy zero a potem +5. Dopiero zmiana na LC_NUMERIC=C daje oczekiwany wynik:


$ export LC_NUMERIC=C

$ sort -g numerki.txt
-4
-0.56
-0.1
+0.12
+0.3
+5
 

Sortowanie nieco bardziej zaawansowane

Wyprodukujmy plik tablica.txt (proponuje skopiować zawartość z przeglądarki zamiast wklepywania zawartości)), który wygląda tak:


$ cat tablica.txt
| 23 | 12 | 32 | 93 | 75 |
| 43 | 17 | 52 | 91 | 22 |
| 77 | 73 | 14 | 48 | 37 |
| 44 | 51 | 52 | 36 | 54 |
| 41 | 25 | 43 | 26 | 99 |
| 17 | 21 | 46 | 31 | 24 |
| 28 | 83 | 85 | 42 | 27 |
| 23 | 14 | 37 | 13 | 75 |
 

Dokumentacja polecenia sort mówi, że wiersze pliku wejściowego można sortować według poszczególnych pół lub grup pól. Służy do tego opcja: -k POS1[,POS2], gdzie POS1 to numer pierwszego pola branego pod uwagę a opcjonalny parametr POS2 to numer ostatniego pola. Zobaczmy jak to działa:


$ sort -n -k 4 tablica.txt
| 23 | 12 | 32 | 93 | 75 |
| 23 | 14 | 37 | 13 | 75 |
| 43 | 17 | 52 | 91 | 22 |
| 17 | 21 | 46 | 31 | 24 |
| 41 | 25 | 43 | 26 | 99 |
| 44 | 51 | 52 | 36 | 54 |
| 77 | 73 | 14 | 48 | 37 |
| 28 | 83 | 85 | 42 | 27 |
 

Cóż, wiersze są posortowane, ale według drugiej kolumny a nie czwartej! Czemu? Ano komputer to nie wróżka, i niby skąd ma wiedzieć co uważamy za "pole" w wierszu? Domyślnym separatorem pól są odstępy! To wyjaśnia powyższy wynik sortowania. Zaradzić temu można przy pomocy opcji -t (--field-separator=SEPARATOR) po której podajemy jaki znak ma być uważany za separator pól. Zatem polecenie:


$ sort -n -t'|' -k 4 tablica.txt
| 77 | 73 | 14 | 48 | 37 |
| 23 | 12 | 32 | 93 | 75 |
| 23 | 14 | 37 | 13 | 75 |
| 41 | 25 | 43 | 26 | 99 |
| 17 | 21 | 46 | 31 | 24 |
| 43 | 17 | 52 | 91 | 22 |
| 44 | 51 | 52 | 36 | 54 |
| 28 | 83 | 85 | 42 | 27 |
 

powinno posortować plik według czwartej kolumny. Jak widać posortowało według trzeciej! Dlaczego? Ano dlatego, że każdy wiersz zaczyna się od znaku zdefiniowanego jako separator, tak więc komputer "widzi" przed nim "puste pole" numer 1. Aby posortować według czwartej kolumny musimy w takim przypadku wydać polecenie:


$ sort -n  -t'|' -k 5 tablica.txt
| 23 | 14 | 37 | 13 | 75 |
| 41 | 25 | 43 | 26 | 99 |
| 17 | 21 | 46 | 31 | 24 |
| 44 | 51 | 52 | 36 | 54 |
| 28 | 83 | 85 | 42 | 27 |
| 77 | 73 | 14 | 48 | 37 |
| 43 | 17 | 52 | 91 | 22 |
| 23 | 12 | 32 | 93 | 75 |
 

Poćwiczmy teraz nieco...

Polecenie $ ls -dla /etc/s* listuje wszystkie pliki i katalogi zawarte w /etc, których nazwy zaczynają się na literę 's' (na każdym komputerze wynik może się nieco różnić):


$ ls -dla /etc/s*
drwxr-xr-x 3 root root    4096 paź  1 12:58 /etc/sane.d
-rw-r--r-- 1 root root     666 sie 25  2009 /etc/scsi_id.config
-rw-r--r-- 1 root root    1287 lis 22  2008 /etc/securetty
drwxr-xr-x 2 root root    4096 paź  1 12:58 /etc/security
-rw-r--r-- 1 root root   71449 sie  2  2008 /etc/sensors3.conf
-rw-r--r-- 1 root root   85602 lip  3  2008 /etc/sensors.conf
-rw-r--r-- 1 root root    2950 cze 29  2001 /etc/serial.conf
-rw-r--r-- 1 root root   18480 sie 28  2008 /etc/services
drwxr-xr-x 3 root root    4096 paź  1 12:58 /etc/sgml
-rw-r----- 1 root shadow   854 lut 24  2010 /etc/shadow
-rw------- 1 root root     854 gru  2  2009 /etc/shadow-
-rw-r--r-- 1 root root     155 mar  3  2010 /etc/shells
drwxr-xr-x 2 root root    4096 paź  1 12:58 /etc/skel
drwxr-xr-x 3 root root    4096 paź  1 12:58 /etc/sound
drwxr-xr-x 2 root root    4096 paź  1 12:58 /etc/ssh
drwxr-xr-x 4 root root    4096 paź  1 12:58 /etc/ssl
drwxr-xr-x 2 root root    4096 paź  1 12:57 /etc/ssmtp
-r--r----- 1 root root     481 gru  1  2009 /etc/sudoers
-rw-r--r-- 1 root root    2275 sty 11  2009 /etc/sysctl.conf
drwxr-xr-x 2 root root    4096 sty 11  2009 /etc/sysctl.d
 

Polecenie ls domyślnie sortuje wiersze alfabetycznie według nazw. Gdybyśmy chcieli posortować wynik tego polecenia według liczby dowiązań do danego pliku lub katalogu (liczba całkowita po opisie uprawnień) to zrobilibyśmy to np. tak:


$ ls  -dla /etc/s* | sort -k2
-rw-r--r-- 1 root root   18480 sie 28  2008 /etc/services
-rw-r--r-- 1 root root   71449 sie  2  2008 /etc/sensors3.conf
-rw-r--r-- 1 root root   85602 lip  3  2008 /etc/sensors.conf
-rw-r--r-- 1 root root    1287 lis 22  2008 /etc/securetty
-rw-r--r-- 1 root root    2275 sty 11  2009 /etc/sysctl.conf
-rw-r--r-- 1 root root    2950 cze 29  2001 /etc/serial.conf
-rw-r--r-- 1 root root     155 mar  3  2010 /etc/shells
-r--r----- 1 root root     481 gru  1  2009 /etc/sudoers
-rw-r--r-- 1 root root     666 sie 25  2009 /etc/scsi_id.config
-rw------- 1 root root     854 gru  2  2009 /etc/shadow-
-rw-r----- 1 root shadow   854 lut 24  2010 /etc/shadow
drwxr-xr-x 2 root root    4096 paź  1 12:57 /etc/ssmtp
drwxr-xr-x 2 root root    4096 paź  1 12:58 /etc/security
drwxr-xr-x 2 root root    4096 paź  1 12:58 /etc/skel
drwxr-xr-x 2 root root    4096 paź  1 12:58 /etc/ssh
drwxr-xr-x 2 root root    4096 sty 11  2009 /etc/sysctl.d
drwxr-xr-x 3 root root    4096 paź  1 12:58 /etc/sane.d
drwxr-xr-x 3 root root    4096 paź  1 12:58 /etc/sgml
drwxr-xr-x 3 root root    4096 paź  1 12:58 /etc/sound
drwxr-xr-x 4 root root    4096 paź  1 12:58 /etc/ssl
 

Wykorzystaliśmy tu dodatkowo mechanizm "potokowania": pionowa kreska po pierwszym poleceniu (ls) powoduje przekazanie jego wyjścia jako wejścia do drugiego polecenia (sort). To z tego powodu w definicji separatora przy sortowaniu tablicy pionową kreskę ujęliśmy w apostrofy!

A teraz ćwiczenia:

  • Posortuj wynik tego samego polecenia malejąco według rozmiaru obiektów
  • Posortuj wynik tego samego polecenia alfabetycznie według miesiąca ostatniej modyfikacji
  • Posortuj wynik tego samego polecenia alfabetycznie według trzeciej litery nazwy pliku lub katalogu
  • Na komputerze w pracowni, w katalogu /home/COMMON/katalogi/marsden są pliki składające się na katalog orbit komet. Skomponuj polecenie, wyświetlające z pliku m4.dat pierwsze 20 komet o najmniejszej odwrotności oryginalnej wielkiej półosi (trzecia liczba od końca w każdym wierszu). Prawdopodobnie przyda się polecenie head. Zapisz wynik w pliku hiperbole.dat w swoim katalogu.


Licencja Creative Commons