体裁はどうでもいいからちゃちゃっとCLIツールをGoで作りたいときにどうしているかというお話。
この記事はパーソルキャリア Advent Calendar 2018の6日目です。
CLIのフレームワークは spf13/cobra を使います。
数年前からあってそれなりにメンテナンスされていて、デファクトスタンダード化しつつあるし楽なので
ディレクトリ構成
パッケージのディレクトリ構成はこんな感じです。
git-ltsがかつて用いていた構成を参考にされてもらってます。
実行ファイルと実際のコマンドを分けています。
これはビルドせずにgo run
で実行したときに、main.go
だけ指定すればいいようにするためです
.
├── main.go
└── commands
├── commands.go
└── commands_xxx.go
ファイル構成
main.go
コマンドを実行する処理を呼んでいるだけです。定形。
package main
import "github.com/yourname/package_name/commands"
func main() {
commands.Run()
}
commands.go
コマンドのメイン処理になります。
foobar
が、コマンド名になるので、適当な名称に差し替えてください。
それ以外はこのままで使えます。
Exit
は、os.Exit
をwrapした処理です。
os.Exit
を使う場合、os.Exit
はエラーメッセージを標準出力できないので、os.Exit
の前にfmt
やlog
を使って出力する必要があります。
log.Fatal
を使う手もあるのですが、log.Fatal
はエラーコードが1固定なので、指定したいときに問題です。
package commands
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var (
// RootCmd defines root command
RootCmd = &cobra.Command{
Use: "foobar",
Run: func(cmd *cobra.Command, args []string) {
cmd.Usage()
},
}
)
// Run runs command.
func Run() {
RootCmd.Execute()
}
// Exit finishes a runnning action.
func Exit(err error, codes ...int) {
var code int
if len(codes) > 0 {
code = codes[0]
} else {
code = 2
}
if err != nil {
fmt.Println(err)
}
os.Exit(code)
}
commands_xxx.go
ファイル名とコード内のxxx
はサブコマンド名です。適当な名称に差し替えてください。
サブコマンドはコマンド単位でファイルを切ります。
コマンドの役割と、内容を明確にするためです。
varとinit()で自動的にサブコマンドがコマンドに登録されるようにしています。
実際の実行はxxxCommand
が、
処理内容はxxxAction
が
担保するようにします。
これによって、テストを書きたいときにxxxAction
が、きちんとテストできていれば問題ないようにします。
またxxxAction
の内容をxxxCommand
で書いてしまうと、xxxCommand
はエラー時にos.Exitを返さないければいけないので、テスト時に複雑性が上がります。
xxxCommand
でエラーコードを指定したい場合は、
エラー毎に定義をして、switch-case文でふりわけるか、xxxAction
でエラーコードもあわせて返すようにします。
package commands
import (
"github.com/spf13/cobra"
)
var (
xxxCmd = &cobra.Command{
Use: "xxx",
Run: xxxCommand,
}
)
func xxxCommand(cmd *cobra.Command, args []string) {
if err := xxxAction(); err != nil {
Exit(err, 1)
}
}
func xxxAction() (err error) {
// 実行したい内容
}
func init() {
RootCmd.AddCommand(xxxCmd)
}