Skip to main content

Module 2 · Shell & Bash

Environment Variables

  • Lesson 6/15
  • 30 min
  • Difficulty: Basic

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

Objective

Diagnosticare perche' un comando funziona a mano e non in automatico, misurando l'ambiente che un processo riceve davvero invece di dedurlo

Prerequisites

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.

  1. Un alias definito in ~/.bashrc è disponibile dentro uno script? Verifica: metti un alias nel file, poi esegui bash -c 'alias' e guarda se compare.
  2. Se esporti una variabile e poi il figlio la modifica, il genitore vede la modifica?
  3. 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.