Bashの履歴展開で!!と!$を使う方法
Bashの履歴展開で使う6つの形式を紹介します。!!で直前のコマンド、!$で最後の引数を再利用し、:pで実行前に内容を確認できます。
Bash の履歴展開の動作
Bash の履歴展開は、bash が実行する前に、シェル履歴から以前のコマンドラインを再構成します。!! は直前のコマンドを繰り返します。!$ は直前のコマンドの最後の引数を挿入します。この置換はテキストとして行われ、bash が行を単語に分割する前に実行されます。そのため、最初に入力した文字がそのまま再利用されます。
サーバー上で使用する操作のほとんどは、次の 6 つの形式で対応できます。
!!は直前の行全体を繰り返し、sudo !!はそれを root として再実行します。!$は直前の行の最後の引数です。!*は直前の行のすべての引数です。!nは番号 n の履歴エントリを実行し、!-nは現在位置から逆方向に数えます。!stringはstringで始まる直近のコマンドを実行します。^old^newは直前の行を再実行し、最初に現れるoldをnewに置き換えます。
以下はすべて、自分のサーバー上の対話型プロンプトで入力します。スクリプトでは履歴展開が無効になっています。この点については最後のセクションで説明します。
一度入力した長いパスを再利用する
この機能が特に役立つ例を見てみます。リリース用ディレクトリを準備していて、パスが長いため、2 回目に入力するとタイプミスが起きやすい状況です。
sudo mkdir -p /srv/www/app/releases/2026-08-07
sudo chown -R deploy:deploy !$
ls -ld !$
sudo -u deploy nano !$/config.env各行を実行する前に、bash は展開後の行を表示します。
sudo chown -R deploy:deploy /srv/www/app/releases/2026-08-07この表示が確認材料になります。コマンドの出力を見る前に表示を読みます。bash が !$ をどのように解釈したかを確認できる唯一の機会だからです。
この連鎖が機能するのは、bash が入力した !$ ではなく、展開後の行を履歴に保存するためです。そのため、3 行目は展開後の 2 行目から最後の引数を取得し、4 行目は 3 行目から取得します。4 行目では、デザイン指定子の後にテキストを続けられることも確認できます。!$/config.env は末尾に /config.env が付いたパスになります。ワードデザイン指定子は / で終わるためです。
同じセッションで、もう 1 つ例を示します。ファイルを編集した後、そのファイルを含むディレクトリを一覧表示するとします。
ls -l !$:h:h はヘッド修飾子です。パスの最後の構成要素を取り除きます。これは dirname と同じ処理です。:t は最後の構成要素だけを残し、:r は拡張子を削除し、:e は拡張子だけを残します。
!! と sudo !! で直前のコマンドを再実行する
sudo を忘れており、サービスマネージャーもそのことを示しています。
systemctl restart nginxFailed to restart nginx.service: Interactive authentication required.sudo !!Bash は !! を直前の行の内容に置き換えるため、シェルは sudo systemctl restart nginx を実行します。
!! は、その行が何であったかにかかわらず、直前の行そのものです。ここで典型的な事故が起きます。失敗したコマンドが直前のコマンドだと思っていても、その後に cd や history を実行していたり、失敗したコマンドが2行前だったりすることがあります。その場合、sudo !! は誤ったコマンドを root の権限で実行します。確信がない場合は、まず内容を表示してください。
sudo !!:p:p は展開後の行を表示するだけで、実行はしません。表示された行は履歴に追加されるため、内容が正しければ、次の行で通常の !! を実行すると、そのコマンドが実行されます。
最後の引数を !$ で再利用する
!$ は、直前の行の最後の単語です。最後の単語は通常、操作対象のパスやサービス名であるため、最もよく使用する形式です。
sudo systemctl status nginx
sudo systemctl reload !$ここでは、2 つの点で意外に感じることがあります。
1 つ目は、!$ が、意図した最後の引数ではなく、最後の 単語 であることです。直前の行がリダイレクトで終わっている場合、最後の単語はリダイレクト先になります。
sudo nginx -T > /tmp/nginx-dump.conf
less !$この例では問題ありません。しかし、journalctl -u nginx > /tmp/log.txt の後では、!$ は nginx ではなく /tmp/log.txt になります。エコーされた行を確認してください。
2 つ目は、展開がテキストとして行われるため、変数は展開されない状態で戻ることです。ls $HOME/backups の後では、!$ によって文字列 $HOME/backups が得られ、bash はそれを通常のパラメーターとして再度展開します。この順序を覚えておいてください。履歴展開はパラメーター展開や $( ) によるコマンド置換 より前に実行されるため、値ではなくテキストだけを扱います。
隣接する形式も覚えておくと便利です。!^ は最初の引数、!:2 は 2 番目の引数、!:2-4 は範囲です。!!:$ は !$ の長い書き方です。
実行する前にテキストを確認したい場合は、Alt-. を押してください(または Esc を押してから . を押します)。Readline により、直前のコマンドの最後の引数がプロンプトに直接挿入されるため、編集できます。もう一度押すと、その 1 つ前のコマンドの最後の引数に戻ります。Enter を押すまで、何も実行されません。
!*
の指定では、すべての引数をそのまま渡します。
!* は、直前の行の最初の単語を除くすべての単語です。
stat /srv/www/app/shared/config.env /srv/www/app/shared/secrets.env
sudo chmod 600 !*「最初の単語を除く」という部分が重要です。!* は単語 0 だけを削除し、それ以外は削除しないため、オプションもパスと一緒に渡されます。ls -l file1 file2 の後では、!* は -l file1 file2 です。そのため、sudo chmod 600 !* は失敗します。chmod が -l を引数として受け取るためです。単語を 0 から数え、代わりにスライスを指定してください。!!:2* は、単語 2 から末尾までを意味します。
sudo でも同じ問題が発生します。sudo chown deploy:deploy /srv/www/app の後では、単語 0 は sudo、単語 1 は chown です。そのため、!* は次のコマンドに chown deploy:deploy /srv/www/app を渡します。これは、ほとんどの場合に意図した動作ではありません。
位置でコマンドを選択するには !n と !-n を使用します
history は、各エントリの前に番号を付けて一覧を表示します。
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 はエントリ 513 を再実行します。!-2 は、入力中の行を基準にして 2 行前のエントリを実行するため、!-1 と !! は同じ意味です。
使用する直前に番号を確認してください。何かを実行するたびに、!-2 が指す対象は変わります。考えずに実行した ls も対象になります。絶対番号は 1 つのセッション内では安定していますが、同じマシンで 2 回目のセッションを開始すると同じ番号にはなりません。また、次回のログイン時に履歴ファイルを読み込んだ後も同じ番号にはなりません。昨日覚えた番号が、今日は別のコマンドを指すことがあります。
!string を接頭辞にしてコマンドを再実行する
`!string は、string` で始まる直近のコマンドを実行します。
!ssこれは、上の一覧にある `ss -tulpn を再実行します。これは、[[what-are-ports-linux|Linux サーバーで開いているポートを確認する方法]]で説明した、待ち受けソケットの確認コマンドです。!?string?` は行頭ではなく行内のどこにでも一致します。そのため、コマンド名を思い出せなくても引数を覚えていれば検索できます。
接頭辞は長めに指定してください。`!s は、直近に実行されたものから順に ss、sudo、systemctl、shutdown のいずれにも一致する可能性があります。実行されるまで、どれに一致するかは分かりません。!string:p を使うと、一致したコマンドを実行せずに表示できます。一致するコマンドがない場合、bash は bash: !ss: event not found` を表示し、何も実行しません。これは安全な動作です。
^old^new で 1 つの誤りを修正する
sudo systemctl status ngnixUnit ngnix.service could not be found.^ngnix^nginxシェルは、直前の行にある最初の ngnix を nginx に置き換えて再実行します。変更されるのは最初に一致した箇所だけです。すべての一致箇所を変更するには、長い形式の !!:gs/ngnix/nginx/ を使用します。s は置換を意味し、g は行全体に適用することを意味します。
実行前に展開結果を表示する
意図しないコマンドを展開によって実行してしまう事故は、2 つの習慣で防げます。
1 つ目は、すでに説明した :p です。展開にこれを追加すると、bash は結果を実行せずに表示します。!!:p または !systemctl:p とします。表示された行は履歴に入るため、その後で !! を実行すると、直前に確認した内容を実行できます。
2 つ目は、何かを覚えておかなくてもすべての展開に適用できる、より強力な方法です。
shopt -s histverifyこの行を ~/.bashrc に追加します。histverify を設定すると、Enter を押しても展開結果は実行されません。Bash は展開後の行をプロンプトに戻すため、その内容を確認して編集できます。実行するには、もう一度 Enter を押します。キー入力は 1 回増えますが、この種の事故をまとめて防げます。readline が必要なため、対話形式のプロンプトでのみ機能します。
もう 1 つの方法は Ctrl-R です。この方法では展開は一切行われません。Ctrl-R を押して、コマンドの一部を入力します。プロンプトが reverse-i-search プロンプトに変わり、入力するたびに最新の一致結果が表示されます。Ctrl-R をもう一度押すと、古い一致結果に移動します。Enter を押すと、表示中の行が実行されます。Ctrl-G を押すと検索をキャンセルし、元のプロンプトに戻ります。左矢印キーを押すと検索が終了し、一致した行が編集可能な状態でプロンプトに残ります。
実行前にコマンドを確認したい場合は、Ctrl-R を使います。コマンドが 1 行前のもので、まだ画面上で読める場合は、!! と !$ を使います。
echo "done!" で event not found と表示されるのはなぜか
echo "deploy done!"bash: !": event not found履歴展開は引用符の解釈より前に実行されるため、二重引用符では ! 文字を保護できません。保護できるのは単一引用符とバックスラッシュだけです。そのため、echo 'deploy done!' は期待どおりに出力されます。バックスラッシュでも展開を停止できますが、二重引用符内では bash がバックスラッシュを出力に残します。したがって、単一引用符を使うのが適切です。
この問題はパスワードで特に起きやすくなります。強力なパスワードには ! が含まれることが多いためです。mysql -u app -p"S3cret!pass" のようなコマンドは event-not-found エラーで失敗するか、履歴項目が偶然一致すると、別の文字列を気付かないまま送信します。単一引用符を使うか、より確実な方法として、ツールに入力を要求させてください。そうすれば、Secret がコマンドラインに一度も現れません。この習慣はアカウント管理の基本の一部です。新しい VPS で鍵とパスワードをどのように扱うべきかは、新しい VPS の最初の 10 分を参照してください。
! 文字を多数含むテキストブロックを貼り付ける場合は、set +H でセッション中の展開を無効にし、set -H で再び有効にします。
HISTSIZE、HISTFILESIZE と履歴の保存場所
2 つの変数があり、名前が似ているため混同されがちです。
HISTSIZEは、実行中の shell がメモリに保持するコマンド数です。HISTFILESIZEは、ディスク上のファイルに保持する行数です。~/.bash_historyただし、HISTFILEで別の値を指定している場合を除きます。
ファイルは shell の終了時に書き込まれ、その時点で HISTFILESIZE 行に切り詰められます。Ubuntu のデフォルトの ~/.bashrc は 1000 と 2000 を設定します。実際の設定値を確認します。
grep HIST ~/.bashrc
echo "$HISTSIZE $HISTFILESIZE $HISTFILE"実行したはずのコマンドが !string の検索で見つからない場合は、両方の値を増やします。10000 と 20000 は一般的な値で、ファイルはプレーンテキストのため、ほとんど容量を消費しません。負の値を指定すると、制限なしになります。
1 週間ぶりにサーバーへ戻る場合は、タイムスタンプが役立ちます。
export HISTTIMEFORMAT='%F %T 'history を設定すると、各履歴項目の前に日時が表示されます。bash が各コマンドの前に、epoch 秒を記録したコメント行を書き込むためです。
よくある問題は、複数の端末を使うと履歴が欠落することです。各 shell はそれぞれ独自の履歴をメモリに保持し、終了時にファイルへ書き込みます。そのため、histappend がないと、最後に終了した shell が他の shell の保存内容を上書きします。次の 2 つの設定で解決できます。
shopt -s histappend
export PROMPT_COMMAND='history -a'histappend を設定すると、shell は履歴をファイルの末尾に追加し、ファイルを置き換えません。history -a は各プロンプトの後に新しい行を追加するため、正常に終了せずに shell が強制終了された場合でも、そのセッションのコマンドが履歴に残ります。Ubuntu のデフォルトの ~/.bashrc には、すでに histappend が設定されています。履歴はユーザーごと、マシンごとに保存されるため、運用するマシンが増えるほど、この点が重要になります。つまり、1 台のワークステーションから複数のサーバーを管理する場合、必要な !$ は別のホストに保存されている可能性があります。
bash の履歴に Secret を残さない
HISTCONTROL は、どの行を保存するかを決めます。
ignorespaceは、スペースで始まる行を保存しません。ignoredupsは、直前の行と同じ行を保存しません。ignorebothは、上記の両方を実行します。erasedupsは、保存する行と同じ古い履歴をすべて削除します。
export HISTCONTROL=ignorebothignorespace が有効な場合、先頭にスペースを 1 つ付けると、コマンドはメモリ上の履歴一覧に入りません。そのため、ファイルにも保存されません。利用する前に、echo "$HISTCONTROL" で値を確認してください。Ubuntu のデフォルトユーザー .bashrc は ignoreboth を設定します。ただし、root shell や最小構成のイメージでは未設定の場合があります。その場合、先頭のスペースは機能せず、行は通常どおり保存されます。
この機能で何が保護されるかを正しく理解してください。行は ~/.bash_history には残りません。ただし、プロセスの実行中は ps の出力から確認できます。そのため、同じサーバー上の別のユーザーがその出力を読めます。sudo は、実行したコマンドを system log に記録します。履歴ファイルは plain text です。モードを 600 に設定し、home directory を読み取れるユーザーは直近数千件のコマンドも読めることを覚えておいてください。
機密情報がすでに履歴一覧にある場合は、その項目を削除してファイルを書き換えます。
history
history -d 517
history -whistory -d はその項目をメモリ上の履歴から削除し、history -w は現在の履歴一覧でファイルを上書きします。history -c は履歴一覧全体を消去します。HISTIGNORE は Secret ではなく不要な出力に関係する設定です。HISTIGNORE='ls:pwd:history:clear' により、そのような行を履歴一覧から除外できるため、検索結果から有用な一致を見つけやすくなります。
シェルスクリプトで履歴展開が何もしない理由
履歴展開は対話シェルの機能です。スクリプトは非対話シェルで実行されるため、履歴リストが有効になっておらず、展開も無効です。そのため、!! と !$ は通常のテキストとして行に残ります。スクリプト内の sudo !! は、sudo に対して !! という名前のコマンドをそのまま実行するよう要求し、失敗します。
現在使用しているシェルを確認します。
echo $-出力には現在のオプションフラグが表示され、himBHs のようになります。i はシェルが対話的であることを示し、H は履歴展開が有効であることを示します。同じ行をスクリプト内で実行すると、どちらの文字も表示されません。
ここが、シェル操作の2つの領域の境目です。プロンプトでは、まだ画面上で確認できるコマンドに対して、!$ や Ctrl-R を使うと入力を省略できます。ファイル内では、代わりに対象を明示します。パスを変数に格納するか、コマンド置換 で出力を取得します。個人の履歴に依存するスクリプトは、別の人が実行すると異なる動作をするため、スクリプト本来の目的に反します。
そのため、このページの例はすべて、起動中のプロンプトで入力することを前提としています。.sh ファイルに貼り付けても、同じ動作にはなりません。
FAQ
bash で !! は何をしますか?
!! は直前のコマンドライン全体に展開されるため、sudo !! を実行すると最後のコマンドを root として再実行します。展開はテキストとして行われ、bash が行を解析する前に完了します。bash は実行直前に、展開後の行を表示します。直前の行が不明な場合は、最初に sudo !!:p と入力してください。:p は展開結果を実行せずに表示し、その表示した行を履歴にも追加します。そのため、続けて !! を実行すると、その行が実行されます。
直前のコマンドの最後の引数を再利用するにはどうすればよいですか?
!$ を使用します。sudo mkdir -p /srv/www/app/releases の後では、行 ls -ld !$ は ls -ld /srv/www/app/releases になります。行の最後の word を取得するため、末尾にあるリダイレクト先も最後の word として扱われます。テキストを追加することもできます。!$/config.env はパスに追加されます。word 指定子が / で停止するためです。対話操作では Alt-. も使用できます。これは同じテキストをプロンプトに挿入するため、Enter を押す前に内容を確認できます。
テキストに感嘆符が含まれると、bash が「event not found」と表示するのはなぜですか?
二重引用符では ! を履歴展開から保護できません。そのため echo "done!" によって bash は履歴イベントを探し、bash: !": event not found を表示します。単一引用符では保護できるため、echo 'done!' と記述してください。バックスラッシュでも展開を停止できますが、二重引用符の内部では bash が出力にバックスラッシュを残します。! を含む長いブロックを貼り付ける場合は、set +H でセッション中の展開を無効にしてください。
シェルスクリプトで !! と !$ が機能しないのはなぜですか?
履歴展開が有効になるのは対話シェルだけです。スクリプトは非対話的に実行されるため、シェルは履歴リストを作成せず、!! は行内に通常のテキストとして残ります。echo $- を実行すると、現在のシェルがどちらかを確認できます。対話シェルでは i と H を含むフラグが表示され、スクリプトではどちらも表示されません。スクリプトでは、代わりに変数またはコマンド置換を使用してください。
bash の履歴にパスワードを残さないようにするにはどうすればよいですか?
~/.bashrc で HISTCONTROL=ignorespace または HISTCONTROL=ignoreboth を設定し、その後にコマンドを単一のスペースで始めると、その行は保存されません。最初に echo "$HISTCONTROL" で値を確認してください。設定されていない場合、先頭のスペースは機能しません。これは ~/.bash_history から行を除外するだけです。実行中のコマンドは ps から引き続き確認でき、sudo には実行した内容が記録されます。Secret がすでに保存されている場合は、history で番号を調べ、その後 history -d <number> に続けて history -w を実行してファイルを書き換えてください。