Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
7
Help us understand the problem. What is going on with this article?
@usk81

どうでもいいからささっと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)
}
7
Help us understand the problem. What is going on with this article?
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
usk81
お仕事内容:プログラマ & たまにインフラ & SEOラングラー & データアナリスト & データサイエンティスト見習い & バリスタじゃないけど、コーヒー淹れる人 & 農家から買い付けて日本茶淹れる人

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
7
Help us understand the problem. What is going on with this article?