接続先サーバのファイルに手を付けずに、sshで接続したインタラクティブシェルで自動的にset -uする #bash #ssh

.bashrc読んだ後に自動的にオプション設定できた!
2020.11.19

こんにちは、ターミナル住人の平野です。

シェルスクリプトの先頭には原則必ずset -euをつけましょうね、 というのはいろんな所で言及されているので見たことがある人も多いと思います。 このうちset -uですが、変数が未定義の時に想定外の挙動になってしまうことが防げるので、 慎重に操作をするような場面では、インタラクティブシェルでも有効にした方がいい場面があります。

sshでEC2にログインした時これを行いたいとして、ログイン後にset -uと毎回やるのは面倒です。 もちろんこう言った設定は.bashrcに書くというのが定石ですね。

しかしバッチサーバー用途のインスタンスなどはユーザを共通で使用することも多く、 そこにある.bashrcを変更してしまうことには抵抗があるという場合も多いと思います。

sshで接続するクライアント側だけの設定で上記ができないかと思い調べてみました。

結論: .ssh/configの設定

.ssh/configに設定を書く場合、 RemoteCommandを使って以下のように書くことで目的を達成することができました。

20201126 修正
--loginでログインをつける方法では、set -uが正しく効いていませんでした。 ひとまず--loginなしで、明示的に.bash_profileを読むことで回避するものを掲載してあります。

.ssh/config

Host HogeHogeServer
    User ec2-user
    Hostname xxx.xxx.xxx.xxx
    IdentityFile /path/to/key.pem
    RequestTTY yes
    RemoteCommand exec bash --init-file <(echo 'source ~/.bash_profile && set -u')

試行錯誤

RemoteCommandを使えばsshでログインした後のコマンドを指定できるという情報はすぐ見つけることができたのですが、 RemoteCommandは普通、指定された処理が終了したらsshのセッションを終了してしまい、インタラクティブモードには入りません。 なので、最後にbashをおくことで、明示的にインタラクティブシェルを起動するようにします。 こうすることで、コマンドを実行させつつ、単純なインタラクティブシェルへの接続のように見せかけることができます。 なお、この際RequestTTY yesという文言が必要なようです。 これがないとインタラクティブシェルは起動しているのですが、その画面が出力されないという状況になってしまいます。

さて、ここまでの知識で、まずはディレクトリの移動で試してみたいと思います。

.ssh/config

RemoteCommand cd / && bash
[ec2-user@ip-1-0-0-187 /]$

確かに/をカレントディレクトリとしたインタラクティブシェルが起動しました。

次に目的のset -uをやってみます。

.ssh/config

RemoteCommand set -u && bash
echo $aaa/bbb
# 出力: bbb

ダメですね。。。set -uはbashの中には引き継がれないようです。

インタラクティブシェルについて

最後のbashが何をしているかというと、これはサブプロセスとして新しくbashを起動しているということです。 そして、サブプロセスに引き継がれる情報は、基本的には環境変数だけです。 set -uで設定した内容は環境変数ではない部分で管理されているようなので、サブプロセスには引き継がれません。 実際envコマンドで環境変数を見てみても、set -uする前後で環境変数に違いは何もありません。

ということで、サブプロセスに引き継げない以上、 新しく立ち上がったプロセス内で設定するしかないということになります。

bashコマンドはそこにオプションを指定することができます。 なので、安直にbash -uとして起動してみればいい、という話になります。 実際やってみます。

.ssh/config

RemoteCommand bash -u
bash: PROMPT_COMMAND: 未割り当ての変数です
bash: COLORTERM: 未割り当ての変数です
bash: local256: 未割り当ての変数です
bash: BASH_COMPLETION_COMPAT_DIR: 未割り当ての変数です
bash: USER_LS_COLORS: 未割り当ての変数です
bash-4.2$ echo $aaa/bbb
bash: aaa: 未割り当ての変数です

後半の方で、set -uがきちんと有効になっていることが確認できました。 しかしその上の表示がめちゃめちゃ気になりますよね。

