Help us understand the problem. What is going on with this article?

どうでもいいからささっとGoでCLIつくりたいとき

More than 1 year has passed since last update.

体裁はどうでもいいからちゃちゃっと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

コマンドを実行する処理を呼んでいるだけです。定形。

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の前にfmtlogを使って出力する必要があります。
log.Fatalを使う手もあるのですが、log.Fatalはエラーコードが1固定なので、指定したいときに問題です。

commands.go
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でエラーコードもあわせて返すようにします。

commands_xxx.go
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)
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした