Bash : utiliser !! et !$ dans l’historique
Découvrez les 6 expansions Bash utiles : !!, !$, !*, !n, !mot et la substitution s///, avec :p pour afficher une commande avant son exécution.
Fonctionnement de l’expansion de l’historique de bash
L’expansion de l’historique de bash reconstruit une ligne de commande précédente à partir de l’historique du shell avant que bash ne l’exécute. !! répète la commande précédente. !$ insère le dernier argument de la commande précédente. La substitution est textuelle et s’effectue en premier, avant que bash ne découpe la ligne en mots. Le résultat contient donc exactement les caractères saisis la première fois.
Six formes couvrent presque tous les cas courants sur un serveur :
!!répète toute la ligne précédente, etsudo !!la réexécute en tant que root.!$désigne le dernier argument de la ligne précédente.!*désigne tous les arguments de la ligne précédente.!nexécute l’entrée d’historique numéro n, et!-ncompte à rebours depuis la position actuelle.!stringexécute la commande la plus récente qui commençait parstring.^old^newréexécute la ligne précédente en remplaçant la première occurrence deoldparnew.
Tout ce qui suit est saisi dans un prompt interactif sur votre propre serveur. L’expansion de l’historique est désactivée dans les scripts, comme l’explique la dernière section.
Un long chemin, saisi une seule fois
Voici le cas où cette fonctionnalité est vraiment utile. Vous préparez un répertoire de release et le chemin est assez long pour qu’une deuxième saisie entraîne facilement une faute de frappe.
sudo mkdir -p /srv/www/app/releases/2026-08-07
sudo chown -R deploy:deploy !$
ls -ld !$
sudo -u deploy nano !$/config.envAvant d’exécuter chaque ligne, bash affiche la ligne produite par l’expansion :
sudo chown -R deploy:deploy /srv/www/app/releases/2026-08-07Cet affichage vous sert de contrôle. Lisez-le avant la sortie de la commande, car c’est la seule occasion de voir ce que bash a interprété comme !$.
La chaîne fonctionne parce que bash enregistre dans l’historique la ligne après expansion, et non le !$ que vous avez saisi. La ligne 3 récupère donc son dernier argument dans la ligne 2 après expansion, puis la ligne 4 le récupère dans la ligne 3. La ligne 4 montre également qu’un texte peut suivre un désignateur : !$/config.env devient le chemin suivi de /config.env, car le désignateur de mot s’arrête au /.
Voici un autre exemple dans la même session. Après avoir modifié le fichier, vous voulez lister le répertoire qui le contient :
ls -l !$:h:h est le modificateur head. Il supprime le dernier composant d’un chemin, comme le fait dirname. :t conserve uniquement le dernier composant, :r supprime l’extension et :e conserve uniquement l’extension.
Répétez la dernière commande avec !! et sudo !!
Vous avez oublié sudo, et le gestionnaire de services vous l’indique :
systemctl restart nginxFailed to restart nginx.service: Interactive authentication required.sudo !!Bash remplace !! par le texte de la ligne précédente. Le shell exécute donc sudo systemctl restart nginx.
!! désigne exactement la ligne précédente, quelle qu’elle soit. C’est là que l’accident classique se produit. Vous pensez que la dernière commande est celle qui a échoué, mais vous avez ensuite exécuté un cd ou un history, ou bien la commande ayant échoué se trouve deux lignes plus haut. sudo !! exécute alors la mauvaise commande avec les privilèges root. Affichez d’abord la commande si vous avez un doute :
sudo !!:p:p affiche la ligne développée sans l’exécuter. La ligne affichée est ajoutée à votre historique. Si elle est correcte, une simple commande !! à la ligne suivante l’exécute.
Réutiliser le dernier argument avec !$
!$ représente le dernier mot de la ligne précédente. C’est la forme que vous utiliserez le plus souvent, car le dernier mot correspond généralement à l’élément sur lequel vous travaillez : un chemin ou un nom de service.
sudo systemctl status nginx
sudo systemctl reload !$Deux points surprennent souvent.
Premièrement, !$ représente le dernier mot, et non le dernier argument auquel vous pensiez. Si la ligne précédente se terminait par une redirection, le dernier mot est la cible de la redirection :
sudo nginx -T > /tmp/nginx-dump.conf
less !$Ici, le résultat est correct. En revanche, après journalctl -u nginx > /tmp/log.txt, !$ correspond à /tmp/log.txt, et non à nginx. Lisez la ligne affichée.
Deuxièmement, l’expansion est textuelle : une variable est donc réintroduite sans être développée. Après ls $HOME/backups, !$ vous donne les caractères $HOME/backups, que bash développe ensuite comme un paramètre normal. Retenez bien cet ordre : l’expansion de l’historique s’exécute avant l’expansion des paramètres et avant la substitution de commande avec $( ). Elle ne voit donc jamais les valeurs, uniquement le texte.
Les formes voisines sont également utiles. !^ représente le premier argument, !:2 le deuxième, et !:2-4 une plage. !!:$ est la forme longue de !$.
Si vous préférez afficher le texte avant de l’exécuter, appuyez sur Alt-. (ou sur Esc, puis .). Readline insère directement dans l’invite le dernier argument de la commande précédente, que vous pouvez modifier. Appuyez à nouveau sur cette touche pour revenir au dernier argument de la commande antérieure. Rien ne s’exécute tant que vous n’appuyez pas sur Entrée.
Transmettre tous les arguments avec !*
!* correspond à chaque mot de la ligne précédente, sauf le premier.
stat /srv/www/app/shared/config.env /srv/www/app/shared/secrets.env
sudo chmod 600 !*La formule « sauf le premier » est importante. !* supprime le mot 0 et rien d’autre, donc les options sont transmises avec les chemins. Après ls -l file1 file2, !* vaut -l file1 file2. sudo chmod 600 !* échoue donc, car chmod reçoit -l comme argument. Numérotez les mots à partir de zéro et utilisez plutôt une tranche : !!:2* signifie « du mot 2 jusqu’à la fin ».
Le même piège se présente avec sudo. Après sudo chown deploy:deploy /srv/www/app, le mot 0 est sudo et le mot 1 est chown. !* transmet donc chown deploy:deploy /srv/www/app à la commande suivante. C’est rarement le résultat recherché.
Choisir une commande par sa position avec !n et !-n
history affiche la liste avec un numéro devant chaque entrée.
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 réexécute l’entrée 513. !-2 exécute l’entrée située deux lignes plus haut, en comptant depuis la ligne que vous saisissez maintenant. !-1 et !! désignent donc la même entrée.
Vérifiez les numéros immédiatement avant de les utiliser. !-2 désigne un nouvel élément chaque fois que vous exécutez une commande, y compris une commande ls lancée sans y penser. Les numéros absolus restent stables pendant une session, mais ils ne sont pas identiques lors d’une deuxième session sur le même serveur. Ils changent également lorsque votre prochaine connexion recharge le fichier d’historique. Un numéro mémorisé hier peut donc désigner une autre commande aujourd’hui.
Relancer une commande par préfixe avec !string
!string exécute la dernière commande qui commence par string.
!ssCette commande relance ss -tulpn dans la liste ci-dessus, la vérification des sockets en écoute décrite dans les ports ouverts sur un serveur Linux. !?string? recherche une correspondance n’importe où dans la ligne, et pas seulement au début. Cela est utile lorsque vous vous souvenez d’un argument, mais pas du nom de la commande.
Utilisez un préfixe suffisamment long. !s correspondra à ss, sudo, systemctl ou shutdown, selon celle qui a été exécutée le plus récemment. Vous ne saurez pas laquelle avant son exécution. !string:p affiche la correspondance sans l’exécuter. Si aucune correspondance n’est trouvée, bash affiche bash: !ss: event not found et n’exécute rien. C’est le comportement le plus sûr.
Corriger une faute avec ^ancien^nouveau
sudo systemctl status ngnixUnit ngnix.service could not be found.^ngnix^nginxLe shell réexécute la ligne précédente en remplaçant le premier ngnix par nginx. Seule la première occurrence est modifiée. Pour modifier toutes les occurrences, utilisez la forme longue !!:gs/ngnix/nginx/, où s signifie remplacer et g signifie effectuer le remplacement sur toute la ligne.
Afficher l’expansion avant de l’exécuter
Deux habitudes évitent qu’une expansion exécute une commande que vous n’aviez pas prévue.
La première est :p, que vous avez déjà vue. Ajoutez-la à une expansion pour que bash affiche le résultat au lieu de l’exécuter : !!:p ou !systemctl:p. La ligne affichée est ajoutée à votre historique. Vous pouvez donc exécuter ensuite !! pour lancer ce que vous venez de lire.
La seconde est plus sûre, car elle s’applique à toutes les expansions sans que vous ayez à vous en souvenir :
shopt -s histverifyAjoutez cette ligne à ~/.bashrc. Lorsque histverify est activée, une expansion ne s’exécute pas quand vous appuyez sur Entrée. Bash réécrit la ligne développée dans votre invite afin que vous puissiez la lire et la modifier. Appuyez une seconde fois sur Entrée pour l’exécuter. Cela coûte une frappe et élimine toute cette catégorie d’accidents. Cette fonction nécessite readline. Elle fonctionne donc dans une invite interactive, et nulle part ailleurs.
Ctrl-R est l’autre méthode, et il n’effectue jamais d’expansion. Appuyez sur Ctrl-R et saisissez une partie d’une commande. L’invite devient une invite reverse-i-search et affiche la correspondance la plus récente au fil de votre saisie. Appuyez de nouveau sur Ctrl-R pour remonter vers les correspondances plus anciennes. Entrée exécute la ligne affichée. Ctrl-G annule la recherche et restaure votre invite initiale. La touche Flèche gauche termine la recherche et laisse la ligne trouvée dans votre invite pour que vous puissiez la modifier.
Utilisez Ctrl-R lorsque vous voulez voir la commande avant de l’exécuter. Utilisez !! et !$ lorsque la commande se trouve une ligne plus haut et que vous pouvez encore la lire à l’écran.
Pourquoi echo "done!" affiche-t-il « event not found » ?
echo "deploy done!"bash: !": event not foundL’expansion de l’historique s’exécute avant la résolution des guillemets, et les guillemets doubles ne protègent pas le caractère !. Seuls les guillemets simples et une barre oblique inverse le protègent. Ainsi, echo 'deploy done!' affiche le résultat attendu. Une barre oblique inverse empêche également l’expansion, mais bash la conserve dans la sortie à l’intérieur de guillemets doubles. Les guillemets simples sont donc la solution propre.
Ce problème est particulièrement fréquent avec les mots de passe, car un mot de passe robuste contient souvent !. Une commande telle que mysql -u app -p"S3cret!pass" échoue avec une erreur « event not found » ou, si une entrée de l’historique correspond, envoie silencieusement une chaîne différente. Utilisez des guillemets simples ou, mieux encore, laissez l’outil vous demander le secret. Celui-ci ne figurera ainsi jamais sur la ligne de commande. Cette habitude fait partie de l’hygiène générale de vos comptes : consultez les dix premières minutes sur un nouveau VPS pour savoir comment gérer les clés et les mots de passe sur un nouveau serveur.
Si vous allez coller un bloc de texte contenant de nombreux caractères !, désactivez l’expansion pour la session avec set +H, puis réactivez-la avec set -H.
HISTSIZE, HISTFILESIZE et emplacement de l’historique
Ces deux variables sont souvent confondues, car leurs noms se ressemblent.
HISTSIZEindique le nombre de commandes que le shell en cours conserve en mémoire.HISTFILESIZEindique le nombre de lignes conservées dans le fichier sur disque,~/.bash_historysauf indication contraire deHISTFILE.
Le fichier est écrit à la fermeture du shell, puis tronqué à HISTFILESIZE lignes à ce moment-là. La valeur ~/.bashrc par défaut d’Ubuntu définit ces variables à 1000 et 2000. Vérifiez les valeurs réellement utilisées :
grep HIST ~/.bashrc
echo "$HISTSIZE $HISTFILESIZE $HISTFILE"Augmentez ces deux valeurs si vos recherches avec !string ne retrouvent pas des commandes que vous savez avoir exécutées. Les valeurs 10000 et 20000 sont courantes et consomment presque aucune ressource, car le fichier est en texte brut. Une valeur négative signifie qu’il n’y a aucune limite.
Les horodatages sont utiles lorsque vous revenez sur un serveur après une semaine d’absence :
export HISTTIMEFORMAT='%F %T 'history affiche alors une date et une heure devant chaque entrée, car bash commence par écrire dans le fichier une ligne de commentaire contenant les secondes depuis l’époque Unix avant chaque commande.
Le problème le plus fréquent est la disparition de l’historique lorsque vous utilisez plusieurs terminaux. Chaque shell conserve sa propre liste en mémoire et l’écrit à sa fermeture. Sans histappend, le dernier shell qui se ferme écrase les données enregistrées par les autres. Deux paramètres corrigent ce problème :
shopt -s histappend
export PROMPT_COMMAND='history -a'histappend demande au shell d’ajouter sa liste à la fin du fichier au lieu de remplacer celui-ci. history -a ajoute les nouvelles lignes après chaque invite. Ainsi, une session arrêtée brutalement au lieu d’être fermée correctement conserve tout de même les commandes déjà saisies. La valeur ~/.bashrc par défaut d’Ubuntu définit déjà histappend. Ce point devient plus important à mesure que vous administrez davantage de machines, car l’historique est stocké par utilisateur et par machine. Lorsque vous administrez plusieurs serveurs depuis un même poste de travail, l’!$ que vous recherchez se trouve peut-être simplement sur un autre hôte.
Garder les secrets hors de l’historique bash
HISTCONTROL détermine quelles lignes sont enregistrées.
ignorespaceignore toute ligne qui commence par une espace.ignoredupsignore une ligne identique à la précédente.ignorebothapplique ces deux comportements.erasedupssupprime toutes les copies antérieures de la ligne enregistrée.
export HISTCONTROL=ignorebothLorsque ignorespace est activé, une espace en début de ligne empêche une commande d’entrer dans la liste en mémoire. Elle n’est donc jamais écrite dans le fichier. Vérifiez sa valeur avant de vous y fier avec echo "$HISTCONTROL". L’utilisateur par défaut d’Ubuntu, .bashrc, définit ignoreboth, mais un shell root ou une image minimale peut ne pas le définir. Dans ce cas, l’espace en début de ligne ne sert à rien et la ligne est enregistrée comme les autres.
Soyez conscient de la portée réelle de cette protection. La ligne n’apparaît pas dans ~/.bash_history. Elle reste visible dans la sortie de ps pendant l’exécution du processus. Tout autre utilisateur de la machine peut donc la lire à cet endroit. sudo enregistre la commande exécutée dans le journal système. Le fichier d’historique est en clair. Conservez-le avec le mode 600 et rappelez-vous que toute personne pouvant lire votre répertoire personnel peut consulter vos dernières milliers de commandes.
Si une information sensible figure déjà dans la liste, supprimez l’entrée et réécrivez le fichier :
history
history -d 517
history -whistory -d supprime cette entrée de la mémoire, et history -w écrit la liste actuelle dans le fichier. history -c efface toute la liste. HISTIGNORE est le paramètre associé pour le bruit plutôt que pour les secrets : HISTIGNORE='ls:pwd:history:clear' empêche ces lignes d’apparaître dans la liste afin que vos recherches renvoient des résultats utiles.
Pourquoi l’expansion de l’historique ne fait rien dans un script shell
L’expansion de l’historique concerne les shells interactifs. Un script s’exécute dans un shell non interactif, où la liste d’historique n’est pas activée et où l’expansion est désactivée. !! et !$ restent donc dans la ligne comme du texte ordinaire. Dans un script, sudo !! demande à sudo d’exécuter une commande nommée littéralement !!, ce qui échoue.
Vérifiez le shell dans lequel vous vous trouvez :
echo $-La sortie contient l’ensemble des indicateurs d’options actifs, par exemple himBHs. i signifie que le shell est interactif et H signifie que l’expansion de l’historique est activée. Exécutez la même ligne dans un script : aucune de ces deux lettres n’y apparaît.
C’est la distinction entre les deux contextes de votre utilisation du shell. À l’invite, !$ et Ctrl-R vous évitent de retaper des commandes que vous pouvez encore consulter. Dans un fichier, vous nommez plutôt les éléments : placez le chemin dans une variable ou récupérez la sortie avec une substitution de commande. Un script qui dépendrait de votre historique personnel produirait un résultat différent pour la personne suivante qui l’exécuterait. Ce serait contraire au rôle d’un script.
Pour cette raison, tous les exemples de cette page sont destinés à être saisis à une invite active. Aucun ne se comporte de la même façon s’il est collé dans un fichier .sh.
FAQ
Que fait !! dans bash ?
!! se développe en texte complet de la ligne de commande précédente. sudo !! réexécute donc votre dernière commande avec les privilèges de root. Le développement est textuel et a lieu avant l’analyse de la ligne par bash. Bash affiche la ligne résultante juste avant de l’exécuter. Si vous n’êtes pas certain de la ligne précédente, saisissez d’abord sudo !!:p. :p affiche le résultat du développement sans l’exécuter et ajoute la ligne affichée à votre historique. Une commande !! exécutée ensuite la lance donc.
Comment réutiliser le dernier argument de la commande précédente ?
Utilisez !$. Après sudo mkdir -p /srv/www/app/releases, la ligne ls -ld !$ devient ls -ld /srv/www/app/releases. Cette syntaxe prend le dernier mot de la ligne. Une cible de redirection placée à la fin compte donc comme dernier mot. Vous pouvez aussi lui ajouter du texte : !$/config.env ajoute du texte au chemin, car le désignateur de mot s’arrête au niveau de /. L’alternative interactive est Alt-.. Elle insère le même texte dans l’invite afin que vous puissiez le vérifier avant d’appuyer sur Entrée.
Pourquoi bash affiche-t-il « event not found » quand mon texte contient un point d’exclamation ?
Les guillemets doubles ne protègent pas ! contre le développement de l’historique. echo "done!" demande donc à bash de rechercher un événement dans l’historique, puis bash affiche bash: !": event not found. Les guillemets simples le protègent. Écrivez donc echo 'done!'. Une barre oblique inverse désactive également le développement, mais bash conserve cette barre oblique inverse dans la sortie lorsqu’elle se trouve entre guillemets doubles. Pour coller un long bloc contenant !, désactivez le développement pour la session avec set +H.
Pourquoi !! et !$ ne fonctionnent-ils pas dans mon script shell ?
Le développement de l’historique est activé uniquement dans les shells interactifs. Un script s’exécute de manière non interactive. Le shell ne construit donc pas de liste d’historique et !! reste du texte brut dans la ligne. Exécutez echo $- pour vérifier le type de shell utilisé. Un shell interactif affiche notamment les indicateurs i et H. Un script n’affiche aucun des deux. Dans les scripts, utilisez plutôt une variable ou une substitution de commande.
Comment empêcher l’enregistrement d’un mot de passe dans l’historique de bash ?
Définissez HISTCONTROL=ignorespace ou HISTCONTROL=ignoreboth dans ~/.bashrc. Commencez ensuite la commande par une espace : elle ne sera jamais enregistrée. Vérifiez d’abord la valeur avec echo "$HISTCONTROL", car une espace en début de ligne ne fait rien si la variable n’est pas définie. Cela empêche uniquement l’enregistrement de la ligne dans ~/.bash_history. La commande reste visible dans ps pendant son exécution, et sudo journalise ce qu’il a exécuté. Si un secret est déjà enregistré, trouvez son numéro avec history, puis exécutez history -d <number> suivi de history -w pour réécrire le fichier.