Terug naar hoofdinhoud
Linux commando: sort
Op deze pagina

Linux commando: sort

04 september 2026

Bijna iedereen leert sort in zijn eerste week Linux en leert het daarna nooit meer. Het zet regels op volgorde, en dat lijkt het hele verhaal. Dat is het niet. sort bepaalt de volgorde met regels die uit je omgeving komen en niet uit je commandoregel, dus hetzelfde commando op hetzelfde bestand kan op twee machines twee verschillende antwoorden geven. Het sorteert getallen verkeerd tenzij je vertelt dat het getallen zijn. De -u kan stilletjes een regel weggooien die je wilde houden, en -k2 betekent bijna nooit wat mensen denken dat het betekent. Het is bovendien een van de weinige commando's in dagelijks gebruik die het geheugen van een server kunnen opmaken. Dit artikel gaat over wat sort echt vergelijkt, en hoe je het laat vergelijken wat je bedoelde.

1. De basis

sort leest regels, zet ze op volgorde en schrijft ze weg. Drie woorden in die zin verbergen het hele artikel: regels, volgorde en leest. Elk daarvan gedraagt zich anders dan mensen verwachten.

1.1 De eenvoudigst mogelijke toepassing

Geef het een bestand, en het drukt het bestand op volgorde af. Het verandert het bestand niet:

$ cat fruit.txt
banana
Apple
apple
Banana

$ sort fruit.txt
apple
Apple
banana
Banana

Zonder bestandsnaam leest het standaardinvoer, en zo brengt het de meeste tijd door:

$ cut -d: -f1 /etc/passwd | sort | head -3
_apt
avahi
backup

Twee opties dekken het meeste alledaagse gebruik af, en allebei zijn ze de moeite waard om meteen te leren:

OptieStaat voorWat het doet
-r reverse Draait het resultaat van elke vergelijking om
-n numeric Vergelijkt de regels als getallen, niet als tekst

-n is geen luxe. Zonder die optie vergelijkt sort cijfers zoals het letters vergelijkt, teken voor teken, en is het antwoord verkeerd op een manier die er bijna goed uitziet:

$ printf '10\n9\n100\n2\n' | sort
10
100
2
9

$ printf '10\n9\n100\n2\n' | sort -n
2
9
10
100

De eerste uitvoer is geen fout. 1 komt echt voor 2, en 10 voor 100 voor 2, als je tekst vergelijkt. sort heeft geen idee dat die regels getallen zijn totdat je het vertelt.

1.2 sort leest alles voordat het iets afdrukt

Dit is het belangrijkste structurele feit over dit commando, en het verklaart bijna al het andere in dit artikel. sort kan zijn eerste regel niet afdrukken voordat het zijn laatste heeft gelezen, want de laatste regel van de invoer hoort misschien bovenaan.

Je kunt het zien weigeren:

$ yes hello | sort | head -1
                          # nothing, forever

$ yes hello | grep -m1 hello
hello                     # instant

grep streamt: het beslist over elke regel zodra die binnenkomt. sort buffert: het houdt de hele invoer vast voordat het ergens over kan beslissen. Vier gevolgen komen daar direct uit voort, en elk krijgt verderop een eigen paragraaf:

  • Het gebruikt geheugen in verhouding tot de invoer, ruwweg elf keer de bestandsgrootte (paragraaf 7.5).
  • Als dat geheugen op raakt, schrijft het tijdelijke bestanden naar /tmp, dat vol kan lopen (paragraaf 6.8 en 6.9).
  • Je kunt het niet midden in een levende pipeline zetten. tail -f app.log | sort drukt niets af totdat het log ophoudt, en dat gebeurt nooit.
  • De invoer moet eindig zijn. Alles wat je in sort pipet moet uit zichzelf stoppen.

grep is een filter. sort is een houder. Alles wat onhandig is aan sort volgt uit het feit dat het je gegevens moet vasthouden voordat het er iets van terug kan geven.

1.3 De locale bepaalt de volgorde

"Alfabetische volgorde" is niet overal dezelfde volgorde. Welke letters meetellen, of hoofdletters uitmaken en wat er met leestekens gebeurt, worden allemaal bepaald door je locale, en specifiek door de instelling LC_COLLATE. Hetzelfde bestand, hetzelfde commando, twee antwoorden:

$ sort fruit.txt
apple
Apple
banana
Banana

$ LC_ALL=C sort fruit.txt
Apple
Banana
apple
banana

Geen van beide is kapot. De eerste is taalkundige volgorde: die groepeert apple en Apple bij elkaar, omdat iemand die een woord opzoekt in een woordenboek niet geeft om hoofdletters. De tweede is bytevolgorde: A is byte 65 en a is byte 97, dus elke hoofdletter komt voor elke kleine letter.

Bij leestekens lopen de twee pas echt uiteen:

$ printf 'a-b\nab\na_b\naa\nAb\n' > punct.txt

$ sort punct.txt          # en_US.UTF-8
aa
a-b
a_b
ab
Ab

$ LC_ALL=C sort punct.txt
Ab
a-b
a_b
aa
ab

In de taalkundige volgorde worden het koppelteken en het liggende streepje bijna genegeerd, dus a-b belandt naast ab. In bytevolgorde is het koppelteken byte 45 en sorteert het voor elke letter, dus a-b belandt bovenin. Niets in de commandoregel wijst hierop.

Het juiste mentale model: sort heeft geen volgorde. Het leent die uit de omgeving. Als twee machines het oneens zijn over de uitvoer, kijk dan naar LC_COLLATE voordat je naar iets anders kijkt.

De praktische regel is kort. Als een mens de uitvoer leest, laat de locale dan zijn werk doen. Als een programma de uitvoer leest, of als een tweede commando het eens moet zijn over de volgorde, forceer dan LC_ALL=C. Paragraaf 7.1 laat zien welk probleem dat voorkomt, en dat is geen theoretisch probleem.

Naar boven

2. Waar komt de naam vandaan?

Hier zit geen raadsel, en dat is op zichzelf al opvallend. De buren uit dezelfde tijd zijn tot op het bot afgekort: ls, cp, mv, rm, du, df, wc. sort hield al zijn vier letters omdat er geen voor de hand liggende manier was om het korter te maken en toch begrepen te worden. srt had een toetsaanslag bespaard en veel duidelijkheid gekost.

De woordenschat is waar de echte verwarring zit, want sort gebruikt een paar gewone woorden op een heel specifieke manier. Elk van deze termen veroorzaakt ergens in dit artikel een misverstand:

line             what sort moves around; the trailing newline is not compared
key              the part of a line sort actually compares (default: all of it)
field            a numbered piece of a line, counting from 1
separator        what divides fields; by default a run of blanks, -t changes it
collation        the rules that decide which string comes first
locale           where those rules come from: LC_COLLATE, or LC_ALL, or LANG
stable           lines that compare equal keep their input order  (-s)
last resort      when every key ties, sort compares the whole line anyway
merge            combining already-sorted inputs without sorting again  (-m)
external sort    sorting more data than fits in memory, using temporary files

Twee daarvan verdienen nu meteen een waarschuwing. Een sleutel is niet hetzelfde als een veld: -k2 benoemt een sleutel die begint bij veld 2 en doorloopt tot het einde van de regel, en dat is paragraaf 5.2 en de meest gemaakte fout met dit commando. En laatste redmiddel betekent dat sort stilletjes dingen vergelijkt die je nooit hebt gevraagd te vergelijken, en dat is paragraaf 6.1 en de reden dat de uitvoer voorspelbaar is waar je een gelijkspel verwachtte.

De handleiding is eerlijk over de locale-valkuil uit paragraaf 1.3, en schreeuwt erover, wat de GNU-handleidingen bijna nooit doen:

$ man sort | grep -A3 WARNING
       ***  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.
Naar boven

3. Een korte geschiedenis

sort is zo oud als Unix zelf. In Multics bestond al een sorteervoorziening voordat Unix er was, en sort verscheen in de allereerste uitgave van Unix, geschreven door Ken Thompson.

PeriodeMijlpaal
Voor Unix Multics biedt een sorteervoorziening. Het idee arriveert al gevormd bij Bell Labs.
3 november 1971 sort verschijnt in Version 1 Unix, geschreven door Ken Thompson.
Version 4 Unix Thompson bouwt het om zodat het in pipes past, en houdt de uitvoerbestandsoptie waarmee het zijn eigen invoer kan overschrijven. Die optie heet vandaag nog steeds -o (paragraaf 4.6).
Version 5 Unix De afspraak dat een bestandsnaam - standaardinvoer betekent, komt hier.
1987 Gestandaardiseerd in de X/Open Portability Guide Issue 2, en later in POSIX. -b -c -d -f -i -m -n -o -r -t -u en -k stammen allemaal uit dat akkoord en werken overal.
GNU coreutils Opnieuw gebouwd door Mike Haertel en Paul Eggert met een merge sort, en daarom kan het bestanden sorteren die groter zijn dan het geheugen en meerdere CPU-kernen gebruiken.
6.8 (2007-02-24) --compress-program voor tijdelijke bestanden, en -C: stil controleren, alleen melden via de exitcode.
7.0 (2008-10-05) -V versiesortering, en --files0-from voor met NUL gescheiden bestandslijsten.
7.5 (2009-08-20) -h, mensleesbare numerieke sortering, waardoor du -h | sort -h eindelijk werkt.
8.6 (2010-10-15) --debug, en parallel sorteren met elke beschikbare processor.
8.8 (2010-12-22) Het standaardaantal threads wordt afgetopt op 8, "due to diminishing performance gains". Paragraaf 7.4 meet precies dat.
glibc 2.28 (2018-08-01) Helemaal geen coreutils-wijziging, maar wel de grootste verandering in de uitvoer van sort in tientallen jaren: de collatiegegevens van de C-bibliotheek worden bijgewerkt naar ISO 14651 en Unicode 9, en de volgorde van teksten met leestekens en gemengde hoofdletters verandert in de meeste locales.

De auteurs staan in de handleiding, en de eerste naam op die lijst is dezelfde persoon die GNU grep schreef:

$ man sort | grep -A2 AUTHOR
       Written by Mike Haertel and Paul Eggert.

3.1 De breuk van glibc 2.28

