GNU/Linux: redirezione avanzata in bash

Qualche tempo fa abbiamo fatto una puntata sulla redirezione, e avevamo visto come mandare l’output di un comando dentro un file con il maggiore e con il doppio maggiore. Quella però era solo la punta dell’iceberg, e per brevità avevamo rimandato un bel po’ di cose. Oggi andiamo fino in fondo: gli errori, il buco nero del sistema, l’input, il here-doc, il here-string e il modo giusto di stampare gli errori dai nostri script. Alla fine sarete dei veri maestri della redirezione.

I tre canali

Per capire tutto quello che vediamo oggi bisogna partire da un concetto. Ogni programma che lanciamo da terminale nasce con tre canali già aperti, e ognuno ha un numero:

  • 0 è lo standard input, da dove il programma legge: normalmente la tastiera;
  • 1 è lo standard output, dove il programma scrive i risultati: normalmente lo schermo;
  • 2 è lo standard error, dove il programma scrive gli errori: e anche questo normalmente è lo schermo.

Output ed errori sullo schermo sembrano la stessa cosa, ma sono due canali diversi. Con gli strumenti di redirezione possiamo cambiare questo stato di cose: leggere l’input da qualcosa che non è la tastiera, e mandare output ed errori da qualche parte che non è lo schermo.

Facciamo la prova con ls, chiedendogli due file: uno che esiste e uno che non esiste.

ls /etc/passwd /nonesiste

L’output è qualcosa del genere:

ls: impossibile accedere a '/nonesiste': File o directory non
esistente
/etc/passwd

Qui è facile distinguere l’errore dal risultato buono, ma con un output lungo avere tutto mescolato sullo schermo fa confusione. Come abbiamo visto la volta scorsa, possiamo salvare il risultato in un file:

ls /etc/passwd /nonesiste > risultato.txt
cat risultato.txt

L’errore continua a comparire sullo schermo, e nel file c’è solo la riga buona. Il motivo è che il maggiore da solo vuol dire “1 maggiore”: redirige lo standard output, e l’errore viaggia su un altro canale.

Redirigere gli errori: 2>

Per redirigere gli errori basta mettere il numero del canale davanti al maggiore:

ls /etc/passwd /nonesiste 2> errori.txt
cat errori.txt

Adesso sullo schermo resta solo la riga buona, e l’errore è finito nel file, esattamente dove ce lo aspettavamo. Come per l’output, il doppio maggiore 2>> aggiunge in fondo al file invece di sovrascriverlo: comodo per tenere il log degli errori di uno script che gira tutte le notti.

Si possono anche fare tutte e due le cose insieme, in una riga sola, con due redirezioni una dopo l’altra: l’output in un file e gli errori in un altro.

ls /etc/passwd /nonesiste > risultato.txt 2> errori.txt
cat risultato.txt
cat errori.txt

Sullo schermo non compare più niente, perché è finito tutto nei file, ognuno al suo posto.

Tutto nello stesso file: 2>&1

E se volessimo output ed errori tutti insieme nello stesso file? Qui c’è una sintassi che a prima vista fa un po’ paura:

ls /etc/passwd /nonesiste > tutto.txt 2>&1
cat tutto.txt

Nel file c’è tutto. Leggiamola con calma: “> tutto.txt” manda l’1 nel file; “2>&1” vuol dire “manda il 2 dove sta andando l’1 in questo momento”. La e commerciale serve a dire che 1 è un canale, e non un file che si chiama “1”.

Ed ecco il trabocchetto, dove si sbaglia sempre: l’ordine conta. Se scriviamo le due redirezioni al contrario:

ls /etc/passwd /nonesiste 2>&1 > tutto.txt

l’errore torna sullo schermo. Nel momento in cui la shell legge 2>&1, l’1 sta ancora andando sullo schermo, e quindi il 2 va sullo schermo; solo dopo l’1 viene spostato nel file. La regola pratica è: prima il file, poi 2>&1.

Se usate bash c’è una scorciatoia che fa la stessa cosa della forma corretta:

ls /etc/passwd /nonesiste &> tutto.txt

Attenzione però: &> è una cosa di bash. In uno script con la shebang /bin/sh potrebbe non funzionare, mentre la forma estesa funziona dappertutto. Il risultato in ogni caso è lo stesso: standard output e standard error insieme dentro tutto.txt.

Il buco nero: /dev/null

E se l’output o gli errori di un comando non ci interessano proprio? Salvarli in un file per cancellarlo subito dopo non ha senso. Il sistema ci mette a disposizione un file molto speciale, /dev/null, che abbiamo già incontrato nella puntata sul file system: tutto quello che ci scriviamo dentro sparisce, come in un piccolo buco nero tascabile.

