Prima di leggere: una domanda che sembra assurda
Esegui ls --version sulla tua macchina e leggi la prima riga. Secondo te che cosa dice?
Scrivi la tua previsione prima di eseguirlo. Se la tua risposta è «GNU coreutils, una qualche
versione», hai dato la risposta che avrei dato io fino a quando non l'ho misurata su questa
macchina, e che era sbagliata.
La domanda non è un gioco di parole. I comandi di base non sono parte del kernel: sono programmi, installati da pacchetti, che una distribuzione può sostituire con altri programmi che accettano le stesse opzioni. Sapere quale stai eseguendo è la differenza fra uno script che funziona su tutti i tuoi server e uno che funziona solo sul tuo portatile.
Il problema: i comandi non sono il sistema
Impari ls, cat, grep, awk, e per un po' tutto
funziona. Poi scrivi uno script che gira bene in locale, lo metti su un server e si comporta in modo
diverso: una colonna in più, un messaggio d'errore con un altro testo, un'opzione che non esiste.
La causa non è il server: è che «il comando ls» non è una cosa sola.
Questa lezione fa due cose insieme. Ti dà la cassetta degli attrezzi minima, cioè i comandi con cui si fa il novanta per cento del lavoro quotidiano e il modo di comporli in sequenza. E ti dà l'abitudine che rende quella cassetta affidabile: prima di scrivere una riga che dipende dal comportamento di un comando, misurare quale comando hai davanti.
Il primo pezzo: la composizione, non i comandi
La parte di Unix che vale la pena imparare non è l'elenco dei comandi, è il modo in cui si collegano. Ogni programma legge da un ingresso, scrive su un'uscita, e segnala con un numero se è andata bene. La barra verticale collega l'uscita di uno all'ingresso del successivo, e da quel momento hai un linguaggio invece di un elenco.
Prendi un file di quattro righe e chiedi chi sta a Roma, in ordine alfabetico. Non esiste un comando che lo faccia: esistono tre comandi che insieme lo fanno, e il valore sta nel saper decidere quali tre.
$ cat utenti.csv
alice,30,roma
bob,25,milano
carla,35,roma
davide,28,torino
$ grep roma utenti.csv | cut -d, -f1 | sort
alice
carla
grep filtra le righe, cut estrae una colonna indicando il separatore con
-d e il campo con -f, sort ordina. Nessuno dei tre sa che
esistono gli altri, e nessuno dei tre tocca il file originale: ciascuno legge e scrive un flusso di
testo. È il motivo per cui questa combinazione si adatta a un problema diverso cambiando un pezzo.
Quando il filtro non basta: awk e sed
Due comandi meritano un posto a sé, perché non filtrano: trasformano. awk guarda ogni
riga come un insieme di campi e ti lascia fare calcoli; sed applica sostituzioni al
testo. Sono piccoli linguaggi, e per il lavoro quotidiano ne serve una fetta minima.
$ awk -F, '{somma+=$2; n++} END {printf "%d righe, media %.2f\n", n, somma/n}' utenti.csv
4 righe, media 29.50
$ sed 's/roma/ROMA/' utenti.csv
alice,30,ROMA
bob,25,milano
carla,35,ROMA
davide,28,torino
In awk, -F, dichiara la virgola come separatore, $2 è il
secondo campo, e il blocco END gira una volta sola alla fine. In sed, il
comando s sostituisce la prima occorrenza per riga, e serve s/.../.../g
per sostituirle tutte. Entrambi scrivono sull'uscita standard e lasciano il file intatto, che è il
comportamento che vuoi mentre stai ancora provando.
Il numero che nessuno guarda
Ogni comando, quando finisce, lascia un numero. Zero significa che è andata bene, qualunque altro
valore significa che non è andata bene, e il significato preciso lo decide il programma. Lo leggi in
$?, e subito dopo il comando, perché il valore successivo lo sovrascrive.
$ grep napoli utenti.csv; echo "exit=$?"
exit=1
$ grep roma utenti.csv > /dev/null; echo "exit=$?"
exit=0
Per grep, uno non è un errore: è «non ho trovato niente». La distinzione fra «non
trovato» e «guasto» è la cosa che rende possibile scrivere automazioni che si accorgono dei
problemi, e ignorarla è il motivo per cui tanti script continuano a girare dopo che un passaggio è
fallito.
Esempio svolto: quale ls sto eseguendo
Il problema: stabilire se i comandi di base su questa macchina sono quelli che mi aspetto, e capire quanto posso fidarmi di un'opzione che userò in uno script destinato ad altri server.
Le informazioni disponibili sono tre, e vanno usate in quest'ordine. La prima è
--version, che dice quale programma sta rispondendo. La seconda è ls -l sul
percorso del comando, che dice se è un file o un collegamento, e dove punta: è la stessa tecnica
della lezione sui collegamenti. La terza è il gestore di pacchetti, che dice chi ha messo lì quel
file.
$ ls --version
ls (uutils coreutils) 0.10.0
$ cp --version
cp (GNU coreutils) 9.7
$ ls -l /usr/bin/ls /usr/bin/cp
lrwxrwxrwx 1 root root 29 Mar 30 2026 /usr/bin/ls -> ../lib/cargo/bin/coreutils/ls
lrwxrwxrwx 1 root root 5 Mar 30 2026 /usr/bin/cp -> gnucp
$ dpkg -S /usr/bin/ls
coreutils-from-uutils: /usr/bin/ls
Il ragionamento che ne esce: su questa macchina ls non è il programma GNU, è una
riscrittura indipendente chiamata uutils, e il collegamento lo dichiara nel percorso di
destinazione. cp invece è ancora GNU, e lo si vede dal fatto che il collegamento punta a
un file il cui nome comincia per gnu. Entrambi i collegamenti appartengono allo stesso
pacchetto, quindi la scelta di quale implementazione usare per quale comando è una decisione della
distribuzione, comando per comando.
Ho contato quanti stanno da una parte e quanti dall'altra, il 2026-10-30, con questi due comandi:
$ find /usr/bin -maxdepth 1 -lname '*cargo/bin/coreutils/*' | wc -l
108
$ find /usr/bin -maxdepth 1 -lname 'gnu*' | wc -l
5
Centootto comandi sono uutils, cinque sono rimasti GNU, e quei cinque sono cp,
mv, rm, df e true. La decisione che prendo per il
mio lavoro: negli script non uso opzioni esotiche dei comandi di base, e quando ne ho bisogno
verifico l'opzione sul sistema di destinazione invece di fidarmi del mio. L'alternativa, cioè
assumere GNU e scoprire la differenza in produzione, è il modo in cui si costruiscono guasti che
sembrano inspiegabili.
La verifica che chiude il ragionamento è un confronto fra due sistemi. Ho eseguito gli stessi comandi dentro un contenitore di Ubuntu 24.04, che tenevo già in locale, e il risultato è questo:
Ubuntu 24.04.4 LTS
ls (GNU coreutils) 9.4
cp (GNU coreutils) 9.4
Ubuntu 26.04
ls (uutils coreutils) 0.10.0
cp (GNU coreutils) 9.7
Due versioni della stessa distribuzione, due risposte diverse alla stessa domanda. L'errore che
avrei potuto commettere, e che avevo commesso per anni, è non porsi la domanda: le pagine di manuale
si leggono senza guardare chi le ha scritte, e su questa macchina anche
man chmod appartiene al pacchetto di uutils, quindi documenta il comportamento di uutils
e non quello di GNU. Lo si verifica con dpkg -S /usr/share/man/man1/chmod.1.gz.
Il caso negativo: due righe che mentono sull'esito
Guarda queste due righe e dimmi, prima di leggere l'output, che cosa stampano e quale codice di uscita lasciano. La prima è una ricerca che non trova nulla seguita da un messaggio; la seconda è una sequenza di comandi in cui il primo fallisce.
$ grep napoli utenti.csv && echo "QUESTO NON SI STAMPA"
$ echo "dopo: exit=$?"
dopo: exit=1
$ cat nonesiste.csv | wc -l; echo "exit=$?"
cat: nonesiste.csv: No such file or directory
0
exit=0
Nel primo caso la congiunzione && esegue il secondo comando solo se il primo
restituisce zero, e grep che non trova restituisce uno: quindi il messaggio non compare
e tutta la riga lascia uno. È il modo in cui una catena di comandi si interrompe a metà senza che
niente lo segnali, ed è un difetto che è già costato tempo su questo progetto: chi scrive
comando | grep -v X ; echo OK vede «OK» anche quando il comando è fallito.
Il secondo caso è peggiore, perché il messaggio d'errore si vede e il codice di uscita dice che è
andato tutto bene. In una sequenza collegata da barre verticali il codice che resta è quello
dell'ultimo comando, e wc -l ha contato zero righe con successo. Una
automazione che controlla $? qui non si accorge di niente.
I due rimedi sono entrambi di una riga. Il primo è set -o pipefail, che fa restituire
alla sequenza il codice del primo comando fallito; il secondo è leggere l'array
PIPESTATUS, che conserva i codici di tutti. Misurati sullo stesso caso:
$ set -o pipefail
$ cat nonesiste.csv | wc -l; echo "exit=$?"
cat: nonesiste.csv: No such file or directory
0
exit=1
$ cat nonesiste.csv | wc -l > /dev/null; echo "PIPESTATUS=${PIPESTATUS[*]}"
PIPESTATUS=1 0
L'errore è plausibile perché il comportamento predefinito è comodo nel lavoro interattivo, dove l'errore lo leggi tu sullo schermo. Diventa un difetto quando la stessa riga finisce in uno script che nessuno guarda. Il sintomo da cui lo riconosci è un'automazione che non ha mai fallito: se un lavoro periodico non ha mai segnalato un problema in sei mesi, la prima ipotesi non è che vada tutto bene, è che non stia controllando niente.
Esercizio 1, esempio con un passaggio mancante
Hai un file di log in cui ogni riga ha la forma data livello messaggio, separata da
spazi. Vuoi contare quante righe per livello, dal più frequente al meno frequente. Due passaggi
sono scritti, i due finali mancano.
$ awk '{print $2}' app.log | ??????? | ???????
Esercizio 2, autonomo: misura la tua cassetta degli attrezzi
Per dieci comandi che usi ogni giorno, stabilisci quale implementazione stai eseguendo e chi l'ha
installata. Poi trovane almeno uno in cui il comando che la shell esegue non è il file che ti
aspettavi, e spiega chi lo sta coprendo. Criterio di successo: per ogni comando hai tre dati
verificati con un comando, cioè il programma che risponde a --version, il percorso
reale del file, e il pacchetto che lo possiede.
Un suggerimento sul terzo punto: type -a nome mostra tutti i modi in
cui la shell può risolvere quel nome, nell'ordine in cui li considera, e include funzioni e alias che
which non vede.
Soluzioni commentate
Esercizio 1. La sequenza completa è
awk '{print $2}' app.log | sort | uniq -c | sort -rn. Il ragionamento, passo per passo:
uniq collassa solo le righe adiacenti uguali, quindi senza
sort prima non conta niente di utile; -c mette il conteggio davanti a ogni
riga; il secondo sort riordina per numero con -n e al rovescio con
-r, perché l'ordinamento di default è alfabetico e metterebbe 10 prima di
9.
Perché le alternative sono peggiori, e non semplicemente diverse. Usare sort -u al posto
di sort | uniq -c elimina i duplicati ma perde il conteggio, che era il
punto dell'esercizio. Usare grep -c una volta per livello funziona se i livelli sono
quattro e li conosci già, e non funziona più il giorno in cui compare un livello nuovo: l'intera
soluzione dipenderebbe da un elenco scritto a mano. Fare tutto in awk con un array
associativo è legittimo e anche più veloce su file enormi, ma qui la sequenza di comandi si legge
riga per riga e si corregge un pezzo alla volta, che per imparare vale più della velocità.
Esercizio 2. La trappola che quasi tutti incontrano è un comando coperto da un alias
o da una funzione di shell, e lì which non aiuta perché guarda il filesystem, non la
shell. Sulla mia sessione ho trovato esattamente questo caso: command -v find rispondeva
find senza percorso, che è il segnale che non si tratta di un file, e
type find ha chiarito che era una funzione definita nel mio ambiente, la quale eseguiva
un programma diverso da GNU findutils. Il file vero stava in /usr/bin/find e rispondeva
find (GNU findutils) 4.10.0.
La conseguenza pratica è la regola che uso da allora: se un comando si comporta in modo che non
riconosco, la prima domanda non è «che opzione sbaglio» ma «che programma sto eseguendo». Si risponde
con type -a, e se la risposta è una funzione o un alias, si aggira con
command davanti o scrivendo il percorso assoluto.
Domande per fissare il concetto
Perché una sequenza collegata da barre verticali restituisce il codice dell'ultimo comando e non del
primo che fallisce? Se un tuo script gira su tre distribuzioni diverse, quali righe diventano
sospette alla luce di quello che hai misurato su ls e cp? E come lo
spiegheresti a un collega che ti dice «ma i comandi di Linux sono sempre gli stessi»?
Che cosa adesso sai fare
Sai comporre una sequenza di comandi per rispondere a una domanda sui dati, e sai scegliere fra
filtrare con grep, estrarre con cut, calcolare con awk e
sostituire con sed. Sai leggere il codice di uscita, sai che per grep uno
significa «non trovato» e non «guasto», e sai che in una sequenza con le barre il codice di default
nasconde i fallimenti intermedi. Sai attivare pipefail e leggere
PIPESTATUS.
E sai fare la domanda che quasi nessuno fa: quale programma sto eseguendo. Sai rispondere con tre
comandi, --version, type -a e dpkg -S, e hai visto che su due
versioni della stessa distribuzione la risposta è diversa. La verifica concreta: esegui
ls --version sul server che gestisci e sul tuo portatile, e confronta.