Prima di leggere: una domanda sulle lezioni precedenti
Nella lezione sui comandi fondamentali hai scoperto che su questa macchina find non era
il programma che credevo: la shell lo risolveva su qualcos'altro. Adesso la domanda: che cosa decide
quale file viene eseguito quando scrivi un nome di comando? E dove sta scritta quella decisione?
Scrivi la risposta prima di proseguire. Se hai detto «la variabile PATH» hai ragione a
metà, e la metà che manca è la parte che ti farà perdere mezza giornata su un lavoro periodico che
non parte.
Il problema: lo script gira a mano e non gira da solo
È la segnalazione più frequente che riceva chi gestisce un server. «Se lancio il comando funziona, se
lo lancia cron non funziona». Oppure: «sul mio portatile va, sul server dà comando non
trovato». Il programma è identico, il file è identico, l'utente è lo stesso. Quello che cambia è
l'ambiente, cioè l'insieme di variabili che ogni processo riceve da chi lo ha avviato.
L'ambiente è invisibile e questo lo rende difficile da sospettare. Non compare nel codice, non compare nei log, e la sua forma dipende da una catena di file di configurazione che la shell legge in un ordine diverso a seconda di come è stata avviata. Qui imparerai a vederlo e a misurarlo, che è l'unica difesa.
Due cose che sembrano una: variabile di shell e variabile d'ambiente
Una variabile di shell esiste solo dentro la shell che l'ha creata. Una variabile d'ambiente viene
copiata nei processi figli quando vengono avviati. La parola che trasforma la prima
nella seconda è export, e la differenza non si vede guardando la variabile: si vede
guardando chi la riceve.
$ SOLO_SHELL=valore1
$ export ESPORTATA=valore2
$ echo "SOLO_SHELL=$SOLO_SHELL ESPORTATA=$ESPORTATA"
SOLO_SHELL=valore1 ESPORTATA=valore2
$ ./figlio.sh
SOLO_SHELL=[<vuota>]
ESPORTATA=[valore2]
Nella shell corrente entrambe hanno un valore. Nel processo figlio solo quella esportata arriva, e l'altra è vuota. Lo script figlio non ha fatto niente di diverso con le due: una gli è stata consegnata e l'altra no.
Esistono due comandi per elencarle, e rispondono a due domande diverse. set senza
argomenti elenca tutte le variabili della shell, esportate o no. env elenca solo
l'ambiente, cioè solo quello che i figli riceveranno. Contati sullo stesso caso di prima:
SOLO_SHELL compare una volta in set e zero volte in env,
ESPORTATA compare in entrambi.
Una variabile per un comando solo
Esiste una terza forma, che è quella che userai di più e che risolve da sola metà dei problemi di configurazione: scrivere l'assegnazione davanti al comando. Vale solo per quel comando, e non lascia traccia nella shell.
$ ESPORTATA=temporanea ./figlio.sh
SOLO_SHELL=[<vuota>]
ESPORTATA=[temporanea]
$ ./figlio.sh
SOLO_SHELL=[<vuota>]
ESPORTATA=[valore2]
La prima invocazione vede il valore temporaneo, la seconda torna al valore di prima. È il modo giusto
di provare una configurazione diversa senza modificare l'ambiente, e il modo giusto di forzare una
lingua o un fuso orario per un solo comando. Il vantaggio su export è che non puoi
dimenticartelo acceso.
Quali file legge la shell, e quando
Qui sta la parte che spiega la segnalazione «funziona a mano e non in automatico». Bash legge file di
configurazione diversi a seconda di come viene avviata, e le tre modalità sono: shell di accesso,
shell interattiva, shell non interattiva. Una shell di accesso legge /etc/profile e poi
~/.profile. Una shell interattiva che non è di accesso legge ~/.bashrc.
Una shell non interattiva, cioè quella che esegue uno script, non legge nessuno dei due.
Quando apri un terminale ottieni una shell interattiva e quindi ~/.bashrc: tutto ciò che
hai messo lì, alias compresi e modifiche al PATH comprese, esiste. Quando
cron esegue uno script, ottieni una shell non interattiva: niente di quello esiste. Il
file e il comando sono identici, l'ambiente no, e questo è l'intero mistero.
C'è una via di mezzo che vale la pena conoscere, perché a volte è la causa di un comportamento
incomprensibile: la variabile BASH_ENV. Se è impostata, una shell non interattiva legge
il file che indica, prima del corpo dello script. Misurata:
$ echo 'echo "BASH_ENV eseguito"' > benv.sh
$ BASH_ENV="$PWD/benv.sh" bash -c 'echo corpo'
BASH_ENV eseguito
corpo
Il confronto: PATH non è un elenco, è una graduatoria
La variabile PATH contiene cartelle separate dai due punti, e la shell le prova
in ordine, fermandosi alla prima che contiene un file eseguibile con quel nome.
Quindi non conta solo che una cartella sia nel PATH: conta in che posizione.
$ which date
/usr/bin/date
$ mkdir -p finto
$ printf '#!/bin/bash\necho "io sono il finto"\n' > finto/date
$ chmod +x finto/date
$ PATH="$PWD/finto:$PATH" bash -c 'command -v date; date'
/home/federicocalo/.../finto/date
io sono il finto
Aggiungendo una cartella in testa, il mio finto date vince sul programma di sistema.
Nessun avviso, nessuna differenza visibile se non il comportamento. La conseguenza di sicurezza è
diretta: una cartella scrivibile da altri messa in testa al PATH è un modo per far
eseguire a te il codice di qualcun altro, e per lo stesso motivo il punto, cioè la cartella corrente,
non va mai nel PATH.
La somiglianza che inganna è fra PATH e le altre variabili. Le altre contengono un
valore che un programma legge; PATH contiene una decisione che la shell prende prima che
il programma esista. Per questo un errore sul PATH non produce un messaggio sul valore
sbagliato: produce il comando non trovato, oppure il comando sbagliato eseguito in silenzio.
Lo stesso vale per un'altra coppia che si confonde: la lingua del sistema non è un dettaglio estetico, perché cambia l'output dei comandi, e l'output dei comandi lo leggono gli script.
$ cat frutta.txt
banana
Ananas
mela
Zucca
$ LC_ALL=C sort frutta.txt
Ananas
Zucca
banana
mela
$ LC_ALL=it_IT.UTF-8 sort frutta.txt
Ananas
banana
mela
Zucca
Lo stesso comando, lo stesso file, due ordinamenti diversi. Con LC_ALL=C l'ordine è
quello dei byte, e tutte le maiuscole vengono prima di tutte le minuscole. Con la lingua italiana è
l'ordine del dizionario, che ignora il caso. Uno script che confronta due elenchi ordinati, o che
cerca una riga in un file ordinato, dà risultati diversi a seconda dell'ambiente di chi lo esegue. La
difesa è dichiarare LC_ALL=C davanti ai comandi il cui output viene elaborato da un
programma, e lasciare la lingua dell'utente a quelli il cui output legge una persona.
Il caso negativo: un PATH che non si può costruire così
Un collega mette questa riga in un file di configurazione di cron e il lavoro periodico
continua a non trovare il suo programma. La riga sembra corretta. Prova a dire perché non funziona.
PATH=$HOME/bin:$PATH
La risposta sta nella pagina di manuale crontab(5), presente sul sistema e consultata il
2026-10-30, che dice esattamente questo: «replacement of variables or tilde(~) expansion, thus lines
like PATH=$HOME/bin:$PATH ... will not work as you might expect». Un file di crontab
non è uno script di shell: le assegnazioni in testa non vengono interpretate da una
shell, quindi $HOME e $PATH restano le quattro o cinque lettere che sono.
Il risultato è un PATH che contiene letteralmente il dollaro e il nome.
L'errore è plausibile perché quella riga è la riga giusta in un file di shell, e il formato dei due
file si somiglia abbastanza da non far sospettare niente. Lo riconosci dal sintomo: comando non
trovato su un comando che esiste, e con il percorso assoluto funziona. La correzione è scrivere il
valore per esteso, senza riferimenti ad altre variabili, oppure, meglio, usare percorsi assoluti nel
comando e non dipendere affatto dal PATH.
Esercizio 1, guidato: tre domande con una risposta misurabile
Per ciascuna domanda, scrivi la previsione e poi il comando con cui la verifichi. La prima ha già il comando, come modello.
- Un alias definito in
~/.bashrcè disponibile dentro uno script? Verifica: metti un alias nel file, poi eseguibash -c 'alias'e guarda se compare. - Se esporti una variabile e poi il figlio la modifica, il genitore vede la modifica?
- Una variabile assegnata davanti a un comando è visibile ai nipoti, cioè ai processi che quel comando avvia a sua volta?
Esercizio 2, autonomo: misura l'ambiente del tuo lavoro periodico
Prendi un lavoro periodico che gira sulla tua macchina, oppure creane uno, e fallo riportare
l'ambiente che riceve davvero invece di indovinarlo. Confronta quel contenuto con l'output di
env dal tuo terminale e scrivi l'elenco delle differenze. Criterio di successo: hai in
mano due file di testo e sai dire quale variabile manca nell'ambiente automatico, perché il tuo
script ne aveva bisogno, e con quale delle due correzioni viste la risolvi.
Il meccanismo per riportarlo è una riga di una riga sola: un lavoro che esegue env e
scrive l'uscita in un file in una cartella su cui hai permesso di scrittura. Attenzione a dove lo
scrivi, alla luce della lezione sulla gerarchia: non in /tmp se vuoi rileggerlo dopo un
riavvio.
Soluzioni commentate
Domanda 1: no. Gli alias di ~/.bashrc non esistono dentro uno script,
per due motivi che si sommano. Il primo è che una shell non interattiva non legge
~/.bashrc. Il secondo è che bash, anche quando gli alias sono definiti, non li espande
nelle shell non interattive se non glielo si chiede. Questo è il motivo per cui un comando che a mano
funziona perché è un alias dà comando non trovato dentro uno script: non è un problema di
PATH, e cercarlo nel PATH fa perdere tempo.
Domanda 2: no, e il motivo è strutturale. L'ambiente viene copiato
nel figlio al momento dell'avvio. Il figlio modifica la propria copia, e quando termina la copia
muore con lui. Non esiste nessun meccanismo per cui un processo figlio cambi una variabile del
genitore, e questo è il motivo per cui uno script che vuole modificare l'ambiente della tua shell
deve essere incluso con source invece che eseguito: source non
avvia un figlio, esegue le righe nella shell corrente.
Domanda 3: sì. Un'assegnazione davanti al comando entra nell'ambiente di quel
comando, e l'ambiente si propaga per copia a tutta la discendenza. È la ragione per cui
LC_ALL=C script.sh funziona anche se è un comando dentro lo script a dover cambiare
comportamento, ed è la ragione per cui quella forma risolve tanti problemi con un solo prefisso.
La distinzione da tenere a mente: si propaga a chi viene avviato dopo, non a chi era già in
esecuzione.
Esercizio 2. La differenza che trovi quasi sempre è su PATH, e la
seconda più frequente è su HOME quando il lavoro gira come utente di servizio. La
correzione che consiglio non è aggiungere la variabile mancante al file del lavoro periodico, ma
togliere la dipendenza: percorsi assoluti nei comandi, e nessuna assunzione sulla cartella corrente.
Il motivo è che la prima correzione va rifatta su ogni macchina e si dimentica, la seconda vale
dappertutto. Se la dipendenza non si può togliere, per esempio perché il programma legge una sua
variabile di configurazione, la forma più solida è dichiararla esplicitamente nel comando del lavoro
periodico, dove chi legge la vede.
Domande per fissare il concetto
Perché source script.sh e ./script.sh non sono la stessa cosa, e in quale
situazione la differenza produce un guasto? Se una cartella scrivibile da tutti finisse in testa al
tuo PATH, quale sequenza di eventi porterebbe all'esecuzione di codice non tuo? E come
spiegheresti a un collega, in due frasi, perché il suo script funziona a mano e non sotto
cron?
Che cosa adesso sai fare
Sai distinguere una variabile di shell da una variabile d'ambiente, e sai verificare la differenza
guardando chi la riceve invece di guardare la variabile. Sai impostare un valore per un solo comando
senza lasciare traccia. Sai quali file legge la shell nelle tre modalità, e sai che uno script non
legge né ~/.bashrc né ~/.profile: è la spiegazione completa di «a mano
funziona».
Sai che PATH è una graduatoria e non un elenco, e che la lingua del sistema cambia
l'ordinamento di sort. La verifica concreta: esegui env nel tuo terminale e
poi fallo eseguire a un lavoro periodico, e confronta i due file. L'elenco delle differenze è la lista
delle cose che il tuo prossimo script non deve dare per scontate.