Die laatste tabelrij verdient een eigen alinea, want hij heeft veel mensen te pakken gehad en gaat er meer te pakken krijgen. In augustus 2018 voegde glibc ruwweg achttien jaar aan opgespaarde locale-updates samen in een enkele uitgave. Sorteerregels die sinds eind jaren negentig stabiel waren, veranderden, en ze veranderden voor gewone ASCII-teksten, niet alleen voor exotische schriften.

Alles wat een gesorteerde volgorde had opgeslagen en verwachtte dat die geldig bleef, was nu fout. De bekendste slachtoffers waren PostgreSQL-indexen: een database die verhuisde van een CentOS 7-machine (glibc 2.17) naar een CentOS 8-machine (glibc 2.28) had stilzwijgend de meeste tekstindexen in de verkeerde volgorde, met queries die onvolledige resultaten teruggaven en unieke sleutels die niets meer garandeerden. De distributies namen de wijziging over in ongeveer een jaar, te beginnen met Fedora 29 en Ubuntu 18.10 in oktober 2018.

Je eigen shellscripts lopen hetzelfde risico, op kleinere schaal. Als een script een lijst op de ene server sorteert en die vergelijkt met een lijst die op een andere server is gesorteerd, kan een upgrade van het besturingssysteem aan een van beide kanten het antwoord veranderen. LC_ALL=C is hier immuun voor, want bytewaarden worden niet bijgewerkt.

3.2 GNU tegenover BusyBox

In een minimale container-image krijg je geen GNU sort, maar BusyBox. De meeste dagelijkse opties zitten erin. Getest tegen BusyBox 1.38:

OptieGNU coreutilsBusyBox 1.38
-b -c -d -f -g -h -i -k -M -n -o -r -s -t -u -V -z Ja Ja
-C (stil controleren) Ja Nee: invalid option
-R (willekeurige sortering) Ja Nee: invalid option
--debug Ja Nee: unrecognized option
--parallel, --files0-from, --batch-size, --compress-program, -T Ja Nee
-m (samenvoegen), -S (buffergrootte) Ja Geaccepteerd en genegeerd. Dat is erger dan ze weigeren.

Die laatste rij is degene om te onthouden, want hij faalt geruisloos. BusyBox neemt -m zonder klagen aan en doet daarna alsnog een volledige sortering:

$ printf '1\n3\n5\n' > a ; printf '5\n1\n' > b

$ sort -m a b             # GNU: a straight merge, so b's disorder shows through
1
3
5
5
1

$ docker run --rm -v "$PWD:/d" busybox sort -m /d/a /d/b
1
1
3
5
5                         # BusyBox sorted everything instead

Bij deze gelegenheid leverde BusyBox de mooiste uitvoer op, en juist daarom is het verschil gevaarlijk. Een script dat -m gebruikt om voorgesorteerde brokken goedkoop samen te voegen, doet stilletjes het dure werk, en een script dat op de samenvoegsemantiek leunt, krijgt een ander antwoord.

BusyBox heeft ook nauwelijks locale-ondersteuning, dus het gedraagt zich als LC_ALL=C wat de omgeving ook zegt:

$ printf 'banana\nApple\napple\n' | sort            # host, en_US.UTF-8
apple
Apple
banana

$ printf 'banana\nApple\napple\n' | docker run --rm -i busybox sort
Apple
apple
banana

Als een build ooit een andere bestandslijst opleverde in een container dan op de laptop van de ontwikkelaar, is dit een goede eerste verdachte. macOS en de BSD's zijn weer een derde dialect, met hun eigen verschillen in de hoeken. Blijf op alles wat misschien geen GNU is bij de POSIX-set uit de rij 1987 hierboven, en raadpleeg de lokale handleiding voor de rest.

Naar boven

4. Eenvoudige toepassingen

4.1 Alfabetisch, en achterstevoren

-r (kort voor reverse) sorteert niet achterstevoren. Het draait het resultaat van elke afzonderlijke vergelijking om, wat op hetzelfde neerkomt bij een gewone sortering maar uitmaakt zodra je meerdere sleutels hebt (paragraaf 5.4):

$ sort -r fruit.txt
Banana
banana
Apple
apple

Meerdere bestanden worden behandeld als een lang bestand: eerst aan elkaar geplakt en dan gesorteerd. Dat is iets anders dan ze allemaal apart sorteren:

$ sort file1.txt file2.txt file3.txt      # one merged, sorted stream

4.2 Getallen: -n, -g en -h

Er zijn drie numerieke sorteringen en ze zijn niet uitwisselbaar. De verkeerde kiezen levert een geloofwaardig ogend fout antwoord op, en dat is de ergste soort.

OptieStaat voorBegrijptGebruik het voor
-n numeric Gewone gehele getallen en decimalen, met een optioneel teken, plus het duizendtalscheidingsteken van je locale Bijna alles: bytetellingen, ID's, poortnummers, kolommen uit awk
-g general numeric Alles wat -n doet, plus wetenschappelijke notatie, inf en nan Wetenschappelijke uitvoer. Trager, en het begrijpt duizendtalscheidingstekens niet
-h human readable Getallen met een achtervoegsel K, M, G, T De uitvoer van du -h, df -h, ls -lh

Het verschil tussen -n en -g is in een voorbeeld te zien:

$ printf '1e3\n5\n0.5\n2\n-1\ninf\nnan\n' > gen.txt

$ sort -n gen.txt
-1
inf                       # -n cannot read these, so both count as 0
nan
0.5
1e3                       # and this counts as 1, not 1000
2
5

$ sort -g gen.txt
nan
-1
0.5
2
5
1e3                       # 1000, correctly at the top
inf

De omgekeerde valkuil is net zo echt. -n respecteert het duizendtalscheidingsteken van je locale en -g niet:

$ printf '1,200\n999\n1,100\n' | sort -n       # en_US.UTF-8
999
1,100
1,200

$ printf '1,200\n999\n1,100\n' | sort -g
1,100                     # both read as "1", then compared as text
1,200
999

-h bestaat omdat -n er een potje van maakt bij mensleesbare groottes. Het leest het achtervoegsel in plaats van ervoor te stoppen:

$ printf '1.4G\n900M\n2K\n1023\n1.1T\n' > sizes.txt

$ sort -h sizes.txt
1023
2K
900M
1.4G
1.1T

$ sort -n sizes.txt
1.1T                      # 1.1, 1.4, 2, 900, 1023: the suffix is ignored
1.4G
2K
900M
1023

De koppelregel is het onthouden waard, want die twee door elkaar halen is een veelvoorkomende pipeline-fout: -h hoort bij uitvoer met achtervoegsels, -n hoort bij uitvoer zonder. du -h | sort -h klopt en du -k | sort -n klopt; du -h | sort -n is het verkeerde antwoord, keurig aangekleed.

4.3 Versies: -V

-V (kort voor Version) sorteert zoals een mens een versienummer leest, en dat is noch tekstvolgorde noch numerieke volgorde:

$ printf 'v1.10\nv1.9\nv1.2\nv1.21\n' > vers.txt

$ sort vers.txt
v1.10
v1.2
v1.21
v1.9

$ sort -V vers.txt
v1.2
v1.9
v1.10
v1.21

Het is niet alleen voor versienummers. Het is het juiste gereedschap voor alles waar cijfers in tekst zitten: log.2 voor log.10, eth1 voor eth10, photo-9.jpg voor photo-10.jpg. Het sorteert toevallig ook IPv4-adressen correct, wat mensen verbaast:

$ printf '10.0.0.9\n10.0.0.100\n10.0.2.1\n9.1.1.1\n10.0.0.10\n' | sort -V
9.1.1.1
10.0.0.9
10.0.0.10
10.0.0.100
10.0.2.1

4.4 Duplicaten verwijderen: -u

-u (kort voor unique) houdt alleen de eerste regel van elke reeks gelijke regels over. Het vervangt het oudere sort | uniq en scheelt een proces:

$ printf 'a\nb\na\nb\na\n' | sort -u
a
b

Twee dingen eraan gaan je later bijten, en allebei hebben ze een eigen paragraaf. -u bepaalt "gelijk" met de vergelijking die op dat moment geldt, niet door bytes te vergelijken, dus sort -uf en sort -un gooien veel meer weg dan je verwacht (paragraaf 7.3). En sort -u vernietigt precies de informatie die uniq -c je had gegeven, en daarom gebruikt de frequentieteller in paragraaf 5.6 uniq in plaats van -u.

4.5 Sorteren is niet hetzelfde als tellen

De verwanten van sort hebben allemaal gesorteerde invoer nodig, en dat is precies waarom sort vooraan in zoveel pipelines opduikt. uniq vergelijkt een regel alleen met de regel er direct voor:

$ printf 'a\nb\na\nb\na\n' | uniq
a
b
a
b
a                         # nothing removed: no two neighbours were equal

$ printf 'a\nb\na\nb\na\n' | sort | uniq
a
b

Hetzelfde geldt voor comm en join, en dat is paragraaf 7.1.

4.6 Het resultaat wegschrijven: -o, en de valkuil ernaast

De voor de hand liggende manier om een bestand ter plekke te sorteren is de manier die het vernietigt:

$ cat danger.txt
c
a
b

$ sort danger.txt > danger.txt
$ wc -c < danger.txt
0                         # the file is gone

Dat is niet de schuld van sort. De shell maakt danger.txt aan en kapt het af voordat sort ook maar begint, dus sort opent een leeg bestand, leest niets en schrijft niets. Elk commando in de shell gedraagt zich zo; sort is toevallig het commando waarmee mensen het proberen.

-o (kort voor output) is de oplossing, en die kent dit geval al sinds Version 4 Unix. sort leest de invoer volledig voordat het de uitvoer opent, dus hetzelfde bestand noemen is uitdrukkelijk veilig:

$ printf 'c\na\nb\n' > safe.txt
$ sort -o safe.txt safe.txt
$ cat safe.txt
a
b
c

Het is zelfs veilig als het uitvoerbestand een van meerdere invoerbestanden is:

$ printf 'c\na\n' > o1.txt ; printf 'd\nb\n' > o2.txt
$ sort -o o1.txt o1.txt o2.txt
$ cat o1.txt
a
b
c
d
Naar boven

5. Gemiddelde toepassingen

Tot nu toe sorteerde alles hele regels. Echte gegevens hebben kolommen, en op het moment dat je op een daarvan wilt sorteren kom je -k tegen, en daar gaat het begrip van sort bij de meeste mensen geruisloos mis.

5.1 Op een kolom sorteren: -k

