SSD Nodes Learn 🎉 VPS desde $4.99/mês
Guias Matt ConnorPor Matt Connor

Expansão de histórico no Bash: !! e !$

Entenda !!, !$ e mais quatro expansões de histórico do Bash, incluindo :p para imprimir um comando antes de executá-lo na shell interativa.

O que a expansão do histórico do bash faz

A expansão do histórico do Bash reconstrói uma linha de comando anterior a partir do histórico da shell antes de o bash a executar. !! repete o comando anterior. !$ insere o último argumento do comando anterior. A substituição é textual e ocorre primeiro, antes de o bash dividir a linha em palavras. Por isso, o resultado contém exatamente os caracteres introduzidos na primeira vez.

Seis formas abrangem quase tudo o que fará num servidor:

  • !! repete a linha anterior completa, e sudo !! executa-a novamente como root.
  • !$ representa o último argumento da linha anterior.
  • !* representa todos os argumentos da linha anterior.
  • !n executa a entrada do histórico com o número n, e !-n conta para trás a partir da posição atual.
  • !string executa o comando mais recente que começou por string.
  • ^old^new executa novamente a linha anterior, substituindo a primeira ocorrência de old por new.

Tudo o que se segue é introduzido numa prompt interativa no seu próprio servidor. A expansão do histórico está desativada nos scripts, como explica a última secção.

Um caminho longo, introduzido uma única vez

Este é o caso em que o recurso se justifica. Está a preparar um diretório de release, e o caminho é suficientemente longo para que introduzi-lo uma segunda vez aumente o risco de erro de digitação.

sudo mkdir -p /srv/www/app/releases/2026-08-07
sudo chown -R deploy:deploy !$
ls -ld !$
sudo -u deploy nano !$/config.env

Antes de executar cada linha, o bash imprime a linha produzida pela expansão:

sudo chown -R deploy:deploy /srv/www/app/releases/2026-08-07

Esse echo é a sua verificação. Leia-o antes de consultar a saída do comando, porque é a única oportunidade de ver o que o bash decidiu que !$ significava.

A cadeia funciona porque o bash guarda no histórico a linha expandida, e não !$ tal como foi introduzido. Assim, a linha 3 obtém o último argumento da linha 2 expandida, e a linha 4 obtém o seu argumento da linha 3. A linha 4 também mostra que pode haver texto depois de um designador: !$/config.env transforma-se no caminho com /config.env no fim, porque o designador de palavra termina em /.

Mais um exemplo da mesma sessão. Depois de editar o ficheiro, quer listar o diretório que o contém:

ls -l !$:h

:h é o modificador head. Remove o último componente de um caminho, fazendo o mesmo que dirname. :t mantém apenas o último componente, :r remove a extensão, e :e mantém apenas a extensão.

Repita o último comando com !! e sudo !!

Esqueceu-se de sudo, e o gestor de serviços informa-o:

systemctl restart nginx
Failed to restart nginx.service: Interactive authentication required.
sudo !!

O Bash substitui !! pelo texto da linha anterior, portanto a shell executa sudo systemctl restart nginx.

!! é exatamente a linha anterior, independentemente do conteúdo dessa linha. É aqui que ocorre o acidente clássico. Pode pensar que o último comando foi o que falhou, mas entretanto executou um cd, ou um history, ou o comando que falhou estava duas linhas atrás. sudo !! executa então o comando errado com privilégios de root. Mostre-o primeiro quando não tiver a certeza:

sudo !!:p

:p mostra a linha expandida e não a executa. A linha mostrada é adicionada ao seu histórico; se estiver correta, um !! simples na linha seguinte executa-a.

Reutilizar o último argumento com !$

!$ é a última palavra da linha anterior. Esta é a forma que utilizará na maioria dos casos, porque a última palavra costuma ser aquilo em que está a trabalhar: um caminho ou o nome de um serviço.

sudo systemctl status nginx
sudo systemctl reload !$

Há dois aspetos que costumam surpreender.

Primeiro, !$ é a última palavra, não o último argumento que tinha em mente. Se a linha anterior terminava com um redirecionamento, a última palavra é o destino do redirecionamento:

sudo nginx -T > /tmp/nginx-dump.conf
less !$

Neste caso, o resultado é o esperado. Mas depois de journalctl -u nginx > /tmp/log.txt, !$ é /tmp/log.txt e não nginx. Leia a linha apresentada.

Segundo, a expansão é textual, por isso uma variável volta sem ser expandida. Depois de ls $HOME/backups, !$ fornece-lhe os caracteres $HOME/backups, que o bash expande novamente como um parâmetro normal. Tenha presente esta ordem: a expansão do histórico ocorre antes da expansão de parâmetros e antes da substituição de comandos com $( ), por isso nunca vê valores, apenas texto.

