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
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 literki.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:
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.UTF-8
LANGUAGE=
LC_CTYPE="pl_PL.UTF-8"
LC_NUMERIC="pl_PL.UTF-8"
LC_TIME="pl_PL.UTF-8"
LC_COLLATE="pl_PL.UTF-8"
LC_MONETARY="pl_PL.UTF-8"
LC_MESSAGES="pl_PL.UTF-8"
LC_PAPER="pl_PL.UTF-8"
LC_NAME="pl_PL.UTF-8"
LC_ADDRESS="pl_PL.UTF-8"
LC_TELEPHONE="pl_PL.UTF-8"
LC_MEASUREMENT="pl_PL.UTF-8"
LC_IDENTIFICATION="pl_PL.UTF-8"
LC_ALL=
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 że ustawienie zmiennej LC_ALL ma najwyższy priorytet. U nas LC_ALL nie jest ustawione i niech tak pozostanie. O kolejności sortowania decyduje wartość zmiennej LC_COLLATE. Spróbujmy:
$ export LC_COLLATE=C
$ locale
LANG=pl_PL.UTF-8
LANGUAGE=
LC_CTYPE="pl_PL.UTF-8"
LC_NUMERIC="pl_PL.UTF-8"
LC_TIME="pl_PL.UTF-8"
LC_COLLATE=C <----------------------
LC_MONETARY="pl_PL.UTF-8"
LC_MESSAGES="pl_PL.UTF-8"
LC_PAPER="pl_PL.UTF-8"
LC_NAME="pl_PL.UTF-8"
LC_ADDRESS="pl_PL.UTF-8"
LC_TELEPHONE="pl_PL.UTF-8"
LC_MEASUREMENT="pl_PL.UTF-8"
LC_IDENTIFICATION="pl_PL.UTF-8"
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. Poradzimy sobie już chyba z subtelnościami sortowania tekstów. 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. Sortowanie według n-tego znaku i-tego pola wymaga opcji: -k i.n
- 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.