SSD Nodes Learn 🎉 VPS desde $4.99/mes
Guías Matt ConnorPor Matt Connor

Expansión del historial de Bash: !! y !$

Aprende las seis expansiones de historial de Bash más útiles: repetir comandos, reutilizar argumentos y usar :p para mostrar una línea antes de ejecutarla.

Qué hace la expansión del historial de bash

La expansión del historial de bash reconstruye una línea de comandos anterior a partir del historial del shell antes de que bash la ejecute. !! repite el comando anterior. !$ inserta el último argumento del comando anterior. La sustitución es textual y se realiza primero, antes de que bash divida la línea en palabras. Por tanto, el resultado contiene exactamente los caracteres que escribió la primera vez.

Seis formas cubren casi todo lo que hará en un servidor:

  • !! repite toda la línea anterior y sudo !! la vuelve a ejecutar como root.
  • !$ representa el último argumento de la línea anterior.
  • !* representa todos los argumentos de la línea anterior.
  • !n ejecuta la entrada del historial número n y !-n cuenta hacia atrás desde la posición actual.
  • !string ejecuta el comando más reciente que comenzó por string.
  • ^old^new vuelve a ejecutar la línea anterior y reemplaza la primera aparición de old por new.

Todo lo que aparece a continuación se introduce en un prompt interactivo del propio servidor. La expansión del historial está desactivada en los scripts, como se explica en la última sección.

Una ruta larga, escrita una sola vez

Este es el caso que justifica el uso de esta función. Está preparando un directorio de lanzamiento y la ruta es lo bastante larga como para que escribirla una segunda vez pueda introducir un error tipográfico.

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 ejecutar cada línea, bash muestra la línea que produjo la expansión:

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

Ese texto es la comprobación. Léalo antes de revisar la salida del comando, porque es la única oportunidad de ver qué decidió bash que significaba !$.

La cadena funciona porque bash guarda en el historial la línea expandida, no !$, que es lo que escribió. Por tanto, la línea 3 toma su último argumento de la línea 2 expandida, y la línea 4 lo toma de la línea 3. La línea 4 también muestra que puede aparecer texto después de un designador: !$/config.env se convierte en la ruta con /config.env al final, porque el designador de palabra termina en /.

Otro ejemplo de la misma sesión. Después de editar el archivo, quiere listar el directorio que lo contiene:

ls -l !$:h

:h es el modificador de cabecera. Elimina el último componente de una ruta, igual que dirname. :t conserva sólo el último componente, :r elimina la extensión y :e conserva sólo la extensión.

Repetir el último comando con !! y sudo !!

Olvida sudo y el gestor de servicios lo indica:

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

Bash sustituye !! por el texto de la línea anterior, por lo que el shell ejecuta sudo systemctl restart nginx.

!! es exactamente la línea anterior, independientemente de cuál fuera esa línea. Aquí ocurre el accidente clásico. Cree que el último comando era el que falló, pero después ejecutó un cd, o un history, o el comando que falló estaba dos líneas más atrás. sudo !! ejecuta entonces el comando equivocado con privilegios de root. Muéstrelo primero si no está seguro:

sudo !!:p

:p muestra la línea expandida y no la ejecuta. La línea mostrada se añade al historial, por lo que, si es correcta, un !! en la línea siguiente la ejecuta.

Reutilizar el último argumento con !$

!$ es la última palabra de la línea anterior. Es la forma que usará con más frecuencia, porque la última palabra suele ser el elemento en el que está trabajando: una ruta o un nombre de servicio.

sudo systemctl status nginx
sudo systemctl reload !$

Aquí hay dos aspectos que suelen sorprender.

Primero, !$ es la última palabra, no el último argumento que tenía en mente. Si la línea anterior terminaba en una redirección, la última palabra es el destino de la redirección:

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

En este caso, el resultado es el esperado. Pero después de journalctl -u nginx > /tmp/log.txt, !$ es /tmp/log.txt y no nginx. Lea la línea mostrada.

Segundo, la expansión es textual, por lo que una variable vuelve sin expandirse. Después de ls $HOME/backups, !$ le proporciona los caracteres $HOME/backups, que bash vuelve a expandir como un parámetro normal. Mantenga presente ese orden: la expansión del historial se ejecuta antes de la expansión de parámetros y antes de la sustitución de comandos con $( ), por lo que nunca ve valores, sólo texto.

Conviene conocer las formas relacionadas. !^ es el primer argumento, !:2 es el segundo y !:2-4 es un intervalo. !!:$ es la forma larga de escribir !$.

