tblsには、tbls-cacoo
のような外部コマンドをtbls cacoo
のようなサブコマンドとして実行できるような機能を実装しています。私はこれを取り敢えず”外部サブコマンド機能”と呼んでいます。
外部サブコマンド機能は、
- コマンド名が
tbls-XXX
という命名規則であること - 1を
tbls XXX
という形であくまでtbls
を実行すること
という2つの条件をみたすことで、環境変数経由でtblsで取得した情報を外部コマンドに渡しています。
これにより、外部コマンド側では少ないコードベースで機能を実現できるようになっています。
実際に、例えばtbls-cacooのコードにはデータベースに接続するような実装は含まれていませんが、以下のようなコマンドでCacooで対象のデータベースのER図を生成するためのCSVを出力できます。
$ TBLS_DSN=postgres://dbuser:dbpass@localhost:5432/dbname tbls cacoo csv --out cacoo.csv
$ ls tbls.yml $ tbls cacoo csv --out cacoo.csv
tblsの外部サブコマンドの仕組み
tblsの外部サブコマンドの仕組みですが、具体的には以下のエントリの 追記
に書かれている手法をほぼそのまま採用しています。
pocketberserker.hatenablog.com
(追記
以外の部分もspf13/cobraの挙動の説明など参考になります)
コードはここらへんで、関係がある部分を抜き出すと以下のようになります。
// rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "tbls", SilenceErrors: true, SilenceUsage: true, Args: cobra.ArbitraryArgs, DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { cmd.Println(cmd.UsageString()) // argsがないのであれば通常通りUsageを表示する return } envs := os.Environ() subCommand := args[0] path, err := exec.LookPath(cmd.Use + "-" + subCommand) // `tbls-*` なコマンドが存在するかを確認 if err != nil { if strings.HasPrefix(subCommand, "-") { cmd.PrintErrf("Error: unknown flag: '%s'\n", subCommand) cmd.HelpFunc()(cmd, args) return } cmd.PrintErrln(`Error: unknown command "` + subCommand + `" for "tbls"`) cmd.PrintErrln("Run 'tbls --help' for usage.") return } configPath, args := parseConfigPath(args[1:]) // argsから `-c` `--config` オプションだけ外部コマンドに渡さずにパースして抜き出す // configPathの処理。ここでデータソースに接続したりして必要な環境変数(envs)を組み立てている c := exec.Command(path, args...) // 外部コマンドの実行 c.Env = envs c.Stdout = os.Stdout c.Stdin = os.Stdin c.Stderr = os.Stderr if err := c.Run(); err != nil { printError(err) os.Exit(1) } }, } func Execute() { if err := rootCmd.Execute(); err != nil { printError(err) os.Exit(1) } }
元のエントリと比べて違う点は、argsから -c
--config
オプションだけ限定してパースしていることくらいです。
外部サブコマンドのbash/zsh補完
spf13/cobraは、簡単なbash補完用の関数やzsh補完用の関数は自動生成してくれる機能を提供してくれています。
tblsではこれに手を加えてtbls [TAB]
でサブコマンドに加えて外部サブコマンドも候補にでるようにしています。
$ tbls build -- tbls-build cacoo -- tbls-cacoo completion -- output shell completion code coverage -- measure document coverage diff -- diff database and document doc -- document a database help -- Help about any command lint -- check database document meta -- tbls-meta out -- analyzes a database and output version -- print tbls version
実装はzshだと、
subcommands=( ${(@f)"$(print -l ${^path}/tbls-*(-*N:t) | sort -u | sed -e 's/^tbls-\(.*\)$/\1:tbls-\1/')"} )
のようにPATHにある実行可能な tbls-XXXX
といった外部コマンドを取得して(ここ)、補完候補に加えています。
そして、補完候補が入力されたときは
case "$words[1]" in completion) _tbls_completion ;; coverage) _tbls_coverage ;; diff) _tbls_diff ;; doc) _tbls_doc ;; help) _tbls_help ;; lint) _tbls_lint ;; out) _tbls_out ;; version) _tbls_version ;; *) if type _tbls-"$words[1]" 1>/dev/null 2>/dev/null then _tbls-"$words[1]" fi ;; esac
のように、tblsが持っていないサブコマンド ( "$words[1]"
)が選択されたときは _tbls-"$words[1]"
(外部コマンド側の補完関数)の存在を確認、存在していればそちらの補完処理を渡すような実装になっています(ここ)。
実際の補完関数は tbls completion bash
や tbls completion zsh
を実行してみてください。
上記の補完関数の実現には、syohexさんやbuty4649さんにbashやzshの補完機能についての情報を提供してもらったり、syohexさんに実際に修正PRをもらったりしました。この場を借りてお礼を申し上げます。ありがとうございました!
というわけで
"外部サブコマンド" という、ニッチな機能実現のお話でした。