-k (kort voor key) zegt welk deel van de regel vergeleken moet worden. De volledige syntaxis ziet er alarmerend uit en is grotendeels optioneel:

-k F[.C][OPTS][,F[.C][OPTS]]

   F      field number, counting from 1
   .C     character position inside that field, counting from 1
   OPTS   ordering letters for this key only: b d f g i M h n R r V
   ,      separates the START of the key from its END

Begin met een bestand en de twee alledaagse vormen:

$ cat staff.txt
sales    kim     42
admin    ali     7
sales    bob     130
admin    zoe     9
support  eve     42

$ sort -k2,2 staff.txt            # by the name column
admin    ali     7
sales    bob     130
support  eve     42
sales    kim     42
admin    zoe     9

$ sort -k3,3n staff.txt           # by the number column, numerically
admin    ali     7
admin    zoe     9
sales    kim     42
support  eve     42
sales    bob     130

Let op waar de n in dat tweede commando staat: binnen de sleutel, eraan vastgeplakt. -k3,3n betekent "vergelijk veld 3, numeriek". Je kunt ook sort -n -k3,3 schrijven en hier hetzelfde resultaat krijgen, maar die globale -n geldt voor elke sleutel, en paragraaf 5.4 laat zien waarom dat uitmaakt.

5.2 De komma is niet optioneel

Dit is veruit de meest gemaakte fout met sort, en hij is zo stil dat mensen het misverstand jarenlang meedragen. -k2 betekent niet "veld 2". Het betekent "vanaf het begin van veld 2 tot het einde van de regel".

Meestal is het verschil onzichtbaar, omdat het eerste veld de vergelijking toch al beslist. Het komt tevoorschijn zodra veld 2 gelijk uitkomt:

$ cat k2.txt
xray  beta  1
yankee beta 0

$ sort -k2 k2.txt              # key is "beta  1" against "beta 0"
yankee beta 0
xray  beta  1

$ sort -k2,2 k2.txt            # key is "beta" against "beta": a tie
xray  beta  1
yankee beta 0

Het eerste commando sorteerde op de 1 en de 0 aan het einde, waar niemand om had gevraagd. Het tweede zag een echt gelijkspel en viel terug op het vergelijken van de hele regel, waardoor xray vooropkwam. Allebei kloppen; maar slechts een van de twee is wat je bedoelde.

De gewoonte die je moet opbouwen is eenvoudig en kost een teken: schrijf altijd het eindveld erbij. -k1,1, -k3,3n, -k2,4. Schrijf -k2 alleen als je echt "veld 2 en verder" bedoelt, en dat is zelden.

Een sleutel is een bereik, geen kolom. -k2 is het bereik "veld 2 tot het einde van de regel"; -k2,2 is het bereik "veld 2 tot veld 2". De komma is geen versiering.

5.3 Een scheidingsteken kiezen: -t

Standaard splitst sort helemaal niet op een teken. Het splitst bij elke overgang van een spatie naar een niet-spatie, wat betekent dat een reeks van tien spaties als een scheidingsteken telt. Dat is meestal wat je wilt bij commando-uitvoer, en daarom werkten de uitgelijnde kolommen in staff.txt zonder enige voorbereiding.

-t (kort voor field terminator) vervangt die regel door een enkel letterlijk teken, en is strenger dan mensen verwachten. Met -t is elk voorkomen van dat teken een scheidingsteken, dus opeenvolgende scheidingstekens leveren lege velden op:

$ cat sp.txt
a   3 x
b   1 y
c   2 z

$ sort -k2,2n sp.txt              # default splitting: field 2 is "3", "1", "2"
b   1 y
c   2 z
a   3 x

$ sort -t' ' -k2,2n sp.txt        # single space: field 2 is EMPTY
a   3 x
b   1 y
c   2 z                           # nothing sorted at all

Het tweede commando leverde de invoervolgorde ongewijzigd op, wat eruitziet alsof het werkte op een bestand dat toevallig al bijna gesorteerd was. Het werkte niet: de sleutel was op elke regel leeg, dus elke regel kwam gelijk uit. Paragraaf 5.5 laat zien hoe je dit ziet in plaats van raadt.

Voor gegevens met echte scheidingstekens van een enkel teken is -t precies goed:

$ sort -t: -k3,3n /etc/passwd | head -4       # by numeric user ID
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin

$ sort -t$'\t' -k2,2n data.tsv               # a tab, in bash

Nog een verrassing: met -t heeft een regel die met het scheidingsteken begint een leeg veld 1, want er is een veld voor dat eerste scheidingsteken. :a:10 heeft veld 1 leeg, veld 2 a en veld 3 10.

5.4 Meerdere sleutels tegelijk

sort vergelijkt sleutels in de volgorde waarin je ze schrijft en stopt bij het eerste verschil. Zo krijg je "groepeer per afdeling, en daarbinnen het grootste getal eerst":

$ sort -k1,1 -k3,3nr staff.txt
admin    zoe     9
admin    ali     7
sales    bob     130
sales    kim     42
support  eve     42

Lees dat commando van links naar rechts: veld 1 als tekst oplopend, dan veld 3 als getal aflopend. De r zit vast aan de tweede sleutel, dus die draait alleen die sleutel om. Een globale -r zou alles omdraaien:

$ cat two.txt
alpha 3
beta 1
alpha 1
beta 3

$ sort -k1,1 -k2,2nr two.txt      # names up, numbers down
alpha 3
alpha 1
beta 3
beta 1

$ sort -r -k1,1 -k2,2n two.txt    # everything down
beta 1
beta 3
alpha 1
alpha 3

Dezelfde regel pakt mensen bij -n. Een globale -n geldt voor elke sleutel, ook de sleutels die tekst bevatten, en een tekstveld dat als getal wordt gelezen is simpelweg nul:

$ cat g2.txt
b 10
b 9
a 100
a 20

$ sort -k1,1 -k2,2n g2.txt        # correct: letter, then number
a 20
a 100
b 9
b 10

$ sort -n -k1,1 -k2,2 g2.txt      # the global -n ruins key 1
b 9
b 10
a 20
a 100

In dat laatste commando werden zowel a als b als 0 gelezen, dus sleutel 1 kwam op elke regel gelijk uit en deed sleutel 2 al het werk. De uitvoer is gesorteerd, alleen niet op iets waar je om vroeg. Zet de sorteerletter op de sleutel, niet ervoor.

5.5 Stop met raden: --debug

Sinds coreutils 8.6 hoef je hier niet meer over na te denken. --debug onderstreept op elke regel precies de bytes die als sleutel worden gebruikt, en waarschuwt voor de fouten hierboven:

$ sort --debug -k2 staff.txt
sort: text ordering performed using 'en_US.UTF-8' sorting rules
sort: leading blanks are significant in key 1; consider also specifying 'b'
admin    ali     7
     _____________
__________________
sales    bob     130
     _______________
____________________
...

Er verschijnen twee onderstrepingen per regel. De eerste markeert de sleutel, de tweede markeert de vergelijking als laatste redmiddel, en dat is de hele regel (paragraaf 6.1). Hier loopt de sleutel zichtbaar door tot het einde van de regel, en dat is het -k2-probleem uit paragraaf 5.2 zichtbaar gemaakt.

De fout uit paragraaf 5.3 is nog duidelijker:

$ sort --debug -t' ' -k2,2n sp.txt
sort: text ordering performed using 'en_US.UTF-8' sorting rules
sort: note numbers use '.' as a decimal point in this locale
a   3 x
  ^ no match for key
_______
b   1 y
  ^ no match for key
_______

no match for key is sort dat je vertelt dat het niets vond om te vergelijken. Het is de melding die je leert herkennen.

--debug noemt bovendien elke keer de locale in zijn eerste regel, en dat is de snelste manier om "waarom sorteert dit anders op de server" te beantwoorden:

$ sort --debug fruit.txt 2>&1 | head -1
sort: text ordering performed using 'en_US.UTF-8' sorting rules

$ LC_ALL=C sort --debug fruit.txt 2>&1 | head -1
sort: text ordering performed using simple byte comparison

De 2>&1 is nodig omdat de aantekeningen naar standaardfout gaan, en dat is met opzet: het betekent dat je --debug in een pipeline kunt laten staan terwijl de gegevens gewoon over standaarduitvoer doorstromen.

De waarschuwingen die het geeft zijn het waard om op het zicht te kennen:

MeldingWat het betekent
no match for key De sleutel is leeg op deze regel. Meestal een verkeerde -t of een veldnummer dat niet bestaat.
leading blanks are significant in key 1 Je sleutel bevat de spaties die ervoor staan. Zet b op de sleutel.
key 1 is numeric and spans multiple fields Je schreef -k2n waar je -k2,2n bedoelde.
note numbers use '.' as a decimal point in this locale Een hint dat er een numerieke sortering geldt en dat de locale bepaalt hoe getallen worden gelezen. Zie paragraaf 9.1.
text ordering performed using simple byte comparison Je zit in de C-locale, met opzet of per ongeluk (paragraaf 7.1).

5.6 De frequentieteller

Dit is de ene pipeline die iedereen uit zijn hoofd zou moeten kennen. Hij beantwoordt "wat komt het vaakst voor in dit bestand", en hij werkt op elke kolom van elk logbestand:

sort | uniq -c | sort -rn | head

Lees het als vier stappen: zet gelijke regels bij elkaar, tel elke groep, zet het grootste aantal vooraan, toon de bovenkant van de lijst. Op een access log van Apache of Nginx beantwoordt hij de meeste vragen waarvoor je anders een dashboard zou openen:

$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5
    125 203.0.113.7
     40 198.51.100.22
     35 192.0.2.44

$ awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -5
     81 /index.php
     44 /index.php?option=com_content
     40 /administrator/index.php
     35 /images/logo.png

$ awk '{print $9}' access.log | sort | uniq -c | sort -rn
    125 200
     40 404
     35 500

De eerste sort doet iets specifieks: hij staat er niet om de uitvoer te ordenen, hij staat er zodat uniq uberhaupt kan werken. uniq vergelijkt alleen buren, dus identieke regels moeten eerst bij elkaar worden gebracht.

De tweede sort gebruikt -rn, wat -r en -n aan elkaar geschreven is. De volgorde maakt niet uit, dus -nr is hetzelfde commando.

Laat je niet verleiden om de eerste sort | uniq -c door sort -u te vervangen. Het ziet er korter uit en het wist het antwoord:

