最近の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