はじめに
bashで!を含む文字列をそのまま実行しようとすると、以下のような見慣れないエラーに遭遇することがあります。
-bash: !foo: event not found
パスワードに!が含まれている、メッセージ文に「〜してください!」と感嘆符を使っている、といった何気ない場面で突然出てくるため、原因が分かりにくいエラーの1つです。本記事では、この現象の正体である履歴展開(history expansion) の仕組みと対処法を整理します。
原因:!はbashにとって特別な文字
bashの対話的シェルには、過去に実行したコマンドを!を使って呼び出す履歴展開という機能が備わっています。デフォルトでは!が履歴展開の開始文字(history expansion character)として扱われます。
!! # 直前に実行したコマンドを再実行する
!123 # 履歴の123番目のコマンドを再実行する
!ls # 履歴の中で直近に実行した「ls」で始まるコマンドを再実行する
この機能があるために、!の直後に何らかの文字列が続いていると、bashはそれを「履歴からコマンドを探せ」という指示だと解釈しようとします。該当する履歴が見つからない場合に返ってくるのが、冒頭のevent not foundエラーです。
見落としがちな注意点:シングルクォートでも防げない
多くのシェル機能は、シングルクォート('...')で囲むことで文字列をそのまま(展開なし)扱えます。しかし、履歴展開だけは対話的シェルにおいて例外で、シングルクォートで囲んでいても!の展開が発生してしまうことがあります。
echo 'Hello!World'
# → シングルクォートの中なのに、bashのバージョン・設定によっては
# history expansion が発生してしまうことがある
これは、履歴展開がシェルの構文解析(クォートの処理)よりも前の、字句解析の早い段階で行われるために起きる、bash特有の癖です。ダブルクォートで囲んだ場合も同様に防げません。
対処法
1. バックスラッシュでエスケープする
!の直前にバックスラッシュ(\)を置くことで、履歴展開の対象から明示的に除外できます。
echo Hello\!World
2. 履歴展開そのものを無効化する(set +H)
そのシェルセッション全体で履歴展開を使う予定がないなら、set +H(またはset +o histexpand)で機能自体を無効化してしまう方法もあります。
set +H
echo 'Hello!World' # もう event not found にはならない
パスワードなど!を含む値をスクリプトの引数として渡す必要がある場面(例: DBのパスワードに記号として!が含まれる場合)では、この方法が特に有効です。元に戻したい場合はset -Hで再度有効化できます。
3. 非対話的シェル(スクリプト)では基本的に問題にならない
履歴展開は「対話的な」シェルセッションでのみデフォルト有効な機能です。.shスクリプトファイルをbash script.shのように実行する場合は、通常この設定はデフォルトで無効になっているため、スクリプト内に!を含む文字列があっても同じ問題は起きません。問題になるのは、主にターミナルで直接コマンドを打つ・貼り付けるような対話的操作の場面です。
まとめ
| 項目 | 内容 |
|---|---|
| 原因 | bashの履歴展開機能が!を「履歴からコマンドを呼び出す指示」と解釈してしまう |
| 発生条件 | 対話的シェルで!の直後に、履歴に一致しない文字列が続いている場合 |
| よくある誤解 | シングルクォート・ダブルクォートで囲んでも、履歴展開自体は防げない |
| 対処法 |
\!のようにバックスラッシュでエスケープする、またはset +Hで履歴展開そのものを無効化する |