SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-10-28

Bash: differenza tra $(...) e backticks

Scopri perché $(...) usa una subshell che annulla variabili e cd, perché i backticks gestiscono male l'annidamento e come usare le tecniche alternative per mantenere lo stato.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 4, 2026.

Cosa fa la sostituzione dei comandi in Bash

La sostituzione dei comandi in Bash sostituisce $(command) con il testo che il comando ha scritto sullo standard output. La sintassi precedente con i backtick svolge la stessa funzione. Tutti i comportamenti inattesi dipendono da due fatti: il comando viene eseguito in un processo separato chiamato subshell e tutti i caratteri di nuova riga alla fine dell'output vengono rimossi.

mkdir -p /tmp/subst-demo
cd /tmp/subst-demo
printf 'alpha\nbeta\ngamma\n' > three.txt
count=$(wc -l < three.txt)
echo "$count"
3

Questa è l'intera funzionalità. wc -l ha scritto 3 seguito da una nuova riga; la nuova riga è stata rimossa e count contiene l'unico carattere desiderato. Si noti che wc -l < three.txt stampa un numero senza altro testo perché GNU wc, quando legge dallo standard input, non ha alcun nome di file da stampare. Scrivere invece wc -l three.txt cattura 3 three.txt, che è una stringa diversa e una causa comune di errori aritmetici successivi.

Il resto di questa guida riguarda il comportamento che molti non si aspettano, perché una subshell è un processo separato e un processo separato non può modificare la shell in cui si stanno digitando i comandi.

Usa $() e smetti di usare i backtick

Entrambe le forme sono valide. $() è conforme a POSIX, quindi dash, ash e busybox sh lo supportano pienamente. Non esistono più motivi di portabilità per scrivere i backtick, mentre esistono due ragioni concrete per evitarli.

I backtick non supportano l'annidamento

echo "$(echo "$(echo hi)")"
echo "`echo `echo hi``"
hi
echo hi

La seconda riga ha stampato letteralmente le parole echo hi. La shell cerca in avanti il primo backtick non preceduto da escape, quindi il secondo backtick digitato ha chiuso il primo. Il comando effettivamente eseguito è stato echo senza argomenti, che ha stampato una riga vuota il cui carattere di newline è stato rimosso. Le parole echo hi sono rimaste come testo semplice e la coppia finale di backtick ha eseguito un comando vuoto.

Per annidare i backtick è necessario eseguire l'escape di ogni backtick interno:

echo "`echo \`echo hi\``"
hi

Ogni livello aggiuntivo raddoppia la necessità di escape. $() non richiede nulla di tutto ciò, poiché il parser abbina le parentesi invece di cercare un carattere delimitatore.

I backtick modificano i backslash prima dell'esecuzione del comando

echo "$(echo 'a\\b')"
echo "`echo 'a\\b'`"
a\\b
a\b

Lo stesso comando interno ha prodotto un output differente. All'interno dei backtick, la shell rimuove un livello di escape tramite backslash prima che il testo interno venga analizzato, quindi gli apici singoli non hanno protetto nulla. All'interno di $(), il testo tra le parentesi viene analizzato come uno script ordinario, quindi gli apici singoli si comportano come previsto. Questo problema si manifesta soprattutto nelle one-liner di sed e awk, dove un backslash mancante trasforma un pattern funzionante in uno differente senza generare errori.

Anche la gestione delle virgolette ricomincia da capo all'interno di $(), il che significa che è possibile annidare virgolette doppie senza doverle sottoporre a escape:

path=/etc/nginx/nginx.conf
echo "$(dirname "$path")"
/etc/nginx

L'equivalente con i backtick richiede \" attorno a $path. Ogni escape è un punto in cui è possibile commettere un errore.

Perché cd all'interno di $() non modifica la shell

Perché $(...) crea un processo figlio. La subshell riceve una copia delle variabili e una copia della directory di lavoro. Modifica la propria copia, stampa qualcosa e termina. La copia viene eliminata insieme al processo.

cd /tmp/subst-demo
pwd
target=$(cd /etc && pwd)
echo "$target"
pwd
/tmp/subst-demo
/etc
/tmp/subst-demo

Non si è verificato alcun errore. cd ha funzionato e pwd all'interno della subshell ha effettivamente stampato /etc. Semplicemente, non esiste alcun percorso che permetta a questa modifica di tornare al processo padre, perché le uniche informazioni che una subshell restituisce al processo padre sono lo standard output e uno stato di uscita.

Le assegnazioni di variabili si comportano allo stesso modo:

count=0
msg=$(count=99; echo "inside: $count")
echo "$msg"
echo "outside: $count"
inside: 99
outside: 0

La stessa regola spiega la variante di questo errore che si verifica molto più spesso e che riguarda una pipeline anziché una sostituzione:

