GoのCLI用ライブラリcobraのzsh用の補完生成機能は弱い (?) のと、それをどうするかの話

フラグのカスタム補完がうまくできなかったので書きました

cobra の補完生成機能とは

みんな大好き cobra には bashzsh 向けの補完生成機能が存在する

詳しくは cobra/bash_completions.md at master · spf13/cobra · GitHub を見てほしいけど、こんな感じ ↓ で簡単に補完の機能を生成し、出力させることが出来る

# https://qiita.com/minamijoyo/items/9dceb1d8a66e48ab45cd
package cmd

import (
    "os"

    "github.com/spf13/cobra"
)

func init() {
    RootCmd.AddCommand(newCompletionCmd())
}

func newCompletionCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "completion",
        Short: "Generates shell completion scripts",
        Run: func(cmd *cobra.Command, args []string) {
            cmd.Help()
        },
    }

    cmd.AddCommand(
        newCompletionBashCmd(),
        newCompletionZshCmd(),
    )

    return cmd
}

func newCompletionBashCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "bash",
        Short: "Generates bash completion scripts",
        Run: func(cmd *cobra.Command, args []string) {
            RootCmd.GenBashCompletion(os.Stdout)
        },
    }

    return cmd
}

func newCompletionZshCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "zsh",
        Short: "Generates zsh completion scripts",
        Run: func(cmd *cobra.Command, args []string) {
            RootCmd.GenZshCompletion(os.Stdout)
        },
    }

    return cmd
}

cobra.CommandGen{Ba,Z}shCompletion という関数が生えているので、それを叩くだけ

例に示した通りに bash 用、zsh 用の関数があることがわかる

これを実装した後はこんな感じ ↓ で補完を読み込むようにして、再度 shell を開くなりして補完を利かすことが出来る

$ echo ". <(hogefuga completion bash)" >> ~/.bashrc
$ hogefuga completion zsh > /usr/local/share/zsh/site-functions/_hogefuga

すごくシンプルなものであればこれで結構事が足りて、用意したフラグ名についても正しく補完されるようになる

custom completion について

たとえば docker の補完の場合、 docker container rm <TAB> と入力すると現在起動しているコンテナ一覧が表示され、容易に好きなコンテナを指定することが出来る

これは、補完のスクリプト内で docker のコンテナ一覧を取得する処理が走っており、任意の変数に代入されることで実現している

これを cobra で実現する場合、次のようなコードを書くことになる

const (
        bash_completion_func = `__docker_get_container()
{
    local docker_output out
    if docker_output=$(docker container ls -q 2>/dev/null); then
        out=($(echo "${docker_output}" | awk '{print $1}'))
        COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
    fi
}

__docker_custom_func() {
    case ${last_command} in
        docker_container_rm)
            __docker_get_container
            return
            ;;
        *)
            ;;
    esac
}
`)

// 省略

cmds := &cobra.Command{
    Run: runSomething,
    BashCompletionFunction: bash_completion_func,
}

詳しく説明しないし、↑ のコードは動かしてないから動作しないかもしれないけど簡単に言うと補完するコマンドが見つからない際に __docker_custom_func が発火され、そこでなんというコマンドが実行されたか見て、ここで docker_container_rm だった場合は __docker_get_container が発火される

__docker_get_container はいろいろやってるけど結局は COMPREPLY にコンテナの id 一覧を代入している

ちなみにフラグにわたす場合はこんな感じ

func rootCmd() *cobra.Command {
    cmd := &cobra.Command{
        RunE: func(c *cobra.Command, args []string) error {
            // something
        },
        BashCompletionFunction: `__docker_get_container()
{
    local docker_output out
    if docker_output=$(docker container ls -q 2>/dev/null); then
        out=($(echo "${docker_output}" | awk '{print $1}'))
        COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
    fi
}
`,
    }

    # しょうりゃく

    cmd.Flags().StringVar(&opts.profile, "container", "fuga", "set container id")
    cmd.MarkFlagCustom("profile", "__docker_get_container")

    # しょうりゃく
}

cmd.MarkFlagCustom で指定のフラグに対する補完用のスクリプトを指定する

zsh では custom completion が効かない

他は大体いい感じにしてくれるのに、custom completion だけ使えない

公式にある zsh_completions.md にはこう書かれている

What's not yet Supported

  • Custom completion scripts are not supported yet (We should probably create zsh specific one, doesn't make sense to re-use the bash one as the functions will be different).
  • Whatever other feature you're looking for and doesn't exist :)

なので、上の例に登場していた BashCompletionFunctionzsh 版みたいなものは存在しない

つまり、素直に GenZshCompletion を叩いたところで custom completion は全く効かない

じゃあどうすればよいのか

でも我々は cobra 製である kubectl、minikube(CLI)あたりに zsh 用の素晴らしい補完が用意されていることを知っている

あれはどうやって custom completion を実現しているのだろうか?

結論はこんなかんじ ↓ だった

  1. bash 用の補完スクリプトを生成する
  2. bash 用の補完スクリプトから zsh 用の補完スクリプトに変換するスクリプトを生成する
  3. 1 と 2 を結合して完成

まじかよ…

実際のコードはこんな感じ

bash to zsh 用テンプレートの中に GenBashCompletion の結果を埋めていることがわかる

各自で開発している CLI にこのテンプレートを埋め込む場合は minikube の GenerateZshCompletion を参考にするとよさそう ( __minikube などの文字列を __hogefuga に置き換えるだけで動いた)

おわりに

あまり zsh と補完に詳しくないからわからないけど、なんで cobraZshCompletionFunction を実装しないの…?