Go Command Line Tool Packagesをざっと試す

Posted on
go cmd

Go言語でコマンドラインツールを作ろうと思いましたが、サブコマンドが必要になりそうだったので調べました。 書きやすいものを探したいので次のコマンドを作るという前提で比較していきます。

  • ./cmd foo -name=foo args
  • ./cmd bar -name=bar args

対象は標準パッケージと、みんなのGo言語にピックアップされている3rd packageも試してみます。

flag (標準)

大変わかりやすかったので、Go by Example: Command-Line Subcommandsをベースに簡略化して考えたいと思います。

package main

import (
	"flag"
	"fmt"
	"os"
)

func main() {
	fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)
	fooName := fooCmd.String("name", "", "name")

	barCmd := flag.NewFlagSet("bar", flag.ExitOnError)
	barName := barCmd.String("name", 0, "name")

	if len(os.Args) < 2 {
		fmt.Println("expected 'foo' or 'bar' subcommands")
		os.Exit(1)
	}

	switch os.Args[1] {

	case "foo":
		fooCmd.Parse(os.Args[2:])
		fmt.Println("subcommand 'foo'")
		fmt.Println("  name:", *fooName)
		fmt.Println("  tail:", fooCmd.Args())
	case "bar":
		barCmd.Parse(os.Args[2:])
		fmt.Println("subcommand 'bar'")
		fmt.Println("  name:", *barName)
		fmt.Println("  tail:", barCmd.Args())
	default:
		fmt.Println("expected 'foo' or 'bar' subcommands")
		os.Exit(1)
	}
}

ライセンス内で改変しています。ライセンス元 by Mark McGranaghan license

subCmd単位で別の関数を実装指定けばシンプルになりそうですが、エラー処理がもう少しシンプルにかけたらいいのになあと感じます。

urfave/cli

そこら中にやってみた系の記事がある有名なpackageですね。star数からも人気ぶりが伺えます。

どうやらNov 2019にv2がリリースされたようなので、今後も考えてv2を用います。マニュアルを読んで、同様な内容を書いてみました。

コマンドを構造体で作成していくのが特徴的ですね。コマンドラインをParseする仕組みを用意するのは簡単そうです。 テスト方法はAction階層のfuncを単位に cli.Context を渡して書いていけば良さそうでしょうか?

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/urfave/cli/v2"
)

