最初はCLI用のフレームワークに codegangsta/cli を使っていました。 サクっと書けるのですけれども、ちょっとややこしいことをしようとした時に悩むんですよね。 ということで、もしかしたら他の方がいいのかなーなどと考えた次第。
で、spf13/cobraなんですが、KubernetesやHugoが採用しているということで興味はあったものの情報があまりなかったので、まじめにREADMEも読んでいなかったのです。 https://github.com/spf13/cobra
あらためてちゃんと見てみたところ、あ、これはすばらしい、という点が多々ありまして、既存のコードをすぐcobraに書き換えました。 ・サブコマンドが個別の.goファイル単位で追加できる ・サブコマンドにコマンドを追加することができる ・フラグにスコープが付けられる(サブコマンド以下へ引き継ぐか否か) ・実行前後にフックが設定できる
作者のSteve Franciaさん、Dockerの中の人なんですね。さすがです。勉強になります。
個人的に最も実現したかったのが「サブコマンドの個別ファイル化」でして、既存ファイルに触れずにサブコマンドを増やしたかったのです。 これについてREADMEにほぼそのままサンプルが書かれていまして、またこの構造がなるほどーと思わせられることしきり。 ディレクトリ構造を
▾ appName/
▾ cmd/
root.go
sub.go
main.go
として、cmd/root.go・cmd/sub.goをcmdパッケージとします。 cmd/root.goに
package cmd
import (
"github.com/spf13/cobra"
// ...
)
var RootCmd = &cobra.Command{
Use: "appName",
Short: "short description",
Long: `long description`,
Run: func(cmd *cobra.Command, args []string) {
// ...
},
}
func init() {
cobra.OnInitialize()
}
などと書いておきます。 (これはRootCmdをcmdパッケージ内に作成するためだけのファイルですね) で、main.goのmain()は
package main
import (
"{appName}/cmd"
// ...
)
func main() {
// READMEに書いてある例:
if err := cmd.RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}
と書いておき、cmd/sub.goでは
package cmd
import (
"github.com/spf13/cobra"
)
var subCmd = &cobra.Command{
Use: "sub",
Short: "sub command",
Long: `sub command`,
Run: func(cmd *cobra.Command, args []string ){
// ...
},
}
init(){
RootCmd.AddCommand(subCmd)
}
という感じに、main.go・cmd/root.goをいじらずにサブコマンドを追加できることになります。
孫コマンドを作成する場合は、たとえばさきほどのcmdディレクトリ内に
▾ appName/
▾ cmd/
root.go
sub.go
sub_child.go
main.go
という感じにsub_child.goを追加して、
var fooCmd = &cobra.Command{
Use: "foo",
Short: "foo command",
Long: `foo - child of sub`,
Run: func(cmd *cobra.Command, args []string ){
// ...
},
}
init(){
subCmd.AddCommand(fooCmd)
}
と、サブコマンドに対してAddCommandすることで、
{appName} sub foo ...
というように孫コマンドが作れますね。 複数追加する場合、sub_child.goに追記することも、別ファイルにすることも可能であると。
で、このようにした場合に、「sub以下は共通のフラグを持たせたい」という意図がよくあると思います。 先ほどの sub.go の例でいうと
var configfile string
init(){
subCmd.PersistentFlags().StringVarP(&configfile, "config", "c","", "config file")
// ...
}
と Persistent Flags を設定しておくと、
{appName} sub -c hoge.json foo ...
{appName} sub foo -c hoge.json ...
と実行してconfigfileに”hoge.json”が入るようになるわけですね。
またこのような際に、「上の-cで指定した設定ファイルをパースして変数にセットしてから孫コマンドで使いたい」というようなことがあると思います。 この場合、cmd/sub.goでたとえば
var configfile string
var configstring string
var subCmd = &cobra.Command{
// ...
PersistentPreRun: func(cmd *cobra.Command, args []string ){
// configfile -> configstring
},
}
init(){
subCmd.PersistentFlags().StringVarP(&configfile, "config", "c","", "config file")
// ...
}
とPersistentPreRunで書いておくと、孫コマンドが実行される際、configfileを読んでconfigstringにセットする、という挙動が事前に実行されることになります。
事前実行を孫コマンド側で制御したい場合は、cmd/sub.goで
var configfile string
var configstring string
init(){
subCmd.PersistentFlags().StringVarP(&configfile, "config", "c","", "config file")
// ...
}
func sub(){
// configfile -> configstring
}
などとしておき、cmd/sub_foo.go側で
var fooCmd = &cobra.Command{
// ...
PreRun: func(cmd *cobra.Command, args []string ){
sub()
},
// ...
}
というようにPreRunで事前実行することができるかと思います。
以上だいぶ雑な例で大変恐縮です。
なお、孫コマンドじゃなくてNamespace(sub:foo)が使いたいんじゃ!という場合には、gosuri/cmdnsというライブラリがあるようです(未使用なので何も書けません)。
ということで、cobraで全部まかなえた、というわけでございました。
一つだけ問題があるとすれば、フラグを重複させてしまった場合、ビルドは通って実行時にpanicが起きることでしょうか。 アホかと言われそうですが、ついうっかりやってしまうのです。 testで書けないかなこれ。