Também é útil conhecer as formas adjacentes. !^ é o primeiro argumento, !:2 é o segundo e !:2-4 é um intervalo. !!:$ é a forma mais longa de escrever !$.

Se preferir ver o texto antes de o executar, prima Alt-. (ou Esc e depois .). O Readline insere diretamente no prompt o último argumento do comando anterior, onde pode editá-lo. Prima novamente para recuar até ao último argumento do comando anterior a esse. Nada é executado até premir Enter.

Encaminhe todos os argumentos com !*

!* contém todas as palavras da linha anterior, exceto a primeira.

stat /srv/www/app/shared/config.env /srv/www/app/shared/secrets.env
sudo chmod 600 !*

A expressão "exceto a primeira" é importante. !* remove a palavra 0 e não altera mais nada, por isso as opções são encaminhadas juntamente com os caminhos. Depois de ls -l file1 file2, !* é -l file1 file2, portanto sudo chmod 600 !* falha, porque chmod recebe -l como argumento. Numere as palavras a partir de zero e use uma fatia: !!:2* significa a palavra 2 até ao fim.

A mesma armadilha ocorre com sudo. Depois de sudo chown deploy:deploy /srv/www/app, a palavra 0 é sudo e a palavra 1 é chown, por isso !* passa chown deploy:deploy /srv/www/app ao comando seguinte. Raramente era isso que pretendia.

Selecionar um comando pela posição com !n e !-n

history mostra a lista com um número à frente de cada entrada.

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 executa novamente a entrada 513. !-2 executa a entrada duas linhas atrás, contando a partir da linha que está a escrever agora, por isso !-1 e !! significam a mesma coisa.

Confirme os números imediatamente antes de os utilizar. !-2 aponta para um local diferente sempre que executa qualquer coisa, incluindo um ls que tenha executado sem pensar. Os números absolutos são estáveis durante uma sessão, mas não são os mesmos numa segunda sessão no mesmo servidor e também não são os mesmos depois de o próximo login carregar novamente o ficheiro de histórico. Um número que memorizou ontem aponta para um comando diferente hoje.

Executar novamente um comando pelo prefixo com !string

!string executa o comando mais recente que começa por string.

!ss

Isto executa novamente ss -tulpn da lista acima, a verificação de sockets em escuta descrita em quais portas estão abertas num servidor Linux. !?string? procura correspondências em qualquer posição da linha, e não apenas no início. Isto é útil quando se recorda um argumento, mas não o nome do comando.

Mantenha o prefixo longo. !s corresponde a ss, sudo, systemctl ou shutdown, consoante o que tiver sido executado mais recentemente. Não saberá qual será usado até o comando ser executado. !string:p mostra a correspondência sem a executar. Se não houver correspondências, o bash mostra bash: !ss: event not found e não executa nada. Esse é o resultado seguro.

Corrigir um erro de digitação com ^old^new

sudo systemctl status ngnix
Unit ngnix.service could not be found.
^ngnix^nginx

O shell executa novamente a linha anterior, substituindo o primeiro ngnix por nginx. Apenas a primeira ocorrência é alterada. Para alterar todas as ocorrências, use a forma longa !!:gs/ngnix/nginx/, em que s significa substituir e g significa aplicar a alteração em toda a linha.

Imprima a expansão antes de a executar

Dois hábitos evitam o acidente em que uma expansão executa algo que não pretendia.

O primeiro é :p, que já viu. Anexe-o a uma expansão e o bash imprime o resultado em vez de o executar: !!:p ou !systemctl:p. A linha impressa fica no seu histórico. Por isso, !! executa depois aquilo que acabou de ler.

O segundo é mais forte, porque se aplica a todas as expansões sem precisar de se lembrar de nada:

shopt -s histverify

Coloque essa linha em ~/.bashrc. Com histverify definido, uma expansão não é executada quando prime Enter. O Bash escreve a linha expandida de volta na linha de comandos, para que a possa ler e editar. Depois, prima Enter uma segunda vez para a executar. Custa uma tecla adicional e elimina toda esta classe de acidentes. Requer readline, por isso funciona numa linha de comandos interativa e em nenhum outro contexto.

Ctrl-R é a outra forma de trabalhar e nunca expande nada. Prima Ctrl-R e escreva parte de um comando. A linha de comandos muda para uma pesquisa reverse-i-search e mostra a correspondência mais recente à medida que escreve. Prima Ctrl-R novamente para recuar para correspondências mais antigas. Enter executa a linha apresentada. Ctrl-G cancela a pesquisa e repõe a linha de comandos original. A tecla de seta para a esquerda termina a pesquisa e deixa a linha correspondente na linha de comandos para edição.