Si prefiere ver el texto antes de ejecutarlo, pulse Alt-. (o Esc y después .). Readline inserta directamente en el indicador el último argumento del comando anterior, donde puede editarlo. Púlselo de nuevo para retroceder hasta el último argumento del comando anterior a ese. No se ejecuta nada hasta que pulse Enter.

Pase todos los argumentos con !*

!* contiene todas las palabras de la línea anterior excepto la primera.

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

La expresión «excepto la primera» es importante. !* elimina la palabra 0 y nada más, por lo que las opciones se pasan junto con las rutas. Después de ls -l file1 file2, !* es -l file1 file2, por lo que sudo chmod 600 !* falla porque chmod recibe -l como argumento. Numere las palabras desde cero y use un segmento en su lugar: !!:2* significa desde la palabra 2 hasta el final.

La misma trampa aparece con sudo. Después de sudo chown deploy:deploy /srv/www/app, la palabra 0 es sudo y la palabra 1 es chown, por lo que !* pasa chown deploy:deploy /srv/www/app al comando siguiente. Casi nunca es lo que quería.

Elija un comando por posición con !n y !-n

history muestra la lista con un número delante 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 vuelve a ejecutar la entrada 513. !-2 ejecuta la entrada de hace dos líneas, contadas desde la línea que está escribiendo ahora, por lo que !-1 y !! significan lo mismo.

Compruebe los números justo antes de usarlos. !-2 apunta a un lugar distinto cada vez que ejecuta algo, incluido un ls que haya ejecutado sin pensarlo. Los números absolutos son estables dentro de una sesión, pero no son los mismos en una segunda sesión en el mismo equipo ni después de que el siguiente inicio de sesión vuelva a cargar el archivo de historial. Un número que memorizó ayer apunta hoy a un comando distinto.

Volver a ejecutar un comando mediante el prefijo con !string

!string ejecuta el comando más reciente que empieza por string.

!ss

Esto vuelve a ejecutar ss -tulpn de la lista anterior, la comprobación de sockets en escucha descrita en qué puertos están abiertos en un servidor Linux. !?string? busca coincidencias en cualquier posición de la línea, no sólo al principio. Esto resulta útil cuando recuerda un argumento, pero no el nombre del comando.

Use un prefijo largo. !s puede coincidir con ss, sudo, systemctl o shutdown, según cuál se haya ejecutado más recientemente. No sabrá cuál es hasta que se ejecute. !string:p muestra la coincidencia sin ejecutarla. Si no hay ninguna coincidencia, bash muestra bash: !ss: event not found y no ejecuta nada. Ese es el resultado seguro.

Corrija un error tipográfico con ^old^new

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

El shell vuelve a ejecutar la línea anterior y reemplaza el primer ngnix por nginx. Sólo se cambia la primera coincidencia. Para cambiar todas las coincidencias, use la forma larga !!:gs/ngnix/nginx/, donde s significa sustituir y g indica hacerlo en toda la línea.

Mostrar la expansión antes de ejecutarla

Dos hábitos evitan que una expansión ejecute algo que no pretendía.

El primero es :p, que ya ha visto. Añádalo a una expansión y bash mostrará el resultado en lugar de ejecutarlo: !!:p o !systemctl:p. La línea mostrada se guarda en el historial, por lo que !! la ejecuta después de que la haya leído.

El segundo es más seguro, porque se aplica a todas las expansiones sin que tenga que recordar nada:

shopt -s histverify

Añada esa línea a ~/.bashrc. Con histverify definido, una expansión no se ejecuta al pulsar Enter. Bash vuelve a escribir la línea expandida en el prompt para que pueda leerla y editarla. Después, pulse Enter una segunda vez para ejecutarla. Sólo requiere una pulsación adicional y elimina toda esta clase de accidentes. Necesita readline, por lo que funciona en un prompt interactivo y en ningún otro contexto.

Ctrl-R es la otra forma de trabajar y nunca expande nada. Pulse Ctrl-R y escriba parte de un comando. El prompt cambia a un prompt de reverse-i-search y muestra la coincidencia más reciente a medida que escribe. Pulse Ctrl-R de nuevo para retroceder a coincidencias anteriores. Enter ejecuta la línea que se muestra. Ctrl-G cancela la búsqueda y recupera el prompt original. La tecla de flecha izquierda finaliza la búsqueda y deja la línea coincidente en el prompt para editarla.