これは、このbash自体が最初からset -uをつけて実行されているので、 /etc/bashrcなどの設定を読み込む際にもset -uが有効になってしまっているためです。 未定義変数が使われている箇所で警告が出てしまい、実際いくつかの変数が適切に設定されていないという状況になってしまいました。 さすがにこの状況ではset -uが自動でセットされたにしても代償が大きすぎます。 何か他の方法を考えないといけません。

--init-fileが勝利の鍵

「新しく立ち上がったプロセスの設定」と言ったら基本的には.bashrcに書くというのが鉄則です。 しかし今回は.bashrcは一切手を入れないということが前提条件ですから、これを変えるわけにはいきません。

そこで使用するのが --init-fileオプション1です。 これは、.bashrcの代わりに読み込む設定ファイルを明示的に指定するオプションです。 これを使えば.bashrcを書き換えることなく、目的の設定を読み込ませることができます。 以下のようにやってみます。

.ssh/config

RemoteCommand bash --init-file <(echo 'set -u')

ここで出てきた<(echo 'set -u')はプロセス置換というもので、 本来であればファイル名を指定する箇所にコマンドの出力を流し込むことができます。 これによって接続先のサーバーにファイルを一切置く必要がなくなるようにできます。 さて、これで試してみます。

echo $aaa/bbb
bash: aaa: unbound variable

set -uがきちんと適用されていますね!!!

.bashrcは必ず読むようにする

気をつけなければいけないのは、--init-fileで指定したファイルは.bashrc代わりに読み込まれるということです。 つまり、上記設定では.bashrcが読み込まれないということです。 これはまずいので、これも対策します。 ただやり方は簡単です。set -uする前に.bashrcを読めばいいので、こんな感じにすればいいことになります。

.ssh/config

RemoteCommand bash --init-file <(echo 'source ~/.bashrc && set -u')

これで.bashrcの読み込み後set -uするという目的が達成できました!!

体裁を整える

最後にちょっとブラッシュアップします。

まずsshでインタラクティブシェル接続した場合はログインになりますので、--loginをつけた方が適切です。 でないと.bash_profileが読まれません。

20201126 修正
--loginでログインをつける方法では、set -uが正しく効いていませんでした。 これ以下の文章は現在修正中です。

また、bashとだけ書くとインタラクティブシェルはサブプロセスとして開始されますので、 exec bashとすることで現行のプロセス自体を新しいbashに置き換えるようにします。

ということで完成したものが、最初にも示した、以下のような設定となります。

.ssh/config

RemoteCommand exec bash --login --init-file <(echo 'source ~/.bashrc && set -u')

読み込まれる設定の確認

/etc/profile, /etc/bashrc, ~/.bash_profile, ~/.bashrcのファイルに、 それぞれ実行されたら自分の名前をechoさせるようにした上で、 通常のsshログインと、上記RemoteCommandを経由したログインを比較してみます。

通常のsshログインの場合

/etc/profile
.bash_profile
.bashrc
/etc/bashrc

RemoteCommandを経由したログイン

.bashrc
/etc/bashrc
/etc/profile
.bash_profile
.bashrc
/etc/bashrc

通常ログインに比べて先頭に.bashrc, /etc/bashrcが余分についていますが、 それ以降は通常ログインと同じ順番で設定ファイルが読まれていますので実害はないものと考えられます。

まとめ

sshで接続したインタラクティブシェルで、.bashrcなどを読んだ後に自動的にオプション設定することに成功しました! .bashrcなどの接続先のファイルに一切影響を与えずに設定ができるので、俺俺設定などしたい時にも役立つかもしれません(やりすぎ注意)。

--init-fileオプションを使えば、 sshに限らず、 「インタラクティブシェル起動したら最初にこのコマンド実行して欲しいんだけど...」な場合にある程度広く対応できそうです。

シェルに関しては調べれば調べるほど、何も知らんかったという情報が出てきて興味が尽きません! 以上何かのご参考になれば幸いです!

参考リンク


  1. これは--rcfileオプションと全く同じようです。命名規則などの理由から--init-fileという別名が作られたようです。