環境
- MacBook Air M2 2022
- Sonoma 14.3.1
- zsh 5.9 (x86_64-apple-darwin23.0)
起こったこと
sshのエラー
ある日、研究室のサーバに ssh しようとすると、以下のようになった。
% ssh remote_host
zsh:1: command not found: ssh
kex_exchange_identification: Connection closed by remote host
Connection closed by UNKNOWN port 65535
command not foundと出たのでパスが通ってないのかな?と思い、確認したが大丈夫そう。
% which ssh
/usr/bin/ssh
% print -l $path | grep /usr/bin
/System/Cryptexes/App/usr/bin
/usr/bin
/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin
/Library/Apple/usr/bin
やったこと
zsh の設定をいじることがあったので、そのせいかな?と思い、下記を実行。
% zsh --no-rcs
% ssh remote_host
zsh:1: command not found: ssh
kex_exchange_identification: Connection closed by remote host
Connection closed by UNKNOWN port 65535
この時もwhich sshをすると見つかるし、$PATHにも/usr/binは追加されている。
だめだ、、、
ただ、その過程でこのようなエラーに出くわした。
% zsh
/etc/zshrc:7: command not found: locale
そんな訳はないだろうと思い、which localeを実行するも、locale not foundとのこと。
え?じゃあ$PATHどうなってるねんということで、確認した結果をいかに示す。
# 上のコマンドで立ち上げているzshの中で実行
% print -l $path
/opt/local/libexec/gnubin
/usr/local/bin
/opt/local/bin
/opt/local/sbin
/usr/local/lib
/usr/sbin
これらはすべて、私の~/.zshenvで設定しているものである。
ちなみに、exitして元の zsh に戻ると$PATHは元通りになっている。
ここまでのまとめ
-
sshを実行するとcommand not foundが出るが、パスは通っていそう -
zsh --no-rcsで新たに立ち上げた shell の中でも同じ結果に -
zshにより立ち上げた shell は様子($PATHの設定)がおかしい
原因
~/.zshenv を以下のように変更していたのが原因だった。
# 変更前
% cat ~/.zshenv
- export PATH="/opt/local/libexec/gnubin:/usr/local/bin:/opt/local/bin:/opt/local/sbin:/usr/local/lib:/usr/sbin:$PATH"
# 変更後
% cat ~/.zshenv
+ export PATH="/opt/local/libexec/gnubin:/usr/local/bin:/opt/local/bin:/opt/local/sbin:/usr/local/lib:/usr/sbin"
差分は最後に$PATHをつけるかつけないか、つまり、$PATHの設定を追加にするか上書きにするかである。
考察
なぜこのようなことがおきたのだろうか?
そもそも、zshenvは zsh の設定ファイルの中で最初に読まれる設定ファイルのため、上書きしても元の$PATHが空なので問題ないはずである。
zsh の設定ファイルと$PATHの設定
設定ファイルの読み込み順
まず、zshの設定ファイルの読み込み順を確認しておこう。
man zshを見てみると、以下のように書いてあった。
Commands are first read from /etc/zshenv; this cannot be overridden.
[中略]
Commands are then read from $ZDOTDIR/.zshenv. If the shell is a login shell, commands are read from /etc/zprofile and then $ZDOTDIR/.zprofile. Then, if the shell is interactive, commands are read from /etc/zshrc and then $ZDOTDIR/.zshrc. Finally, if the shell is a login shell, /etc/zlogin and $ZDOTDIR/.zlogin are read.
[中略]
If ZDOTDIR is unset, HOME is used instead. Files listed above as being in /etc may be in another directory, depending on the installation.
要するに、$ZDODIRが設定されていない私のような環境では、zsh を立ち上げた時以下の順で設定ファイルが読み込まれる。
| 順番 | 設定ファイル名 | 備考 |
|---|---|---|
| 1 | /etc/zshenv |
私の環境にはない |
| 2 | ~/.zshenv |
|
| 3 | /etc/zprofile |
ログインシェルの時のみ |
| 4 | ~/.zprofile |
ログインシェルの時のみ |
| 5 | /etc/.zshrc |
|
| 6 | ~/.zshrc |
|
| 7 | /etc/zlogin |
ログインシェルの時のみ (私の環境にはない) |
| 8 | ~/.zlogin |
ログインシェルの時のみ (私の環境にはない) |
$PATHに関する設定
次に、$PATHに関する設定を確認しておく。上に示した設定ファイルのうち、$PATHに関する設定を行うものを 1 つずつ確認していく。
-
~/.zshenv- 先ほど示したように、主に
/opt以下のパスを追加している。 (MacPorts のため)
- 先ほど示したように、主に
-
/etc/zprofile-
/etc/zprofileでは、path_helperというコマンドを呼んでいる。 -
man path_helperによると、path_helperは、/etc/pathsと/etc/paths.d/*に書かれた内容を元に、$PATHを設定する。 -
/etc/pathsの中身は以下の通り。/usr/local/bin /System/Cryptexes/App/usr/bin /usr/bin /bin /usr/sbin /sbin -
/etc/paths.d/*の中身は以下の通り。/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin /opt/X11/bin /Library/Apple/usr/bin
-
-
~/.zprofile-
~/.zshenvと同様、MacPorts 関係の設定をしている。
-
zsh から zsh を立ち上げた時の挙動
ここまででこの記事で何を言いたいのか分かってきたと思うが、zshenvを$PATH 上書き方式にした時にzshを立ち上げた時の挙動を確認しておこう。
まず、ログインシェルを立ち上げた時には、上の 3 つの設定ファイルの$PATHが問題なく読み込まれる。
なぜならば、~/.zshenvより前に$PATHについての設定はされないからである。
厳密には、~/.zshenvを読む前に/usr/bin:/binは$PATHに追加されるようだ。
(~/.zshenv の 1 行目にecho $PATHを追加して確認した)
また、terminal 特有の$PATHの設定も~/.zshenvの読み込みの前にされるようだ。
私は kitty を使っているが、その場合、上記に加えて /Applications/MacPorts/kitty.app/Contents/MacOS:/usr/sbin:/sbinなども$PATHに追加されていた。
しかしながら、ログインシェルからzshを立ち上げた場合、~/.zshenvが最初に読み込まれるため、今までの$PATHは上書きされてしまう。
/etc/zprofileはログインシェルの時のみ読み込まれるためため、/etc/zprofileで設定した/usr/binや/binは$PATHに追加されない。
そのため、localeなどのコマンドが見つからないというエラーが出てしまったのである。
sshの挙動について
次に、sshの挙動について考えてみる。
なぜログインシェルでsshを実行した時に、command not foundが出たのか。
先程の話から考えると、ログインシェルでは$PATHの設定は問題ないはずである。
ここで、zshenvを$PATH 上書き方式にした時のsshの挙動について以下のことがわかった。
前提として、私が ssh しようとしたサーバは、別のサーバを経由して接続するような設定になっている。
Host public_host
User hoge
HostName public_host.hoge.jp
Host remote_host
HostName remote_host.hoge.jp
ProxyCommand ssh -W %h:%p public_host.hoge.jp
この時、public_hostの方には問題なく ssh できるのだ。
しかしながら、remote_hostの方には接続できない。
public_host にもsshはインストールされており、$PATHにもsshまでのパスは通ってため、public_host の問題によるエラーではなさそうだ。
今までの話を考慮して、sshでは踏み台になるサーバを経由してsshする場合に限って、shell を新たに立ち上げているのではないかと考えた。
そこで、以下の github を見てsshの実装を確認してみることにした。
すると、sshconnect.cに定義されたssh_proxy_connect関数の中に以下のようなコードがあった。
ssh_proxy_connect(struct ssh *ssh, const char *host, const char *host_arg,
u_short port, const char *proxy_command)
{
[中略]
if ((shell = getenv("SHELL")) == NULL || *shell == '\0')
shell = _PATH_BSHELL;
[中略]
/* Fork and execute the proxy command. */
if ((pid = fork()) == 0) {
[中略]
argv[0] = shell;
argv[1] = "-c";
argv[2] = command_string;
argv[3] = NULL;
/*
* Execute the proxy command. Note that we gave up any
* extra privileges above.
*/
ssh_signal(SIGPIPE, SIG_DFL);
execv(argv[0], argv);
perror(argv[0]);
exit(1);
}
[中略]
}
おそらくこの部分でforkして新たに shell をexecして呼んでいるのだろう。
そのため、ProxyCommand を指定している場合には、sshを実行した時に新たに shell が呼ばれ、上記の$PATHの設定が誤った適用されてしまうと考えられる。
まとめ
-
~/.zshenvの設定を上書き方式にしてしまったため、ログインシェルでない shell を立ち上げた時に、$PATHが上書きされてしまった。 -
sshでは、踏み台になるサーバを経由してsshする場合に限って、shell を新たに立ち上げているため、その場合にはcommand not foundが出てしまう。