$ printf 'a\nb\na\nb\na\n' | sort | uniq -c
      3 a
      2 b

$ printf 'a\nb\na\nb\na\n' | sort -u
a
b                         # the counts are gone

uniq heeft nog twee modi die goed samengaan met sort en veel scriptwerk besparen:

$ printf 'a\na\nb\nc\nc\nc\n' | sort | uniq -d
a
c                         # only the values that appear more than once

$ printf 'a\na\nb\nc\nc\nc\n' | sort | uniq -u
b                         # only the values that appear exactly once

Met uniq -d vind je dubbele regels in een export, een mailinglijst of een databasedump in een enkele regel.

Naar boven

6. Gevorderde toepassingen

6.1 Gelijkspel, stabiliteit en de vergelijking als laatste redmiddel

Wat doet sort als twee regels op elke sleutel die je hebt opgegeven gelijk uitkomen? De meeste mensen nemen aan dat het ze met rust laat. Dat doet het niet. GNU sort valt terug op het vergelijken van de hele regel, alsof je helemaal geen opties had opgegeven behalve -r. De handleiding noemt dit de last-resort comparison.

$ cat stab.txt
2 zebra
1 apple
2 apple
1 zebra

$ sort -k1,1n stab.txt
1 apple
1 zebra
2 apple
2 zebra                   # the second column got sorted too, uninvited

Veld 2 werd nergens op die commandoregel genoemd. sort vergeleek het toch, omdat beide regels op veld 1 gelijk uitkwamen. -s (kort voor stable) zet die terugval uit, en gelijke regels houden dan hun oorspronkelijke volgorde:

$ sort -s -k1,1n stab.txt
1 apple
1 zebra
2 zebra
2 apple                   # input order preserved within each group

Dit is belangrijker dan het lijkt. De vergelijking als laatste redmiddel is wat GNU sort voorspelbaar maakt: draai het twee keer op dezelfde invoer en je krijgt dezelfde uitvoer, ongeacht hoe de samenvoeging de regels toevallig door elkaar heeft geweven. Zet het uit met -s en de volgorde binnen een groep wordt de invoervolgorde, en dat is wat je wilt als je gegevens sorteert die al een betekenisvolle volgorde hebben, zoals een logbestand dat je op ernst hebt gesorteerd maar binnen elke ernst op tijd wilt houden.

Het lastige is dat -u die terugval ook uitzet, en stilzwijgend. Die combinatie is paragraaf 7.3, en er kunnen gegevens door verloren gaan.

6.2 Controleren in plaats van sorteren: -c en -C

-c (kort voor check) sorteert niet. Het meldt of de invoer al op volgorde staat en vertelt je de eerste regel die dat niet is:

$ printf 'a\nc\nb\n' > unsorted.txt
$ sort -c unsorted.txt
sort: unsorted.txt:3: disorder: b
$ echo $?
1

$ printf 'a\nb\nc\n' | sort -c
$ echo $?
0

-C is dezelfde controle zonder melding, voor scripts die alleen de exitcode willen. De drie exitcodes zijn het kennen waard, want het zijn niet de gebruikelijke twee:

ExitcodeBetekenis
0 Gelukt. Met -c of -C: de invoer stond op volgorde.
1 Alleen van -c of -C: de invoer stond niet op volgorde. Geen fout.
2 Een echte fout: bestand niet gevonden, uitvoer niet kunnen schrijven, geen tijdelijke ruimte meer.
$ sort /nonexistent
sort: cannot read: /nonexistent: No such file or directory
$ echo $?
2

-u aan een controle toevoegen maakt er een controle op strikte volgorde van, wat betekent dat duplicaten nu als wanorde tellen. Dat is een duplicaatdetector van een enkele regel:

$ printf 'a\na\nb\n' | sort -c
$ echo $?
0                         # sorted: duplicates are allowed

$ printf 'a\na\nb\n' | sort -cu
sort: -:2: disorder: a
$ echo $?
1                         # not strictly sorted: there is a duplicate

In een script is sort -C veel goedkoper dan een groot bestand sorteren waarvan je vermoedt dat het al gesorteerd is, want het buffert niets: het streamt, vergelijkt elke regel met de vorige, en stopt bij het eerste probleem.

6.3 Al gesorteerde bestanden samenvoegen: -m

-m (kort voor merge) combineert bestanden die al gesorteerd zijn, zonder ze opnieuw te sorteren. Het streamt, dus het heeft vrijwel geen geheugen nodig, hoe groot de bestanden ook zijn:

$ printf '1\n3\n5\n' > m1.txt ; printf '2\n4\n6\n' > m2.txt
$ sort -m m1.txt m2.txt
1
2
3
4
5
6

Het vertrouwt je volledig. Geef het ongesorteerde invoer en het levert ongesorteerde uitvoer zonder enige waarschuwing:

$ printf '5\n1\n' > m3.txt
$ sort -m m1.txt m3.txt
1
3
5
5
1                         # garbage, silently

Er is een tweede eis die makkelijker te missen is, want elke invoer kan perfect gesorteerd zijn en de samenvoeging levert alsnog onzin op. De invoeren moeten volgens dezelfde regels gesorteerd zijn. Voeg een bestand dat in de C-locale is gesorteerd samen met een bestand uit een UTF-8-locale en het resultaat staat in geen van beide volgordes:

$ printf 'a-b\nab\nAb\n' | LC_ALL=C sort > p1.txt      # byte order
$ printf 'a-c\nac\nAc\n' | sort > p2.txt              # locale order

$ LC_ALL=C sort -m p1.txt p2.txt
Ab
a-b
a-c
ab
ac
Ac

$ LC_ALL=C sort -m p1.txt p2.txt | LC_ALL=C sort -c
sort: -:6: disorder: Ac                # the merge output is not sorted at all

Hetzelfde geldt voor de sleutel. sort -m -k2,2 over bestanden die op veld 1 zijn gesorteerd, weeft ze door elkaar op veld 2 en levert rommel op, zonder een woord van protest. Een samenvoeging is niet beter dan de belofte die je doet over de invoer, en sort neemt die belofte volledig op vertrouwen aan.

-m is dus het juiste gereedschap voor geroteerde logbestanden, dagelijkse exports en al het andere dat in voorgesorteerde brokken binnenkomt, zolang elke brok door hetzelfde commando in dezelfde omgeving is gemaakt. Het is het verkeerde gereedschap voor alles wat je niet hebt gecontroleerd, dus combineer het met -C en zet de locale aan beide kanten uitdrukkelijk vast:

$ export LC_ALL=C
$ for f in chunk-*.txt; do sort -C "$f" || { echo "$f is not sorted"; exit 1; }; done
$ sort -m chunk-*.txt > merged.txt

6.4 Tekenposities binnen een veld

Het .C-deel van een sleutel selecteert tekens binnen een veld, waarmee je op een deel van een veld kunt sorteren zonder de regel eerst uit elkaar te knippen. Met tijdstempels van vaste breedte:

$ cat logs.txt
2026-03-01 12:00:00 ERROR disk
2026-01-15 08:30:00 WARN  net
2026-01-15 22:00:00 INFO  boot
2025-12-31 23:59:59 ERROR eol

$ sort -k2.1,2.2 logs.txt         # by the hour only
2026-01-15 08:30:00 WARN  net
2026-03-01 12:00:00 ERROR disk
2025-12-31 23:59:59 ERROR eol
2026-01-15 22:00:00 INFO  boot

$ sort -k1.6,1.7 logs.txt         # by the month inside the date
2026-01-15 08:30:00 WARN  net
2026-01-15 22:00:00 INFO  boot
2026-03-01 12:00:00 ERROR disk
2025-12-31 23:59:59 ERROR eol

-k1.6,1.7 hardop lezen helpt: "veld 1, tekens 6 tot en met 7". Dat is de maand in een ISO-datum.

Dit werkt alleen op gegevens die echt een vaste breedte hebben. Als het veld een teken kan opschuiven, bouw dan eerst een fatsoenlijke kolom met awk. Dat brengt het algemene principe naar boven: sorteren op een ISO 8601-datum heeft helemaal geen opties nodig, want YYYY-MM-DD is zo ontworpen dat tekstvolgorde en datumvolgorde hetzelfde zijn. Als jij de opmaak bepaalt, is dat de opmaak die je kiest.

6.5 Maanden, spaties, hoofdletters en leestekens

Er bestaan vier kleinere sorteeropties, en drie ervan zijn nuttiger dan ze lijken.

-M (kort voor Month) begrijpt maandafkortingen, waarbij alles wat het niet herkent vooraan sorteert:

$ printf 'Mar\nJan\nFeb\nDec\nfoo\nJul\n' | sort -M
foo
Jan
Feb
Mar
Jul
Dec

Het maakt geen onderscheid tussen hoofdletters en kleine letters, en het accepteert volledige maandnamen naast namen van drie letters. De namen die het accepteert komen uit je locale, waardoor het nutteloos is op een server waarvan je de locale niet beheert en een valkuil op een server waarvan de locale niet Engels is.

-b (kort voor ignore leading blanks) haalt spaties aan het begin uit de sleutel voordat er wordt vergeleken. In bytevolgorde sorteert een spatie voor elke letter, dus zonder deze optie drijven ingesprongen regels naar boven:

$ printf '  zulu\nbravo\n' > blank.txt

$ LC_ALL=C sort blank.txt
  zulu                    # the two spaces win
bravo

$ LC_ALL=C sort -b blank.txt
bravo
  zulu

-f (kort voor case fold) maakt de vergelijking hoofdletterongevoelig. In een UTF-8-locale ligt dat toch al dicht bij het standaardgedrag; in de C-locale verandert het alles:

$ printf 'Zebra\napple\nBanana\n' > f.txt

$ LC_ALL=C sort f.txt
Banana
Zebra
apple                     # all capitals first

$ LC_ALL=C sort -f f.txt
apple
Banana
Zebra

-d (kort voor dictionary order) kijkt alleen naar letters, cijfers en spaties, en negeert leestekens. -i (kort voor ignore non-printing) laat stuurtekens zoals tabs en escape-reeksen buiten de vergelijking, wat af en toe nuttig is bij gekleurde commando-uitvoer die naar een bestand is weggeschreven.

LC_ALL=C sort -df is een redelijke benadering van "sorteer dit zoals een telefoonboek het zou doen", en anders dan een UTF-8-locale gedraagt het zich na een glibc-upgrade nog steeds hetzelfde.

