はじめに
TIG真野です。育休明けです。
GoでCLI(コマンドラインインタフェース)の開発に役立つツールといえばいくつか選択肢があります。大きく分ければフラグのパースを支援するもの(標準のflagパッケージ、alecthomas/kong/go-flags、alecthomas/kongなど)と、開発フレームワークと言っても良い包括的に支援するもの(urfave/cli、spf13/cobraなど)の2つに分けられるかなと思います(※概念的に分けてみただけで捉え方によっては全てパーサだしフレームワークとみなしても良いかもしれません。あくまで個人的なイメージです)。
私は urfave/cli
を利用することが多いのですが、spf13/cobra
も人気ですよね。どちらも広く利用されていますが、支配的と言った感じではなく、例えば私がよく用いるterraformコマンドはmitchellh/cliというライブラリが使われていましたし、Go製のテンプレートエンジンで有名なHugoは、Hugoの要件にフィットするようにbep/simplecobraというライブラリを開発しているようでした。Protocol Bufferのprotocコマンドに至っては標準パッケージのflagを使っています。気に入ったのを好きに使えば良いんだ感があります。
subcommands
そんな中、今回取り上げるのはgoogle/subcommands です。
私がこの存在を認識したのは、Google Go Coding Guide のベストプラクティス編のcomplex-command-line-interfacesに記載されているのを読んだことがキッカケです。
その部分を引用+意訳します。
kubectl create
、kubectl run
といったサブコマンドを含むCLI開発の場合は、シンプルで正しく利用しやすいsubcommands
がお勧めsubcommands
が提供されていない機能を求める場合はcobra
がお勧め
subcommands
は開発元がGoogleだけあって推しを感じますね(なお、READMEには “This is not an official Google product(「Google公式プロダクトじゃないよ」)” とあります)。。ちなみに、kubectl
はcobra
を使っています。さらに余談ですが、 docker
コマンドも cobra
で開発されています。
Goのコーディング規約として、Google Coding Guide
には今後少なからず影響を受けていくと思うので、 subcommands
について理解を深めようと思います。
subcommands を使っているプロダクト
subcommandsのGoDocにあるimportedbyから調べると、wire、gvisitor、vulsなどのプロダクトなどがsubcommandsを利用しています。Vuls、お前もそうだったのか。
importbyはForkされたリポジトリ数も拾われますし、スター数で絞れるわけではないので単純化できませんが、2023.10.13時点でsubcommandsは628パッケージインポートされていました。ちなみに、cobraは9.4万、urfave/cliは1.4万で桁違いでした。擁護するわけではないですがsubcommandsの公開が2019年2月(1.0.0のRelease日)と比較的新しいことがあるかもしれません(cobra v0.0.1の2017年10月、urfave/clin v0.0.1の2013年6月。どのバージョンと比較するのが適切か難しいですが)。
使ってみた
subcommandsですが、利用ガイド的なものは見当たらなく、README.mdも色気は無いですが、実装はsubcommands.goのみ(!)で、こちらが500行程度と、とても薄いライブラリだという事がわかります。この薄さが魅力だと感じるかどうかがsubcommandsを使う判断ポイントな気がします。READMEにはprintコマンドのサンプルが載っていますが、少しだけオリジナリティを出すため簡単なオプションを追加したクリップボードを読み取り/書き込みする簡単なツールを作ります。
なお、クリップボードを操作するためのパッケージはgolang-design/clipboardを使いました。
最初に、printCmd
、writeCmd
を実装していきます。実装すべきは Name()
、Synopsis()
、 Usage()
、SetFlags()
、Execute()
です。Name()
、Synopsis()
、 Usage()
はヘルプメッセージに用いるメソッドで、実態は SetFlags()
、Execute()
の2種類です。シンプルですね。
package main |
宣言した、printCmd
, writeCmd
を subcommands
パッケージに登録します。
package main |
これをビルドして、ヘルプコマンドを表示します。
$ go build -o subclip . |
見ると分かる通り、Name()
で宣言したコマンドの一覧と Synopsis()
で書いた説明が表示されます。commands
, flags
, help
は subcommands
パッケージに予め宣言されたコマンドたちで、main関数内で登録しています。特にhelpは必須かなと思います。
さて、printにはオプションを2つ追加しています。どうやって確認するのでしょうか。答えはflagsかhelp の引数に、オプションを確認したいコマンド名を渡す必要があります。
$ subclip flags print |
こうしてみると、 flags
は help
に包含されている内容であるため、コマンドラインとして用意しなくても良い気がしますね(wireなんかはすべて登録しているので、subcommandsを利用する場合はすべて登録する流れかもしれませんが)。
続いて予め用意されたcommands
ですが、これはコマンドの一覧を表示します。help
で詳細を確認するとその通りの内容です(どのようなケースで嬉しいのかいまいち掴みきれませんが)。
$ subclip commands |
help
と commands
の並び順も異なるのが気になりましたが、、おそらく仕様なのでしょう。
help
でサブコマンドのオプションを表示する。
利用頻度が高く重要なオプションは、 help
コマンドで表示してほしいことも多いと思います。subcommands.ImportantFlag()
が対応してくれそうですが、これはトップレベルのフラグにしか対応していないようです(awscli で言えば、 –profile などの全コマンドに適用するオプションのイメージ)。
そのため、必要であれば、 Synopsis()
に利用例を書くなどの工夫が必要そうです。
グループ化
subcommandsに登録する際、第2引数にgroup名を登録することが可能です。以下の様に書き換えます。
func main() { |
そうすると help
メッセージを出すときにグルーピングが行われます。類似性の高いサブコマンドごとに設定すると便利かもしれません。
$ subclip help |
サブコマンドのエイリアス
サブコマンドのエイリアスもつけることができます。 subcommands.Alias()
を利用すればいけました。
func main() { |
ヘルプメッセージにも表現されています。
$ subclip help |
利用頻度が高そうだと思いました。
サブコマンドのサブコマンド
Goでsubcommandsを使う - yunomuのブログ にかかれている通り、subcommands.Commander
を自前で重ねることでN階層にネストしたコマンドを作れるそうです。READMEに実装例が無かったので実現できないと私は最初、勘違いしていました。おそらく勘違いしやすいポイントなので、覚えておくと良いと思います。
その他の機能
以下のような機能は無さそうでした。
フラグのパースは標準パッケージのflagを用いているため、同様の壁がある。
--number
,-n
のような、ロング・ショートバージョンのオプション- フラグのパースは、標準パッケージのflagを使っているため、必要であれば自前で実装する必要があります
- 環境変数からオプション指定、上書き
- 標準パッケージのflagを用いているため、必要であれば自前で実装する必要があります
コマンドのtypoから一番近いコマンドを提案するような機能。
subclip wite
じゃなくて、subclip write
みたいな提案をする機能は無いです- 存在しないコマンドを指定した場合、
help
が表示されます
- 存在しないコマンドを指定した場合、
まとめ
subcommandsは非常に薄く、シンプルであるため機能を特化したCLIツールを作るのに適していると思います。また、subcommandsのgo.mod を見ると3rdパーティパッケージの依存がゼロなため依存先のパッケージのアップデートで壊れるといったことが無いため安定的で、おそらくバイナリサイズも小さくできると思います(こちらは誤差レベルでしょうが)。
その割にはコマンドのエイリアスや階層化できたりとパワフルなところもあり、リッチに作り込むこともできます。
オプションのショート・ロングバージョンの準備や、環境変数とのマージなど、細かな作り込みを不要とできるのであれば採用してみても良いのではないでしょうか。