history expansion(履歴展開)入門 - !! や !$ でコマンド再利用を高速化

history expansion(履歴展開)入門 - !! や !$ でコマンド再利用を高速化

この記事でわかること

  • history expansion(履歴展開)が「過去コマンドを呼び出す記法」だと分かる
  • !! / !$ でコマンドや引数を一瞬で再利用できる
  • !string / !:n / :h :t :r で履歴を細かく取り出せる
  • ^old^new^ のクイック置換と、histverify で安全に使う方法が分かる
リナ: ライニー先輩、sudo !! とか cd !$ ってよく見るんですけど、あの ! マークって何なんですか?
ライニー先輩: それは「history expansion(履歴展開)」っていう bash の機能だよ。! から始まる記法を打つと、過去のコマンドや引数を呼び出して再利用できるんだ。タイプ量がぐっと減るよ。

1. history expansion とは何か?

結論: history expansion は ! で始まる記法を、過去のコマンドや引数に展開する bash の機能。タイプ量を減らし再入力ミスを防ぐ。

リナ: 「展開」ってどういうことですか?
ライニー先輩: コマンドを実行する前に、bash が !! のような記法を「実際のコマンド文字列」に置き換えることだよ。例えば !! は直前のコマンド全体に置き換わるんだ。

history expansion は大きく 3 つの部品でできています。

部品 役割
イベント指定子(event) どのコマンドを呼ぶか !! / !42
ワード指定子(word) そのコマンドのどの単語を取り出すか !$ / !^
修飾子(modifier) 取り出した単語をどう加工するか :h / :t

history expansion は主に bash の対話シェル で有効です。スクリプト内ではデフォルト無効なので、「便利だけどスクリプトには書かない」と覚えておきましょう。

2. イベント指定子:どのコマンドを呼ぶか

結論: !! は直前、!n は履歴番号 n、!string は string で始まる直近、!?string? は string を含む直近のコマンドを指す。

リナ: まず「どのコマンドを呼ぶか」を決めるんですね。
ライニー先輩: そう。一番よく使うのが !! =直前のコマンド。sudo を付け忘れたときの定番テクだよ。

!! — 直前のコマンド全体

$ apt update
E: Could not open lock file /var/lib/dpkg/lock-frontend ...

$ sudo !!
sudo apt update

!n / !-n — 履歴番号で指定

$ history | grep tar
  765  tar -czf backup.tar.gz /etc

$ !765
tar -czf backup.tar.gz /etc

!n は履歴番号 n のコマンド、!-n は「n 個前」のコマンドを指します(!-1!! と同じ)。

!string — string で始まる直近のコマンド

$ !ssh
ssh user@server

!string は「string で始まる、もっとも最近のコマンド」を再実行します。

!?string? — string を含む直近のコマンド

$ !?server?
ssh user@server

!?string? は「string をどこかに含む直近のコマンド」を探します。先頭以外にもマッチする点が !string との違いです。

!string は確認なしで即実行されます。意図しない古いコマンドが呼ばれることもあるので、不安なときは次章の :p(表示のみ)で中身を確かめましょう。

3. ワード指定子:引数だけを取り出す

結論: !$ は最後の引数、!^ は最初の引数、!* は全引数、!:n は n 番目の単語を取り出す。長いパスの再入力を防げる。

リナ: コマンド全体じゃなくて、引数だけ使い回したいときもありますよね。
ライニー先輩: そこでワード指定子。!$ は「直前コマンドの最後の引数」。長いパスをもう一度打たずに済むんだ。

!$ — 最後の引数

$ mkdir -p /var/log/myapp
$ cd !$
cd /var/log/myapp

!^ と !* — 最初の引数 / 全引数

$ cp config.yml config.yml.bak
$ vim !^
vim config.yml

!^ は最初の引数、!* はコマンド名を除いたすべての引数を表します。

!:n — n 番目の単語

単語には 0 から番号が振られ、0 がコマンド名そのものです。