6.6 Bestandsnamen: -z en --files0-from

Bestandsnamen mogen regeleindes bevatten. Dat is toegestaan op Linux, en het betekent dat elke pipeline die op regels is gebouwd door een bestandsnaam kapot kan gaan, met opzet of per ongeluk. -z (kort voor zero-terminated) zet zowel invoer als uitvoer om naar NUL-bytes, en die kunnen niet in een bestandsnaam voorkomen:

$ find /var/www -type f -print0 | sort -z | xargs -0 ls -l

Elk gereedschap in die keten gebruikt de NUL-afspraak: find -print0, sort -z, xargs -0. Ze mengen met een regelgebaseerd gereedschap doet het nut teniet. grep -z, sed -z en du --files0-from=- zijn de andere leden van de familie.

sort heeft ook --files0-from=F, dat de lijst met invoerbestanden uit een met NUL gescheiden lijst leest in plaats van van de commandoregel. Daarmee vermijd je de grens "argument list too long" als je heel veel bestanden hebt:

$ find /var/log -name '*.csv' -print0 > list0
$ sort --files0-from=list0 -t, -k2,2n > combined.csv

6.7 Schudden: -R

-R (kort voor Random) wordt in de handleiding beschreven als "shuffle, but group identical keys", en die tweede helft is het deel dat ertoe doet. Het hasht de sleutel en sorteert op de hash, dus identieke regels komen alsnog naast elkaar te staan:

$ printf 'a\na\nb\nb\nc\nc\n' | sort -R --random-source=seed
b
b
c
c
a
a                         # groups intact, group order random

$ printf 'a\na\nb\nb\nc\nc\n' | shuf --random-source=seed
b
c
b
a
c
a                         # genuinely shuffled

De precieze volgorde hangt af van het seedbestand, dus je eigen resultaat ziet er anders uit. Wat niet verandert is de vorm: sort -R houdt de paren bij elkaar en shuf niet.

Als je echt wilt schudden, gebruik dan shuf. Als je de volgorde van groepen willekeurig wilt maken terwijl elke groep bij elkaar blijft, is sort -R het enige dat dat doet. Allebei accepteren ze --random-source, wat het resultaat herhaalbaar maakt: hetzelfde seedbestand geeft elke keer dezelfde volgorde, en dat is wat je in een test wilt.

6.8 Als het niet past: de external merge sort

Paragraaf 1.2 zei dat sort de hele invoer moet vasthouden voordat het iets kan uitvoeren. Dat roept een voor de hand liggende vraag op: wat gebeurt er als die hele invoer niet in het geheugen past? Veel mensen nemen aan dat het faalt, of gaat swappen. Het doet geen van beide. Het wisselt van strategie, en die strategie is het begrijpen waard, want hij verklaart elke afstemoptie in de volgende paragraaf.

De techniek heet een external merge sort, en werkt in twee fasen:

PHASE 1  make runs                PHASE 2  merge runs
read as much as the buffer holds  open 16 runs at once
sort it in memory                 read the smallest line from each
write it out as a sorted "run"    write it to the output
repeat until the input is gone    repeat until every run is empty
                                  more than 16 runs left? merge again

Elke run is gesorteerd, dus voor het samenvoegen is er per run maar een regel tegelijk in het geheugen nodig, hoe groot de runs ook zijn. Dat is de hele truc: sorteren heeft alles tegelijk nodig, samenvoegen niet.

Je kunt het zien gebeuren. Geef sort een kleine buffer, laat -T naar een map wijzen die je kunt zien, en tel de bestanden erin terwijl de sortering loopt:

$ sort -S 10M -T ./tmpdir big.txt -o /dev/null &
$ while kill -0 $! 2>/dev/null; do ls tmpdir | wc -l; done | uniq

0 1 2 3 4 5 6 7 8 ... 66 67 68 69      <- phase 1: one run at a time
69 63 57 54 53 46 39 35 29 24 23       <- phase 2: runs consumed and merged
16 10 8 3 2 1 0

De klim naar 69 is fase 1: de invoer van 62 MB gedeeld door een buffer van 10 MB. De daling is fase 2. Het is geen nette trap, want elke samenvoeging verwijdert zijn zestien invoeren een voor een terwijl het ze leegt en zijn eigen uitvoerbestand er al staat, dus het aantal wiebelt omlaag in plaats van in blokken te zakken.

Zestien is geen gok: --batch-size staat standaard op 16, en de coreutils-handleiding zegt dat ook. Met 69 runs betekent dat dat sort het niet in een enkele ronde kan. Het voegt groepen van 16 samen tot nieuwe tijdelijke bestanden en voegt die daarna weer samen, dus de gegevens worden onderweg naar buiten twee keer gelezen en herschreven. De volgende alinea's zetten een getal op wat dat kost.

De grootte van de buffer bepaalt hoeveel runs er zijn, en dat is het enige wat -S echt regelt:

BufferAangemaakte runsPiekgeheugenTijd
-S 5M 136 8 MB 1.47 s
-S 10M 69 - 1.52 s
-S 50M 14 54 MB 0.84 s
-S 200M 4 208 MB 0.88 s
-S 2G 0 691 MB 0.81 s
helemaal geen -S 0 691 MB 0.86 s

Lees de laatste drie rijen goed, want daar staat het nuttigste praktische feit uit deze paragraaf. -S 50M was net zo snel als de standaardinstelling en gebruikte een dertiende van het geheugen. Zodra het aantal runs onder de batchgrootte zakt, is een enkele samenvoegronde genoeg, en levert meer geheugen kopen daarna niets meer op. Merk ook op dat -S 2G geen 2 GB gebruikte: -S is een plafond, geen reservering.

De reden zie je het makkelijkst als je stopt met seconden tellen en verplaatste bytes gaat tellen. Voor een external sort is dat de eenheid die telt, want elke samenvoegronde leest en herschrijft de volledige gegevensset. Gemeten op hetzelfde bestand van 62 MB, als totaal aantal gelezen plus geschreven bytes door het proces:

in memory, no runs        61 MB read  +  61 MB written  =   123 MB   ( 2x )
-S 50M,  14 runs, 1 pass 123 MB read  + 123 MB written  =   247 MB   ( 4x )
-S 10M,  69 runs, 2 pass 182 MB read  + 182 MB written  =   365 MB   ( 6x )

Elke samenvoegronde kost een extra volledige leesbeurt en een extra volledige schrijfbeurt. Dat is het hele kostenmodel, en het verandert --batch-size van een mysterie in rekenwerk. Als je meer rondes afdwingt op dezelfde 136 runs:

$ sort -S 5M --batch-size=2   ...   1091 MB moved
$ sort -S 5M --batch-size=4   ...    611 MB moved
$ sort -S 5M --batch-size=16  ...    365 MB moved    (the default)
$ sort -S 5M --batch-size=200 ...    247 MB moved    (all 136 runs at once)

Voor een sortering die in het geheugen past, denk je in seconden. Voor een sortering die dat niet doet, denk je in verplaatste bytes. Elke extra samenvoegronde herschrijft je volledige gegevensset, en de buffergrootte is alleen interessant omdat die bepaalt hoeveel rondes er zijn.

Dit verklaart ook de fout No space left on device die nachtelijke taken velt. De runs bestaan allemaal tegelijk voordat het samenvoegen begint, dus een sortering heeft tijdelijke ruimte nodig van ongeveer de grootte van de invoer, in /tmp, bovenop de ruimte die de uitvoer ergens anders inneemt.

6.9 Een grote sortering afstemmen

Vijf opties regelen de machinerie uit de vorige paragraaf. Alleen de eerste twee zijn het waard om vaak aan te komen.

OptieWat het regeltWanneer je eraan komt
-S SIZE De hoofdgeheugenbuffer. Accepteert K M G of een percentage van het RAM, zoals -S 50% Om te voorkomen dat sort een server opeet, of om het juist meer te geven zodat het tijdelijke bestanden helemaal vermijdt
-T DIR Waar tijdelijke bestanden heen gaan. Standaard $TMPDIR, daarna /tmp Als /tmp klein is, in RAM zit (tmpfs), of op dezelfde schijf staat waarvan je leest
--parallel=N Hoeveel threads er tegelijk sorteren Om te voorkomen dat sort elke kern op een gedeelde machine inpikt
--compress-program=PROG Comprimeert de tijdelijke bestanden met PROG Alleen als tijdelijke ruimte de beperking is. Het kost CPU
--batch-size=N Hoeveel tijdelijke bestanden er tegelijk worden samengevoegd Bijna nooit. Verlaag het alleen als je tegen een limiet op open bestanden aanloopt

Alle getallen hier komen van hetzelfde bestand van 5.000.000 regels en 62 MB op een machine. Tot het uiterste doorgetrokken levert een piepkleine buffer veel runs en een trage sortering op:

$ sort -S 1M -T ./tmpdir big.txt -o /dev/null       1.66 s, 717 temporary files
$ sort            big.txt -o /dev/null              0.82 s, no temporary files

Voor -T volgen daar twee dingen uit. Ten eerste faalt een grote sortering met No space left on device als /tmp klein is, ook al heeft de schijf met je gegevens ruimte zat, want de tijdelijke bestanden belanden ergens anders. Het piekgebruik aan tijdelijke ruimte was in de run hierboven ongeveer 87 MB voor een invoer van 62 MB. Ten tweede is -T naar een tmpfs laten wijzen niet altijd winst: tmpfs zit in RAM, dus je hebt het geheugenprobleem verplaatst en niet opgelost.

Voor -S is het praktische advies het tegenovergestelde van wat mensen verwachten. Grijp niet naar een groot getal. Grijp naar een getal dat het aantal runs onder de batchgrootte brengt en stop daar: op dit bestand evenaarde -S 50M de snelheid van de standaardinstelling met een dertiende van het geheugen. Een plafond beschermt ook de rest van de machine, want de standaardinstelling pakt wat het nodig heeft.

Compressie is de optie waar mensen het eerst naar grijpen en die ze het minst nodig hebben:

$ sort -S 1M -T ./tmpdir big.txt -o /dev/null                        1.61 s
$ sort -S 1M -T ./tmpdir --compress-program=gzip big.txt -o /dev/null 7.37 s

Op een NVMe-SSD maakte het comprimeren van tijdelijke bestanden de sortering vierenhalf keer trager. Het loont alleen als het tijdelijke bestandssysteem echt te klein of echt traag is, zoals een netwerkschijf. Meet voordat je het toevoegt.