func main() {
	app := &cli.App{
		Commands: []*cli.Command{
			{
				Name:    "foo",
				Aliases: []string{"f"},
				Usage:   "subcommand foo",
				Action: func(c *cli.Context) error {
					fmt.Println(c.String("name"))
					fmt.Println("I am foo: ", c.Args().First())
					return nil
				},
				Flags: []cli.Flag{
					&cli.StringFlag{
						Name:    "name",
						Aliases: []string{"n"},
						Value:   "default",
						Usage:   "name",
					},
				},
			},
			{
				Name:    "bar",
				Aliases: []string{"b"},
				Usage:   "subcommand bar",
				Action: func(c *cli.Context) error {
					fmt.Println(c.String("name"))
					fmt.Println("I am bar: ", c.Args().First())
					return nil
				},
				Flags: []cli.Flag{
					&cli.StringFlag{
						Name:    "name",
						Aliases: []string{"n"},
						Value:   "default",
						Usage:   "default",
					},
				},
			},
		},
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

話が変わりますが、Subcommandsの例にある、次のような階層型のサブコマンドは標準パッケージで作るのはしんどそうなので簡単に作れるのはいいですね。

./cmd - add
	  - complete
      - template - add
	             - remove

spf13/cobra

こちらも大変有名ですね。KubernetesやHugoなど色々なプロジェクトに使われていてリッチな本格CMDが作れるそうです。

cobraには推奨のdir構造があり、その雛形を作成してくれるコマンド cobra が用意されています。その説明はcobra/cobraにあります。

$ cobra init --pkg-name github.com/sourjp/practice/cmd --viper=false
$ cobra add hoge

$ tree
.
├── cmd
│   ├── root.go		// cobra init
│   └── hoge.go		// cobra add
└── main.go			// cobra init

$ cobra init で、cmdを実行する main.gocmd/root.go を次のように作成してくれます。中身を抜粋したものを貼り付けましたが、見ればわかるように rootCmd の実行を root.go で実行するため、サブコマンドは $ cobra add によって rootCmd.AddCommand() で追加されています。

注意点は $ cobra init におけるpackage nameで、これは main.go から、/cmd をimportするpathになるので、正しく設定しましょう。 viper という設定管理ファイルを容易に操作できるpackageと連動する仕組みもありますが、今回の趣旨ではないので不要にしています。

package main

import "github.com/sourjp/practice/cmd"

func main() {
	cmd.Execute()
}
package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
	"os"
)

var rootCmd = &cobra.Command{
	Use:   "sample",
	Short: "A brief description of your application",
	Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func init() {
	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var hogeCmd = &cobra.Command{
	Use:   "hoge",
	Short: "A brief description of your command",
	Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("hoge called")
	},
}

func init() {
	rootCmd.AddCommand(hogeCmd)
}

続いて、hogeオプションをつけて見ました。fugaは全く同じ構造になるので省略します。

package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var (
	name string

	hogeCmd = &cobra.Command{
		Use:   "hoge",
		Short: "A brief description of your command",
		Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("hoge called", name, args)
		},
	}
)

func init() {
	hogeCmd.PersistentFlags().StringVarP(&name, "name", "n", "hoge", "hoge name")
	rootCmd.AddCommand(hogeCmd)
}

簡単に追加できますね。この場合もtestはRun部分の関数を切り出せば良さそうです。

mitchellh/cli

みんGo でも取り扱っているパッケージです。ドキュメントが全くないのでExampleとソースコードを見てざっくり理解しました。

特徴的などは後述することとし、とりあえず実装してみました。 Barは同じ実装になるのでオプションコマンドは省略してます。

package main

import (
	"flag"
	"fmt"
	"log"
	"os"

	"github.com/mitchellh/cli"
)

func main() {
	c := cli.NewCLI("app", "1.0.0")
	c.Args = os.Args[1:]
	c.Commands = map[string]cli.CommandFactory{
		"foo": func() (cli.Command, error) {
			return &FooCmd{}, nil
		},
		"bar": func() (cli.Command, error) {
			return &BarCmd{}, nil
		},
	}

	exitStatus, err := c.Run()
	if err != nil {
		log.Println(err)
	}

	os.Exit(exitStatus)
}

type FooCmd struct{}

func (c *FooCmd) Help() string {
	return "./cmd foo --help"
}

func (c *FooCmd) Synopsis() string {
	return "./cmd --help"
}

func (c *FooCmd) Run(args []string) int {
	var name string
	fs := flag.NewFlagSet(name, flag.ExitOnError)
	fs.StringVar(&name, "name", "foo", "This is foo name options")
	fs.Parse(args)

	fmt.Println("This is foo cmd: ", args)
	fmt.Println(name)
	return 0
}

type BarCmd struct{}

func (c *BarCmd) Help() string {
	return "./cmd foo --help"
}

func (c *BarCmd) Synopsis() string {
	return "./cmd --help"
}

func (c *BarCmd) Run(args []string) int {
	fmt.Println("This is bar cmd: ", args)
	return 0
}

特徴は何と言っても map[string]cli.CommandFactory{} だと思います。 keyがStringで指定するコマンドで、 cli.CommandFactory{} が実装です。

これは type CommandFactory func() (Command, error) と、Commanderrorを返す関数を受け取るようです。 次に Command を参照します。

type Command interface {
	Help() string			// ./cmd foo --helpで返すhelp
	Run(args []string) int  // サブコマンドの処理
	Synopsis() string 		// ./cmd --helpで返すhelp
}

つまりこの引数を満たす構造体を第一引数としてCommandFactory()に渡せば良さそうです。このことから みんGo にも書いてあるようにクロージャーで返すのが良さそうです。

サブコマンドでオプションを作る際には、上の FooCmd.Run() で書いたように、標準パッケージと同じようにflagを使います。

使ってみた感じシンプルで良さそうです。サブコマンドまではパッケージで管理され、以降は標準パッケージと同じ感覚で作っていく流れでしょうか。またコマンド登録単位にパッケージを分ければテストも用意そうです。

ただ上の試したツールと比べるとhelpの表示内容が質素など細かい部分で物足りなく感じます。必要十分に絞った感じでしょうか。

他の人の使い方を調べていたら、いい感じ解説してあるサイトを見つけました。

google/subcommands

googleという名前がつきながら公式ではないやつです。ソースコードも subcommands.go と1ファイルでシンプルそうで気になるところですが・・・、Sep 2019 を境に更新が止まってますね。これを試すのはまた今度にしようと思います。

まとめ

ざっと試した所感では次でいこうと思います。

  • 簡単なコマンドラインツールは flag
  • リッチなコマンドラインツールは spf13/cobra

urfave/cliの階層の深さはあまり好みではありません。。 mitchellh/cliもシンプルで良さそうですが、これなら標準パッケージだけ書く方が依存関係も減らせて良さそうに感じました。 google/subcommandsはまたの機会にしようと思います。

さぁ書くぞー