Aanwijzen van de fout: compiler-stijl diagnostiek in uutils coreutils
Al 50 jaar lang zijn Coreutils niet gestopt met evolueren. Nu zetten we die innovatie verder door opnieuw te kijken naar de manier waarop fouten worden gerapporteerd.
Unix-tools rapporteren fouten meestal als een enkele regel op stderr. Die regel vertelt wat er misging, maar niet waar. Voor de meeste commando's is er geen andere plek om naar te wijzen, maar een aantal commando's accepteren argumenten die in feite kleine talen zijn: een test-expressie, een chmod-modus, een sorteersleutel of een tr-set. Wanneer het parsen hiervan mislukt, wil je eigenlijk weten bij welk argument, of bij welk teken daarvan, de parser is gestruikeld.
rustc beantwoordt die vraag al jaren met een caret (^), en de bibliotheek ariadne biedt dezelfde weergave als dependency aan. Het idee om dit naar een command-line tool te brengen komt voort uit uutils awk, dat fouten in een awk-programma al op deze manier rapporteert. Coreutils-argumenten zijn weliswaar kleinere talen, maar ze worden op dezelfde manier geparsed. Vanaf versie 0.11.0 maakt coreutils hier gebruik van. Wanneer stderr een terminal is, wordt een parse-fout geprint als een rapport: de argumenten worden als een bronregel teruggegeven, een caret markeert de boosdoener, en een help-regel legt de syntaxis uit wanneer daar nuttige informatie over beschikbaar is.
Hoe het eruitziet
tr
Beginnen met tr, waarvan het GNU-bericht ervan uitgaat dat je al weet wat een collating sequence is.
Voor:
$ tr 'qw[y-b]' x
tr: range-endpoints of 'y-b' are in reverse collating sequence order
Na:
$ tr 'qw[y-b]' x
tr: range-endpoints of 'y-b' are in reverse collating sequence order
╭─[ tr:1:7 ]
│
1 │ tr qw[y-b] x
│ ─┬─
│ ╰─── bedoelde u 'b-y'?
│
│ Help: een bereik loopt van het lagere teken naar het hogere teken, zoals in a-z
───╯
cut
Een cut-lijst kan lang zijn, met daarin één foutief item.
Voor:
$ cut -f 1,4-2,9-12 notes.txt
cut: invalid decreasing range
Try 'cut --help' for more information.
Na:
$ cut -f 1,4-2,9-12 notes.txt
cut: invalid decreasing range
╭─[ cut:1:10 ]
│
1 │ cut -f 1,4-2,9-12 notes.txt
│ ─┬─
│ ╰─── dit bereik eindigt voordat het begint
│
│ Help: een lijst is N, N-M, N- of -M, gescheiden door komma's, zoals in -f1,4-6,9-
───╯
Try 'cut --help' for more information.
chmod
De caret hoeft niet een heel argument te beslaan. Hij kan ook op één enkel teken landen.
Voor:
$ chmod 'g+rw?x' notes.txt
chmod: invalid operator (expected +, -, or =, but found ?)
Na:
$ chmod 'g+rw?x' notes.txt
chmod: invalid operator (expected +, -, or =, but found ?)
╭─[ chmod:1:5 ]
│
1 │ g+rw?x notes.txt
│ ─
│
│ Help: een modus is ofwel octaal, zoals in 644, of clausules zoals u+rwx,go-w
───╯
sort
Sorteersleutels zijn kort genoeg waardoor een verdwaald teken gemakkelijk over het hoofd wordt gezien.
Voor:
$ sort -k2.3x notes.txt
sort: stray character in field spec: invalid field specification '2.3x'
Na:
$ sort -k2.3x notes.txt
sort: stray character in field spec: invalid field specification '2.3x'
╭─[ sort:1:11 ]
│
1 │ sort -k2.3x notes.txt
│ ─
│
│ Help: een sleutel is FIELD[.CHAR][OPTS][,FIELD[.CHAR][OPTS]], zoals in -k2.3,4nr
───╯
env -S
env -S neemt een volledige opdrachtregel en splitst deze op de manier waarop een shell dat zou doen. Het oude bericht kon alleen het foutieve fragment citeren. Let op dat de string spaties bevat, dus deze wordt geciteerd weergegeven, waarbij de caret nog steeds binnen de aanhalingstekens landt.
Voor:
$ env -S 'echo ${1FOO}'
env: only ${VARNAME} expansion is supported, error at: ${1FOO}
Na:
$ env -S 'echo ${1FOO}'
env: only ${VARNAME} expansion is supported, error at: ${1FOO}
╭─[ env:1:14 ]
│
1 │ env -S 'echo ${1FOO}'
│ ─┬─
│ ╰─── een variabelenaam kan niet beginnen met een cijfer
│
│ Help: alleen $NAME en ${NAME} worden uitgebreid; andere shell-vormen worden niet ondersteund
───╯
test
test bouwt zijn expressie op uit afzonderlijke argumenten. Het rapport toont de expressie apart, zonder het woord test ervoor, en markeert het argument dat de fout veroorzaakte.
Voor:
$ test 7 -eq zap
test: invalid integer 'zap'
Na:
$ test 7 -eq zap
test: invalid integer 'zap'
╭─[ test:1:7 ]
│
1 │ 7 -eq zap
│ ───
│
│ Help: -eq, -ne, -lt, -le, -gt and -ge vergelijken integers; gebruik =, !=, < of > om strings te vergelijken
│ -eq gelijk, -ne niet gelijk, -lt kleiner dan, -le kleiner dan of gelijk, -gt groter dan, -ge groter dan of gelijk
───╯
head
Een SIZE is een getal gevolgd door een eenheid. Het rapport geeft aan welk deel werd afgewezen.
Voor:
$ head -c 1fb notes.txt
head: invalid number of bytes: '1fb'
Na:
$ head -c 1fb notes.txt
head: invalid number of bytes: '1fb'
╭─[ head:1:10 ]
│
1 │ head -c 1fb notes.txt
│ ─┬
│ ╰── geen bekende eenheid
│
│ Help: een grootte is een getal en een optionele eenheid: K, M, G enz. voor 1024, KB, MB, GB voor 1000
───╯
Opmerking: Eén parser handelt elke SIZE in de suite af, dus hetzelfde rapport verschijnt voor tail -c, truncate -s, split -b, shred -s, od -N, sort -S, de blokgroottes van du -B, df -B en ls --block-size, en de drempelwaarde van du -t.
numfmt
numfmt --format is een printf-stijl formaat dat precies één conversie toestaat. Het oude bericht herhaalde enkel de regel. De annotatie benoemt de conversie die je daadwerkelijk hebt geschreven.
Voor:
$ numfmt --format=%q 1000
numfmt: invalid format '%q', directive must be %[0]['][-][N][.][N]f
Na:
$ numfmt --format=%q 1000
numfmt: invalid format '%q', directive must be %[0]['][-][N][.][N]f
╭─[ numfmt:1:18 ]
│
1 │ numfmt --format=%q 1000
│ ┬
│ ╰── f is de enige conversie die numfmt heeft; %d, %e, %g en de andere C-conversies worden niet geaccepteerd
│
│ Help: een formaat is [PREFIX]%[0]['][-][WIDTH][.PRECISION]f[SUFFIX], zoals in "%'-10.2f"
───╯
csplit
csplit-patronen bevatten regexes, en de regex-engine weet al op welk teken hij vastliep. Voorheen werd die positie simpelweg weggegooid.
Voor:
$ csplit notes.txt '/a{2,1}/'
csplit: '/a{2,1}/': invalid pattern
Na:
$ csplit notes.txt '/a{2,1}/'
csplit: '/a{2,1}/': invalid pattern
╭─[ csplit:1:20 ]
│
1 │ csplit notes.txt /a{2,1}/
│ ──┬──
│ ╰──── ongeldig bereik voor herhalingsaantal, het startpunt moet <= het eindpunt zijn
│
│ Help: een patroon is een regelnummer N, /REGEXP/[OFFSET] of %REGEXP%[OFFSET], elk optioneel gevolgd door {N} of {*}
───╯
Opmerking: Dat label komt rechtstreeks uit de regex-engine en is niet vertaald, aangezien dit de enige plek is waar deze bewoording voorkomt.
Waar het van toepassing is
In versie 0.11.0 maken 28 utilities gebruik van deze functionaliteit.
| Hulpprogramma | Waar de caret naar wijst |
|---|---|
test | het argument dat de expressie deed mislukken |
expr | het argument dat de expressie deed mislukken |
chmod | de foutieve clausule (of het teken) van een ongeldige symbolische of octale modus |
mkdir | het foutieve deel van de modus gegeven aan -m/--mode |
mkfifo | het foutieve deel van de modus gegeven aan -m/--mode |
mknod | het foutieve deel van de modus gegeven aan -m/--mode |
install | het foutieve deel van de modus gegeven aan -m/--mode |
tr | het deel van een set dat foutief is (foutieve klasse, omgekeerd bereik, foutief herhalingsaantal, …) |
sort | het foutieve deel van een -k/--key of veldspecificatie, of van de SIZE gegeven aan -S |
numfmt | het foutieve deel van een --format of --field specificatie, de waarde gegeven aan --from, --to, --from-unit, --to-unit, --padding of --header, of het input-getal zelf |
printf | de foutieve conversie of escape in de format-string |
seq | de foutieve conversie in het formaat gegeven aan -f/--format |
stat | de foutieve directive van een -c/--format of --printf formaat |
env | het foutieve deel van een -S/--split-string string |
dd | de foutieve sleutel, waarde of flag van een KEY=VALUE operand |
join | het foutieve veld van het outputformaat gegeven aan -o |
cut | het foutieve bereik in de lijst gegeven aan -b, -c, -f of -F |
csplit | het foutieve patroon-operand, het teken van de regex dat brak, of het formaat gegeven aan -b/-n |
split | het foutieve deel van de SIZE gegeven aan -b, -C of -l |
shred | het foutieve deel van de SIZE gegeven aan -s/--size |
head | het foutieve deel van de SIZE gegeven aan -c of -n |
tail | het foutieve deel van de SIZE gegeven aan -c of -n |
truncate | het foutieve deel van de SIZE gegeven aan -s/--size |
od | het foutieve deel van de SIZE gegeven aan -j, -N, -S of -w |
du | het foutieve deel van de SIZE gegeven aan -B/--block-size of -t/--threshold |
df | het foutieve deel van de SIZE gegeven aan -B/--block-size |
ls | het foutieve deel van de SIZE gegeven aan --block-size (ook dir en vdir) |
stdbuf | het foutieve deel van de buffering-modus gegeven aan -i, -o of -e |
Compatibiliteit staat voorop
Omdat het een directe vervanging (drop-in replacement) voor GNU coreutils moet zijn, is dit strikt een interactieve extra:
- Rapporten worden alleen weergegeven wanneer
stderreen terminal is. In een script, een pipe of een testsuite blijft elk hulpprogramma precies het eenvoudige bericht van één regel printen (zoals getoond bij "Voor" in de voorbeelden), zodat alles watstderrfiltert metgrepblijft werken. - Exit-codes zijn ongewijzigd.
- Kleuren worden alleen gebruikt op een terminal en houden rekening met
NO_COLOR. - Net als de rest van uutils zijn de berichten, labels en help-regels gelokaliseerd; vertalingen worden beheerd via Weblate.
- Het kan worden uitgeschakeld tijdens het compileren om ruimte te besparen: de weergave zit achter de
feat_diagnosticscargo-feature (standaard aan). Bouwen zonder deze feature verwijdert deariadnedependency, terwijl elk hulpprogramma zijn eenvoudige berichten behoudt.
In- en uitschakelen
Standaard wordt de weergave bepaald door of stderr een terminal is. De omgevingsvariabele UUTILS_DIAG overschrijft dit:
always: tekent het rapport altijd, zelfs naar een bestand of pipe, en behoudt nooit de eenvoudige regel.never: behoudt altijd de eenvoudige regel, zelfs op een terminal.auto(of ongestelde variabele): beslist op basis vanstderrzoals voorheen.
Een niet-herkende waarde wordt bewust niet als fout gezien. Dit is het soort variabele dat mensen één keer exporteren in een shell-profiel en vervolgens vergeten; een typefout hierin mag niet ervoor zorgen dat een hulpprogramma faalt.
Er is geen command-line flag voor deze functie. De hulpprogramma's die deze het hardst nodig zouden hebben, kunnen deze niet hebben: in test, printf en expr zou een nieuwe optie illegaal zijn of ambigu met de operandens zelf.
Om een rapport uit een script of een CI-log te krijgen (bijvoorbeeld om in een bugrapport te plakken):
$ UUTILS_DIAG=always sort -k2.3x notes.txt 2> parse.log
$ cat parse.log
sort: stray character in field spec: invalid field specification '2.3x'
╭─[ sort:1:11 ]
│
1 │ sort -k2.3x notes.txt
│ ─
│
│ Help: a key is FIELD[.CHAR][OPTS][,FIELD[.CHAR][OPTS]], as in -k2.3,4nr
───╯
Kleuren worden apart bepaald, nog steeds door de terminal: een rapport dat geforceerd naar een bestand wordt geschreven, wordt zonder kleuren geschreven, zodat er geen escape-sequenties verwijderd hoeven worden. NO_COLOR grijpt in op een terminal, waar het rapport nog steeds wordt getekend, maar in platte tekst.
De twee trucs die mensen gebruikten voordat de variabele bestond, werken nog steeds. Het sturen van stderr naar iets dat geen terminal is, geeft de eenvoudige regel:
$ sort -k2.3x notes.txt 2>&1 | cat
sort: stray character in field spec: invalid field specification '2.3x'
En het geven van een pty aan een commando (bijvoorbeeld via script -qec of unbuffer van expect) geeft het rapport terug.
Wat volgt er nu
Coreutils is waar dit begint, niet waar het stopt. De weergave bevindt zich in uucore::diagnostics, waar andere uutils-projecten al van afhankelijk zijn. Het implementeren ervan is nu vooral een kwestie van het doorgeven van een span van de parse-fout. findutils en sed worden momenteel gekoppeld; een find-expressie en een sed-script zijn precies het soort kleine talen waarbij een caret helpt. grep, awk en de rest hebben dezelfde regexes en format-strings om naar te wijzen.
Als een hulpprogramma dat u gebruikt nog steeds een onbehulpzame regel van één regel print, zijn patches welkom!
Groetjes,