先日、SPMというコマンドラインツールをリリースしましたが、golangでコマンドラインツールを作成する上で便利だったツールや考え方等を備忘として残します。
CLIのベース
3rd Partyのライブラリを利用せずとも標準のflagパッケージで処理を書けますが、色々と面倒なのでurfave/cliのパッケージを利用しました。このパッケージは以下のように、簡単にサブコマンドやフラグを定義できます。app := cli.NewApp()
app.Name = "spm"
app.Usage = "Salesforce Package Manager"
app.Version = APP_VERSION
app.Commands = []cli.Command{
{
Name: "install",
Aliases: []string{"i"},
Usage: "Install salesforce packages on public remote repository(i.g. github)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "username, u",
Destination: &c.Config.Username,
EnvVar: "SF_USERNAME",
},
cli.StringFlag{
Name: "password, p",
Destination: &c.Config.Password,
EnvVar: "SF_PASSWORD",
},
...
},
Action: func(ctx *cli.Context) error {
fmt.Println(ctx.Args().First()) // 最初の引数を取得
...
return nil
},
},
}
app.Run(args)
エラーハンドリングはActionの関数の戻り値にerrorを入れれば良く、errorがある場合にはHandleExitCoderを呼び出します。HandleExitCoder内ではErrWriter経由でerror#Error()が出力され、OsExiterが呼び出されます。デフォルト値はErrWriterがos.Stderr、OsExiterがos.Exitなので、そのまま使うと標準エラー出力に出力して終了コード付きでプロセスが終了します。ErrWriterとOsExiterは外部公開されているので、他のio.Writerや関数をセットすれば柔軟にエラーハンドリングが可能です。
YAMLのロード
設定のロードでYAMLを利用する際はgopkg.in/yaml.v2を使います。使い方はxmlやjsonのUnmarshalと同じく、structを定義してマッピングします。
type Config struct {
Aaa struct {
Bbb string `yaml:"bbb"`
} `yaml:"aaa"`
Ccc string `yaml:"ccc"`
}
config := &Config{}
err := yaml.Unmarshal([]byte("aaa:\n bbb: hoge\nccc: fuga"), &config)
ロガー
標準のロガーであるlogパッケージはレベル付きのロギングをサポートしていなかったり、フォーマッティングは自分でやらないといけなかったりと不便なので、3rd PartyのSirupsen/logrusを使いました。使い方はこんな感じ。
import log "github.com/Sirupsen/logrus"
func main() {
log.Info("hoge")
log.Infof("hoge %s", "aaa")
log.Warning("hoge")
log.Warningf("hoge %s", "bbb")
log.Error("hoge")
log.Errorf("hoge %s", "ccc")
}
あるいは
import (
"github.com/Sirupsen/logrus"
"os"
)
func main() {
var logger = logrus.New()
logger.Out = os.Stderr
logger.Info("aaabbb")
}
のような使い方でもOK。前者の方法は一つのチャネルにしかロギングできませんが、後者の方法であれば複数のloggerインスタンスを作成できるため、複数のチャネル(例えばstdoutとstderr)にロギングすることができます。
実際には、このloggerを直接使うのではなくラップした独自のloggerを定義して利用した方が変更に強い実装になりそうです。
テスト
DIの話と同様に、入出力のストリームを置き換えできるようなインターフェースにしておくとテストが楽です。CLIの場合は入力のストリームはコマンドライン引数であり、出力ストリームは標準出力になります。これらをテスト時には文字列の配列、Bufferに置き換えることになります。SPMだとロガーのデフォルトはStdout、Stderrにしており、テスト時はbytes.Bufferに書き込むようにしています。StdoutはFileの型なので、コマンド利用時とテスト時の両方に適用できる型として、io.Writerのインターフェースを指定していることに注意してください。
type CLI struct {
Client *ForceClient
Config *Config
Logger *Logger
Error error
}
func (c *CLI) Run(args []string) (err error) {
if c.Logger == nil {
c.Logger = NewLogger(os.Stdout, os.Stderr)
}
...
}
func NewLogger(outStream io.Writer, errStream io.Writer) *Logger {
...
}
func main() {
cli := &CLI{}
...
}
// For Test
func before() (*CLI, *bytes.Buffer, *bytes.Buffer) {
outStream, errStream := new(bytes.Buffer), new(bytes.Buffer)
cli := &CLI{Logger: NewLogger(outStream, errStream)}
return cli, outStream, errStream
}
またテストはstretcher/testifyのライブラリを利用するとアサーションが楽です。
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestHoge(t *testing.T) {
assert.Equal(t, 123, 123, "they should be equal")
assert.Contains(t, "aaabb", "aaa")
}