Use Ctrl-R cuando quiera ver primero el comando. Use !! y !$ cuando el comando tenga una línea de antigüedad y todavía pueda leerlo en pantalla.

Por qué echo "done!" muestra event not found

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

La expansión del historial se ejecuta antes de resolver las comillas, y las comillas dobles no protegen el carácter !. Sólo lo protegen las comillas simples y la barra inversa. Por eso echo 'deploy done!' muestra el resultado esperado. La barra inversa también detiene la expansión, pero dentro de comillas dobles bash deja la barra inversa en la salida. Por tanto, las comillas simples son la solución más limpia.

Esto afecta especialmente a las contraseñas, porque una contraseña segura suele contener !. Un comando como mysql -u app -p"S3cret!pass" puede fallar con un error event not found o, si alguna entrada del historial coincide, enviar silenciosamente una cadena diferente. Use comillas simples o, mejor aún, deje que la herramienta solicite la contraseña. Así, el secreto nunca aparece en la línea de comandos. Este hábito forma parte de la higiene de sus cuentas. Consulte los primeros diez minutos en un VPS nuevo para saber cómo gestionar claves y contraseñas en un servidor recién instalado.

Si está a punto de pegar un bloque de texto con muchos caracteres !, desactive la expansión durante la sesión con set +H y vuelva a activarla con set -H.

HISTSIZE, HISTFILESIZE y dónde se guarda el historial

Son dos variables, y suelen confundirse porque sus nombres son parecidos.

  • HISTSIZE indica cuántos comandos conserva en memoria el shell en ejecución.
  • HISTFILESIZE indica cuántas líneas se conservan en el archivo del disco, ~/.bash_history salvo que HISTFILE indique lo contrario.

El archivo se escribe cuando el shell termina y, en ese momento, se trunca a HISTFILESIZE líneas. El valor predeterminado de ~/.bashrc en Ubuntu establece 1000 y 2000. Compruebe qué valores tiene realmente:

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

Aumente ambos valores si las búsquedas de !string no encuentran comandos que sabe que ejecutó. Los valores 10000 y 20000 son habituales y apenas consumen recursos, porque el archivo es texto sin formato. Un valor negativo significa que no hay ningún límite.

Las marcas de tiempo ayudan cuando vuelve a un servidor después de una semana:

export HISTTIMEFORMAT='%F %T '

history muestra entonces la fecha y la hora delante de cada entrada, porque bash empieza a escribir una línea de comentario con los segundos desde epoch antes de cada comando del archivo.

Una queja habitual es que el historial desaparece cuando se usa más de un terminal. Cada shell mantiene su propia lista en memoria y la escribe al terminar. Por tanto, sin histappend, el último shell que se cierra sobrescribe lo que guardaron los demás. Dos opciones solucionan este problema:

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

histappend hace que el shell añada su lista al final del archivo en lugar de reemplazarlo. history -a añade nuevas líneas después de cada prompt, de modo que una sesión terminada por la fuerza, en lugar de cerrarse correctamente, también conserva sus comandos. El valor predeterminado de ~/.bashrc en Ubuntu ya establece histappend. Esto es más importante cuantos más equipos administre, porque el historial se guarda por usuario y por equipo. Por eso, cuando está administrando varios servidores desde una estación de trabajo, el !$ que busca puede estar simplemente en otro host.

Cómo evitar que los secretos queden en el historial de bash

HISTCONTROL determina qué líneas se guardan.

  • ignorespace descarta cualquier línea que empiece con un espacio.
  • ignoredups descarta una línea idéntica a la anterior.
  • ignoreboth hace ambas cosas.
  • erasedups elimina todas las copias anteriores de la línea que se va a guardar.
export HISTCONTROL=ignoreboth

Con ignorespace activo, un espacio inicial mantiene un comando fuera de la lista en memoria, por lo que tampoco llega al archivo. Compruebe el valor antes de confiar en este comportamiento con echo "$HISTCONTROL". El usuario predeterminado de Ubuntu, .bashrc, establece ignoreboth, pero un shell de root o una imagen mínima pueden dejarlo sin definir. En ese caso, el espacio inicial no tiene ningún efecto y la línea se almacena como cualquier otra.

Sea consciente de lo que esto protege. La línea no aparece en ~/.bash_history. Mientras el proceso está en ejecución, sigue siendo visible en la salida de ps, por lo que cualquier otro usuario del sistema puede leerla allí. sudo registra en el registro del sistema el comando que se ejecutó. El archivo de historial es texto sin formato. Manténgalo con permisos 600 y recuerde que cualquiera que pueda leer su directorio personal puede leer sus últimos miles de comandos.