n=0
cat three.txt | while read -r line; do n=$((n+1)); done
echo "$n"
0

Ogni fase di una pipeline viene eseguita nella propria subshell, quindi il ciclo while ha incrementato una copia di n e poi è terminato. Sostituire la pipe con un redirect fa eseguire il ciclo nella shell corrente:

n=0
while read -r line; do n=$((n+1)); done < three.txt
echo "$n"
3

Bash può eseguire l'ultima fase di una pipeline nella shell corrente con shopt -s lastpipe, ma solo quando il controllo dei job è disattivato; in una shell interattiva il controllo dei job è attivo per impostazione predefinita. Usare il redirect.

Dove è finito il prompt? Comandi interattivi all'interno di $()

La sostituzione di comando reindirizza lo standard output in una pipe e lascia invariato lo standard input. Un programma che stampa la propria domanda sullo standard output e poi attende una risposta perde la domanda, non l'attesa. Il terminale sembra bloccato.

ask() { printf 'Username: '; read -r u; printf '%s\n' "$u"; }
ask
Username: deploy
deploy

La parola deploy sulla prima riga è ciò che hai digitato. Ora esegui la stessa funzione all'interno di una sostituzione:

v=$(ask)
deploy

Appaiono solo i tasti premuti, visualizzati dal driver del terminale anziché dal programma. Il prompt è finito altrove:

echo "$v"
Username: deploy

Il prompt si trova ora all'interno della variabile, unito alla risposta, perché $() ha catturato tutto ciò che la funzione ha scritto sullo standard output. La tua digitazione ha comunque raggiunto read, poiché lo standard input non è mai stato toccato. Questa è l'esatta firma del bug report che recita "il mio script si blocca e non stampa nulla".

Alcuni strumenti scrivono i prompt sullo standard error o direttamente su /dev/tty, così da sopravvivere a questo processo. Molti altri non lo fanno. Se una funzione deve rimanere all'interno di una sostituzione, invia tu stesso il prompt allo standard error:

ask() { printf 'Username: ' >&2; read -r u; printf '%s\n' "$u"; }
v=$(ask)
echo "$v"
Username: deploy
deploy

Lo standard error non viene catturato, quindi il prompt raggiunge il tuo terminale e solo la risposta finisce in v.

Perché ls ha un aspetto diverso all'interno di $()?

Perché ls invoca isatty sul descrittore di file 1 e modifica il formato di output in base alla risposta. Al prompt, quel descrittore è il tuo terminale, quindi ls distribuisce i nomi su più colonne lungo la riga. All'interno di una sostituzione, si tratta di una pipe, quindi ls passa a un nome per riga.

mkdir -p /tmp/tty-demo
cd /tmp/tty-demo
touch alpha beta delta gamma
echo "$(ls)"
alpha
beta
delta
gamma

Lo stesso controllo disabilita il colore in grep --color=auto e disattiva il pager in git. Questa è una funzionalità. Significa che uno script ottiene un output stabile e leggibile da una macchina senza doverlo richiedere esplicitamente.

Questo risponde anche a una domanda che viene posta spesso riguardo alle pipeline. ls | sort al prompt e $(ls | sort) stampano la stessa cosa, perché ls aveva una pipe sul suo output in entrambi i casi. Ciò che cambia all'interno di una sostituzione è l' ultima fase della pipeline. sort non controlla mai la presenza di un terminale, quindi il suo output non cambia mai. Inserisci un comando che rileva il terminale alla fine della pipeline e vedrai che cambierà; ecco perché una pipeline testata visivamente può comportarsi in modo diverso nel momento in cui la racchiudi in $().

Un avvertimento che ne consegue: non analizzare l'output di ls in uno script, anche se sembra comodo. I nomi dei file possono contenere spazi e caratteri di nuova riga. Usa un glob, oppure find -print0 con read -d ''.

Le nuove righe finali che scompaiono silenziosamente

La sostituzione di comando rimuove ogni carattere di nuova riga alla fine dell'output. Non solo l'ultima. Tutte.

cd /tmp/subst-demo
printf 'hello\n\n\n' > blanks.txt
wc -c < blanks.txt
v=$(cat blanks.txt)
printf '%s' "$v" | wc -c
8
5

Il file contiene hello più tre nuove righe, quindi 8 byte. La variabile contiene hello, quindi 5 byte. Tre byte sono svaniti senza alcun avviso.

Questa rimozione è intenzionale e solitamente utile. È ciò che permette a stamp=$(date -u +%Y%m%dT%H%M%SZ) di produrre un frammento di nome file utilizzabile invece di un nome contenente un'interruzione di riga, motivo per cui il pattern è sicuro in contesti come uno script di backup restic pianificato:

stamp=$(date -u +%Y%m%dT%H%M%SZ)
printf 'backup-%s.tar.gz\n' "$stamp"
backup-20260804T031500Z.tar.gz

