bash 3.2の変数と日本語
macOSに入っている /bin/bash は、バージョン3.2.57。ここで "$WT、" のように、変数の直後に日本語を書くと、変数が空になり、日本語の一部も消える。
Mac miniのホスト側スクリプトで、AIへの依頼文が途中で欠けたまま実行された。経緯は開発日記のminiに任せた初日、3回失敗したにある。ここでは、再現した挙動と、見つけ方、直し方だけを書く。
何が起きるか
次の1行で再現できる。
/bin/bash -c 'WT=abc; printf "%s" "[$WT、]" | od -An -tx1'
期待する出力は 5b 61 62 63 e3 80 81 5d([、abc、、、])。UTF-8のロケールでは、実際は次になる。
5b 80 81 5d
abc が消え、、(UTF-8で e3 80 81 の3バイト)の先頭1バイトも消えて、残り2バイトだけが出る。
日本語だけではない
直後の文字を変えて、UTF-8のロケールと LC_ALL=C で出力のバイト列を比べた。変数は WT=abc、見ているのは出力のバイト列。
| 直後の文字 | 文字のバイト列 | UTF-8のロケール | LC_ALL=C |
|---|---|---|---|
| 、 | e3 80 81 | 80 81 だけ残る | 正常 |
| を | e3 82 92 | 82 92 だけ残る | 正常 |
| 日 | e6 97 a5 | 97 a5 だけ残る | 正常 |
| :(全角コロン) | ef bc 9a | bc 9a だけ残る | 正常 |
| → | e2 86 92 | 86 92 だけ残る | 正常 |
| é | c3 a9 | a9 だけ残る | 正常 |
表の文字は en_US.UTF-8 と C で測った。、 だけは ja_JP.UTF-8 と C.UTF-8 でも測り、en_US.UTF-8 と同じ結果だった。ASCII以外のすべての文字が対象で、日本語に限らない。 ロケールが C なら起きない。手元のシェルと、launchdやsshで起動したシェルでロケールが違うと、手元では通って夜間だけ壊れる、という形になりうる。
なぜロケールで変わるのかは、ソースを読んでいないので分からない。測った結果だけを書く。
壊れなかった書き方も測った。
"${WT}、"(波括弧で囲む)"$WT"、(引用符を先に閉じる)"$1、"(位置パラメータ)"$WT,"(直後がASCIIの記号)
直後がASCIIの英数字("$WTx")なら、WTx という別の変数を読む。これは通常のbashと同じ挙動。
どこで止まるか
set -u(未設定の変数を使ったらエラーにする)を付けていても、書き方によって止まる場合と止まらない場合がある。すべて set -u を付け、UTF-8のロケールで測った。
実際に起きたのは、3行目の形。AIへの依頼文を cat <<EOF で組み立てていて、その中に 合図のworktree名は $WT_NAME、コミットメッセージは… と書いていた。set -u を付けていたが、エラーは出ただけで、スクリプトは先へ進み、欠けた依頼文のままAIが実行された。 再現したときの出力は次のとおり。
line 5: WT�: unbound variable
次の行まで進んだ rc=1
出力ファイル: 0 バイト
WT のあとの文字化けが、変数名に飲み込まれた先頭1バイト。
見つける
書いた本人は、壊れる書き方に気づけない。見た目はふつうの日本語の文で、エラーになる経路は、失敗したときにしか通らない場所に隠れていることがある。 この落とし穴は、数日前(10月13日まで)にも別のスクリプトで踏み、そのときは落ちた1か所だけを直した。同じ書き方が残っていたので、ホスト側の全スクリプトを静的に調べるテストを入れた。
import re
pat = re.compile(r'\$[A-Za-z_][A-Za-z0-9_]*(?=[^\x00-\x7f])')
変数の直後がASCII以外の文字になる箇所を、全 .sh の、コメント行を除く各行から探す。見つかったら失敗にする。ASCII以外のすべてを対象にしているのは、上の表のとおり、日本語に限らないため。
同じ書き方は、最初に気づいた依頼文のほかに、2か所あった。どちらも、失敗したときにしか通らない経路にあった(待ち受けスクリプトの「作業場所が無い」のログと、型検査の「判断できない」のメッセージ)。通常の実行では一度も表に出ない。直したあと、ホスト側の全スクリプト(33ファイル)にこの検査を流し、今後は書くとテストが落ちる。
直す
- 変数は、常に
${変数}と書く。 - 日本語の直前に変数を置くときは、
"$WT"、のように引用符を先に閉じてもよい。 - 依頼文のように、あとで中身を確かめられるものは、実行する前に中身を確かめる。 欠けると困る印(必須の文言・文字数)が入っているかを、AIを動かす前に調べる。
確かめていないこと
- bash 4以降での挙動。このMacには3.2.57しか入っておらず、新しいbashでは測っていない。
- Mac miniのホスト側のロケール。実際にどのロケールで壊れたかは、確かめていない。
関連
- 開発日記: miniに任せた初日、3回失敗した — このバグが、依頼文の欠けとして現れた経緯
- ガードが素通りする書き方 — 止める仕組みが、効いていなかった実例
- OpenClawの切り分け — 動かないとき、どの層で起きているかを先に決める
- 鍵を渡さないpush設計 — ホスト側の固定スクリプトの設計