GolangでCLIの場合にcobraを使うことにした件


最初は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で書けないかなこれ。

最新記事

すべて表示

SQLite(sqlite3)で “no such table”

小ネタです。 SQLiteを使っていて "no such table" とエラーが出た場合、 DBファイル名の指定が空になっている、という凡ミスを起こしていないかを確認してみましょう。 ・・・ そういう凡ミスをしてしばし悩んだので… ファイル名の指定が空になっている場合、一時的なインメモリDBとして保存されます(※1)。 つまりDB接続を切断すると中身は消えます。 なので接続

アプリケーションサーバにポートを指定せずに起動すると?

最近、 Goで書かれたアプリケーションサーバが起動しない! ->原因: .env ファイルが欠けていた というドタバタがありました。 結局Goと関係ないですが、この時、 「あまりGoに慣れてないのでGoの問題かと…」「DockerまだよくわかってなくてDockerの問題かと…」 というような声があったのて、あえてGoで検証してみようと思ったわけです。 さて、Goでサーバサイドのシステムを作

GitLab 9.1.2 (MySQL) を 11.4.0 (PostgreSQL) にアップグレード

弊社ではかなり前からGitLab(CE)を自社環境で運用しているのですが、ふと気付くと、バージョンがだいぶ先に行ってしまっていました。 とくに最近のバージョンでは Auto DevOps なども使えるようになっていたりするので、さすがにそろそろキャッチアップしたいと考えたわけです。 現行の環境は次の通りです: GitLab 9.1.2 sameersbn/gitlab 使用 MySQL 5.6