Il timestamp sarà diverso. Ciò che conta è che il nome sia su una sola riga.

Il costo è che non è possibile utilizzare $() per spostare esattamente i byte di un file. Se sono necessarie le nuove righe finali, occorre aggiungere un carattere sentinella all'interno della sostituzione e rimuoverlo in seguito:

v=$(cat blanks.txt; printf x)
v=${v%x}
printf '%s' "$v" | wc -c
8

Il carattere x si trova dopo le nuove righe, quindi non rimangono nuove righe finali da rimuovere. ${v%x} elimina poi il sentinella e lascia i byte originali.

Due dettagli correlati. Per leggere un intero file, v=$(<blanks.txt) svolge lo stesso lavoro senza eseguire cat, poiché bash apre il file direttamente. Rimuove le nuove righe finali in modo identico. Inoltre, le here-string si muovono nella direzione opposta, aggiungendo una nuova riga che non è stata scritta:

wc -c <<< 'abc'
4

Racchiudere il risultato tra virgolette, altrimenti bash eseguirà word splitting e globbing

Una sostituzione non racchiusa tra virgolette subisce word splitting e successivamente pathname expansion. Una sostituzione racchiusa tra virgolette non subisce nessuno dei due processi.

printf 'a b\tc\nd\n' > words.txt
echo $(cat words.txt)
echo "$(cat words.txt)"
a b c d
a b	c
d

Senza virgolette, bash ha suddiviso l'output in base ai caratteri definiti in IFS, che per impostazione predefinita sono spazio, tabulazione e nuova riga, e echo ha poi unito i quattro frammenti con singoli spazi. Con le virgolette, il testo è arrivato come un'unica parola, mantenendo intatte la tabulazione e la nuova riga interna.

Il globbing rappresenta la metà più pericolosa:

mkdir -p /tmp/glob-demo
cd /tmp/glob-demo
touch one.txt two.txt
printf '*\n' > pattern.txt
p=$(cat pattern.txt)
echo $p
echo "$p"
one.txt pattern.txt two.txt
*

L'assegnazione in sé era sicura, poiché le assegnazioni non eseguono word splitting né globbing. Il danno si è verificato in echo $p, dove * è stato espanso rispetto alla directory corrente. Uno script che legge un pattern da un file di configurazione e dimentica le virgolette agirà inavvertitamente su ogni file visibile. Racchiudere ogni espansione tra virgolette elimina l'intera classe di bug. Omettere le virgolette solo quando è effettivamente necessario lo splitting, il che è raro.

Perché local x=$(cmd) restituisce sempre 0?

Perché local è di per sé un comando e $? riporta lo stato di local, non lo stato della sostituzione che conteneva.

check_bad() { local out=$(false); echo "status: $?"; }
check_bad
status: 0

false è terminato con 1, local ha completato con successo la dichiarazione della variabile e l'1 è stato scartato. declare, export, typeset e readonly si comportano tutti allo stesso modo. Anche set -e non rileverà l'errore, poiché dal punto di vista della shell non si è verificato alcun fallimento.

Separare la dichiarazione dall'assegnazione:

check_good() { local out; out=$(false); echo "status: $?"; }
check_good
status: 1

Un'assegnazione semplice al livello principale riporta già lo stato dell'ultima sostituzione di comando:

out=$(exit 3)
echo $?
3

Questo aspetto è critico in un controllo di integrità eseguito tramite un servizio e un timer systemd, dove un codice di uscita mascherato fa sì che l'unità riporti un successo a ogni esecuzione, anche se il lavoro che avrebbe dovuto verificare non è mai avvenuto.

Le alternative in-shell da usare

Molti usano $(...) perché vogliono memorizzare i dati in una variabile. Spesso, però, ciò che serve davvero è l'input, non la cattura dell'output. Queste quattro forme mantengono lo stato nella shell corrente.

Un redirect sul loop

cd /tmp/subst-demo
while read -r line; do printf 'got: %s\n' "$line"; done < three.txt
got: alpha
got: beta
got: gamma

Per l'input non viene creato alcun processo, quindi tutte le variabili impostate nel corpo del loop restano disponibili al termine del loop.

Process substitution

while read -r line; do printf 'got: %s\n' "$line"; done < <(sort -r three.txt)
got: gamma
got: beta
got: alpha

<(command) fornisce un path, ad esempio /dev/fd/63, dal quale leggere l'output del comando. Il comando viene comunque eseguito in un processo separato. Il loop while invece no: è proprio questo il punto. Lo spazio in < <( è obbligatorio: <<( viene interpretato come l'inizio di un here-document e non viene analizzato correttamente. La process substitution è una funzionalità di bash, quindi uno script con #!/bin/sh su Ubuntu o Debian viene eseguito da dash e fallisce. Usare #!/bin/bash.

