bashのエラーにハマって直った

最近は作業記録をけっこう細かい粒度で付けているのでわかるのだけど、昨日普段どおりに付けた最後の記録が

2017/06/14 Wed 13:07:02 今から**やる

だったので(**の部分は業務内容なので割愛)、少なくともその時点までは正常だったのが、次の記録では

2017/06/14 Wed 14:12:29 Dropboxのファイル移動が怪しいかな

となっていて、その1時間ほどの間に何かが起きていた。

現象

なんの話かというと、その14時頃に遭遇していた問題というのがあって、ターミナルを開くと

-bash: /Users/note103/.git-completion.bash: line 52: syntax error near unexpected token `elif'
-bash: /Users/note103/.git-completion.bash: line 52: `	elif [ -d "$1/.git" ]; then'
-bash: /Users/note103/.git-prompt.sh: line 120: syntax error near unexpected token `;;'
-bash: /Users/note103/.git-prompt.sh: line 120: `			;;'
-bash: /Users/note103/.bash_profile: line 82: syntax error: unexpected end of file
-bash: __git_ps1: command not found

みたいなエラーがザバッと出てくる。

あれ、と思って.bashrcを読み込み直すと、

$ source ~/.bashrc
-bash: /Users/note103/.bashrc: line 12: syntax error near unexpected token `}'
-bash: /Users/note103/.bashrc: line 12: `}'
-bash: __git_ps1: command not found

あらら。慌てて今度は.bash_profileを読み込み直すと、こうなる。

$ source ~/.bash_profile
-bash: /Users/note103/.bash_profile: line 82: syntax error: unexpected end of file
-bash: __git_ps1: command not found

さらには、僕は普段から自分で作ったPerl製のコマンドラインツールをよく使っていて、冒頭の作業記録もそれを使って記入しているのだけど*1、そのツールを使って「なんかよくわからんことが起きている」というメモを書こうとしたらこんなエラーが出て、

Can't locate TinyCalcs.pm in @INC (you may need to install the TinyCalcs module) (@INC contains: /Library/Perl/5.18/darwin-thread-multi-2level /Library/Perl/5.18 /Network/Library/Perl/5.18/darwin-thread-multi-2level /Network/Library/Perl/5.18 /Library/Perl/Updates/5.18.2/darwin-thread-multi-2level /Library/Perl/Updates/5.18.2 /System/Library/Perl/5.18/darwin-thread-multi-2level /System/Library/Perl/5.18 /System/Library/Perl/Extras/5.18/darwin-thread-multi-2level /System/Library/Perl/Extras/5.18 .) at /Users/note103/path/to/memo.pl line 8.
BEGIN failed--compilation aborted at /Users/note103/path/to/memo.pl line 8.
-bash: __git_ps1: command not found

メモすら取れなくなってしまった。

ちなみに、ここに出てくるTinyCalcs.pmというのは、その自分で使っているメモツールでuseしている自作モジュールなので、これはそのモジュールが読み込めてないよ、というエラー。

さらに言うと、ここで注目すべきはPerlバージョンで、上には5.18と出ているけど、普段ぼくはplenvというPerlバージョン管理ツールでインストールした5.24を使っているので、なぜかそれが無視されてPerlバージョンまでダウングレードしてしまっている。

という、なかなか味わったことのないカタストロフィーにこの段階でゾ〜〜……ッとして、軽くパニックに陥ってしまった。

検証

上のエラーに戻ると、まず

-bash: /Users/note103/.git-completion.bash: line 52: syntax error near unexpected token `elif'
-bash: /Users/note103/.git-prompt.sh: line 120: syntax error near unexpected token `;;'
-bash: /Users/note103/.bash_profile: line 82: syntax error: unexpected end of file

あたりは、当のコードを見てもとくにそれ自体に問題があるとは思えない。

ちなみに、その初めの2行で出てくるシェルスクリプトはどちらもGitを便利にする系のツールで、前者はGitコマンドをタブで補完するためのもので、後者はプロンプトにブランチ名を表示してくれるもの。

解説は以下あたりがわかりやすいか。
qiita.com

それから、どのエラーにも等しく顔を出してくるこれ、

-bash: __git_ps1: command not found