Use Ctrl-R quando quiser ver primeiro o comando. Use !! e !$ quando o comando tiver apenas uma linha e ainda o conseguir ler no ecrã.

Por que echo "done!" retorna event not found

echo "deploy done!"
bash: !": event not found

A expansão do histórico ocorre antes de as aspas serem interpretadas, e as aspas duplas não protegem o caractere !. Apenas as aspas simples e a barra invertida fazem isso. Por isso, echo 'deploy done!' imprime o resultado esperado. A barra invertida também impede a expansão, mas, dentro de aspas duplas, o bash deixa a barra invertida na saída. Portanto, as aspas simples são a solução mais limpa.

Isto causa mais problemas com palavras-passe, porque uma palavra-passe forte normalmente contém !. Um comando como mysql -u app -p"S3cret!pass" falha com um erro event-not-found ou, se alguma entrada do histórico coincidir, envia silenciosamente uma string diferente. Use aspas simples ou, melhor ainda, deixe a ferramenta solicitar o valor para que o segredo nunca apareça na linha de comandos. Esse procedimento faz parte da higiene da conta: consulte os primeiros dez minutos num VPS novo para saber como as chaves e as palavras-passe devem ser tratadas num servidor novo.

Se estiver prestes a colar um bloco de texto com muitos caracteres !, desative a expansão durante a sessão com set +H e volte a ativá-la com set -H.

HISTSIZE, HISTFILESIZE e onde o seu histórico é guardado

São duas variáveis, e é comum confundi-las porque os nomes são parecidos.

  • HISTSIZE define quantos comandos o shell em execução mantém na memória.
  • HISTFILESIZE define quantas linhas são mantidas no ficheiro em disco, ~/.bash_history, exceto quando HISTFILE indica o contrário.

O ficheiro é escrito quando o shell termina e, nesse momento, é truncado para HISTFILESIZE linhas. O valor predefinido do Ubuntu, ~/.bashrc, define 1000 e 2000. Verifique o que o seu sistema contém efetivamente:

grep HIST ~/.bashrc
echo "$HISTSIZE $HISTFILESIZE $HISTFILE"

Aumente ambos os valores se as pesquisas de !string não encontrarem comandos que sabe que executou. Valores de 10000 e 20000 são comuns e quase não têm custo, porque o ficheiro é texto simples. Um valor negativo significa que não existe limite.

Os carimbos de data e hora ajudam quando volta a um servidor depois de uma semana:

export HISTTIMEFORMAT='%F %T '

history passa então a apresentar a data e a hora antes de cada entrada, porque o bash começa a escrever, no ficheiro, uma linha de comentário com os segundos desde a época antes de cada comando.

A queixa comum é o desaparecimento do histórico quando utiliza mais de um terminal. Cada shell mantém a sua própria lista na memória e escreve-a ao terminar. Sem histappend, o último shell a fechar substitui o que os outros guardaram. Duas definições resolvem o problema:

shopt -s histappend
export PROMPT_COMMAND='history -a'

histappend faz com que o shell acrescente a sua lista ao fim do ficheiro em vez de o substituir. history -a acrescenta novas linhas depois de cada prompt, para que uma sessão terminada à força, em vez de ser fechada corretamente, ainda deixe os seus comandos guardados. O valor predefinido do Ubuntu, ~/.bashrc, já define histappend. Isto é ainda mais importante quanto mais máquinas administra, porque o histórico é guardado por utilizador e por máquina. Assim, quando está a administrar vários servidores a partir de uma estação de trabalho, o !$ que procura pode simplesmente estar noutro host.

Como manter segredos fora do histórico do bash

HISTCONTROL determina quais linhas são guardadas.

  • ignorespace descarta qualquer linha que comece com um espaço.
  • ignoredups descarta uma linha idêntica à anterior.
  • ignoreboth faz as duas operações acima.
  • erasedups remove todas as cópias anteriores da linha que está a ser guardada.
export HISTCONTROL=ignoreboth

Com ignorespace ativo, um espaço inicial mantém um comando fora da lista em memória, pelo que ele também nunca chega ao ficheiro. Verifique o valor antes de depender deste comportamento com echo "$HISTCONTROL". O utilizador predefinido do Ubuntu, .bashrc, define ignoreboth, mas uma shell root ou uma imagem mínima pode deixá-lo indefinido. Nesse caso, o espaço inicial não tem efeito e a linha é guardada como qualquer outra.

