Skip to main content

Module 2 · Shell & Bash

Fundamental Commands

  • Lesson 4/15
  • 45 min
  • Difficulty: Basic

This lesson is not available in English yet: you are reading it in Italian.

Objective

Comporre una sequenza di comandi per rispondere a una domanda sui dati, leggere il codice di uscita senza farsi ingannare dalle barre verticali, e stabilire con un comando quale implementazione di un comando di base si sta eseguendo

Prerequisites

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.