
Linux commando: tail
Er is iets mis op een server, en het eerste wat iedereen typt is tail -f op een logbestand. Het is waarschijnlijk het meest gebruikte commando in systeembeheer, en bijna iedereen gebruikt het op een manier die vannacht om middernacht stilletjes ophoudt te werken, wanneer het log roteert. Dit artikel gaat over de standaard van tien regels, het plusteken dat verandert wat het getal betekent, en de ene letter die bepaalt of je morgenochtend nog steeds naar je log kijkt.
1. De basis
head en tail zijn een tweetal met samen een taak: je een deel van een bestand tonen zonder je het hele bestand te tonen. head neemt het begin, tail het einde. Ze delen bijna al hun opties, dus wie de een leert kent het meeste van de ander.
1.1 De eenvoudigst mogelijke toepassing
Beide tonen tien regels als je niets anders zegt. Dat getal is niet instelbaar en het is al decennia tien:
$ seq 1 20 > n.txt
$ head n.txt
1 2 3 4 5 6 7 8 9 10 # shown on one line here to save space
$ tail n.txt
11 12 13 14 15 16 17 18 19 20
Zonder bestand lezen ze standaardinvoer, en dat maakt ze bruikbaar in een pipeline:
$ ps aux --sort=-%mem | head -n 5
$ dmesg | tail -n 20
1.2 Kiezen hoeveel
Twee opties bepalen de hoeveelheid, en ze zijn op beide commando's gelijk:
| Optie | Afkorting van | Betekenis |
|---|---|---|
-n NUM |
number of lines | Toon NUM regels in plaats van 10 |
-c NUM |
characters (bytes) | Toon NUM bytes in plaats van regels |
$ head -n 3 n.txt 1 2 3
$ tail -n 3 n.txt 18 19 20
$ printf 'abcdefghij' > b.bin
$ head -c 4 b.bin abcd
$ tail -c 4 b.bin ghij
-c accepteert de vermenigvuldigingssuffixen die je zou verwachten, en het is goed te weten dat K voor 1024 staat en kB voor 1000:
$ tail -c 1K big.txt | wc -c
1024
1.3 Het plusteken verandert wat het getal betekent
Dit is het deel dat mensen overslaan, en het verdubbelt wat beide commando's kunnen. Een kaal getal telt vanaf het dichtstbijzijnde uiteinde. Een getal met een teken telt vanaf het andere uiteinde.
| Vorm | Betekent | Op een bestand van 20 regels |
|---|---|---|
tail -n 3 |
De laatste 3 regels | 18 19 20 |
tail -n +18 |
Alles vanaf regel 18 | 18 19 20 |
head -n 3 |
De eerste 3 regels | 1 2 3 |
head -n -18 |
Alles behalve de laatste 18 | 1 2 |
Die vier geven op een bestand van 20 regels hetzelfde antwoord, wat toeval van de rekensom is en een mooie manier om het verschil te zien. Op een bestand waarvan je de lengte niet kent zijn het compleet verschillende vragen. tail -n 3 geeft je altijd drie regels. tail -n +18 geeft je hoeveel regels er ook vanaf 18 bestaan, en dat kunnen er drie zijn of drie miljoen.
Naar bovenHet juiste mentale model: zonder teken vraag je "hoeveel regels wil je?". Met een teken vraag je "waar wil je beginnen?". Vrijwel elke slimme oneliner met
headoftaildraait om dat onderscheid.
2. Waar komen de namen vandaan?
De namen zijn letterlijk. Een bestand heeft een kop en een staart zoals een wachtrij of een lijst die heeft, en de commando's tonen de een of de ander. Er is geen afkorting en geen verborgen grap.
Wat wel het weten waard is, is de scheefheid eronder. Die twee lijken broers en zussen die samen geschreven zijn, en dat waren ze niet. Ze komen uit verschillende Unix-lijnen, ze verschenen een jaar na elkaar, en maar een van beide kreeg ooit de functie die hem beroemd maakte. tail kreeg -f. head had die nooit nodig: een bestand groeit niet aan de voorkant.
Die ene optie is de reden dat tail een begrip is en head niet. Het vocabulaire eromheen is klein en het is goed het helder te hebben:
-f, --follow keep the file open and print new data as it is appended
-F shorthand for --follow=name --retry (see section 6)
descriptor follow THIS file, even if it gets renamed (the -f default)
name follow whatever has THIS name, reopening as needed
--retry keep trying to open a file that is missing or unreadable
rotation when a log is renamed or truncated and a fresh one begins
inotify the kernel interface that tells tail the moment a file changes
Twee daarvan zijn belangrijker dan de rest. Descriptor versus naam is de hele paragraaf 6, en het is het verschil tussen een tail -f die de nacht overleeft en een die dat niet doet. En inotify is waarom een moderne tail -f direct reageert in plaats van een keer per seconde wakker te worden om te kijken.
3. Een korte geschiedenis
De twee commando's kwamen niet samen aan. head kwam uit Berkeley en tail uit Bell Labs, ongeveer een jaar na elkaar, en daarom lijken ze zo op elkaar terwijl ze nooit echt als paar zijn ontworpen.
| Tijdvak | Mijlpaal |
|---|---|
| 1978 | head verschijnt voor het eerst in 1BSD, de eerste Berkeley Software Distribution. |
| 1979 | tail verschijnt voor het eerst in Version 7 Unix bij Bell Labs. |
| Vanaf de jaren tachtig | tail -f verspreidt zich, en logs volgen wordt een gewoonte in plaats van een techniek. |
| 1992 | POSIX standaardiseert beide. De syntaxis die het zegent is -n NUM, niet het oudere -NUM. |
| Jaren negentig | GNU bouwt beide opnieuw voor coreutils, met -F, --retry, --pid en de negatieve en plusvormen van het getal erbij. |
| Vanaf 2000 | Linux krijgt inotify, en tail -f stopt stilletjes met pollen en gaat direct reageren. |
De handleidingpagina's op elke Linux-machine noemen nog steeds de mensen die het GNU-werk deden:
$ man tail | grep -A2 AUTHOR
Written by Paul Rubin, David MacKenzie, Ian Lance Taylor, and Jim Meyering.
$ man head | grep -A2 AUTHOR
Written by David MacKenzie and Jim Meyering.
De POSIX-regel in die tabel heeft een levend gevolg. De oude syntaxis was een kaal getal, en die werkt nog steeds omdat hem breken dertig jaar aan scripts zou breken:
$ tail -5 n.txt # works, but it is the obsolete form
16 17 18 19 20
$ tail -n 5 n.txt # the POSIX form: use this one
16 17 18 19 20
Schrijf -n 5. De korte vorm is prima als je achter een prompt zit, maar hij is niet overdraagbaar, hij kan -n +5 niet ondubbelzinnig uitdrukken, en paragraaf 7.3 toont het geval waarin hij een werkelijk verwarrend resultaat oplevert.
Overdraagbaarheid is een minuut van je tijd waard, want dit artikel gaat je vertellen -F te gebruiken en je schrijft misschien voor een container-image en niet voor een volledige distributie. BusyBox, wat je in de meeste minimale images aantreft, implementeert meer dan mensen verwachten:
| Functie | GNU coreutils | BusyBox 1.38 |
|---|---|---|
-n, -c, -q, -v, -f, -s |
Ja | Ja |
-F |
Ja | Ja ("same as -f, but keep retrying") |
-n +N, -n -N, -c -N |
Ja | Ja, met dezelfde uitkomsten |
--pid |
Ja | Nee |
--retry, --follow=name |
Ja | Nee: helemaal geen lange opties |
$ docker run --rm busybox tail --pid=1 -f /tmp/a
tail: unrecognized option '--pid=1'
$ docker run --rm busybox tail --retry /tmp/a
tail: unrecognized option '--retry'
Het belangrijkste advies overleeft dus een minimale image: -F werkt. Wat niet meereist zijn --pid, --retry en de lange schrijfwijzen, dus een script dat in een container kan draaien houdt het beter bij de korte vormen. macOS en de BSD's zijn weer een derde dialect; hun handleidingen documenteren -F ook, maar niet de lange GNU-opties. Controleer met tail --help of de lokale handleidingpagina in plaats van iets aan te nemen.
4. Eenvoudige toepassingen
4.1 Naar de uiteinden van dingen kijken
Het dagelijks gebruik heeft geen uitleg nodig, alleen de gewoonte:
$ head -n 20 /etc/services # what is at the top of this file?
$ tail -n 50 /var/log/nginx/error.log # what just went wrong?
$ head -n 1 *.csv # the header row of every file
$ tail -n 1 /etc/passwd # the most recently added account
4.2 Een kopregel overslaan
Dit is het nuttigste dat het plusteken doet, en het komt voortdurend voor bij CSV-bestanden en bij commando's die een titelregel afdrukken:
$ tail -n +2 data.csv # everything EXCEPT the header
$ ps aux | tail -n +2 # the processes, without the column titles
$ df -h | tail -n +2 | sort -k5 -hr # sort by use%, header out of the way
Lees -n +2 als "begin bij regel 2". Het is de idiomatische manier om een kopregel in een shell-pipeline te laten vallen, en het is veel veiliger dan grep -v op de koptekst, wat ook elke gegevensregel wist die er toevallig op lijkt.
4.3 Meerdere bestanden tegelijk
Geef een van beide commando's meer dan een bestand en het labelt ze voor je:
$ tail -n 1 f1.txt f2.txt
==> f1.txt <==
a2
==> f2.txt <==
b2
Twee opties bepalen die labels, en beide zijn af en toe precies wat je nodig hebt:
$ tail -q -n 1 f1.txt f2.txt # -q for quiet: no headers, just the lines
a2
b2
$ tail -v -n 1 f1.txt # -v for verbose: force a header on ONE file
==> f1.txt <==
a2
-q is degene die je wilt als je het resultaat aan een ander commando voert, want die ==>-regels zijn gegevens voor zover het volgende programma weet. Een snelle manier om de laatste regel van elk log in een map te krijgen:
$ tail -q -n 1 /var/log/*.log | sort
4.4 Bytes in plaats van regels
-c is het juiste gereedschap als het bestand geen bruikbare regels heeft, of als het je om grootte gaat en niet om structuur:
$ head -c 512 disk.img | xxd | head -n 4 # look at a boot sector
$ head -c 2M huge.log > sample.log # grab a manageable sample
$ tail -c 100 file.bin | xxd # the last 100 bytes
head -c is de nette manier om een heel groot of binair bestand te bemonsteren, omdat het stopt met lezen zodra het heeft wat het nodig heeft. Paragraaf 7.1 meet precies hoeveel dat scheelt.
Een waarschuwing bij tekst. Bytes zijn geen tekens, en -c telt bytes. Snijd op de verkeerde plek en je hakt een teken van meerdere bytes doormidden:
$ printf 'aeiou \xc3\xa9\xc3\xa8\xc3\xaa done\n' > u.txt # three 2-byte chars
$ tail -c 12 u.txt | xxd
00000000: c3a9 c3a8 c3aa 2064 6f6e 650a # lands on a boundary: fine
$ tail -c 11 u.txt | xxd
00000000: a9c3 a8c3 aa20 646f 6e65 0a # starts mid-character
$ tail -c 11 u.txt | iconv -f UTF-8 -t UTF-8
iconv: illegal input sequence at position 0
Een losse vervolgbyte is geen geldige UTF-8, dus het eerste teken komt eruit als een vervangingsteken of helemaal niet. Tel bij alles met accenten, emoji of niet-Latijns schrift liever regels dan bytes.
Naar boven5. Gemiddelde toepassingen
5.1 Het midden eruit knippen
Geen van beide commando's kan je in zijn eentje de regels 8 tot 12 tonen. Samen kunnen ze het, op twee manieren:
$ head -n 12 n.txt | tail -n 5 # take the first 12, then the last 5 of those
8 9 10 11 12
$ tail -n +8 n.txt | head -n 5 # start at line 8, then take 5
8 9 10 11 12
Ze geven hetzelfde antwoord, maar ze zijn niet even goed. De tweede vorm begint bij regel 8 en stopt na vijf regels, dus hij leest de rest van het bestand nooit. Op een klein bestand doet dat er niet toe. Op een log van meerdere gigabytes is het het verschil tussen direct en traag, en het is de moeite waard om de tweede vorm je gewoonte te maken.
sed -n '8,12p' en awk 'NR>=8 && NR<=12' doen hetzelfde werk. Ze zijn flexibeler en ze zijn trager, omdat ze elke regel ontleden. Gebruik die als het bereik van de inhoud afhangt; gebruik head en tail als het alleen van de positie afhangt.
5.2 Een log volgen
-f (afkorting van follow) is waarom tail beroemd is. Het toont de laatste tien regels en blijft daarna open staan, en drukt nieuwe regels af zodra ze geschreven worden:
$ tail -f /var/log/nginx/access.log
$ tail -n 100 -f /var/log/syslog # start with more context
$ tail -f /var/log/mail.log # Ctrl-C to stop
-n met -f combineren is een kleine gewoonte die de moeite waard is. De standaard van tien regels geeft zelden genoeg context als je probeert te begrijpen wat vooraf ging aan het ding waarop je wacht.
5.3 Meerdere logs tegelijk volgen
tail -f accepteert meerdere bestanden, en drukt een kopregel af telkens als de uitvoer van het ene naar het andere overschakelt:
$ tail -f m1.log m2.log
==> m1.log <==
from one
==> m2.log <==
from two
==> m1.log <==
one again
Voor een webserver die toegang en fouten apart logt is dat echt handig:
$ tail -f /var/log/nginx/access.log /var/log/nginx/error.log
De kopregels verschijnen alleen als de bron wisselt, dus een reeks regels uit een bestand blijft bij elkaar en blijft leesbaar.
5.4 Doorsturen naar grep, en waarom er niets verschijnt
Iedereen schrijft dit, en op een rustig log staat iedereen daarna voor een raadsel:
$ tail -f app.log | grep ERROR
# ... an error is written to the log ...
# ... and nothing appears. For a long time.
Er is niets kapot. Als grep naar een terminal schrijft leegt hij elke regel meteen, maar als hij naar een pipe of een bestand schrijft buffert hij ongeveer 4 kB voordat hij leegt, omdat dat sneller is voor bulkwerk. Jouw ene foutregel zit in die buffer op gezelschap te wachten. Gemeten met een enkele passende regel en twee seconden wachten:
grep ERROR lines after 2s: 0
grep --line-buffered ERROR lines after 2s: 1
stdbuf -oL grep ERROR lines after 2s: 1
Twee oplossingen, en beide werken. Vraag grep om per regel te legen, of verpak een willekeurig commando in stdbuf om de buffering van buitenaf te veranderen:
$ tail -f app.log | grep --line-buffered ERROR
$ tail -f app.log | grep --line-buffered ERROR | tee errors.txt
$ tail -f app.log | stdbuf -oL cut -d' ' -f1-3
$ tail -f app.log | awk '/ERROR/ { print; fflush() }'
De regel geldt veel breder dan alleen grep. Elk commando midden in een tail -f-pipeline buffert tenzij je zegt dat het dat niet moet doen, want geen van alle kan vanaf die plek een terminal zien. awk heeft fflush() nodig, sed heeft -u nodig, en al het andere heeft stdbuf -oL nodig.
5.5 Automatisch stoppen
--pid vertelt tail -f te stoppen zodra een opgegeven proces klaar is, en dat maakt van "kijk naar dit log terwijl die klus loopt" een enkel commando:
$ ./long-import.sh &
$ tail -f import.log --pid=$!
line 1
line 2
line 3
$ echo $?
0 # tail noticed the writer had gone
Zonder die optie hou je een tail over die nooit terugkomt, en dat is prima achter een prompt en een probleem in een script. Paragraaf 6.5 behandelt de rest van het scriptverhaal.
6. Gevorderde toepassingen
6.1 Logrotatie, en de letter die ertoe doet
Dit is de belangrijkste paragraaf van het artikel. Elk log op een productieserver wordt geroteerd, meestal 's nachts, en de standaard tail -f overleeft dat niet. Hij waarschuwt je niet. Hij stopt gewoon met iets tonen, voorgoed, terwijl hij er precies zo uitziet als wanneer hij werkt.
Hier het experiment. Twee tails op hetzelfde bestand, een met -f en een met -F, over een rotatie die hernoemt en opnieuw aanmaakt:
$ echo "line 1 before rotation" > app.log
$ tail -f app.log > out-f.txt &
$ tail -F app.log > out-F.txt &
$ echo "line 2 before rotation" >> app.log
$ mv app.log app.log.1 # what logrotate does
$ echo "line 3 AFTER rotation" > app.log
$ echo "line 4 AFTER rotation" >> app.log
En de twee uitkomsten, naast elkaar:
----- tail -f saw -----
line 1 before rotation
line 2 before rotation
# and then nothing. Ever.
----- tail -F saw -----
line 1 before rotation
line 2 before rotation
tail: 'app.log' has become inaccessible: No such file or directory
tail: 'app.log' has appeared; following new file
line 3 AFTER rotation
line 4 AFTER rotation
De reden is een stukje Unix dat het een keer begrijpen waard is, want het verklaart nog een tiental andere dingen. Een bestandsnaam is niet een bestand. Er zijn drie afzonderlijke lagen bij betrokken:
| Laag | Wat het is | Wat hernoemen ermee doet |
|---|---|---|
| Pad | Een regel in een map: /var/log/app.log |
Verandert. Meer is hernoemen niet. |
| Inode | Het eigenlijke bestand: de gegevens en metagegevens, zonder eigen naam | Onaangeroerd. Zelfde inode, zelfde bytes. |
| Bestandsdescriptor | Een nummer in een proces, dat naar een geopende inode wijst | Onaangeroerd, en nog steeds volkomen geldig. |
Dus als logrotate app.log hernoemt naar app.log.1, gebeurt er niets met het bestand dat tail open heeft. De mapregel verhuisde; de inode niet. tail -f volgt nog steeds trouw dezelfde bytes als altijd, die nu app.log.1 heten en waar nooit meer iets naartoe geschreven wordt.
Dat is de hele fout, en het is een verdedigbaar ontwerp en geen vergissing. -f volgt de bestandsdescriptor, en dat is juist voor "kijk naar dit exacte bestand" en precies verkeerd voor "kijk naar dit log".
-F volgt de naam. Zodra de naam niet meer wijst naar het bestand dat hij open had, zegt hij dat en opent hij opnieuw. Het is een afkorting voor twee langere opties:
$ tail -F app.log
$ tail --follow=name --retry app.log # identical
Maak -F je standaard voor alles onder /var/log. Het kost niets: op een bestand dat nooit roteert gedraagt -F zich precies als -f.
6.2 De andere soort rotatie
De regel hierboven heeft een eerlijke nuance nodig, want logrotate heeft een tweede modus. Met copytruncate wordt het bestand gekopieerd en daarna ter plekke leeggemaakt in plaats van hernoemd, dus de descriptor blijft geldig. Een kale -f kan daarmee overweg:
$ cp t.log t.log.1 && : > t.log # copytruncate, in two commands
$ echo "after truncate" >> t.log
# what tail -f printed:
before 1
before 2
tail: t.log: file truncated
after truncate # it kept up
Welke rotatiestijl je server gebruikt bepaalt dus of een kale -f stilletjes faalt. Je kunt het opzoeken:
$ grep -r copytruncate /etc/logrotate.conf /etc/logrotate.d/ | head
Het praktische advies verandert niet. Gebruik -F en je hoeft het niet te weten of erom te geven, want die kan met allebei overweg.
6.3 Wachten op een bestand dat nog niet bestaat
-F bevat --retry, en dat betekent dat je kunt beginnen met kijken voordat er iets is om naar te kijken. Dat is precies wat je wilt als je op het punt staat een dienst te starten waarvan het log pas bestaat als het bestaat:
$ tail -F /var/log/myapp/app.log
tail: cannot open '/var/log/myapp/app.log' for reading: No such file or directory
tail: '/var/log/myapp/app.log' has appeared; following new file
[2026-08-24 09:15:02] starting up
Een kale tail -f op een ontbrekend bestand drukt een fout af en stopt meteen. -F wacht. Start eerst de tail, dan de dienst, en je mist de eerste regels niet.
6.4 inotify, en waar -s nog voor dient
Een moderne tail -f polt niet. Op Linux vraagt hij de kernel om hem te melden wanneer het bestand verandert, via dezelfde inotify-interface die andere bestandsbewakers gebruiken. Je kunt het bevestigen op een draaiende tail:
$ tail -f w.log &
$ ls -l /proc/$!/fd | grep -c inotify
1 # one inotify descriptor
Daarom verschijnen nieuwe regels direct in plaats van tot een seconde later. De optie -s, die een wachttijd tussen controles instelt, is een overblijfsel uit het polltijdperk en doet op een lokaal bestand vandaag vrijwel niets. In een geval telt hij nog wel: inotify werkt niet betrouwbaar op netwerkbestandssystemen, dus op een log op een NFS-koppeling valt tail terug op pollen en wordt -s weer echt.
$ tail -f -s 5 /mnt/nfs/shared/app.log # check every 5s on a network mount
6.5 In scripts
head en tail gedragen zich netjes in scripts zolang je -f vermijdt, die per ontwerp nooit terugkomt. Drie patronen dekken vrijwel alles:
# bounded: read a fixed amount and move on
last_line=$(tail -n 1 "$logfile")
# follow, but with an owner: tail dies when the job does
./deploy.sh & tail -F deploy.log --pid=$!
# follow, but with a deadline
timeout 300 tail -F deploy.log
Let op een wisselwerking. Schrijf je tail -f log | grep -q PATROON in de verwachting dat het bij de eerste treffer stopt, bedenk dan dat grep -q inderdaad stopt, maar dat tail dat pas merkt als hij weer probeert te schrijven, dus de pipeline kan blijven hangen. Geef hem een deadline met timeout, of gebruik de --pid-vorm hierboven.
Er is een tweede wisselwerking, en die heeft meer scripts gebroken dan wat dan ook in dit artikel. Paragraaf 7.2 laat zien dat head zijn producent met SIGPIPE doodt, wat die afsluitcode 141 geeft. Normaal verbergt de shell dat, want een pipeline meldt alleen de status van zijn laatste commando. Zet pipefail aan en het verbergen houdt op:
$ bash -c 'yes | head -n 2 >/dev/null; echo $?'
0 # the usual, comfortable answer
$ bash -c 'set -o pipefail; yes | head -n 2 >/dev/null; echo $?'
141 # SIGPIPE, now visible
Bedenk nu hoe vrijwel elk zorgvuldig script begint:
#!/bin/bash
set -euo pipefail # the standard safety boilerplate
count=$(seq 1 2000000 | head -n 1) # perfectly ordinary line
echo "reached this line, c=$count" # never printed
$ ./script.sh; echo "script exit: $?"
script exit: 141
set -e zag een status die niet nul was en stopte. De pipeline deed precies wat de bedoeling was, en het script stierf zonder bericht. Dit treft mensen die alles goed hebben gedaan. Drie uitwegen, gemeten:
seq 1 2000000 | head -n 1 -> 141 the problem
seq 1 2000000 | head -n 1 || true -> 0 tolerate it where it is harmless
seq 1 2000000 | sed -n '1p' -> 0 sed keeps reading, so no SIGPIPE
seq 1 2000000 | sed -n '1{p;q}' -> 141 ... but quit early and it is back
Die derde regel heeft een prijs die het benoemen waard is: sed -n '1p' ontloopt het signaal juist doordat hij alle twee miljoen regels leest in plaats van te stoppen. Hij ruilt een statuscode in voor het werk dat head je bespaarde. Op een grote invoer is || true het betere antwoord.
Hetzelfde geldt voor grep -q en voor al het andere dat een pipe sluit voordat zijn producent klaar is.
7. Iets wat de meeste gebruikers niet weten
7.1 tail leest je bestand niet
Mensen vermijden tail op enorme bestanden, in de veronderstelling dat hij zich door gigabytes heen worstelt om het einde te bereiken. Dat doet hij niet. Als de invoer een echt bestand is springt hij rechtstreeks naar het einde en leest van daar terug. head doet het spiegelbeeld: hij leest vanaf het begin en stopt zodra hij genoeg heeft.
Gemeten op een tekstbestand van 1,1 GB:
$ tail -n 5 huge.txt 0.00 s # seeks to the end
$ head -n 5 huge.txt 0.00 s # stops after 5 lines
$ cat huge.txt | tail -n 5 0.32 s # the pipe forces all 1.1 GB through
$ sed -n '5p' huge.txt 0.43 s # parses every line to find one
Twee lessen in een tabel. Ten eerste: tail op een enorm log is gratis, dus twijfel niet langer. Ten tweede is het klassieke useless use of cat hier niet alleen onelegant: cat huge.txt | tail -n 5 leest werkelijk een gigabyte die tail huge.txt nooit aanraakt, omdat je in een pipe niet kunt springen. Het bestandsargument is geen stijlvoorkeur.
Je kunt het zien gebeuren. strace op een bestand van 1.288.895 bytes toont het hele algoritme in vier systeemaanroepen:
$ strace -e trace=lseek,read tail -n 3 s.txt
lseek(3, 0, SEEK_CUR) = 0
lseek(3, 0, SEEK_END) = 1288895 # how big is it?
lseek(3, 1286144, SEEK_SET) = 1286144 # jump to one block from the end
read(3, "199608\n199609\n199610\n"..., 2751) = 2751
read(3, "", 0) = 0 # done
Grootte, springen, een blok lezen, terugtellend nieuwe regels tellen, afdrukken. Hij las 2751 bytes van de 1,3 megabyte. Had dat blok geen drie regeleindes bevat, dan was hij nog een blok teruggestapt en had hij het opnieuw geprobeerd, en dat is de hele implementatie.
Nu hetzelfde commando met het bestand dat via een pipe binnenkomt:
$ cat s.txt | strace -e trace=lseek tail -n 3
+++ exited with 0 +++ # not a single lseek
Geen enkele sprong, want er valt niets te springen. Een pipe heeft geen grootte en geen verleden: bytes komen een keer langs en zijn dan weg. Dus schakelt tail volledig van strategie en houdt hij een rollende buffer bij van de meest recente regels terwijl de stroom langskomt, waarbij elke regel weggegooid wordt zodra er een nieuwere komt. Het werkt, het is begrensd, en het kost het lezen van elke afzonderlijke byte.
Dat is de echte reden dat de twee commando's in de tabel hierboven niet een paar procent maar oneindig veel schelen. Het is niet dat cat op een abstracte manier verspillend is. Het is dat tail een pipe geven hem de enige truc ontneemt die hij heeft.
7.2 head doodt wat hem voedt
Wat gebeurt er met het commando links zodra head genoeg gezien heeft?
$ yes | head -n 3
y
y
y
$ bash -c 'yes | head -n 2 >/dev/null; echo "producer=${PIPESTATUS[0]}"'
producer=141
141 is 128 plus 13, en signaal 13 is SIGPIPE. Als head stopt sluit hij de leeskant van de pipe, de volgende schrijfactie van yes kan nergens heen, en de kernel doodt hem. Dat is het mechanisme dat yes | head laat stoppen in plaats van je schijf te vullen, en het is waarom eencommando | head -n 5 veilig is op iets wat anders eeuwig zou doorgaan.
Het verklaart ook een foutmelding die mensen verontrustend vinden. Een script dat naar head doorstuurt kan afsluitcode 141 melden, of "Broken pipe" afdrukken als de producent ervoor koos het signaal luid af te handelen. Dat is geen mislukking, dat is de pipeline die werkt zoals bedoeld.
7.3 De verouderde syntaxis die nog werkt, tot hij dat niet doet
tail -5 is de oude vorm van voor POSIX. Hij werkt nog en hij is prima achter een prompt. De valkuil is dat de moderne optieletters dezelfde ruimte innemen:
$ tail -5 file # 5 lines. The obsolete form.
$ tail -n 5 file # 5 lines. The POSIX form.
$ tail -c 5 file # 5 BYTES, not lines.
$ tail -f -5 file # works, but now you are mixing two eras
Elk script dat lang genoeg meegaat wordt uiteindelijk gelezen door iemand die moet uitzoeken of -5 regels of bytes betekende. Schrijf -n 5 en de vraag komt nooit op. Hetzelfde geldt voor head.
7.4 De negatieve en plusvormen die niemand gebruikt
Paragraaf 1.3 introduceerde ze; ze verdienen een tweede blik omdat elk ervan een probleem oplost dat mensen meestal op de moeilijke manier oplossen.
$ head -n -1 file.csv # everything except the LAST line
$ head -c -4 file.bin # everything except the last 4 bytes
$ tail -c +2 file.txt # from byte 2 onwards: strips one leading byte
$ tail -n +2 file.csv # from line 2 onwards: strips the header
head -n -1 is degene om te onthouden. Een afsluitende samenvattingsregel, een totaalregel of een verdwaalde lege regel aan het eind van een gegenereerd bestand laten vallen is anders een klus voor sed of awk en een moment nadenken. En tail -c +2 heeft een echte niche: een byte-order mark of een enkel verdwaald teken vooraan een bestand weghalen zonder de rest aan te raken.
7.5 Een ontbrekend laatste regeleinde blijft ontbreken
Tekstbestanden horen op een regeleinde te eindigen, en heel wat doen dat niet. tail repareert dat niet stilletjes voor je:
$ printf 'x1\nx2' > nn.txt # no newline after x2
$ tail -n 1 nn.txt | xxd
00000000: 7832 x2 # two bytes. No 0a.
Dat klopt en het verrast af en toe. Bouw je een bestand door in een lus tail -n 1-uitvoer toe te voegen, dan lopen de regels aan elkaar zodra een bronbestand zijn laatste regeleinde mist. Het is ook waarom je shellprompt soms tegen het eind van de commando-uitvoer aan geplakt lijkt.
7.6 tail in containers, inclusief de regel die iedereen kopieert
Containers veranderen het antwoord twee keer, en beide zijn het weten waard voordat je uit gewoonte een shell in een container opent.
Ten eerste schrijft een goed gebouwde container-image geen logbestanden. Hij schrijft naar standaarduitvoer en standaardfout, en de runtime verzamelt dat. Dus docker exec gevolgd door tail -f /var/log/iets vindt meestal niets, omdat er niets is:
$ docker logs -f --tail 100 mycontainer # the actual equivalent
$ docker compose logs -f web
$ kubectl logs -f deploy/web --tail=100
Dat zijn de tail -F van de containerwereld, en ze nemen dezelfde twee argumenten die je al kent: hoeveel historie, en of je wilt volgen. Grijp alleen naar tail binnen een container als de applicatie werkelijk bestanden schrijft, en dat betekent meestal dat hij in een container gezet is zonder aangepast te worden.
Ten tweede is er een regel die je in honderd Dockerfiles en Compose-bestanden tegenkomt:
command: tail -f /dev/null
Dit heeft niets met logs te maken. Een container leeft precies zo lang als zijn hoofdproces, en /dev/null levert nooit gegevens, dus tail -f erop blokkeert eeuwig zonder CPU te gebruiken. Het is een goedkope manier om te zeggen "start deze container en houd hem draaiend zodat ik erin kan".
Als debugtruc is dat prima. In een vastgelegd bestand is het een luchtje, want het betekent dat de container geen echt hoofdproces heeft: niets om te bewaken, niets waarvan het stoppen een storing signaleert, en een herstartbeleid dat nooit kan afgaan omdat tail nooit doodgaat. Healthchecks slagen terwijl de eigenlijke applicatie helemaal niet draait. Vind je het in een productie-composebestand, dan is de vraag wat die container geacht wordt te doen.
sleep infinity doet hetzelfde werk met een verrassing minder, en geen enkele lezer verwart het met iets wat met loggen te maken heeft.
7.7 Weten waar head en tail ophouden
tail -f is vaker wel dan niet het juiste gereedschap, maar het heeft duidelijke randen:
| Als je nodig hebt | Grijp naar |
|---|---|
| Terugscrollen terwijl je blijft volgen | less +F: Ctrl-C om te bladeren, F om verder te gaan |
| Het systemd-journaal in plaats van een bestand | journalctl -f, dat ook -n en -u heeft |
| Veel logs op een scherm, met kleur | multitail, of lnav voor gestructureerde logs |
| Logs van meerdere machines | Centrale logging; tail -F over SSH schaalt niet |
| Iets uitvoeren zodra een bestand verandert | entr of inotifywait, geen tail |
| Een bereik dat van inhoud afhangt, niet van positie | sed -n '/start/,/eind/p' of awk |
| Een waarde zien veranderen, geen log zien groeien | watch, dat in plaats daarvan een commando herhaalt |
less +F verdient de bovenste regel. Het doet alles wat tail -f doet, en daarna zet Ctrl-C je in een volwaardige pager over hetzelfde bestand, zodat je terug kunt zoeken door wat al voorbijgescrold is en met F weer kunt gaan volgen. Voor elke sessie waarin je verwacht te lezen zowel als te kijken is het simpelweg beter.
8. Best practices
- Gebruik
-Fen niet-fop alles in/var/log. Rotatie via hernoemen maakt een kale-fstilletjes doof, en-Fkost niets op een bestand dat nooit roteert. - Schrijf
-n 20, niet-20. Het kale getal is de verouderde vorm van voor POSIX. Hij werkt nog en hij zal de volgende lezer nog steeds in verwarring brengen over regels versus bytes. - Leer
tail -n +2. "Begin bij regel 2" is de nette manier om een kopregel te laten vallen, en het is beter dangrep -vop de koptekst, dat ook passende gegevens opeet. - Onthoud
head -n -1. "Alles behalve de laatste regel" scheelt naarsedgrijpen zodra een bestand een afsluitend totaal of een samenvatting heeft. - Geef de bestandsnaam mee, gebruik geen
catervoor.tail -n 5 bestandspringt naar het einde;cat bestand | tail -n 5leest het hele ding. Op een log van een gigabyte is dat 0,00 s tegen 0,32 s. - Voeg
--line-bufferedtoe aangrepin elketail -f-pipeline, en-uaansed,fflush()aanawk, ofstdbuf -oLaan al het andere. Anders blijven je treffers in een buffer van 4 kB zitten. - Begin met meer context dan de standaard.
tail -n 100 -Flaat je zien wat voorafging aan het ding waarop je wacht; tien regels doen dat zelden. - Gebruik
--pid=$!als je het log van een klus volgt. De tail eindigt dan wanneer de klus eindigt, in plaats van eeuwig te blijven hangen. - Zet nooit een kale
tail -fin een script. Hij komt per ontwerp niet terug. Begrens hem met--pidoftimeout. - Gebruik
-qals je meerdere bestanden doorstuurt. Die==>-kopregels zijn gegevens voor het volgende commando in de pipeline. - Grijp naar
less +Fals je wilt lezen zowel als kijken. Ctrl-C om terug te scrollen en te zoeken,Fom verder te volgen. - Kies
tail -n +8 | head -n 5bovenhead -n 12 | tail -n 5voor een bereik in het midden. Zelfde antwoord, en het stopt met lezen zodra het heeft wat het nodig heeft. - Bescherm
| headin elk script metset -o pipefail. Schrijf| head -n 1 || truewaar de SIGPIPE onschuldig is, anders sterft het script op afsluitcode 141 zonder iets verkeerd te hebben gedaan. - Tel regels en geen bytes bij tekst met accenten.
-ckan een UTF-8-teken doormidden snijden en ongeldige uitvoer opleveren. - Gebruik binnen containers het commando van de runtime zelf.
docker logs -f --tail 100enkubectl logs -fzijn de echte equivalenten, want een goede image logt naar stdout en heeft geen bestand om te tailen. - Lees de handleiding voor de twee die je gaat vergeten.
--retryen--follow=namezijn waar-Fuit bestaat, en dat weten maakt zijn gedrag vanzelfsprekend in plaats van magisch.
$ man 1 tail # short, and the -f/-F explanation is worth reading
$ man 1 head
$ info coreutils 'tail invocation' # deeper than the man page
$ man 1 less # for +F
$ man 5 logrotate.conf # to see which rotation style your server uses
Naar boven9. Veelgemaakte fouten
9.1 Mythe versus werkelijkheid
| Mythe | Werkelijkheid |
|---|---|
"tail -f blijft mijn log volgen." |
Tot het roteert. Dan volgt hij voorgoed het hernoemde oude bestand, stilletjes. Gebruik -F (paragraaf 6.1). |
"tail moet het hele bestand lezen om het einde te bereiken." |
Hij springt. 0,00 s op een bestand van 1,1 GB. Het is cat bestand | tail dat alles leest. |
"tail -f | grep is kapot, er verschijnt niets." |
grep buffert ongeveer 4 kB als hij niet naar een terminal schrijft. Voeg --line-buffered toe (paragraaf 5.4). |
"tail -n +5 toont de laatste vijf regels." |
Het toont alles van regel 5 tot het einde. Zonder plus is het een aantal; met plus een beginpunt. |
"-5 en -n 5 zijn hetzelfde." |
Ze geven hetzelfde resultaat, maar -5 is de verouderde vorm, en -c 5 ernaast betekent bytes. |
"head en tail zijn als paar geschreven." |
head kwam uit 1BSD in 1978, tail uit Version 7 Unix in 1979. Verschillende herkomst, een jaar ertussen. |
"Doorsturen naar head geeft een Broken pipe-fout." |
Het geeft SIGPIPE, afsluitcode 141, en dat is het ontwerp: zo krijgt de producent te horen dat hij kan stoppen. |
"tail -f polt het bestand een keer per seconde." |
Niet op Linux. Het gebruikt inotify en reageert direct. -s telt alleen op netwerkbestandssystemen. |
"tail -f op een bestand dat nog niet bestaat faalt gewoon." |
Een kale -f wel. -F bevat --retry en wacht tot het bestand verschijnt. |
"Een kale -f breekt altijd bij rotatie." |
Niet bij copytruncate, waar de descriptor geldig blijft. Hij drukt "file truncated" af en gaat door (paragraaf 6.2). |
"tail -n 1 geeft altijd een complete regel." |
Heeft het bestand geen laatste regeleinde, dan heeft de uitvoer dat ook niet. Het kopieert wat er staat. |
"Doorsturen naar head is in een script altijd veilig." |
Onder set -o pipefail meldt de pipeline 141, en set -e breekt het script dan af. Zeer gangbare standaardregels, volledig stille mislukking (paragraaf 6.5). |
"-F is een GNU-uitbreiding waar ik niet op kan bouwen." |
BusyBox heeft hem ook. Wat BusyBox niet heeft zijn --pid, --retry of enige lange optie (paragraaf 3). |
"tail -c 100 geeft me de laatste 100 tekens." |
Bytes, geen tekens. Op UTF-8-tekst kan het midden in een teken beginnen en uitvoer opleveren die geen geldige UTF-8 is (paragraaf 4.4). |
"tail -f /dev/null in een Dockerfile heeft met loggen te maken." |
Het is een "houd deze container in leven"-hack. Het betekent ook dat de container geen echt hoofdproces heeft om te bewaken (paragraaf 7.6). |
"Ik kan tail -f in een script zetten en het loopt af." |
Hij komt nooit terug. Begrens hem met --pid of timeout. |
9.2 Andere valkuilen om te vermijden
- Een symlink volgen met
-f. Laat de link naar een nieuw bestand wijzen en-fblijft voorgoed op het oude doel.-Fmerkt het wel, maar door periodiek te controleren en niet direct, dus verwacht een paar seconden stilte voordat hijtail: 'link.log' has been replaced; following new filezegt. Goed om te weten voordat je concludeert dat het niet werkt. -qvergeten bij het doorsturen van meerdere bestanden. De==>-kopregels worden invoer voor wat er volgt, en ze bederven stilletjes een telling of een sortering.headgebruiken om een binair bestand te bemonsteren zonder-c. Regelgebaseerde tools op binaire gegevens kunnen escapereeksen naar de terminal sturen en je prompt verhaspelen. Gebruik-cen stuur door naarxxd.- Aannemen dat de laatste regel compleet is. Lees de laatste regel van een log dat op dit moment geschreven wordt en je vangt hem misschien half af. Dit is een echte bron van ontleedfouten in bewakingsscripts.
- Een log bekijken dat je niet mag lezen.
tail -Fblijft het proberen in plaats van het duidelijk te zeggen. Controleer eerst metls -lals er niets verschijnt. tail -flaten staan op een log dat verwijderd is. De descriptor vasthouden houdt de inode in leven, dus de ruimte komt niet vrij. Gemeten met een bestand van 200 MB dat onder een draaiende tail verwijderd werd: de vrije ruimte bewoog niet bijrm, en alle 200 MB kwam terug op het moment dattailstopte. Dit is de klassieke reden datdfenduhet oneens zijn, en waarom een volle schijf zichzelf soms leegt als je een vergeten terminal sluit.- Vergeten dat logbestanden geheimen bevatten. Sessietokens, API-sleutels, wachtwoordherstellinks en persoonsgegevens komen allemaal in toegangslogs terecht. Een
tail -fop een gedeeld scherm, in een opgenomen gesprek, of geplakt in een ticket lekt alles wat voorbijkwam. - Een trage afnemer de producent laten afremmen. Een pipe heeft een vaste buffer. Kan het commando na
tail -fhet niet bijhouden, dan loopt de pipe vol en blokkeren schrijfacties, dus een zwaar filter verderop kan het ding vertragen waar je naar kijkt. Houd het filter goedkoop en doe het dure werk later. tailgebruiken voor een "willekeurige" steekproef. Het einde van een log is de meest recente data, niet een representatieve. Gebruik voor steekproevenshuf -n.tail | headde trage kant op ketenen.head -n 1000000 | tail -n 10leest een miljoen regels;tail -n +999991 | head -n 10niet.- Verwachten dat
-fje laat zien wat je gemist hebt. Hij begint bij het huidige einde plus de laatste-nregels. Alles wat daarvoor geschreven is, verder terug, wordt niet getoond. tail -Fover SSH draaien als bewakingsstrategie. Het werkt voor een server en een middag. Het is geen logging-infrastructuur.
10. Samenvatting
head en tail horen bij de eerste commando's die iemand leert en bij de laatste waarvan iemand de handleiding leest. De delen die het weten waard zijn: het plusteken, de letter F, en de buffer midden in je pipeline.
- Beide staan standaard op tien regels.
-nverandert het aantal,-cschakelt over naar bytes, en beide lezen standaardinvoer als je geen bestand meegeeft. - Het teken verandert de vraag.
tail -n 3is "de laatste drie";tail -n +3is "van regel 3 tot het einde".head -n -3is "alles behalve de laatste drie". tail -n +2laat een kopregel vallen.head -n -1laat een afsluitende samenvatting vallen. Die twee dekken het meeste waarvoor mensensedschrijven.tail -fsterft stilletjes wanneer een log roteert. Hij volgt de bestandsdescriptor en kijkt dus naar het hernoemde oude bestand.-Fvolgt de naam, zegt wat er gebeurde, en opent opnieuw.-Fis precies--follow=name --retry, wat ook betekent dat hij wacht op een bestand dat nog niet bestaat.- De uitzondering: bij
copytruncate-rotatie blijft de descriptor geldig, dus een kale-fredt het en drukt "file truncated" af. Gebruik-Fen je hoeft nooit te weten welke stijl je server gebruikt. tail -f | greplijkt kapot omdatgrepbuffert. Voeg--line-bufferedtoe, ofstdbuf -oLvoor al het andere in de pipeline.tailspringt: 0,00 s om het einde van een bestand van 1,1 GB te lezen.cat bestand | tailleest alles, omdat je in een pipe niet kunt springen. Geef de bestandsnaam mee.headdie een pipe sluit stuurt SIGPIPE naar de producent, die met 141 stopt. Dat is watyes | headlaat stoppen in plaats van eeuwig door te gaan.- Op Linux gebruikt
tail -finotify en reageert direct.-sis een optie uit het polltijdperk die alleen op netwerkbestandssystemen telt. --pid=$!laat een follow eindigen wanneer de klus die hij volgt eindigt. Zet nooit een kaletail -fin een script.- Meerdere bestanden krijgen
==>-kopregels, ook tijdens het volgen.-qhaalt ze weg,-vdwingt ze af. - Een bestand zonder laatste regeleinde levert uitvoer zonder laatste regeleinde.
tailkopieert wat er staat. headkomt uit 1BSD in 1978 entailuit Version 7 Unix in 1979. Schrijf-n 5en niet-5: het kale getal dateert van voor POSIX.- De sprong is zichtbaar in vier systeemaanroepen:
SEEK_ENDvoor de grootte,SEEK_SETnaar een blok voor het einde, en een leesactie van 2751 bytes van de 1,3 MB. Via een pipe zijn er helemaal geen sprongen, dus valttailterug op een rollende buffer en leest hij elke byte. - Een bestandsnaam is niet een bestand. Hernoemen verplaatst een regel in een map; de inode en elke geopende descriptor blijven onaangeroerd. Precies daarom blijft
tail -feen log lezen waar niets meer naartoe geschreven wordt. - Onder
set -o pipefaillaat| headeen pipeline 141 melden, enset -ebreekt het script dan af. Gebruik|| truewaar de SIGPIPE onschuldig is. -Fwerkt ook op BusyBox.--pid,--retryen de lange opties zijn alleen GNU, en dat telt binnen minimale container-images.-ctelt bytes en kan dus een UTF-8-teken splitsen en ongeldige uitvoer geven. Tel regels bij tekst met accenten.- Gebruik in containers
docker logs -fofkubectl logs -f. Entail -f /dev/nullis geen loggen: het is een keep-alive-hack die verbergt dat de container geen echt hoofdproces heeft. - Voor lezen zowel als kijken verslaat
less +Fdetail -f. Voor het journaaljournalctl -f.
Dit is het overzicht dat het bewaren waard is:
THE BASICS (both default to 10 lines)
head -n 20 FILE first 20 lines
tail -n 20 FILE last 20 lines
head -c 512 FILE first 512 bytes (K=1024, kB=1000)
tail -c 100 FILE last 100 bytes
THE SIGN CHANGES THE QUESTION
tail -n +2 FILE from line 2 on <- drops a header row
head -n -1 FILE all but the last line <- drops a summary row
tail -c +2 FILE from byte 2 on <- strips a leading byte
head -c -4 FILE all but the last 4 bytes
A RANGE IN THE MIDDLE (lines 8-12)
tail -n +8 FILE | head -n 5 preferred: stops reading early
head -n 12 FILE | tail -n 5 same answer, reads more
FOLLOWING A LOG
tail -F FILE USE THIS. = --follow=name --retry
tail -f FILE follows the descriptor: DIES on rotation
tail -n 100 -F FILE start with real context
tail -F a.log b.log several at once, with ==> headers
tail -F FILE --pid=$! exit when the writer exits
timeout 300 tail -F FILE exit after 5 minutes
less +F FILE follow, Ctrl-C to scroll back, F to resume
UNBUFFERING A FOLLOW PIPELINE (or nothing appears)
tail -F log | grep --line-buffered ERROR
tail -F log | sed -u 's/x/y/'
tail -F log | awk '/E/ { print; fflush() }'
tail -F log | stdbuf -oL ANY-OTHER-COMMAND
MULTIPLE FILES
tail -q -n 1 *.log no ==> headers (use when piping onward)
tail -v -n 1 one.log force a header on a single file
IN CONTAINERS (a good image logs to stdout, so there is no file)
docker logs -f --tail 100 NAME
docker compose logs -f SERVICE
kubectl logs -f deploy/NAME --tail=100
tail -f /dev/null NOT logging: a keep-alive hack. Prefer sleep infinity.
PORTABILITY
-n -c -q -v -f -F -s -n +N -n -N GNU and BusyBox both
--pid --retry --follow=name GNU only (absent in BusyBox)
SCRIPTS
set -o pipefail + | head -> exit 141, and set -e aborts. Use || true.
tail -c N on UTF-8 text can split a character. Count lines instead.
tail SEEKS to the end: pass the filename, never cat into it
exit 141 from a producer = SIGPIPE = head closed the pipe. Normal.
-5 is the obsolete pre-POSIX form. Write -n 5.
Neem je een gewoonte mee uit dit artikel, maak er dan de hoofdletter F van. De meeste mensen ontdekken het verschil op de harde manier, om twee uur 's nachts, starend naar een log dat om middernacht ophield iets te zeggen, zich afvragend waarom het probleem stil werd. En blijft een server symptomen geven die niemand kan aanwijzen, dan staat het antwoord meestal al in een log dat niemand goed volgde.


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