Naar boven

7. Iets wat de meeste gebruikers niet weten

7.1 Je cronjob sorteert anders dan je shell

Dit is degene die mensen echt tijd kost, want het symptoom is "het werkt als ik het draai en het faalt 's nachts".

Je interactieve shell heeft een locale, ingesteld vanuit je loginomgeving. cron en systemd-units starten met een vrijwel lege omgeving en helemaal geen LANG, en sort zonder locale valt terug op bytevergelijking. Hetzelfde script levert dus twee verschillende volgordes op, afhankelijk van wie het start:

$ sort fruit.txt | tr '\n' ' '
apple Apple banana Banana

$ env -i /usr/bin/sort fruit.txt | tr '\n' ' '     # what cron sees
Apple Banana apple banana

--debug zegt het hardop, en dit is de snelst mogelijke diagnose:

$ env -i /usr/bin/sort --debug /dev/null
/usr/bin/sort: text ordering performed using simple byte comparison

Op zichzelf is een andere volgorde ongevaarlijk. Het houdt op ongevaarlijk te zijn zodra een tweede commando het eens moet zijn over de volgorde. comm en join eisen allebei dat hun invoer op dezelfde manier is gesorteerd, en allebei falen ze op een manier die op een gegevensprobleem lijkt in plaats van op een locale-probleem:

$ printf 'a-b\nab\nAb\n' | sort > c1.txt              # locale order
$ printf 'a-b\nab\nAb\n' | LC_ALL=C sort > c2.txt    # byte order

$ comm c1.txt c2.txt
a-b
ab
		Ab
comm: file 2 is not in sorted order
	a-b
	ab
comm: input is not in sorted order

comm meldt hier dat drie identieke regels deels in het ene bestand en deels in het andere staan, wat onzin is, en noemt de echte oorzaak pas daarna. Dit is precies de fout die opduikt als "de backupvergelijking zegt dat er bestanden ontbreken die er duidelijk wel zijn".

De oplossing is een export bovenaan elk script dat sorteert:

#!/bin/bash
export LC_ALL=C

Dat geeft je een volgorde die op elke machine hetzelfde is, in elke locale, onder cron, in een container, en na een glibc-upgrade. Gebruik de locale-volgorde voor uitvoer die een mens leest, en LC_ALL=C voor alles wat een programma leest. Het juiste idioom voor verzamelingsbewerkingen ziet er dan zo uit:

$ comm -23 <(LC_ALL=C sort live.txt) <(LC_ALL=C sort backup.txt)   # only in live
$ comm -13 <(LC_ALL=C sort live.txt) <(LC_ALL=C sort backup.txt)   # only in backup
$ comm -12 <(LC_ALL=C sort live.txt) <(LC_ALL=C sort backup.txt)   # in both

Lees de vlaggen als "onderdruk kolom N": -23 verbergt kolom 2 en 3, waardoor alleen overblijft wat uniek is voor het eerste bestand.

7.2 LC_ALL=C is ook een snelheidsknop

Locale-bewust vergelijken is niet gratis. Elke vergelijking in een UTF-8-locale moet tekens decoderen en collatieregels op meerdere niveaus toepassen; in de C-locale is het een memcmp. Op hetzelfde bestand van 5.000.000 regels:

$ sort big.txt -o /dev/null                        1.98 s
$ LC_ALL=C sort big.txt -o /dev/null                0.85 s

$ sort --parallel=1 big.txt -o /dev/null            6.22 s
$ LC_ALL=C sort --parallel=1 big.txt -o /dev/null   1.88 s

Twee keer zo snel met alle kernen, ruim drie keer zo snel op een enkele kern. De parallelliteit in het moderne sort verbergt een groot deel van die kosten, en daarom is dit minder bekend dan vroeger, maar op een kleine VPS met een of twee vCPU's is het verschil goed zichtbaar.

LC_ALL=C koopt dus correctheid en snelheid. Het enige wat het kost is een volgorde die voor mensen prettig leest, en die hebben de meeste pipelines niet nodig.

7.3 sort -u met een sleutel gooit een regel weg die je misschien wilde

Deze kan geruisloos gegevens kwijtraken, en verdient het beter bekend te zijn.

-u houdt een enkele regel over uit elke groep die gelijk uitkomt. Als de vergelijking de hele regel is, zijn de overlevenden allemaal identiek en gaat er niets verloren. Zodra je een sleutel toevoegt, zijn de regels in een groep niet identiek, en bewaart sort er precies een enkele van:

$ cat u.txt
alice  10
alice  99
bob    5

$ sort -u -k1,1 u.txt
alice  10
bob    5                  # "alice 99" is gone

Welke overleeft? Niet de kleinste, en niet de grootste. -u zet de vergelijking als laatste redmiddel uit (paragraaf 6.1), dus binnen een groep wint de regel die de sortering toevallig het eerst bereikte, en dat is in de praktijk de invoervolgorde:

$ printf 'zeta 1\nalpha 1\nmid 1\n' | sort -u -k2,2
zeta 1

$ printf 'alpha 1\nzeta 1\nmid 1\n' | sort -u -k2,2
alpha 1                   # same data, different input order, different answer

Wil je een bepaalde vertegenwoordiger, zeg het dan hardop: sorteer op de sleutel en daarna op de beslisser die je belangrijk vindt, en laat -u de eerste van elke groep pakken. Let op dat -u bij een sleutellijst hoort die eindigt op de sleutel waarop je ontdubbelt, dus dit idioom gebruikt twee commando's:

$ sort -k1,1 -k2,2nr u.txt | sort -u -k1,1 --stable
alice  99
bob    5                  # the biggest number per name

Of ontloop de hele vraag en gebruik awk, dat duidelijker is over wat het bewaart:

$ awk '!seen[$1]++' u.txt
alice  10
bob    5                  # explicitly the first one seen

Dezelfde valkuil verschijnt zonder enige sleutel, zodra een sorteeroptie verschillende regels gelijk laat uitkomen:

$ printf 'Apple\napple\nAPPLE\n' | sort -uf
Apple                     # -f made all three equal

$ printf '1\n1.0\n01\n1.00\n2\n' | sort -un
1
2                         # -n made four lines equal

Geen van beide is een fout. -u betekent "uniek volgens de vergelijking die geldt", en de vergelijking die geldt is degene waar je om vroeg. Maar als je bedoeling was "verwijder exact dubbele regels", dan is de veilige schrijfwijze een kale sort -u zonder andere sorteeropties, of LC_ALL=C sort -u om zeker te weten dat de vergelijking byte voor byte gaat.

7.4 sort is sinds 2010 multithreaded, en helpt niet meer voorbij 8

Sinds coreutils 8.6 gebruikt sort meerdere CPU-kernen zonder dat je erom vraagt. Sinds 8.8 topt het zichzelf standaard af op acht threads, en de release-notitie geeft als reden "diminishing performance gains". Je meet de curve op je eigen machine in een minuut:

$ LC_ALL=C sort --parallel=1  big.txt -o /dev/null    1.87 s
$ LC_ALL=C sort --parallel=2  big.txt -o /dev/null    1.21 s
$ LC_ALL=C sort --parallel=4  big.txt -o /dev/null    0.91 s
$ LC_ALL=C sort --parallel=8  big.txt -o /dev/null    0.79 s
$ LC_ALL=C sort --parallel=16 big.txt -o /dev/null    0.79 s   (16 cores available)

Verdubbelen van 1 naar 2 scheelde 0,66 seconde. Verdubbelen van 8 naar 16 scheelde helemaal niets, op een machine met 16 kernen te vergeven. De standaardwaarde is goed gekozen en er is zelden reden om hem te verhogen.

Er is wel een reden om hem te verlagen. Op een gedeelde webserver pakt een ongeremde sort van een groot logbestand acht kernen zolang hij loopt, en de rest van de machine merkt dat. --parallel=2, of nice en ionice, is de attente keuze tijdens kantooruren.

7.5 sort gebruikt ongeveer elf keer de bestandsgrootte aan geheugen

Omdat sort alles moet vasthouden voordat het iets kan uitvoeren (paragraaf 1.2), schaalt het geheugengebruik mee met de invoer. Niet een op een. Elke regel wordt een record met pointers en sleutelposities, dus de overhead is fors:

sort   12 MB / 1,000,000 lines   →  137 MB resident
sort   61 MB / 5,000,000 lines   →  674 MB resident

Ruwweg elf keer de bestandsgrootte, en het schaalt met het aantal regels in plaats van het aantal bytes, dus veel korte regels kosten meer dan een paar lange. Een logbestand van een gigabyte op een VPS met twee gigabyte sorteert niet in het geheugen; het valt terug op /tmp, en als /tmp ook klein is faalt het regelrecht met No space left on device terwijl df de gegevensschijf halfleeg laat zien.

Twee verdedigingen, en het zijn de eerste dingen om naar te grijpen als een nachtelijke taak begint om te vallen:

$ sort -S 200M -T /var/tmp big.log -o sorted.log       # cap RAM, spill somewhere roomy

En vraag je daarvoor af of je uberhaupt moet sorteren. sort -C controleert zonder te bufferen. awk '!seen[$0]++' ontdubbelt zonder te sorteren. grep, cut en awk streamen allemaal. Het bestand kleiner filteren voor de sortering in plaats van erna is meestal de hele oplossing:

$ grep ' 500 ' access.log | awk '{print $1}' | sort | uniq -c | sort -rn

7.6 tsort is de andere sort

Coreutils levert een tweede sorteercommando dat vrijwel niemand ooit heeft gebruikt. tsort doet een topologische sortering: je geeft het paren die betekenen "dit moet voor dat komen", en het produceert een volgorde die aan alle paren voldoet.

$ printf 'b c\na b\nc d\n' | tsort
a
b
c
d

Dat is afhankelijkheden oplossen in een enkel commando: pakket A voor pakket B, migratie 3 voor migratie 7, deze dienst voor die dienst. Het merkt ook kringverwijzingen op en meldt ze, en dat is vaak de vraag die je eigenlijk had.