Si ya hay información confidencial en la lista, elimine la entrada y vuelva a escribir el archivo:

history
history -d 517
history -w

history -d elimina esa entrada de la memoria y history -w escribe la lista actual sobre el archivo. history -c borra toda la lista. HISTIGNORE es el parámetro relacionado con el ruido, no con los secretos: HISTIGNORE='ls:pwd:history:clear' mantiene esas líneas fuera de la lista para que las búsquedas devuelvan resultados útiles.

Por qué la expansión del historial no hace nada en un script de shell

La expansión del historial pertenece a los shells interactivos. Un script se ejecuta en un shell no interactivo, donde la lista del historial no está habilitada y la expansión está desactivada, por lo que !! y !$ permanecen en la línea como texto normal. sudo !! dentro de un script pide a sudo que ejecute literalmente un comando llamado !!, y falla.

Compruebe cualquier shell en el que esté trabajando:

echo $-

La salida es el conjunto de indicadores de opciones actuales, algo parecido a himBHs. i significa que el shell es interactivo y H significa que la expansión del historial está habilitada. Ejecute la misma línea dentro de un script y ninguna de las dos letras aparecerá.

Esta es la diferencia entre las dos partes de su trabajo con el shell. En el indicador, !$ y Ctrl-R permiten reutilizar comandos que todavía puede ver. En un archivo, debe nombrar los elementos: almacene la ruta en una variable o capture la salida mediante sustitución de comandos. Un script que dependiera de su historial personal se comportaría de forma distinta para la siguiente persona que lo ejecutara, justo lo contrario de la finalidad de un script.

Por ese motivo, todos los ejemplos de esta página están pensados para escribirse en un indicador activo. Ninguno se comporta igual si se pega en un archivo .sh.

FAQ

¿Qué hace !! en bash?

!! se expande al texto completo de la línea de comando anterior, por lo que sudo !! vuelve a ejecutar el último comando como root. La expansión es textual y se realiza antes de que bash analice la línea. bash muestra la línea completa justo antes de ejecutarla. Si no está seguro de cuál era la línea anterior, escriba primero sudo !!:p. :p muestra la expansión sin ejecutarla y añade la línea mostrada al historial, por lo que un !! posterior la ejecuta.

¿Cómo reutilizo el último argumento del comando anterior?

Use !$. Después de sudo mkdir -p /srv/www/app/releases, la línea ls -ld !$ se convierte en ls -ld /srv/www/app/releases. Toma la última palabra de la línea, por lo que un destino de redirección al final cuenta como la última palabra. También puede añadirle texto: !$/config.env lo añade a la ruta, porque el designador de palabra se detiene en /. La alternativa interactiva es Alt-., que inserta el mismo texto en el prompt para que pueda leerlo antes de pulsar Enter.

¿Por qué bash muestra "event not found" cuando el texto contiene un signo de exclamación?

Las comillas dobles no protegen ! frente a la expansión del historial, por lo que echo "done!" hace que bash busque un evento del historial y muestre bash: !": event not found. Las comillas simples sí lo protegen, por lo que debe escribir echo 'done!'. Una barra inversa también detiene la expansión, pero dentro de comillas dobles bash deja la barra inversa en la salida. Para pegar un bloque largo que contenga !, desactive la expansión durante la sesión con set +H.

¿Por qué !! y !$ no funcionan en mi script de shell?

La expansión del historial sólo está habilitada en shells interactivos. Un script se ejecuta de forma no interactiva, por lo que el shell nunca crea una lista del historial y !! queda en la línea como texto normal. Ejecute echo $- para comprobar en qué tipo de shell está: un shell interactivo muestra indicadores que incluyen i y H, mientras que un script no muestra ninguno. En los scripts, use una variable o una sustitución de comandos.

¿Cómo evito que una contraseña se guarde en el historial de bash?

Establezca HISTCONTROL=ignorespace o HISTCONTROL=ignoreboth en ~/.bashrc. Después, inicie el comando con un espacio y no se guardará. Confirme primero el valor con echo "$HISTCONTROL", porque si no está establecido, el espacio inicial no hace nada. Esto sólo evita que la línea se guarde en ~/.bash_history. El comando seguirá siendo visible en ps mientras se ejecuta, y sudo registra lo que ejecuta. Si el secreto ya está guardado, busque su número con history y ejecute history -d <number> seguido de history -w para volver a escribir el archivo.

#bash#shell#history#productivity#cli