Un classico è find. Se cerchiamo un file in tutto il disco da utente normale, find prova a entrare in un sacco di cartelle in cui non abbiamo il permesso di entrare, e lo schermo si riempie di “permesso negato”:

find / -name passwd

In mezzo a tutto quel caos il file che cerchiamo rischia di passare inosservato (si ferma con Ctrl+C). Allora buttiamo gli errori nel buco nero:

find / -name passwd 2> /dev/null

Ecco che restano soltanto le righe buone.

Redirigere L’input: <

Per completezza vediamo anche la redirezione dell’input, cioè quando sostituiamo lo standard input, normalmente la tastiera, con un’altra cosa, per esempio un file. Diciamo che voglio contare le righe di /etc/passwd. Di solito si fa così:

cat /etc/passwd | wc -l

Funziona, ma è un po’ macchinoso. Con il minore diciamo a wc di leggere lo standard input direttamente dal file:

wc -l < /etc/passwd

Il risultato è lo stesso, ed è anche più chiaro da leggere: si vede subito da dove arrivano i dati.

Il here-doc: <<

Non si può parlare di redirezione senza il here-doc, il “documento qui”, che serve a passare a un comando un input su più righe. Si scrive il doppio minore seguito da una parola a piacere, che fa da segnale di fine e dice a bash quando il testo è finito:

cat << FINE
Questo è un testo
su più righe.
Il tuo utente è $USER
FINE

cat stampa tutto il blocco, e le variabili vengono espanse: $USER diventa il nostro nome utente. La parola di fine deve stare da sola sulla sua riga, senza spazi davanti, altrimenti la shell non la riconosce: è un errore che si fa spesso. Se non volete che le variabili vengano espanse, mettete la parola di fine fra apici: << ‘FINE’.

A riga di comando non è utilissimo, ma negli script diventa comodissimo. Pensate di dover scrivere un file di configurazione: con un echo per ogni riga, ognuno rediretto nel file, lo script diventa lungo e difficile da leggere. Molto meglio un cat con un here-doc. Scriviamo uno script che si chiama heredoc:

#!/bin/bash
cat > config.txt << FINE
nome=prova
porta=8080
FINE

Qui combiniamo due redirezioni: il blocco di testo entra in cat, e quello che esce da cat va nel file. Diamo i permessi di esecuzione e lanciamolo:

chmod +x heredoc
./heredoc
cat config.txt

Il file config.txt è stato creato e contiene esattamente il nostro here-doc.

Il here-string: <<<

La sorella piccola del here-doc è il here-string: si scrive con tre minori e passa come standard input una sola stringa, una frase semplice.

tr a-z A-Z <<< "ciao mondo"

L’output è:

CIAO MONDO

Il here-string ha passato la stringa a tr come input, senza bisogno di echo e pipe. Negli script si usa soprattutto con le variabili, e rende la logica un po’ più leggibile:

wc -w <<< "$frase"

Gli errori dei nostri script: >&2

Chiudiamo con un aspetto molto importante: come mandare sullo standard error i messaggi di errore dei nostri script. Di solito li stampiamo con echo, che scrive sullo standard output. Ma gli errori dovrebbero andare sullo standard error, così chi usa lo script può separarli. Basta redirigere l’echo verso il canale 2. Scriviamo lo script script-errori, che controlla il numero di argomenti:

#!/bin/bash
if [ $# -lt 1 ]; then
    echo "manca l'argomento" >&2
    exit 1
fi

“>&2” vuol dire “manda questo dove va il canale 2”. Proviamolo:

chmod +x script-errori
./script-errori

Senza argomenti si lamenta che manca l’argomento. Adesso buttiamo via lo standard output:

./script-errori > vuoto.txt

L’errore compare lo stesso, perché non passa dallo standard output. Se invece redirigiamo gli errori:

./script-errori 2> script-errori.txt
cat script-errori.txt

sullo schermo non compare niente, e il messaggio sta nel file, proprio dove ce lo aspettavamo. Esattamente come succede con i comandi veri del sistema: è un piccolo dettaglio, ma è quello che distingue uno script fatto bene.

Riassumendo

  • 0, 1 e 2 sono input, output ed errori;
  • 2> manda gli errori in un file, 2>> li aggiunge in fondo;
  • > file 2>&1 (o &> in bash) mette tutto nello stesso file, e l’ordine conta;
  • /dev/null butta via quello che non ci interessa;
  • < legge lo standard input da un file;
  • << e <<< passano testo direttamente, su più righe o in una stringa;
  • >&2 manda sullo standard error i messaggi dei nostri script.

Adesso avete tutti gli strumenti: aprite la vostra Linux box e cominciate a sperimentare con i vostri comandi preferiti, separando l’output dagli errori. Sono cose che si imparano solo con la pratica.

Link e risorse