Het ontstaansverhaal is een mooi verhaal. Heel vroege Unix-linkers lazen een archiefbestand precies eenmaal, op volgorde, en besloten onderweg of elk object nodig was. Als scanf.o iets aanriep in read.o maar er achter stond, mislukte het linken. Een shellscript met de naam lorder produceerde daarom de afhankelijkheidsparen, tsort zette ze in een werkbare volgorde, en die volgorde bepaalde hoe objecten aan het archief werden toegevoegd. De coreutils-handleiding merkt op dat die hele procedure sinds ongeveer 1980 achterhaald is, omdat archieven sindsdien een symbooltabel meedragen. Het commando bleef, en het is nog steeds de kortste manier om "in welke volgorde kan ik deze dingen doen" te beantwoorden.

7.7 Weten waar sort ophoudt

Als je nodig hebtGrijp naar
Echt schudden shuf. sort -R groepeert identieke regels (paragraaf 6.7)
Tellen, niet alleen ordenen uniq -c, en uniq -d / uniq -u
Verzamelingsbewerkingen tussen twee bestanden comm, en join voor een relationele koppeling. Allebei hebben ze overeenkomende sorteervolgordes nodig
Volgorde op afhankelijkheid tsort
CSV met aangehaalde velden die het scheidingsteken bevatten csvsort (csvkit), mlr (Miller), qsv. sort -t, kan het niet
JSON jq 'sort_by(.field)', of jq -S om de sleutels van een object te sorteren
Ontdubbelen met behoud van de eerst geziene volgorde awk '!seen[$0]++'
Sorteren op bestandsgrootte of tijdstempel ls -S, ls -t, of find -printf '%T@ %p\n' | sort -k1,1nr
Processen sorteren op geheugen of CPU ps -eo pid,rss,comm --sort=-rss. ps sorteert zelf
Gegroepeerde statistiek: sommen, gemiddelden, aantallen per groep datamash, of awk
Alles wat relationeel is en zich herhaalt SQLite. sqlite3 :memory: '.import file.csv t' 'select ...'
Een volgorde die een OS-upgrade moet overleven LC_ALL=C, altijd (paragraaf 3.1)

De CSV-rij is degene die mensen te pakken krijgt, want sort -t, ziet eruit alsof het zou moeten werken en levert uitvoer op in plaats van een foutmelding. Bij een aangehaald veld met een komma erin schuiven alle veldnummers op:

$ cat bad.csv
"Smith, John",Utrecht,300
"Doe, Jane",Amsterdam,100

$ sort -t, -k3,3n --debug bad.csv
"Doe, Jane",Amsterdam,100
            ^ no match for key
_________________________
"Smith, John",Utrecht,300
              ^ no match for key
_________________________

Veld 3 is Amsterdam en niet 100, want de aangehaalde komma werd als scheidingsteken geteld. --debug ving het op. Zonder --debug had je twee regels gekregen in een volgorde die niets betekent.

Naar boven

8. Best practices

  • Zet export LC_ALL=C bovenaan elk script dat sorteert. Het maakt de volgorde identiek op elke machine, onder cron, in een container en na een OS-upgrade, en het is ruwweg twee keer zo snel. Laat de locale alleen met rust voor uitvoer die een mens gaat lezen.
  • Schrijf altijd het einde van de sleutel erbij. -k2,2, niet -k2. De komma is het verschil tussen "veld 2" en "veld 2 tot het einde van de regel", en de verkeerde vorm is meestal per ongeluk goed totdat hij dat niet is.
  • Zet de sorteerletter op de sleutel. -k3,3n, niet -n -k3,3. Een globale -n geldt voor elke sleutel en verandert je tekstsleutel in nul.
  • Grijp naar --debug zodra er iets vreemds uitkomt. Het onderstreept precies de bytes die worden vergeleken en noemt de locale. Het beantwoordt in een seconde wat raden in tien minuten beantwoordt.
  • Leer de frequentieteller uit je hoofd: sort | uniq -c | sort -rn | head. Hij beantwoordt de meeste logvragen.
  • Gebruik -o bestand, nooit > bestand, als de uitvoer terug naar de invoer gaat. De shell kapt het bestand af voordat sort het opent.
  • Kies de numerieke sortering die bij de gegevens hoort. -h voor groottes met achtervoegsel, -n voor gewone getallen, -V voor versie- en releasenummers, -g alleen voor wetenschappelijke notatie.
  • Gebruik ISO-datums en je hebt geen datumsortering nodig. YYYY-MM-DD HH:MM:SS sorteert correct als gewone tekst, in elke locale, zonder opties.
  • Wees voorzichtig met -u als je ook een sleutel of een sorteeroptie hebt opgegeven. Het houdt een willekeurige regel per groep over. Bedoel je "verwijder exacte duplicaten", gebruik dan een kale LC_ALL=C sort -u.
  • Controleer voordat je samenvoegt. sort -m vertrouwt zijn invoer volledig en levert geruisloos rommel op als die invoer niet gesorteerd is. sort -C is een goedkope bewaker.
  • Top het geheugen van grote sorteringen af, en top het laag af: -S 50M -T /var/tmp. De standaardinstelling pakt wat het nodig heeft (hier 691 MB voor een bestand van 62 MB) en een kleine buffer evenaarde dat precies, want het enige wat telt is het aantal runs onder de batchgrootte krijgen. De tijdelijke bestanden gaan naar /tmp, vaak het kleinste bestandssysteem dat je hebt.
  • Tel bij een sortering die naar schijf uitwijkt verplaatste bytes, geen seconden. Elke samenvoegronde leest en herschrijft de volledige gegevensset. Daarom helpt -S en daarom doet --batch-size verlagen pijn (paragraaf 6.8).
  • Voeg nooit brokken samen die volgens verschillende regels zijn gesorteerd. -m gaat uit van een volgorde over alle invoer. Zet LC_ALL=C en dezelfde sleutel aan de kant die de brokken schrijft en aan de kant die ze samenvoegt.
  • Filter voor je sorteert, niet erna. grep, cut en awk streamen en kosten niets; sort buffert alles.
  • Gebruik -z met find -print0 en xargs -0 zodra bestandsnamen ergens vandaan komen waar jij niet over gaat.
  • Wees attent op gedeelde servers. Een grote sortering pakt standaard acht kernen. --parallel=2 en nice houden de rest van de machine bruikbaar.
  • Voeg --compress-program niet toe zonder te meten. Op snelle lokale opslag maakt het sorteren enkele keren trager.
  • Lees de handleiding eenmaal door. Hij is kort, en de schreeuwende waarschuwing onderaan is de belangrijkste zin over dit commando.
$ man 1 sort
$ sort --help              # the KEYDEF paragraph at the bottom is the useful part
$ info coreutils 'sort invocation'
$ man 1 uniq               # sort's constant companion
$ man 1 comm ; man 1 join  # both need matching sort orders
$ man 1 shuf ; man 1 tsort
Naar boven

9. Veelgemaakte fouten

9.1 Mythe versus werkelijkheid

MytheWerkelijkheid
"sort geeft overal hetzelfde resultaat." Het geeft het resultaat waar je locale om vraagt. Je shell, je cronjob en je container kunnen elk een andere volgorde uit hetzelfde bestand halen (paragraaf 7.1).
"-k2 sorteert op de tweede kolom." Het sorteert vanaf het begin van veld 2 tot het einde van de regel. -k2,2 sorteert op de tweede kolom (paragraaf 5.2).
"sort -n -k3,3 en sort -k3,3n zijn hetzelfde." Alleen als er een sleutel is. De globale -n geldt voor elke sleutel, dus een tekstsleutel wordt nul en sorteert niets meer (paragraaf 5.4).
"sort -u verwijdert dubbele regels." Het verwijdert regels die gelijk uitkomen. Met -f, -n of een sleutel is dat een veel grotere verzameling dan "identiek" (paragraaf 7.3).
"sort -u -k1,1 houdt de kleinste regel van elke groep." Het houdt de regel die als eerste in de invoer stond. -u zet de vergelijking als laatste redmiddel uit, dus de overlevende is willekeurig (paragraaf 7.3).
"Regels die gelijk uitkomen houden hun oorspronkelijke volgorde." Niet zonder -s. GNU sort valt terug op het vergelijken van de hele regel, dus het sorteert kolommen die je nooit hebt genoemd (paragraaf 6.1).
"sort bestand > bestand sorteert het bestand ter plekke." Het leegt het bestand. De shell kapt het doel af voordat sort begint. Gebruik sort -o bestand bestand (paragraaf 4.6).
"-t' ' is de veilige manier om op spaties te splitsen." Meestal precies andersom. Met -t scheidt elke losse spatie, dus uitgelijnde kolommen worden lege velden. De standaardsplitsing gaat wel goed om met reeksen spaties (paragraaf 5.3).
"sort -n kan elk getal aan." Geen wetenschappelijke notatie: 1e3 wordt gelezen als 1. Gebruik daarvoor -g, en accepteer dat -g dan duizendtalscheidingstekens verliest (paragraaf 4.2).
"sort -n werkt in elke locale hetzelfde." Het gebruikt het decimaalteken en het duizendtalscheidingsteken van de locale. In en_US wordt 1,5 gelezen als vijftien. Een Europese CSV met decimale komma's sorteert in onzin.
"du -h | sort -n toont de grootste mappen." -n negeert het achtervoegsel, dus 1.1T sorteert onder 1023. -h leest achtervoegsels; gebruik du -h | sort -h of du -k | sort -n.
"sort -V is alleen voor versienummers." Het is het juiste gereedschap voor elk cijfer dat in tekst zit: log.2 voor log.10, eth1 voor eth10, en IPv4-adressen ook (paragraaf 4.3).
"sort -m controleert of de invoer gesorteerd is." Het controleert niets. Ongesorteerde invoer geeft ongesorteerde uitvoer zonder waarschuwing (paragraaf 6.3). BusyBox maakt het erger door -m te accepteren en toch volledig te sorteren.
"sort -R schudt." Het maakt de volgorde van groepen willekeurig maar houdt identieke regels bij elkaar. shuf schudt (paragraaf 6.7).
"sort is een streamend filter zoals grep." Het buffert eerst de hele invoer. tail -f log | sort drukt nooit iets af (paragraaf 1.2).
"Een groot bestand sorteren kost alleen schijf-I/O." Het kost ongeveer elf keer de bestandsgrootte aan RAM, en wijkt uit naar /tmp zodra dat op is (paragraaf 7.5).
"Exitcode 1 van sort betekent dat het mislukt is." Van -c of -C betekent het "niet gesorteerd", en dat is een antwoord, geen fout. Echte fouten geven 2 (paragraaf 6.2).
"Een gesorteerde lijst blijft gesorteerd." Niet over een glibc-upgrade heen. glibc 2.28 veranderde in 2018 de collatie voor gewone ASCII-teksten en brak opgeslagen volgordes, database-indexen inbegrepen (paragraaf 3.1).
"Een grotere -S is altijd sneller." Alleen totdat de runs in een samenvoegronde passen. -S 50M evenaarde hier de standaardinstelling met 54 MB tegenover 691 MB (paragraaf 6.8).
"-S 2G laat sort 2 GB reserveren." Het is een plafond, geen reservering. Op een bestand van 62 MB piekte -S 2G op dezelfde 691 MB als helemaal geen -S.
"sort -m heeft alleen nodig dat de invoer gesorteerd is." Het heeft nodig dat die volgens dezelfde regels gesorteerd is. Een C-gesorteerd bestand samenvoegen met een locale-gesorteerd bestand geeft uitvoer die sort -c afkeurt (paragraaf 6.3).
"sort -t, kan CSV aan." Alleen CSV zonder aangehaalde scheidingstekens. Een enkele "Smith, John" verschuift elk veldnummer erna (paragraaf 7.7).

