こうなる。
$ echo "$(echo '!')"
bash: !': event not found
何が起きているのか
Bashにはヒストリ展開という機能がある。
この機能によって、コマンド中の!foo
のような文字列は、実行したことのあるfoo
から始まるコマンドの中で最も最近のものに置き換えられる。
例えば、以下は!t
がt
から始まる最近のコマンドに置き換えられ、!c
がc
から始まる最近のコマンドに置き換えられた例。
$ touch empty.txt
$ cat empty.txt
$ echo !t !c
echo touch empty.txt cat empty.txt
touch empty.txt cat empty.txt
実はこのヒストリ展開は非常に早いタイミングで行われる。
つまり、ダブルクォーテーションで囲まれた文字列では、コマンド置換よりも先にヒストリ展開が発生してしまう。
echo "$(echo '!')"
この場合、Bashはなんと!'
の部分を、実行したことのある'
から始まるコマンドの中で最も最近のものに置き換えようとする。
そのようなコマンドは今までに実行したことがないのでエラーになる。
対処法
- ヒストリ展開を一時的に無効化する
$ set +H
$ echo "$(echo '!')"
!
$ set -H
2. zshを使う
% echo "$(echo '!')"
!
おまけ: 詳しい挙動(の仮説)
Bashの処理の流れは厳密にはおそらく1次のようになっている。
1. |
、&
、;
、(
、)
、<
、>
、スペース、タブで入力行を区切ってトークンのリストを作る。
ただし'
か"
でクォートされている部分は一塊のトークンになる。
echo "$(echo '!t ')"
↓
echo
,,
"$(echo '!t ')"
echo $(echo "!t")
↓
echo
,,
$
,(
,echo
,,
"!t"
,)
2. '
でクォートされたトークンを除き、トークンの文字列中からヒストリ展開可能な部分文字列をヒストリ展開する。
echo
,,
"$(echo '!t ')"
↓
echo
,,
"$(echo 'touch empty.txt ')"
echo
,,
$
,(
,echo
,,
"!t"
,)
↓
echo
,,
$
,(
,echo
,,
"touch empty.txt"
,)
3. コマンド置換などの通常の置換を実行する。
echo
,,
"$(echo 'touch empty.txt ')"
↓
echo
,,
'touch empty.txt '
echo
,,
$
,(
,echo
,,
"touch empty.txt"
,)
↓
echo
,,
'touch'
,,
'empty.txt'
なので、以下の例の場合はヒストリ展開が起こらない。
echo '!'
echo $(echo '!')
以下の例でも、!
だけではヒストリ展開可能な文字列にならないので、ヒストリ展開が起こらない。
echo !
echo "!"
echo $(echo "!")
以下の例の場合はヒストリ展開が起こる。
echo !t # !tで展開
echo "!t" # !tで展開
echo $(echo "!t") # !tで展開
echo "$(echo "!")" # !"で展開
echo "$(echo '!')" # !'で展開
参考リンク
-
詳しく解説したページが見つからなかったので挙動から逆算したが、多分不正確な説明になっていると思う ↩