SSH の接続先のホスト名を tmux に表示させるプラグインを作りました

SSH の接続先のホスト名を tmux に表示する方法は何人かの方が公開されていますが、設定や手順が複雑化しがちだったので、プラグイン化してみました。
プラグインは tmux 用プラグインマネージャーの [TPM](https://github.com/tmux-plugins/tpm) から呼び出すことを想定して作っています。

* [yuuan/tmux-ssh-host: This plugin displays the hostname connect by ssh command.](https://github.com/yuuan/tmux-ssh-host)

このプラグインを導入すると、`window-status-format` や `window-status-current-format` に `#{ssh-host}` と書くことで、ペインで起動されている `ssh` コマンドの、接続先のホスト名が表示できるようになります。
具体的な設定例はこんな感じ。

```tmux:~/.tmux.conf
set-window-option -g window-status-format '#I: #{?#{m:ssh,#W},#{ssh-host},#W} '
set-window-option -g window-status-current-format '#I: #{?#{m:ssh,#W},#{ssh-host},#W} '

set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'yuuan/tmux-ssh-host'

run -b '~/.tmux/plugins/tpm/tpm'
```

前半の `set-window-option` のところが、実際にこのプラグインを利用するための設定になります。

`#{?` から始まる書式は、if 文のようなもので、`#{?<条件式>,<真の場合の値>,<偽の場合の値>}` という意味になります。[^1]
また、`#{m:` から始まる書式は、`#{m:<パターン>,<文字列>}` と書くことで、文字列がパターンにマッチするかどうかを返します。[^2]

この場合、`#W` がパターン `ssh` にマッチする場合は `#{ssh-host}` を、そうでなければ `#W` を表示するように設定しています。

後半は [TPM でプラグインを読み込むための設定](https://github.com/tmux-plugins/tpm#installation) です。


## 実装方法の説明

### TPM プラグインの作成方法

TPM プラグインの作り方は、以下のページを参考にしました。

* [tmuxを使いこなす / プラグイン開発で機能を拡張 ｜ DevelopersIO](https://dev.classmethod.jp/devenv/mastering-tmux-with-tpm-plugin/)

`tmux-#{プラグイン名}.tmux` というシェルスクリプトを作っておくと、プラグイン読み込み時にこのファイルを読み込んでくれるので、この中に tmux の設定を書き換える処理を書いておきます。
今回の場合ですと、`window-status-format` と `window-status-current-format` に含まれる `#{ssh-host}` という文字列を、`#(./scripts/ssh-host.sh #{pane_pid})` に書き換えるための処理が書いてあります。

```bash:tmux-ssh-host.tmux
#!/usr/bin/env bash

CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

script="#($CURRENT_DIR/scripts/ssh-host.sh #{pane_pid})"
pattern="\#{ssh-host}"

do_interpolation() {
    local string="$1"
    local interpolated="${string/$pattern/$script}"

    echo "$interpolated"
}

update_tmux_window_option() {
    local option="$1"
    local option_value="$(tmux show-window-option -gv "$option")"
    local new_option_value="$(do_interpolation "$option_value")"

    tmux set-window-option -g "$option" "$new_option_value"
}

update_tmux_window_option "window-status-format"
update_tmux_window_option "window-status-current-format"
```

ここで使っている `#{pane_pid}` は、ペインで開いているコマンドのプロセス ID に置き換えられます。[^2]
たいていの場合はシェルのものになるかと思います。
それを、別に用意したシェルスクリプトに渡している訳ですね。

### SSH コマンドの取得方法

`scripts/ssh-host.sh` の中では、まずは `#{pane_pid}` からそのペインで実行されている `ssh` コマンドのコマンドライン引数を取得し、次にその中からホスト名を取り出しています。

`ssh` コマンドのコマンドライン引数の取得方法は、以下のページを参考にしました。

* [SSH接続先のホスト名をtmuxのペインに表示する(大事件) - Qiita](https://qiita.com/arks22/items/7bf7da1433ef9c57b457)

```bash:scripts/ssh-host.sh
get_ssh_arguments() {
    local parent_pid=$1
    local arguments=$({ \pgrep -flaP $parent_pid; \ps -o command -p $parent_pid; } \
        | \xargs -I{} echo {} \
        | \awk '/ssh/' \
        | \head -n 1 \
        | \sed -E 's/^[0-9]*[[:blank:]]*ssh //' \
    )

    echo $arguments
}
```

まず、`ps` コマンドで `#{pane_pid}` のプロセスが実行されたときのコマンドを取得し、`pgrep` で `#{pane_pid}` の子プロセスが実行されたときのコマンドを取得しています。[^3]
次に、`awk` コマンドを使ってパターン `/ssh/` を含むもののみに絞り込みを行います。
この際に、同じペインで複数の `ssh` を実行している場合もあるので、その場合は `head` コマンドを使って一番初めに見付かったものを採用しています。
最後に、`sed` コマンドで `pgrep` コマンドで付与される余計なプロセス番号と、`ssh` コマンドのコマンド名自体を除去しています。

これで、そのペインで実行されている `ssh` コマンドのコマンドライン引数が取得できました。

### SSH の接続先のホスト名の取得方法

続いて、`ssh` コマンドのコマンドライン引数の中から、接続先ホスト名を取得します。
`man` コマンドで見てみると、`ssh` コマンドのパラメータは、以下のように指定するそうです。

```
NAME
     ssh — OpenSSH SSH client (remote login program)

SYNOPSIS
     ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface] [-b bind_address] [-c cipher_spec] [-D [bind_address:]port] [-E log_file] [-e escape_char] [-F configfile]
         [-I pkcs11] [-i identity_file] [-J destination] [-L address] [-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port] [-Q query_option] [-R address]
         [-S ctl_path] [-W host:port] [-w local_tun[:remote_tun]] destination [command]
```

これによると、指定できるオプションの中で、`-46AaCfGgKkMNnqsTtVvXxYy` は値をとらず、`-BbcDEeFIiJLlmOopQRSWw` は値をとることがわかります。
つまり、後者のオプションの次に現れる値は、接続先のホスト名ではないってことですね。

```bash
get_ssh_host() {
    local ssh_command="$1"

    while (($# > 0)); do
        case "$1" in
            -[BbcDEeFIiJLlmOopQRSWw])
                shift
                ;;
            -*)
                ;;
            *)
                echo "$1" && break
                ;;
        esac
        shift
    done
}
```

コマンドライン引数をひとつずつ while 文で回してチェックして、`-BbcDEeFIiJLlmOopQRSWw` が現れた場合は、そのオプションとその次の引数を破棄し、それ以外のオプションが現れた場合はオプションだけ破棄してます。
そして、それ以外の値が来たときに、それを接続先のホスト名として採用しています。

### やらなかったこと

最後に、検討はしたものの、やらなかったことを書いておきます。

`~/.ssh/config` を使うと、接続先のホスト名に別名を付けることができます。
別名を付けた場合は別名を表示させたかったので、`~/.ssh/config` の中までは見てません。

ホスト名の前に `<ユーザ名>@` を付けると、ユーザーを指定してログインをすることができます。
`ssh` コマンド実行時に、あえてユーザー名を指定した場合は、それも一緒に表示させたかったので、これを取り除くような処理は書いていません。


[^1]: `#{?...}` や `#{m:...}`、`#W` などの書式について、詳しくは tmux ドキュメントの [FORMATS の章](http://man.openbsd.org/OpenBSD-current/man1/tmux.1#FORMATS) を参照してみてください。
[^2]: パターンマッチには [fnmatch](http://man.openbsd.org/fnmatch.3) が使われているようです。
[^3]: コマンドの前に `\` を付けているのは、`.bashrc` とかでエイリアスを設定されていた場合に、それを無効にするためです。