Seja realista sobre aquilo que isto protege. A linha fica fora de ~/.bash_history. Continua visível na saída de ps enquanto o processo estiver em execução, pelo que qualquer outro utilizador do sistema pode lê-la nesse local. sudo regista no log do sistema o comando que foi executado. O ficheiro de histórico é texto simples. Mantenha-o no modo 600 e lembre-se de que qualquer pessoa que consiga ler o seu diretório home também pode ler os últimos milhares de comandos.

Se já houver informação sensível na lista, remova a entrada e reescreva o ficheiro:

history
history -d 517
history -w

history -d remove essa entrada da memória, e history -w escreve a lista atual sobre o ficheiro. history -c limpa a lista inteira. HISTIGNORE é a opção relacionada com ruído, e não com segredos: HISTIGNORE='ls:pwd:history:clear' mantém essas linhas fora da lista para que as pesquisas devolvam resultados úteis.

Por que a expansão do histórico não faz nada num script de shell

A expansão do histórico pertence às shells interativas. Um script é executado numa shell não interativa, na qual a lista do histórico não está ativada e a expansão está desativada. Por isso, !! e !$ permanecem na linha como texto comum. Dentro de um script, sudo !! pede ao sudo para executar literalmente um comando chamado !!, e a execução falha.

Verifique qualquer shell que esteja a utilizar:

echo $-

A saída mostra o conjunto de flags de opções atuais, algo semelhante a himBHs. i significa que a shell é interativa e H significa que a expansão do histórico está ativada. Execute a mesma linha dentro de um script e nenhuma das duas letras estará presente.

Essa é a diferença entre as duas formas de trabalhar com a shell. Na linha de comandos, !$ e Ctrl-R evitam que tenha de voltar a escrever comandos que ainda consegue consultar. Num ficheiro, nomeie os elementos: coloque o caminho numa variável ou capture a saída com substituição de comandos. Um script que dependesse do seu histórico pessoal produziria resultados diferentes para a pessoa seguinte que o executasse. Isso é o oposto da finalidade de um script.

Por esse motivo, todos os exemplos desta página devem ser introduzidos numa linha de comandos ativa. Nenhum deles se comporta da mesma forma quando é colado num ficheiro .sh.

FAQ

O que faz !! no bash?

!! expande para o texto completo da linha de comando anterior, por isso sudo !! executa novamente o último comando como root. A expansão é textual e ocorre antes de o bash analisar a linha. O bash mostra a linha finalizada imediatamente antes de a executar. Se não tiver a certeza de qual foi a linha anterior, escreva primeiro sudo !!:p. :p mostra a expansão sem a executar e adiciona a linha mostrada ao histórico, por isso um !! seguinte executa-a.

Como reutilizo o último argumento do comando anterior?

Use !$. Depois de sudo mkdir -p /srv/www/app/releases, a linha ls -ld !$ transforma-se em ls -ld /srv/www/app/releases. Este recurso usa a última palavra da linha, por isso um destino de redirecionamento no fim conta como a última palavra. Também pode acrescentar texto: !$/config.env adiciona texto ao caminho, porque o designador de palavra termina em /. A alternativa interativa é Alt-., que insere o mesmo texto no prompt para que possa lê-lo antes de premir Enter.

Porque é que o bash diz "event not found" quando o meu texto contém um ponto de exclamação?

As aspas duplas não protegem ! da expansão do histórico, por isso echo "done!" faz com que o bash procure um evento do histórico e mostre bash: !": event not found. As aspas simples protegem-no, por isso escreva echo 'done!'. Uma barra invertida também impede a expansão, mas dentro de aspas duplas o bash mantém a barra invertida no resultado. Para colar um bloco longo que contenha !, desative a expansão durante a sessão com set +H.

Porque é que !! e !$ não funcionam no meu script de shell?

A expansão do histórico só está ativada em shells interativos. Um script é executado de forma não interativa, por isso o shell nunca cria uma lista de histórico e !! permanece na linha como texto simples. Execute echo $- para ver em que tipo de shell está: um shell interativo mostra flags que incluem i e H, enquanto um script não mostra nenhuma delas. Nos scripts, use uma variável ou uma substituição de comandos.

Como mantenho uma palavra-passe fora do meu histórico do bash?

Defina HISTCONTROL=ignorespace ou HISTCONTROL=ignoreboth em ~/.bashrc. Depois, inicie o comando com um espaço e ele nunca será guardado. Confirme primeiro o valor com echo "$HISTCONTROL", porque, se a variável não estiver definida, o espaço inicial não terá efeito. Isto apenas mantém a linha fora de ~/.bash_history. O comando continua visível em ps enquanto é executado, e sudo regista o que executou. Se um segredo já estiver guardado, encontre o respetivo número com history. Em seguida, execute history -d <number> seguido de history -w para reescrever o ficheiro.

#bash#shell#history#productivity#cli