Here-string

read -r first rest <<< 'alpha beta gamma'
echo "$first"
echo "$rest"
alpha
beta gamma

<<< fornisce una stringa allo standard input di un comando. read viene eseguito nella shell corrente, quindi entrambe le variabili vengono impostate nel contesto in cui è possibile utilizzarle. read -r a b <<< "$(some-command)" è il metodo standard per estrarre due campi da una riga di output.

mapfile per file interi

mapfile -t lines < three.txt
echo "${#lines[@]}"
echo "${lines[1]}"
3
beta

mapfile, chiamato anche readarray, legge un file in un array nella shell corrente. -t rimuove il newline finale da ogni elemento. mapfile richiede bash 4 o versioni successive. Ubuntu 24.04 include bash 5.2 e Ubuntu 26.04 include bash 5.3, quindi è disponibile in qualsiasi immagine server corrente.

Una checklist prima di eseguire lo script

  • Scrivi $(command) e racchiudilo tra virgolette come "$(command)", a meno che tu non voglia esplicitamente una suddivisione.
  • Dai per scontato che i caratteri di fine riga finali vengano rimossi. Aggiungi un carattere sentinella se hai bisogno di ripristinarli.
  • Escludi i comandi interattivi dalle sostituzioni o invia i loro prompt allo standard error.
  • Scrivi local out su una riga separata quando lo stato di uscita di out=$(command) è rilevante.
  • Per impostare variabili dall'input, usa un redirect o una process substitution invece di una pipe.

Queste configurazioni compaiono nei primi piccoli script scritti da chi sta configurando una nuova VPS, e i fallimenti rimangono silenziosi. Uno script di backup che cattura un prompt nel nome di un file, o un controllo di integrità che maschera un codice di uscita, continua a segnalare un successo. Il costo aumenta quando esegui lo stesso script su diversi server, perché l'output che non hai mai letto è ora un output che non leggi su venti macchine diverse.

FAQ

Perché cd all'interno di $() non cambia la mia directory corrente?

$(...) esegue il comando in una subshell, ovvero un processo separato che possiede una copia della directory di lavoro e delle variabili. Il comando cd modifica tale copia, ma al termine del processo la copia viene eliminata. Una subshell può restituire solo lo standard output e un codice di uscita; non esiste alcun meccanismo per propagare il cambio di directory alla shell genitore. Se serve il percorso della directory, acquisiscilo con target=$(cd /etc && pwd) e usa "$target". Se vuoi che la shell si sposti, esegui cd direttamente, senza racchiuderlo in una sostituzione.

Qual è la differenza tra $() e i backtick in bash?

Producono lo stesso risultato per i comandi semplici, ma differiscono in due aspetti rilevanti. $() supporta l'annidamento diretto, poiché il parser riconosce le parentesi, mentre i backtick richiedono un backslash di escape per ogni livello di annidamento. I backtick rimuovono inoltre un livello di escape tramite backslash prima che il comando interno venga analizzato, quindi ` echo 'a\\b' prints a\b while $(echo 'a\\b') prints a\\b. $() is in POSIX and works in dash and busybox sh`, pertanto non esiste alcun motivo di portabilità per preferire i backtick.

Perché il mio script si blocca senza mostrare il prompt quando un comando pone una domanda?

La sostituzione di comando reindirizza lo standard output in una pipe, ma lascia lo standard input collegato al terminale. Un programma che scrive il proprio prompt sullo standard output vede tale prompt catturato nella variabile, mentre il comando read sottostante resta in attesa di un input. Il terminale mostra solo i caratteri digitati, inviati dal driver del terminale. Sposta la domanda al di fuori della sostituzione, oppure scrivi il prompt sullo standard error con printf 'Username: ' >&2 in modo che non venga catturato.

Perché le righe vuote alla fine della mia variabile sono scomparse?

La sostituzione di comando rimuove ogni newline finale, non solo l'ultima. printf 'hello\n\n\n' > f; v=$(cat f) lascia v con cinque byte, mentre il file ne contiene otto. Per conservarle, aggiungi un marcatore (sentinel) all'interno della sostituzione e rimuovilo in seguito con v=$(cat f; printf x) seguito da v=${v%x}. Il marcatore si trova dopo i newline, quindi non c'è nulla alla fine che bash possa rimuovere.

Perché local out=$(cmd) segnala sempre successo?

local è un comando a sé stante e $? dopo quella riga riporta se local ha avuto successo nel dichiarare la variabile. Il codice di uscita della sostituzione viene consumato e scartato, il che significa anche che set -e non interromperà lo script. declare, export, typeset e readonly si comportano allo stesso modo. Scrivi local out su una riga e out=$(cmd) su quella successiva, così $? riporterà il codice di uscita reale.

Questo articolo ti è stato utile?

#bash#shell-scripting#linux#subshell#coreutils