$ cp src.txt dst.txt
$ echo !:1
echo src.txt
$ echo !:2
echo dst.txt
ワード指定子 意味
!^ 最初の引数(!:1 と同じ)
!$ 最後の引数
!* すべての引数(!:1-$
!:0 コマンド名そのもの
!:n n 番目の単語
!:n-m n から m 番目までの単語

!$ などは「直前コマンド」が暗黙の対象です。別のコマンドの引数がほしいときは !ssh:$ のようにイベント指定子と組み合わせます。

4. 修飾子:取り出した単語を加工する

結論: :h はディレクトリ部分、:t はファイル名、:r は拡張子を除いた部分、:e は拡張子を取り出す。:p は実行せず表示のみ。

リナ: パスから「ディレクトリだけ」「ファイル名だけ」を取りたいことがあるんですが。
ライニー先輩: それは修飾子の出番。ワード指定子のうしろに :h:t を付けると、パスを部品に分解できるよ。
$ ls /var/log/syslog
$ echo !$:h
echo /var/log
$ echo !$:t
echo syslog
修飾子 動作 例(/var/log/app.log
:h head:末尾の要素を除く(ディレクトリ) /var/log
:t tail:末尾の要素のみ(ファイル名) app.log
:r root:拡張子を除く /var/log/app
:e extension:拡張子のみ log
:p print:展開結果を表示するだけ(非実行)

:p で「展開結果だけ」を確認する

$ !ssh:p
ssh user@server

:p を付けると、history expansion の結果を実行せずに表示します。展開結果は履歴に追加されるので、確認してから !! で実行できます。

rmsudo を含むコマンドを !string で呼ぶときは、まず :p で中身を確認する習慣をつけると事故を防げます。

5. クイック置換:^old^new^

結論: ^old^new^ は直前コマンドの最初の old を new に置換して再実行する短縮記法。タイプミスの修正に最適。

リナ: 直前のコマンドの一部だけ打ち間違えたとき、全部打ち直すのは面倒です。
ライニー先輩: ^old^new^ を使えば、直前コマンドの oldnew に置き換えて再実行できるよ。タイプミス修正の鉄板テクだね。
$ cat /etc/hosst
cat: /etc/hosst: No such file or directory

$ ^hosst^hosts^
cat /etc/hosts

^old^new^!!:s/old/new/ の短縮形で、最初に一致した 1 箇所だけを置換します。すべて置換したいときは !!:gs/old/new/ を使います。

末尾の ^ は省略できます(^hosst^hosts でも動作)。ただし置換後にさらに文字を足したい場合は末尾 ^ まで明示しましょう。

6. histverify で安全に使う

結論: shopt -s histverify を設定すると、history expansion が即実行されず編集行に展開される。確認してから Enter できる。

リナ: !! って確認なしで実行されるのが、ちょっと怖いです。
ライニー先輩: その不安、histverify で解消できるよ。設定しておくと、!! を展開した結果がいったん入力行に表示されて、Enter を押すまで実行されないんだ。

~/.bashrc に次の 1 行を追記します。

shopt -s histverify

設定後は source ~/.bashrc で反映します。これ以降、!!!string を打つと、展開後のコマンドが入力行に現れ、内容を確認・編集してから Enter で実行できます。

histverify は破壊的コマンドの誤実行を防ぐ安全装置です。history expansion を日常的に使うなら設定しておくと安心です。

7. まとめ

結論: イベント指定子・ワード指定子・修飾子の 3 部品を組み合わせれば、過去コマンドを自在に再利用できる。

history expansion の基本記法を一覧で振り返ります。

記法 意味
!! 直前のコマンド全体
!n / !-n 履歴番号 n / n 個前のコマンド
!string string で始まる直近のコマンド
!$ / !^ 最後の引数 / 最初の引数
!* すべての引数
!$:h :t パスのディレクトリ / ファイル名
^old^new^ 直前コマンドを置換して再実行
!!:p 展開結果を表示のみ(非実行)
リナ: ! ひとつでこんなに呼び出し方があるんですね。まずは !!!$ から使ってみます!
ライニー先輩: それで十分。慣れてきたら !string^old^new^ を足していけば、コマンド入力がどんどん速くなるよ。

次に読む