9.2 Andere valkuilen om te vermijden

  • In uniq pipen zonder eerst te sorteren. uniq vergelijkt alleen buren, dus het verwijdert niets uit ongesorteerde invoer en geeft je een vals schoon rapport.
  • sort | uniq -c vervangen door sort -u. Korter, en het wist precies de aantallen waar je naar wilde kijken.
  • Een bestand sorteren waar op dat moment naar wordt geschreven. sort is niet atomair. Op een actief logbestand krijg je een momentopname die is uitgesmeerd over de tijd die het lezen kostte.
  • Aannemen dat sort | head -10 goedkoop is. De hele invoer wordt nog steeds gelezen en gesorteerd; alleen het schrijven stopt eerder.
  • Vergeten dat -r elke sleutel omdraait. Wil je maar een enkele sleutel omdraaien, plak dan r aan die sleutel: -k1,1 -k2,2nr.
  • Uitvoer van ls -l op kolomnummer sorteren. Het aantal kolommen verandert bij symbolische links en bij bestanden met ongebruikelijke datums. Gebruik ls -S, ls -t, of find -printf met een opmaak die jij hebt gekozen.
  • /tmp als tijdelijke map laten staan voor een nachtelijke taak. Op veel servers is /tmp klein, of is het een tmpfs in RAM. -T /var/tmp is meestal de veiligere keuze.
  • Op de ene plek met een locale sorteren en op de andere plek vergelijken. comm en join melden een gegevensprobleem terwijl het echte probleem is dat de twee lijsten volgens verschillende regels zijn gesorteerd.
  • Brokken samenvoegen die door verschillende machines, taken of locales zijn gemaakt. sort -m belooft niets over invoer die het oneens is over de volgorde, en het resultaat doorstaat geen enkele controle die jij niet zelf hebt geschreven.
  • --batch-size verlagen om "geheugen te besparen". Het kost in plaats daarvan I/O, en flink wat: op hetzelfde bestand verplaatste --batch-size=2 1091 MB waar de standaardinstelling 365 MB verplaatste.
  • Een sortering in een minimale container vertrouwen. BusyBox gedraagt zich als LC_ALL=C wat je ook instelt, negeert -m en -S stilzwijgend, en heeft geen --debug om je dat te vertellen.
  • -b gebruiken en verwachten dat het elke sleutel raakt. Het werkt op sleutels, en --debug vertelt je wanneer een sleutel spaties heeft opgepikt die je niet wilde.
  • Door gebruikers aangeleverde bestandsnamen als regels sorteren. Een regeleinde in een bestandsnaam is toegestaan en breekt de pipeline. Gebruik -z met -print0 en xargs -0.
  • Midden op de dag een ongeremde sortering draaien op een gedeelde webserver. Acht kernen en gigabytes RAM, zolang het duurt. nice --parallel=2 -S 200M is de beleefde versie.
Naar boven

10. Samenvatting

sort ziet eruit als het eenvoudigste commando in de gereedschapskist en is een van de subtielste. Alles wat er onhandig aan is komt voort uit drie feiten, en zodra je die drie in je hoofd hebt, houdt de rest op verrassend te zijn.

Het leent zijn volgorde uit de omgeving. Er is geen ingebouwde alfabetische volgorde; er is LC_COLLATE, en dat verschilt tussen je shell, je cronjob, je container en de laptop van je collega, en het veranderde in 2018 voor iedereen.

Het vergelijkt een sleutel, geen regel. De standaardsleutel is toevallig de hele regel, en dat verbergt het onderscheid totdat je je eerste -k schrijft en het misgaat.

Het moet alles lezen voordat het iets kan afdrukken. Die ene beperking levert het geheugengebruik op, de tijdelijke bestanden, het ontbrekende streamgedrag, en de reden waarom filteren voor het sorteren de moeite waard is.

  • export LC_ALL=C in elk script dat sorteert. Overal dezelfde uitvoer, immuun voor OS-upgrades, en ongeveer twee keer zo snel.
  • Schrijf -k2,2, niet -k2. Zonder het eindveld loopt de sleutel door tot het einde van de regel, en dat is bijna nooit wat je bedoelde.
  • Zet de sorteerletter op de sleutel, zoals in -k3,3n. Een globale -n plet je tekstsleutels tot nul.
  • --debug beslecht elke discussie over dit commando. Het onderstreept de bytes die worden vergeleken, noemt de locale, en zegt no match for key als je sleutel leeg is.
  • Kies de juiste numerieke sortering: -h voor 1.4G, -n voor gewone getallen, -V voor log.2 voor log.10, -g alleen voor 1e3.
  • sort | uniq -c | sort -rn | head beantwoordt de meeste logvragen. De eerste sort staat er zodat uniq kan werken.
  • sort bestand > bestand leegt het bestand. Gebruik sort -o bestand bestand, dat sinds Version 4 Unix veilig is.
  • -u houdt een willekeurige regel per gelijke groep over, niet de kleinste, en met -f, -n of een sleutel is "gelijk" veel breder dan "identiek".
  • Gelijkspel wordt niet met rust gelaten: sort vergelijkt als laatste redmiddel de hele regel. -s zet dat uit en houdt de invoervolgorde aan.
  • -m voegt samen zonder te controleren. Bewaak het met -C, dat 1 teruggeeft voor "niet gesorteerd" en 2 voor een echte fout.
  • Grote sorteringen kosten ongeveer elf keer de bestandsgrootte aan RAM en wijken uit naar /tmp. -S 50M -T /var/tmp is de oplossing, en het is geen compromis: een kleine buffer evenaarde de snelheid van de standaardinstelling met een dertiende van het geheugen. Eerst filteren is nog beter.
  • Als het uitwijkt, wordt het een external merge sort: gesorteerde runs op schijf, 16 tegelijk samengevoegd. Tel verplaatste bytes in plaats van seconden, want elke extra samenvoegronde herschrijft de volledige gegevensset (123 MB in het geheugen, 247 MB met een ronde, 365 MB met twee).
  • Het is sinds 2010 multithreaded en wint niets meer voorbij 8 threads: 1,87 s bij --parallel=1, 0,79 s bij 8, en 0,79 s bij 16.
  • -R is geen shuf, sort -t, is geen CSV-parser, en tsort is een heel ander commando dat afhankelijkheden oplost.

Dit is het spiekbriefje dat je wilt bewaren:

THE ONES TO REMEMBER
sort | uniq -c | sort -rn | head    what appears most often
LC_ALL=C sort                       same order everywhere, and faster
sort -o file file                   sort in place  ( > file EMPTIES it )
sort --debug ...                    show me the key you are actually using

ORDERING
-n    plain numbers          -h    1023 < 2K < 900M < 1.4G
-g    also 1e3, inf, nan     -V    log.2 before log.10, and IPv4
-r    reverse EVERY key      -M    Jan Feb Mar (locale month names)
-f    ignore case            -b    ignore leading blanks
-d    letters/digits only    -i    ignore control characters

KEYS   -k F[.C][OPTS][,F[.C][OPTS]]
-k2      field 2 TO END OF LINE      <- almost never what you want
-k2,2    field 2 only                <- write this
-k3,3n   field 3, numeric            <- letter ON the key, not global
-k1,1 -k3,3nr   name up, number down
-k1.6,1.7       field 1, characters 6-7
-t:      one literal separator; repeats make EMPTY fields
default  a RUN of blanks is one separator

MODES
-u    keep one line per equal group  (arbitrary; disables last resort)
-s    stable: equal lines keep input order
-c    is it sorted?   -C  same, quietly.  exit 1 = no, 2 = error
-cu   is it sorted AND free of duplicates?
-m    merge pre-sorted files; checks NOTHING, and assumes every
      input used the SAME locale and the SAME key

BIG FILES   ( when it does not fit: sorted runs on disk, merged 16 at a time )
-S 50M         cap the buffer LOW; matched the default at 1/13th the RAM
-T /var/tmp    spill here, not /tmp   ( needs ~1x the input, plus the output )
--parallel=2   be polite on a shared box   ( default caps at 8 )
--compress-program=gzip   only if temp SPACE is the problem; costs speed
--batch-size   leave it alone. lowering it multiplies the bytes moved
every merge pass re-reads AND re-writes everything: 2x, 4x, 6x the file

SAFE FILENAMES
find . -print0 | sort -z | xargs -0 ...

TEAMMATES
uniq -c/-d/-u   comm -12/-23/-13   join   shuf   tsort

LC_ALL=C for programs.  The locale for people.  --debug when in doubt.

Meestal is sort een commando van vijf seconden dat precies doet wat je verwacht. De ellende begint zodra de uitvoer ergens anders in gaat: een vergelijking met de lijst van gisteren, een comm tegen een backup, een ontdubbelde export, een nachtelijke taak die twee jaar prima liep en na een upgrade omvalt. Dat zijn de momenten waarop de volgorde belangrijker is dan het sorteren, en ze zijn meestal goedkoper te voorkomen dan te onderzoeken. Heb je een server waar een geplande taak stilletjes het verkeerde antwoord is gaan geven, en kan niemand zeggen wat er veranderd is, dan is dat precies het soort rustige werk waar ik graag bij help.

Naar boven
Linux commando: sort
Peter Martin
Peter Martin
Joomla Specialist

Peter is Joomla specialist en Linux admin voor snelle, veilige en schaalbare websites.

Gerelateerde artikelen