Espansione cronologia Bash: usare !! e !$
Scopri le sei espansioni della cronologia Bash più utili: !! ripete l’ultimo comando, !$ ne recupera l’ultimo argomento e :p lo stampa senza eseguirlo.
Funzione dell’espansione della cronologia di Bash
L’espansione della cronologia di Bash ricostruisce una riga di comando precedente dalla cronologia della shell prima che Bash la esegua. !! ripete il comando precedente. !$ inserisce l’ultimo argomento del comando precedente. La sostituzione è testuale e avviene prima che Bash suddivida la riga in parole, quindi il risultato contiene esattamente i caratteri digitati la prima volta.
Sei forme coprono quasi tutte le operazioni eseguite su un server:
!!ripete l’intera riga precedente, mentresudo !!la esegue nuovamente come root.!$rappresenta l’ultimo argomento della riga precedente.!*rappresenta tutti gli argomenti della riga precedente.!nesegue la voce della cronologia con numero n, mentre!-nconta a ritroso dalla posizione corrente.!stringesegue il comando più recente che iniziava constring.^old^newesegue nuovamente la riga precedente sostituendo il primooldconnew.
Tutto ciò che segue viene digitato in un prompt interattivo sul proprio server. Negli script l’espansione della cronologia è disattivata, come spiegato nell’ultima sezione.
Un percorso lungo, digitato una sola volta
Questo è il caso in cui il metodo è particolarmente utile. Stai preparando una directory di rilascio e il percorso è abbastanza lungo da rendere rischiosa una seconda digitazione, perché può introdurre un errore.
sudo mkdir -p /srv/www/app/releases/2026-08-07
sudo chown -R deploy:deploy !$
ls -ld !$
sudo -u deploy nano !$/config.envPrima di eseguire ogni riga, bash stampa la riga risultante dall'espansione:
sudo chown -R deploy:deploy /srv/www/app/releases/2026-08-07Questa stampa è il controllo da eseguire. Leggila prima dell'output del comando, perché è l'unica occasione per vedere quale significato bash ha assegnato a !$.
La catena funziona perché bash salva nella cronologia la riga espansa, non !$ come l'hai digitata. Di conseguenza, la riga 3 ricava l'ultimo argomento dalla riga 2 espansa e la riga 4 lo ricava dalla riga 3. La riga 4 mostra inoltre che un designatore può essere seguito da altro testo: !$/config.env diventa il percorso con /config.env alla fine, perché la parola del designatore termina al /.
Ecco un altro esempio dalla stessa sessione. Dopo aver modificato il file, vuoi elencare la directory che lo contiene:
ls -l !$:h:h è il modificatore head. Rimuove l'ultimo componente da un percorso, come fa dirname. :t conserva soltanto l'ultimo componente, :r rimuove l'estensione e :e conserva soltanto l'estensione.
Ripetere l'ultimo comando con !! e sudo !!
Dimentichi sudo e il gestore dei servizi lo segnala:
systemctl restart nginxFailed to restart nginx.service: Interactive authentication required.sudo !!Bash sostituisce !! con il testo della riga precedente, quindi la shell esegue sudo systemctl restart nginx.
!! corrisponde esattamente alla riga precedente, qualunque essa fosse. È qui che si verifica il classico errore. Si pensa che l'ultimo comando sia quello che ha avuto esito negativo, ma nel frattempo è stato eseguito un cd, oppure un history, oppure il comando non riuscito risaliva a due righe prima. sudo !! esegue quindi il comando sbagliato con i privilegi di root. Quando non si è certi, visualizzarlo prima:
sudo !!:p:p stampa la riga espansa senza eseguirla. La riga stampata viene aggiunta alla cronologia, quindi, se è corretta, un semplice !! nella riga successiva la esegue.
Riutilizzare l'ultimo argomento con !$
!$ è l'ultima parola della riga precedente. È la forma che userai più spesso, perché l'ultima parola è generalmente l'elemento su cui stai lavorando: un percorso o il nome di un servizio.
sudo systemctl status nginx
sudo systemctl reload !$Qui ci sono due aspetti che spesso sorprendono.
Primo: !$ è l'ultima parola, non l'ultimo argomento a cui stavi pensando. Se la riga precedente terminava con un reindirizzamento, l'ultima parola è la destinazione del reindirizzamento:
sudo nginx -T > /tmp/nginx-dump.conf
less !$In questo caso il risultato è quello atteso. Ma dopo journalctl -u nginx > /tmp/log.txt, !$ è /tmp/log.txt e non nginx. Leggi la riga visualizzata.
Secondo: l'espansione è testuale, quindi una variabile viene reinserita senza espansione. Dopo ls $HOME/backups, !$ restituisce i caratteri $HOME/backups, che bash espande poi nuovamente come un parametro normale. Tieni presente questo ordine: l'espansione della cronologia viene eseguita prima dell'espansione dei parametri e prima della sostituzione di comando con $( ), quindi non vede i valori, ma soltanto il testo.
Vale la pena conoscere anche le forme adiacenti. !^ è il primo argomento, !:2 è il secondo e !:2-4 indica un intervallo. !!:$ è la forma estesa di !$.
Se preferisci visualizzare il testo prima di eseguirlo, premi Alt-. (oppure Esc e quindi .). Readline inserisce direttamente nel prompt l'ultimo argomento del comando precedente, dove puoi modificarlo. Premilo di nuovo per tornare all'ultimo argomento del comando ancora precedente. Nulla viene eseguito finché non premi Invio.
Passare ogni argomento con !*
!* contiene tutte le parole della riga precedente tranne la prima.
stat /srv/www/app/shared/config.env /srv/www/app/shared/secrets.env
sudo chmod 600 !*L’espressione «tranne la prima» è fondamentale. !* elimina la parola 0 e nient’altro, quindi le opzioni vengono passate insieme ai percorsi. Dopo ls -l file1 file2, !* è -l file1 file2, quindi sudo chmod 600 !* ha esito negativo perché chmod riceve -l come argomento. Numerate le parole a partire da zero e usate invece una porzione: !!:2* indica dalla parola 2 fino alla fine.
Lo stesso problema si presenta con sudo. Dopo sudo chown deploy:deploy /srv/www/app, la parola 0 è sudo e la parola 1 è chown, quindi !* passa chown deploy:deploy /srv/www/app al comando successivo. Nella maggior parte dei casi non è ciò che volevate.
Selezionare un comando in base alla posizione con !n e !-n
history visualizza l'elenco con un numero davanti a ogni voce.
history 5 512 sudo nginx -t
513 sudo systemctl reload nginx
514 ss -tulpn
515 sudo tail -f /var/log/nginx/error.log
516 history 5!513 esegue di nuovo la voce 513. !-2 esegue la voce di due righe precedente, contando dalla riga che si sta digitando, quindi !-1 e !! indicano la stessa voce.
Controllare i numeri immediatamente prima di usarli. !-2 indica ogni volta una posizione diversa dopo l'esecuzione di qualsiasi comando, incluso un ls eseguito senza pensarci. I numeri assoluti restano stabili all'interno della stessa sessione, ma non corrispondono agli stessi comandi in una seconda sessione sullo stesso sistema e cambiano anche dopo il login successivo, quando la cronologia viene riletta dal file. Un numero memorizzato ieri può indicare un comando diverso oggi.
Ripetere un comando in base al prefisso con !string
!string esegue il comando più recente che inizia con string.
!ssIn questo modo viene ripetuto ss -tulpn dell’elenco precedente, ovvero il controllo dei socket in ascolto descritto in quali porte sono aperte su un server Linux. !?string? cerca una corrispondenza in qualsiasi posizione della riga, non soltanto all’inizio. Questo è utile quando si ricorda un argomento, ma non il nome del comando.
Mantenere il prefisso il più lungo possibile. !s può corrispondere a ss, sudo, systemctl o shutdown, in base a quale comando è stato eseguito più di recente. Non è possibile sapere quale verrà usato finché non viene eseguito. !string:p mostra la corrispondenza senza eseguirla. Se non trova corrispondenze, bash stampa bash: !ss: event not found e non esegue nulla: è il comportamento sicuro.
Correggere un refuso con ^old^new
sudo systemctl status ngnixUnit ngnix.service could not be found.^ngnix^nginxLa shell riesegue la riga precedente sostituendo la prima occorrenza di ngnix con nginx. Viene modificata solo la prima corrispondenza. Per sostituire tutte le corrispondenze, usare la forma estesa !!:gs/ngnix/nginx/, dove s significa sostituire e g indica di applicare la sostituzione all'intera riga.
Stampa l'espansione prima di eseguirla
Due abitudini evitano che un'espansione esegua qualcosa di diverso da ciò che intendevi.
La prima è :p, che hai già visto. Aggiungila a un'espansione e bash stampa il risultato invece di eseguirlo: !!:p oppure !systemctl:p. La riga stampata viene inserita nella cronologia, quindi !! la esegue dopo che l'hai appena verificata.
La seconda è più efficace, perché si applica a ogni espansione senza richiedere di ricordare nulla:
shopt -s histverifyInserisci quella riga in ~/.bashrc. Con histverify impostato, un'espansione non viene eseguita quando premi Invio. Bash riscrive la riga espansa nel prompt, così puoi leggerla e modificarla, quindi premi Invio una seconda volta per eseguirla. Richiede un solo tasto aggiuntivo e rimuove l'intera classe di errori. Richiede readline, quindi funziona in un prompt interattivo e in nessun altro contesto.
Ctrl-R è l'altro metodo e non espande mai nulla. Premi Ctrl-R e digita una parte del comando. Il prompt diventa un prompt reverse-i-search e mostra la corrispondenza più recente mentre digiti. Premi di nuovo Ctrl-R per passare alle corrispondenze precedenti. Invio esegue la riga visualizzata. Ctrl-G annulla la ricerca e ripristina il prompt originale. Il tasto freccia sinistra termina la ricerca e lascia la riga corrispondente nel prompt, pronta per la modifica.
Usa Ctrl-R quando vuoi visualizzare prima il comando. Usa !! e !$ quando il comando è nella riga precedente e puoi ancora leggerlo sullo schermo.
Perché echo "done!" restituisce «event not found»
echo "deploy done!"bash: !": event not foundL'espansione della cronologia viene eseguita prima della risoluzione delle virgolette e le virgolette doppie non proteggono il carattere !. Lo fanno soltanto le virgolette singole e il carattere di escape \. Di conseguenza, echo 'deploy done!' stampa il risultato previsto. Anche un carattere \ impedisce l'espansione, ma all'interno delle virgolette doppie bash lascia il carattere \ nell'output. Per questo le virgolette singole sono la soluzione più semplice.
Il problema è particolarmente critico per le password, perché una password robusta contiene spesso !. Un comando come mysql -u app -p"S3cret!pass" restituisce un errore event not found oppure, se una voce della cronologia corrisponde, invia in modo silenzioso una stringa diversa. Usa le virgolette singole oppure, meglio ancora, lascia che lo strumento richieda il valore interattivamente, così il secret non compare mai nella riga di comando. Questa abitudine rientra nelle normali procedure di gestione degli account: consulta i primi dieci minuti su un nuovo VPS per sapere come gestire chiavi e password su un server appena configurato.
Se stai per incollare un blocco di testo che contiene molti caratteri !, disattiva l'espansione per la sessione con set +H e riattivala con set -H.
HISTSIZE, HISTFILESIZE e dove viene conservata la cronologia
Due variabili che vengono spesso confuse perché i nomi sono simili.
HISTSIZEindica il numero di comandi che la shell in esecuzione mantiene in memoria.HISTFILESIZEindica il numero di righe conservate nel file su disco,~/.bash_historysalvo diversa indicazione diHISTFILE.
Il file viene scritto quando la shell termina e in quel momento viene troncato a HISTFILESIZE righe. Il valore predefinito di Ubuntu, ~/.bashrc, imposta rispettivamente 1000 e 2000. Verifica il contenuto effettivo:
grep HIST ~/.bashrc
echo "$HISTSIZE $HISTFILESIZE $HISTFILE"Aumenta entrambi i valori se le ricerche !string non trovano comandi che sai di avere eseguito. Valori come 10000 e 20000 sono comuni e hanno un costo trascurabile, perché il file è testo semplice. Un valore negativo indica l'assenza di limiti.
Le marche temporali sono utili quando torni su un server dopo una settimana:
export HISTTIMEFORMAT='%F %T 'history stampa quindi data e ora davanti a ogni voce, perché bash inizia a scrivere nel file una riga di commento contenente i secondi dall'epoch prima di ogni comando.
Il problema più comune si verifica quando usi più terminali e la cronologia scompare. Ogni shell mantiene il proprio elenco in memoria e lo scrive alla chiusura. Senza histappend, l'ultima shell a terminare sovrascrive ciò che le altre hanno salvato. Due impostazioni risolvono il problema:
shopt -s histappend
export PROMPT_COMMAND='history -a'histappend fa aggiungere alla shell il proprio elenco alla fine del file invece di sostituirlo. history -a aggiunge le nuove righe dopo ogni prompt, quindi una sessione terminata forzatamente, anziché chiusa correttamente, conserva comunque i comandi eseguiti. Il valore predefinito di Ubuntu, ~/.bashrc, imposta già histappend. Questo è ancora più importante quando gestisci più macchine, perché la cronologia viene memorizzata per ogni utente e per ogni macchina. Quando gestisci più server da una workstation, il !$ che cerchi potrebbe trovarsi semplicemente su un altro host.
Mantenere i secret fuori dalla cronologia di bash
HISTCONTROL stabilisce quali righe vengono salvate.
ignorespacescarta ogni riga che inizia con uno spazio.ignoredupsscarta una riga identica a quella precedente.ignorebothapplica entrambe le regole precedenti.erasedupsrimuove ogni copia precedente della riga da salvare.
export HISTCONTROL=ignorebothCon ignorespace attivo, uno spazio iniziale impedisce che un comando venga inserito nell'elenco in memoria, quindi il comando non viene scritto neppure nel file. Verifica il valore prima di farci affidamento con echo "$HISTCONTROL". L'utente predefinito di Ubuntu, .bashrc, imposta ignoreboth, ma una shell root o un'immagine minimale potrebbero non impostarlo. In tal caso, lo spazio iniziale non ha effetto e la riga viene salvata come tutte le altre.
Considera con precisione che cosa protegge questa impostazione. La riga resta fuori da ~/.bash_history. Durante l'esecuzione del processo è comunque visibile nell'output di ps, quindi qualsiasi altro utente del server può leggerla. sudo registra il comando eseguito nel log di sistema. Il file della cronologia è testo semplice: impostalo con modalità 600 e ricorda che chiunque possa leggere la tua directory home può leggere anche le ultime migliaia di comandi.
Se nella cronologia è già presente un dato sensibile, rimuovi la voce e riscrivi il file:
history
history -d 517
history -whistory -d rimuove la voce dalla memoria e history -w scrive l'elenco corrente nel file, sovrascrivendolo. history -c cancella l'intero elenco. HISTIGNORE è l'impostazione correlata per il rumore, non per i secret: HISTIGNORE='ls:pwd:history:clear' mantiene quelle righe fuori dall'elenco, così le ricerche restituiscono risultati utili.
Perché l'espansione della cronologia non produce alcun effetto in uno script shell
L'espansione della cronologia è disponibile nelle shell interattive. Uno script viene eseguito in una shell non interattiva, nella quale l'elenco della cronologia non è attivo e l'espansione è disabilitata, quindi !! e !$ restano nella riga come testo ordinario. In uno script, sudo !! chiede a sudo di eseguire letteralmente un comando denominato !! e l'operazione non riesce.
Controlla la shell in cui ti trovi:
echo $-L'output è l'insieme dei flag delle opzioni attive, ad esempio himBHs. i indica che la shell è interattiva, mentre H indica che l'espansione della cronologia è abilitata. Esegui la stessa riga all'interno di uno script: nessuna delle due lettere sarà presente.
Questa è la distinzione tra i due contesti del lavoro nella shell. Al prompt, !$ e Ctrl-R riducono la digitazione dei comandi che puoi ancora visualizzare. In un file, invece, devi assegnare nomi agli elementi: inserisci il percorso in una variabile oppure acquisisci l'output con sostituzione di comando. Uno script che dipendesse dalla cronologia personale produrrebbe risultati diversi per la persona successiva che lo esegue, cioè l'opposto dello scopo di uno script.
Per questo motivo, tutti gli esempi di questa pagina devono essere digitati in un prompt attivo. Nessuno si comporta allo stesso modo se viene incollato in un file .sh.
FAQ
Che cosa fa !! in bash?
!! si espande nel testo completo della riga di comando precedente, quindi sudo !! esegue di nuovo l'ultimo comando come root. L'espansione è testuale e avviene prima che bash analizzi la riga; bash stampa la riga completata subito prima di eseguirla. Se non si è certi di quale fosse la riga precedente, digitare prima sudo !!:p. :p stampa l'espansione senza eseguirla e aggiunge la riga stampata alla cronologia, quindi un successivo !! la esegue.
Come posso riutilizzare l'ultimo argomento del comando precedente?
Usare !$. Dopo sudo mkdir -p /srv/www/app/releases, la riga ls -ld !$ diventa ls -ld /srv/www/app/releases. Il comando usa l'ultima parola della riga, quindi un target di redirezione alla fine conta come ultima parola. È possibile anche aggiungere testo: !$/config.env aggiunge testo al percorso, perché l'identificatore della parola si ferma a /. L'alternativa interattiva è Alt-., che inserisce lo stesso testo nel prompt, consentendo di leggerlo prima di premere Invio.
Perché bash visualizza "event not found" quando il testo contiene un punto esclamativo?
Le virgolette doppie non proteggono ! dall'espansione della cronologia, quindi echo "done!" induce bash a cercare un evento nella cronologia e a visualizzare bash: !": event not found. Le virgolette singole lo proteggono, quindi scrivere echo 'done!'. Anche una barra rovesciata interrompe l'espansione, ma all'interno delle virgolette doppie bash lascia la barra rovesciata nell'output. Per incollare un blocco lungo contenente !, disattivare l'espansione per la sessione con set +H.
Perché !! e !$ non funzionano nello script della shell?
L'espansione della cronologia è abilitata soltanto nelle shell interattive. Uno script viene eseguito in modo non interattivo, quindi la shell non crea alcun elenco della cronologia e !! resta nella riga come testo normale. Eseguire echo $- per verificare il tipo di shell: una shell interattiva stampa flag che includono i e H, mentre uno script non stampa nessuno dei due. Negli script, usare una variabile o una sostituzione di comando.
Come posso impedire che una password venga salvata nella cronologia di bash?
Impostare HISTCONTROL=ignorespace o HISTCONTROL=ignoreboth in ~/.bashrc, quindi iniziare il comando con uno spazio e la riga non verrà mai salvata. Verificare prima il valore con echo "$HISTCONTROL", perché se la variabile non è impostata lo spazio iniziale non ha alcun effetto. Questo impedisce di salvare la riga soltanto in ~/.bash_history. Durante l'esecuzione, il comando resta visibile in ps e sudo registra ciò che ha eseguito. Se un secret è già stato salvato, individuarne il numero con history, quindi eseguire history -d <number> seguito da history -w per riscrivere il file.