これは、.bash_profileに記述されている以下に対応している。

PS1='[\h] \W$(__git_ps1 ":%s")\$ '

ようは、上記のコマンド補完スクリプト( git-prompt.sh )を読み込めないのでそのコマンドもないよ、というメッセージ。

あとは.bashrcや.bash_profileに関する以下のエラーに関しても、

-bash: /Users/note103/.bashrc: line 12: syntax error near unexpected token `}'
-bash: /Users/note103/.bash_profile: line 82: syntax error: unexpected end of file

ここで示されている各該当部分はとくに問題なさそうというか、むしろその直前まで何も問題なかったのだから、これを真に受けていろいろいじってしまったらかえって二次災害に繋がってしまう。

よって、具体的に手を動かすこともできないまま、「一体なにが原因なんだろう……」と悩みはじめ、まあ経験的にはどう考えてもその直前に無意識にやったことが原因であることは間違いないのだけど、あまりにもいろんな作業を並行して進めていたせいで、原因になりそうな特定の作業をまったく思い出せない。

そしてその時点では想像できなかったのだけど、これにハマったまま結局夜になってしまった。*2

もちろんその間も、いろんなサイトを検索して見て回ったのだけど、

結果的には、今回の件に関係あるものはひとつもなかった。

一方、じつは最初の段階で、「前にも似たようなエラーでハマって解決したことがあったんだよな……なんだっけあれは……」と思っていて、少ししてからそれは.bashrcに仕込んでいたエイリアスに「done」というエイリアス名を使っていたことが原因のエラーだったことを思い出したのだけど、とはいえその後は教訓を活かして「done」というエイリアス名は使っていなかったので、「ん〜、似てると思ったけどそれはナイか……」と却下して、いよいよ出口がなくなった。

こういう場合、プログラミングを職業としている人ならいつまでも個人的にハマっているわけにはいかないから、職場の先輩や同僚などにヘルプを求めると思うのだけど、自分はそういう環境もないし、Perl入学式の先輩たちに意見を聞くことも可能ではあったのだけど、再現条件を示すのも大変そうだったので、そのままさらに独自対応の道を進むことに。

で、とにかくもういろんな「あの手この手」の試行錯誤をして、何か必要な不可視ファイルを捨ててしまったのではないかとか(冒頭のメモに書いた「Dropbox」はその流れで疑った)、いっそMacのTime Machine機能を使って半日ほど前の状態に復元してしまうべきか、とかも考えたのだけど、そんな中でもとりあえず、Perlのエラーについてはたぶん.bash_profileに記載している以下の設定、

if [ -d ${HOME}/.plenv  ] ; then
    export PATH=${HOME}/.plenv/bin:${HOME}/.plenv/shims:${PATH}
    eval "$(plenv init -)"
fi

このplenvの設定を読み込んでいないことによるものだろうとは思っていたし(5.18というのもダウングレードというよりシステムPerlに戻った状態なのだろうと)、そうであるならひとまずはbashまわりのミスに狙いを絞っていいはずだと自分に言い聞かせて、気持ちを落ち着かせるようにした。

しかしそれ以上のことはまったくわからないので、とりあえず「もうどんな手を使ってもいいから一旦このエラーを全部消してみよう」と思って、まずはどのファイルのどの記述がエラーを出しているのか確認するために、.bash_profileと.bashrcに

echo 1

から

echo 5

までを適当に散らばらせてみた。

で、その状態で新たにターミナルを開いたら、こんな感じになった。

1
3
4
5
-bash: /Users/note103/.git-completion.bash: line 52: syntax error near unexpected token `elif'
-bash: /Users/note103/.git-completion.bash: line 52: `	elif [ -d "$1/.git" ]; then'
-bash: /Users/note103/.git-prompt.sh: line 120: syntax error near unexpected token `;;'
-bash: /Users/note103/.git-prompt.sh: line 120: `			;;'
-bash: /Users/note103/.bash_profile: line 84: syntax error: unexpected end of file
-bash: __git_ps1: command not found

おっと、面白い。出力された数字から「2」が抜けている。

具体的には、このとき1と2は.bash_profileのトップと最終行に入れていて、3,4,5はそれぞれ.bashrcのトップ、真ん中、最終行に入れている。
(.bash_profileは80行程度であるのに対して、.bashrcは500行を超えているのでそのように配分した)

で、なぜ出力がこうなるのか考えてみると、.bash_profileの上の方には以下の記述があるので、

if [ -f ~/.bashrc ] ; then
    source ~/.bashrc
fi

おそらくコンピュータは最初に.bash_profileの1を出力して、その後に上記の呼び出しを伝って.bashrcの3,4,5を吐き出して、また.bash_profileに戻ってきたところでエラーを吐いて、2に至る前に死んでいる。

では、という感じで、今度はその呼び出しをコメントアウトして、

# if [ -f ~/.bashrc ] ; then
#     source ~/.bashrc
# fi

その状態で新たにターミナルを開いてみると……

1
2

おお。エラーが消えた。そしてさっきは出てこなかった「2」(.bash_profileの最終行)がちゃんと出てる。

ということは、問題は.bash_profileの方にはない。
加えて、マシン自体やその他のアプリケーションとかからの影響で不具合が起きてるわけでもない。

これはやはり、.bashrcで何かよからぬものを拾って、その状態で.bash_profileに戻ってきたから発生したエラーなのだ。

……という仮説に基づいて、ここからは.bashrcの検証へ。

追跡

まずは先ほどの.bash_profileのコメントアウトを外して、さっきまでと同様のエラーがすべて出る状態に戻す。

1
3
4
5
-bash: /Users/note103/.git-completion.bash: line 52: syntax error near unexpected token `elif'
-bash: /Users/note103/.git-completion.bash: line 52: `	elif [ -d "$1/.git" ]; then'
-bash: /Users/note103/.git-prompt.sh: line 120: syntax error near unexpected token `;;'
-bash: /Users/note103/.git-prompt.sh: line 120: `			;;'
-bash: /Users/note103/.bash_profile: line 84: syntax error: unexpected end of file
-bash: __git_ps1: command not found

次に、500行以上ある.bashrcのファイルのうち、1行目に置いている

echo 3

から、真ん中あたりに仕込んでいる

echo 4

までを残して、そこから最終行までの下半分を一旦カットしてターミナルを開いてみる。

1
3
4
2

ヤッホー! エラーが消えた。そして残したecho文の数字も全部出ている。

ということは、.bashrcの真ん中に設置した

echo 4

の次の行から、先ほどカットした最終行の

echo 5

までの間に、問題の「それ」はあると考えられる。

ここまで来ると、もう先ほどまでの苦悩やヤケ感は何だったのかと思うほど体がラクになっている。

あとは単純に、これまでの繰り返し。機械的に対象領域をどんどん半分に区切っていって、最後までエラーを吐いている行を追い詰めればいい。

で、追い詰めた犯人はこれ。

alias fi="perl /path/to/findline.pl"

(パス名は仮のもの)

ここでようやく、「あ〜……なんだよやっぱり、前に経験したやつと同じじゃん!」と気がついた。

つまり、さっき上の方で「それはナイ」と却下した「done」と同じ。

簡単に解説すると、そもそも「done」をエイリアス名に使って何が問題だったのかというと、おそらくシェルスクリプトのfor文でそれを使うからだろう。

こんな感じのやつ。

for i in foo bar baz
do
    echo $i
done #<=ココ

で、この「fi」というのもまさにそれで、「fi」はシェルスクリプトのif文でこのように使う。

s=foo
if [ $s == foo ] ; then
    echo FOO!
else
    echo BAR!
fi #<=ココ

(この場合は「FOO!」と出力される)

つまり、今回も「done」のときと同様に、構文に出てくる「fi」をエイリアス名に使ってしまったから各種のエラーが生じたのだと思われる。

実際、とりあえずその「fi」を別のものにしたらエラーはすべて消えたし、それによってPerlも元通り5.24.0に戻ったし、そのPerlを使った自分のモジュール&コード群もすぐ使えるようになった。

この時の記録。

2017/06/14 Wed 19:40:55 Dropbox全然関係なかったじゃん

まとめ

*1:いずれここでも紹介したいが。

*2:一応業務もやっていたけど、解決しないまま夜になるとは思っていなかった。