最近のgoでCLIのコマンドを作るときの雛形のメモ (updated)
昔にも似たような記事を書いていたみたいだった。最近のgoでのCLIツールを書く際の心境の変化があったのでその辺をメモしておく。
心境の変化
心境の変化は2つ
- コマンドラインオプションをstructで囲む理由が特になくなった
- サブコマンドがやっぱりほしい
コマンドラインオプションをstructで囲む理由が特になくなった
昔はわざわざコマンドラインオプションをstructで囲んで、Run()
関数に渡していたのだけれど。そもそもRun()関数が長々と書かれていたらそれはmain.goの記述が長すぎる。
一度しか使われないものはグローバル変数で良いということでグローバルの変数としてポインタを保持する形式になった。
Run()に対して引数として渡す必要もなくて、dereferenceしてあげればおしまい。それ以前の段階で引数のvalidationは通っているはずなので。 Run()がエラー値を返すのはそのまま。
サブコマンドがやっぱりほしい
サブコマンドのものとコマンドが1つだけのものをほとんど同様の記述で書きたい。そういうわけで少し不思議なコードになっている。
(どの辺が不思議かというと、例えば Cmd = kingpin.CommandLine
のあたり)
コマンドが1つだけの場合
以下の様な感じで書く。kingpinを使うのは惰性なので他のライブラリが便利ならそれを使っても良い。
package main import ( "fmt" "log" "os" kingpin "gopkg.in/alecthomas/kingpin.v2" ) var ( // Cmd ... Cmd = kingpin.CommandLine debug = Cmd.Flag("debug", "Enable debug mode.").Bool() name = Cmd.Arg("name", "name").Required().String() ) func init() { Cmd.Name = "hello world" Cmd.Help = "hello world example" Cmd.Version("0.0.1") } func main() { _, err := Cmd.Parse(os.Args[1:]) if err != nil { Cmd.FatalUsage(fmt.Sprintf("\x1b[33m%+v\x1b[0m", err)) } if err := run(); err != nil { log.Fatalf("!!%+v", err) } } func run() error { name := *name fmt.Println("hello", name) return nil }
実行結果
ヘルプメッセージ
usage: hello world [<flags>] <name> hello world example Flags: --help Show context-sensitive help (also try --help-long and --help-man). --debug Enable debug mode. --version Show application version. Args: <name> name
実行結果
$ go run 00onefile/main.go world hello world
サブコマンド対応
(手元で動かすためにgo.modやimport pathにhackが入っているけれどその辺りは読み替えてほしい)
サブコマンドを定義するときにはだいたい以下の様な構造にしている。結構何も考えずにパッケージを分けちゃったほうがあとあと楽という結論になった。
この例では以下2つのサブコマンドを定義している
- register
- post
$ tree -P "*.go" 01multifiles/ 01multifiles/ ├── main.go ├── post │ └── cmd.go └── register └── cmd.go
go.mod
module m go 1.13 require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 )
ポイントはサブコマンドとしての定義とそうではない場合の1つのコマンドとしての定義の記述が概ね同じような形になるようにしている点。あと気持ちの問題でヘルプメッセージに色を付けている。
main.go
package main import ( "fmt" "log" post "m/01multifiles/post" register "m/01multifiles/register" "os" kingpin "gopkg.in/alecthomas/kingpin.v2" ) var ( // Cmd ... Cmd = kingpin.CommandLine ) func init() { Cmd.Name = "multi commands" Cmd.Help = "multi commands example" Cmd.Version("0.0.1") } func main() { command, err := Cmd.Parse(os.Args[1:]) if err != nil { Cmd.FatalUsage(fmt.Sprintf("\x1b[33m%+v\x1b[0m", err)) } switch command { // Register user case register.Cmd.FullCommand(): err = register.Run() // Post message case post.Cmd.FullCommand(): err = post.Run() } if err != nil { log.Fatalf("!!%+v", err) } }
register/cmd.go
package register import ( "fmt" kingpin "gopkg.in/alecthomas/kingpin.v2" ) var ( // Cmd ... Cmd = kingpin.Command("register", "Register a new user.") nickname = Cmd.Flag("nickname", "Nickname for user.").Default("anonymous").String() name = Cmd.Arg("name", "Name of user.").Required().String() ) // Run ... func Run() error { name := *name nickname := *nickname fmt.Println("register, name=", name, "nickname=", nickname) return nil }
post/cmd.go
package post import ( "fmt" kingpin "gopkg.in/alecthomas/kingpin.v2" ) var ( // Cmd ... Cmd = kingpin.Command("post", "Post a message to a channel.") image = Cmd.Flag("image", "Image to .").File() channel = Cmd.Arg("channel", "Channel to to.").Required().String() text = Cmd.Arg("text", "Text to .").Strings() ) // Run ... func Run() error { image := *image channel := *channel fmt.Println("post, image=", image, "channel=", channel) return nil }
実行結果
ヘルプメッセージ (全体)
usage: multi commands [<flags>] <command> [<args> ...] multi commands example Flags: --help Show context-sensitive help (also try --help-long and --help-man). --version Show application version. Commands: help [<command>...] Show help. post [<flags>] <channel> [<text>...] Post a message to a channel. register [<flags>] <name> Register a new user.
ヘルプメッセージ (サブコマンド)
usage: multi commands register [<flags>] <name> Register a new user. Flags: --help Show context-sensitive help (also try --help-long and --help-man). --version Show application version. --nickname="anonymous" Nickname for user. Args: <name> Name of user.
実行結果
$ go run 01multifiles/main.go register world register, name= world nickname= anonymous