fzf を使って SORACOM CLI をもっといい感じに補完する


この記事は[株式会社ソラコム Advent Calendar 2021](https://qiita.com/advent-calendar/2021/soracominc) の 14日目の記事です。

## TL;DR;

- fzf の Custom fuzzy completion を使って、SORACOM CLI で imsi 等のリソースの値を、裏で CLI 呼んでいい感じに選択できるような設定を書いてみたで
   - ここにおいとくから良かったら参考にしてな ([GitHub](https://github.com/akirakono/soracom-cli-fzf-completion))
- fzf の Custom fuzzy completion 、bash/zsh の 補完を自分で書くより簡単やからおすすめやで

<script id="asciicast-9nEWq3Uvd8r1bH6FyWirJTino" src="https://asciinema.org/a/9nEWq3Uvd8r1bH6FyWirJTino.js" async></script>

## [SORACOM CLI](https://users.soracom.io/ja-jp/tools/cli/)

SORACOM は IoT プラットフォームとして通信を中心とした様々なサービスを提供していますが、その殆どの機能をユーザーコンソールや API のほかに CLI で操作することができます。aws cli などと似たような雰囲気で、例えば

```shell
$ soracom sims get-data --sim-id XXXXXXXXXXXXXXX
```
のようにすると、[SORACOM Harvest Data](https://users.soracom.io/ja-jp/docs/harvest/) に蓄積されている、SORACOM IoT SIM を搭載した IoT デバイスから送信されたデータを簡単に取得できるわけです。

そんな SORACOM CLI （、にかぎらずCLI コマンド全般ですが）は、ちょっとした操作や自動化、データ整形等にとっても便利ですが、多くあるコマンドやオプションを覚えるのは大変ですし、毎回マニュアルを見たり web で検索するのも面倒です。

そんなときに役立つのはシェルの補完機能です。コマンドを書きかけの状態で `<TAB>` や `<Ctrl+I>` キーを押すと、候補のオプションや値を表示して選択させてくれるえらいやつです。

SORACOM CLI でも bash/zsh 向けの補完機能が提供されていて、ご機嫌にオプションをバシバシ補完していけるわけですが、上の例の `--sim-id` のような、静的に決まらない値は補完できないので、結局別の CLI 結果やユーザーコンソールからコピペすることになるわけです。困ったなー[^1]

## fzf の Fuzzy completion 機能

[fzf](https://github.com/junegunn/fzf) は [peco](https://github.com/peco/peco) や [percol](https://github.com/mooz/percol) と同じような、CLI で動作するインタラクティブなフィルターツールです。
普段から Unix 系 OS や wsl 等で CLI を利用される方は、いずれかのツールを利用されている方も多いのではないでしょうか？
私自身もよく使っていて、これらのツールが無いと生産性とやる気が30%ぐらい低下する気がします。

ところで fzf には、他のツールと同様に pipe して選択肢を絞り込んだりする他に、"Fuzzy completion" という機能が付属（というかスクリプトが同梱）しています。
この Fuzzy completion は、コマンドを入力中に `**` をキーワードとして入力して `<TAB>` キーを押すと、そのとき入力しているコマンドラインのコンテキストに応じて、いい感じに選択肢を表示 => fzfで絞り込んでその結果を元のコマンドラインに補完、ということをしてくれます。

こんな感じです。

<script id="asciicast-aiX5RP0t9eMrRAgE3FTW4e2Gj" src="https://asciinema.org/a/aiX5RP0t9eMrRAgE3FTW4e2Gj.js" async></script>
いいですね。

その他の例は [README](https://github.com/junegunn/fzf#fuzzy-completion-for-bash-and-zsh) を見てもらうのが早いのですが、`kill **<TAB>` とするとプロセス一覧から fzf で選択できたり、`export **<TAB>` すると変数一覧を出してくれたりして素敵なわけです。

さて、この Fuzzy completion はデフォルトを `pwd` 以下のファイルの補完としつつ、組み込みでいくつかのコマンドをサポートしているわけですが、自分で設定を書いて自分の好きなコマンドの補完をさせることもできます。

というわけで今回は、SORACOM CLI 向けの Custom Fuzzy Completion を書いてみて、もっといい感じに補完してやろうという試みです。

ちなみに、この `**` をキーワードとして入力して`<TAB>`、というのに最初は慣れなかったんですが、最近は 「fzf/peco を使ったオレオレ便利 function/alias キーバインドも使わないとすぐ忘れるから、`**` に集約したほうが合理的」かも、と言うふうに思うようになってきました。

:::note info
fzf の Fuzzy completion 機能は `apt` 等のパッケージマネージャ経由でインストールしている場合はバンドルされていない場合があるようです。その場合は <a href="https://github.com/junegunn/fzf#using-git">install スクリプト</a> を使っての再インストールを試してみてください。
:::

## fzf の Custom Fuzzy Completion を設定する

:::note
以下では基本的に zsh を前提にしています。bash でも大体は動くと思いますが一部追加の設定が必要だったりするので、詳しくは本家のドキュメントをご覧ください。
:::
:::note warn
Custom fuzzy completion は実験的な機能なので、今後のアップデートで使い方が変わる可能性があります。
:::

fzf の Custom fuzzy completion を設定するのはとっても簡単です。
[README](https://github.com/junegunn/fzf#fuzzy-completion-for-bash-and-zsh) にある、架空の `foo` コマンドに対する設定を見てみましょう。

```sh
_fzf_complete_foo() {
  _fzf_complete --multi --reverse --header-lines=3 -- "$@" < <(
    ls -al
  )
}

_fzf_complete_foo_post() {
  awk '{print $NF}'
}
```
このように`_fzf_complete_<コマンド名>` と言う名前の関数をつくって `.zshrc` に書いておけば、あとは fzf が zsh の機能を使って勝手に紐づけてくれます。

関数の中では例にあるような形で`_fzf_complete` を呼び出します。
`--multi --reverse --header-lines=3 `の部分は無くてもOKで、任意の `fzf` コマンドへのオプションを渡せます。
 `<( )` の部分が補完候補を生成する部分で、プロセス置換を使っています。任意のコマンドを実行して改行区切りで出力すれば OK です。
また、 `_fzf_complete_<コマンド名>_post` は、↑の選択された候補から、実際に補完される文字列を取り出す処理です。


この設定を書くと、以下のような感じの動きになります。

<script id="asciicast-qJI3ZpBrXcoM9OAGYdpRwmqJU" src="https://asciinema.org/a/qJI3ZpBrXcoM9OAGYdpRwmqJU.js" async></script>
見てもらえばわかる通り、`ls -al` の結果を候補としてし表示されています。実際に補完されるファイル名だけでなく permission や ファイル容量等も同時に確認＆絞り込みのフィルター文字列として使えるのが良いですね。

## SORACOM CLI 向けの Custom Fuzzy Completion を設定する（シンプル）

というわけで書いてみたのが下記です。
<script id="asciicast-wEwG8jDPy8pntF7RmlClqDeO5" src="https://asciinema.org/a/wEwG8jDPy8pntF7RmlClqDeO5.js" async></script>
いい感じじゃないでしょうか？
実際に入力したいのは UUID のグループ ID ですが、human readable なグループ名で絞り込めるのが good です。

下記に抜粋を載せて説明します。
コード全体は [github に置いておいた](https://github.com/akirakono/soracom-cli-fzf-completion/blob/main/soracom_cli_fzf_custom_complete)のでそちらを参照してください。

```zsh
_fzf_complete_soracom() {
  if [[ "$@" =~ '--coverage-type *$' ]] then
    _fzf_complete --no-sort --preview "echo hello {}" -- "$@" < <(_soracom_coverage_type_candidates )
  elif [[ "$@" =~ '--imsi *$' ]] then
    _fzf_complete --no-sort -- "$@" < <(_soracom_imsi_candidates )
  else
    eval "zle ${fzf_default_completion:-expand-or-complete}"
  fi
}

_fzf_complete_soracom_post() {
  awk '{print $1}'
}

function _soracom_coverage_type_candidates() {
  echo 'g'
  echo 'jp'
}

function _soracom_imsi_candidates() {
  soracom subscribers list --fetch-all | jq -r '.[] | [.imsi, .tags.name] |@tsv' 
}

```

簡単に解説すると、
`_fzf_complete_soracom` 関数では、 `$@` に 入力中のコマンドライン文字列（から`**`を取り除いたもの）が入っているので、 `=~` を使って正規表現で入力中のオプションをマッチさせています。[参考](https://zsh.sourceforge.io/Doc/Release/Conditional-Expressions.html)

マッチした先の関数では、 `soracom subscribers list --fetch-all | jq -r '.[] | [.imsi, .tags.name] |@tsv' ` のような形で SORACOM CLI の実行結果を `jq` コマンドで `tsv` 形式に変換しています。

さらに `_fzf_complete_soracom_post` 関数で、選択された後に補完される値（空白区切りで最初の値）を抽出する、という流れです。

簡単ですね！
私のシェルスクリプト力はアレなので愚直な感じですが、やりたいことは素直に表現できていると思います。

## SORACOM CLI 向けの Custom Fuzzy Completion をもっといい感じにする

実際にはこれまでのところで個人的な要件としては事足りているわけですが、改善アイデアが思い浮かぶと試してみたくなるのがエンジニアの性（個人の意見）です。

私が思ったのは、

- fzf って `--preview` オプションがイケてるから、使ってよりいい感じにしたいなー
- 補完するたびに裏で API 呼びに行くのなんか嫌だなー、ローカルにキャッシュしたいなー

ということでした。
というわけで休日の貴重な自由時間を費やして、 zsh と戦いながら書いたのがこちらです。

<script id="asciicast-9nEWq3Uvd8r1bH6FyWirJTino" src="https://asciinema.org/a/9nEWq3Uvd8r1bH6FyWirJTino.js" async></script>

良い。良いですね。何がというか個人的に「イケてる」感がでて満足です。

こちらでは、

- 補完用のCLI の実行結果（加工前） を `/tmp/soracom-value-complete-cache-imsi` のような tmp ファイルとして保存する
- ↑のファイルを、TTL 5分のキャッシュとして使用する。（５分以内ならキャッシュファイルを使う、５分以上経過していたらもう一度 CLI を実行して新しい結果をとりにいく）
- `fzf` の preview に、選択されているアイテムのより詳細な情報（キャッシュファイルからフィルタした json）を表示する

というような機能を実現しています。

コードは[こちら](https://github.com/akirakono/soracom-cli-fzf-completion/blob/main/soracom_cli_fzf_custom_complete_with_cache)です

中身は見てもらえれば大体わかると思うのですが、１点私が今回調べていて読み解くのにすごく時間がかかったところだけ解説しておきます。

#### zsh の機能で TTL つきの簡易キャッシュ

```sh
_soracom_fzf_cache_valid() {
  local cache_file_name="$1"

  # declare an array
  local -a ok 

  # check the last modification time is in 5 minutes
  # See http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers
  ok=( "$cache_file_name"(Nmm-5) )

  # exit with 0 if valid cache exists (= num of array is 1), non-zero otherwise
  (( $#ok ))
}
```
これは、[zsh のマニュアル](https://zsh.sourceforge.io/Doc/Release/Completion-System.html#Completion-Functions) にあった例を改変したもので、引数で与えられたファイルが５分以内に更新されていれば true (=0) を返す関数なんですが、はじめ私のシェル力では何が起きているのか全然わからず、かつなんとググれば良いかもわからずに途方にくれました。
その後がんばって解読したので以下に説明します

- `local -a ok` は ok を配列として変数宣言
- `"$cache_file_name"(Nmm-5)` は zsh のファイル名展開機能の [Glob Qualifier](https://zsh.sourceforge.io/Doc/Release/Expansion.html#Glob-Qualifiers) を使っていて、
   - N: `NULL_GLOB` をon = 展開結果がない場合に空文字列になるようにする
   - mm-5: は last modification time が 5分以内のファイルのみ選択
      - 他にも ah-5 だと 「直近5時間にアクセスされたファイル」とか
      - 他にも m+7 だと 「直近7日間に変更されていないファイル」とかが表現できる
- `ok=( "$cache_file_name"(Nmm-5) )` となっているのは array への代入で、すなわち
      - 「5分以内に更新されたキャッシュファイルがある」場合は、キャシュファイル名の文字列を要素として含む配列
      - そうでない場合は空配列
- `$#ok` は配列の要素数
- `(( $#ok ))` は [Arithmetic Evaluation](https://zsh.sourceforge.io/Doc/Release/Arithmetic-Evaluation.html) で、中身が non-zero の値として評価されると 0 を返す

というわけで完結に TTL つきのキャッシュを実現できてすごいんですが、
知らないと全くわからないので、よい子は利用する際にコメントを書いておきましょう。

## Future Work
今回できていないところとして下記が思いついていますが、今必要ないのでエンハンスしたい気持ちをグッとこらえました。
もし、必要になって作った方がいらっしゃれば是非シェアさせてください。

- そもそも補完候補が足りていない
- 複数の coverage type を使うユースケースに対応していない
    - SORACOM には jp と global の２つの coverage type があって、それぞれ異なるリソースを扱えるんですが、今回のキャッシュ機能はその区別をしていないです
- SIM とか Device とかを補完候補として、あるいは preview でみるときに、group の id だけでなく名前も表示したい（SIM 等の JSON データには group の id のみが含まれているけど、group の結果もキャッシュするようにしているのでそのキャシュファイルから引けば表示できるはず）

## さいごに
fzf の custom fuzzy completion よい

[^1]: このブログを書いている途中で、ソラコムの同僚が２年前に [sorashell](https://0x6b.github.io/super-charge-your-soracom-command-line-experience-with-sorashell/) というすごいやつを作っていたことに気が付きましたが、ひとまずそっと閉じました