taisablog

taisa's engineer blog

Go

【3分で作れる】Goのコマンドラインツール by Cobra

投稿日:


同僚のおすすめでコマンドラインツール作成にCobraを使いました。ものすごく簡単につくれるのですが、それでも少しハマったところがあったので、コマンドラインツールを作るまでの流れを書いておきます。

前提

Index

雛形を作成する

雛形作成にはgeneratorを使います。go getしてcobraコマンドを利用可能にします。

go get -u github.com/spf13/cobra/cobra

cobraコマンドを実行すると以下のようなusageが表示されます。

$ cobra
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  help        Help about any command
  init        Initialize a Cobra Application

Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)

Use "cobra [command] --help" for more information about a command.

cobra initコマンドを実行するとpkg-nameが必須だと怒られます。

$ cobra init                                             
Error: required flag(s) "pkg-name" not set
Usage:
  cobra init [name] [flags]

# --- 省略 ---

cobra init--pkg-nameを指定して雛形を作成します。--pkg-nameにはgithub.comから始まるパッケージへのパスを指定します。ここではsandbox-cobraという名前で作っていきます。

$ cobra init --pkg-name github.com/taisa831/sandbox-cobra sandbox-cobra

コマンドラインツールのテンプレートができました。

sandbox-cobra
├── LICENSE
├── cmd
│   └── root.go
└── main.go

コマンドを追加する

go modで初期設定しておきます。

$ cd sandbox-cobra
$ go mod init
$ go mod tidy

cobra addコマンドでサンプルの実行用コマンド(hello)を作成します。

$ cobra add hello

hello.go コマンドが新規で追加されました。

.
├── LICENSE
├── cmd
│   ├── hello.go ← 新しくできたコマンド
│   └── root.go
├── go.mod
├── go.sum
└── main.go

この時点でコマンド実行するとhello calledが表示されます。これでコマンドの追加は完了です。hello.goRUNに処理を書いていけばいいわけです。

$ go run main.go hello
hello called
Run: func(cmd *cobra.Command, args []string) {
   // ここに処理を書く
   fmt.Println("hello called")
},

コンフィグを利用する

続いてコンフィグを利用可能にします。コマンドラインツールのhelpをみてみると、Global Flagsdefault is $HOME/.sandbox-cobra.yamlとあるのが分かります。デフォルトではこのコンフィグファイルが読み込み対象となります。

$ go run main.go hello --help

# --- 省略 ---
Usage:
  sandbox-cobra hello [flags]

Flags:
  -h, --help   help for hello

Global Flags:
      --config string   config file (default is $HOME/.sandbox-cobra.yaml)

$HOME/.sandbox-cobra.yamlに例としてDB用コンフィグを作成します。

$ vi $HOME/.sandbox-cobra.yaml
DBConfig:
  SCHEMA: sandbox-cobra
  User: admin
  Password: password
  Host: localhost
  Port: 3306

続いてconfig/config.goを作成します。構成は以下通りです。

.
├── LICENSE
├── cmd
│   ├── hello.go
│   └── root.go
├── config
│   └── config.go
├── go.mod
├── go.sum
└── main.go

config/config.goにコンフィグのstructを記述します。

package config

var Conf Config

type Config struct {
	DBConfig dbConfig
}

type dbConfig struct {
	Schema string `env:"SCHEMA" envDefault:"sandbox-cobra"`
	User string `env:"USER" envDefault:"root"`
	Password string `env:"PASSWORD" envDefault:"password"`
	Host string `env:"HOST" envDefault:"localhost"`
	Port string `env:"PORT" envDefault:"3306"`
}

ここまでできたら、root.goinitConfig()にコンフィグの読み込み用コードを追記します。initConfig()ではviperでコンフィグの初期化コードがgeneratorで生成されるのでそれを利用します。

// initConfig reads in config file and ENV variables if set.
func initConfig() {
        // --- 省略 ---

	if err := viper.Unmarshal(&config.Conf); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

読み込みめるようになったかhello.goにコンフィグを出力して確認してみます。

	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("configFile: %s\nconfig: %#v\n", cfgFile, config.Conf)

		fmt.Println("hello called")
	},

$HOME/.sandbox-cobra.yamlに指定したコンフィグが無事読み込めました。

$ go run main.go hello
Using config file: /Users/tt-dev/.sandbox-cobra.yaml
configFile: 
config: config.Config{DBConfig:config.dbConfig{Schema:"sandbox-cobra", User:"admin", Password:"password", Host:"localhost", Port:"3306"}}
hello called

ただ$HOMEではなくプロジェクトルートにコンフィグを置きたいので、root.goinit()の記述を変え、プロジェクトルートの`config.yaml`を読むように変更します。

func init() {
	cobra.OnInitialize(initConfig)

        // --- 省略---

	//rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sandbox-cobra.yaml)")
	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.yaml", "load config file (default is config.yaml)")
        
        // --- 省略 ---
}

defaultconfig.yamlに変わりました。

$ go run main.go hello --help

# --- 省略 ---

Global Flags:
      --config string   load config file (default "config.yaml")

先程作成した$HOME/.sandbox-cobra.yamlは消しておきます。

rm -rf $HOME/.sandbox-cobra.yaml

ここまでの構成は以下の通りです。

.
├── LICENSE
├── cmd
│   ├── hello.go
│   └── root.go
├── config
│   └── config.go
├── config.yaml
├── go.mod
├── go.sum
└── main.go

あらためてhelloコマンドを実行してみると問題なくコンフィグが読めていることが分かります。

$ go run main.go hello
Using config file: config.yaml
configFile: config.yaml
config: config.Config{DBConfig:config.dbConfig{Schema:"sandbox-cobra", User:"admin", Password:"password", Host:"localhost", Port:"3306"}}
hello called

サブコマンドを追加する

続いてサブコマンドを追加します。サブコマンドもgeneratorでサンプルコードが生成されているのでそれを利用します。hello.goinit()のサブコマンドのコメントアウトを外します。

func init() {
	rootCmd.AddCommand(helloCmd)

        // --- 省略 ---

	// and all subcommands, e.g.:
	helloCmd.PersistentFlags().String("foo", "", "A help for 
        
        // --- 省略 ---
}

あらためて--helpUsageを確認してみると、--fooが追加されいるのが確認できます。

 go run main.go hello --help

# --- 省略 ---

Usage:
  sandbox-cobra hello [flags]

Flags:
      --foo string   A help for foo
  -h, --help         help for hello

Global Flags:
      --config string   load config file (default "config.yaml")

最後にhello.goにサブコマンドの値を読み込む設定を追加したら完了です。

var helloCmd = &cobra.Command{
	Use:   "hello",
	Short: "A brief description of your command",
	Long: `省略`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("configFile: %s\nconfig: %#v\n", cfgFile, config.Conf)

		fmt.Println("hello called")

          // 読み込みコード追加
		foo, err := cmd.PersistentFlags().GetString("foo")
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		fmt.Println(foo)
	},
}

サブコマンドをつけて実行するとfooが出力されました。

$ go run main.go hello --foo=foo
Using config file: config.yaml
configFile: config.yaml
config: config.Config{DBConfig:config.dbConfig{Schema:"sandbox-cobra", User:"admin", Password:"password", Host:"localhost", Port:"3306"}}
hello called
foo

まとめ

ここまでくればコマンドラインツールでやりたいことができるようになると思います。他にも様々なオプションがあるので使いこなしたいところです。Cobra使いたてなので何かあればコメントもらえると嬉しいです。

-Go
-,

執筆者:

関連記事

Go言語 GORM+GinでTODOリストのAPIを作ってみた

前回の「Go言語 GORM+GinでTODOリストを作ってみた」に続いて「GORM+Gin」でTODOリストのAPIを作ってみました。ソースコードは前回からの差分だけを記載しています。できたものは下記URLから確認できます。 http://sandbox.taisablog.com/api/v1/ GinのGithub 事前情報 Webフレームワーク:Gin (https://github.com/gin-gonic/gin) ORM:GORM (https://gorm.io/docs) DB:MySQL ルーティングは今回はAPIなので以下としました。モデルをtasksにすればよかったと思いましたが一旦このままにしておきます。 [GIN-debug] GET /todo –> main.main.func1 (3 handlers) // 一覧表示 [GIN-debug] POST /todo –> main.main.func2 (3 handlers) // 新規作成 [GIN-debug] GET /todo/:id –> main.main.func3 (3 handlers) // 編集画面表示 [GIN-debug] PUT /todo/:id –> main.main.func4 (3 handlers) // 編集 [GIN-debug] DELETE /todo/:id –> main.main.func5 (3 handlers) // 削除 ディレクトリ構成 . ├── api │   └── v1 │   └── todo.go ├── controllers │   └── todo.go ├── db │   └── db.go ├── main.go ├── models │   └── todo.go ├── router    └── router.go router.go router.goにr.Group(“/api/v1”)のAPI用のグループを追加してルーティングを追加しました。 package router import ( “github.com/gin-contrib/cors” “github.com/gin-gonic/gin” “github.com/jinzhu/gorm” v1 “github.com/taisa831/sandbox-gin/api/v1” “github.com/taisa831/sandbox-gin/controllers” …

[Golang]Goのio/ioutilパッケージは分かりやすくて使いやすい

Goのioパッケージは主にインターフェースになっていて他のパッケージで多く実装されています。またioパッケージにもパブリックな関数がありファイルの入出力はできますが少し細かい処理になります。io/ioutilパッケージを使うとファイルの入出力処理が簡単にできます。以下にio/ioutilパッケージを使った処理とそれに対するテストコードを記載します。 io/ioutil/ioutil.go ReadAll() func ReadAll() string { file, _ := os.Open(“testdata/src.txt”) b, _ := ioutil.ReadAll(file) return string(b) } ReadAllテスト func TestReadAll(t *testing.T) { str := ReadAll() if str != “0123456789” { t.Errorf(“TestReadAll Error. %s”, str) } } ReadFile() func ReadFile() string { b, _ := ioutil.ReadFile(“testdata/src.txt”) return string(b) } ReadFileテスト func TestReadFile(t *testing.T) { str := ReadFile() if str != “0123456789” { t.Errorf(“TestReadAll Error. %s”, str) } } WriteFile() func WriteFile() string { b := []byte(“0123456789”) _ = ioutil.WriteFile(“testdata/dst.txt”, b, os.ModePerm) b, _ = ioutil.ReadFile(“testdata/dst.txt”) return string(b) } WriteFileテスト func TestWriteFile(t *testing.T) { str := WriteFile() if str != “0123456789” …

no image

docker-composeでGoのWebサーバーを起動する

ミニマムにやっておかないと忘れがちなのでメモ サンプル用Goサーバを作成する package main import ( “encoding/json” “net/http” ) type User struct { FirstName string `json:”firstName”` LastName string `json:”lastName”` } func users(w http.ResponseWriter, req *http.Request) { w.Header().Set(“Content-Type”, “application/json”) user := User{ FirstName: “John”, LastName: “Doe”, } var users []User users = append(users, user) json.NewEncoder(w).Encode(users) } func main() { http.HandleFunc(“/users”, users) http.ListenAndServe(“:8002”, nil) } 普通に起動して動作確認をします。 $ go run main.go http://localhost:8002/usersにアクセスするとJSON結果が出力されます。 [ { “firstName”: “John”, “lastName”: “Doe” } ] Dockerfile FROM golang:1.14 #FROM golang:1.14-alpine # コンテナログイン時のディレクトリ指定 WORKDIR /opt/sandbox-docker-compose-go # ホストのファイルをコンテナの作業ディレクトリにコピー COPY . . # ADD . . # ビルド RUN go build -o app main.go # 起動 CMD [“/opt/sandbox-docker-compose-go/app”] docker-compose.ymlファイル version: ‘3’ …

no image

docker-compose&dockerizeでGo+MySQLのWebサーバーを起動する

docker-composeでGo + MySQLを起動する場合、MySQLの起動を待ってGoのWebサーバーを起動する必要があります。実現するにはどうやらスクリプトを書かないとだめらしいですが、dockerizeを使うと簡単に実現できます。 サンプル用Goサーバを作成する 起動時にusersテーブルを作成し、リクエスト時にusersテーブルにレコードをinsertする簡単なサンプルアプリケーションです。 package main import ( “database/sql” “encoding/json” “fmt” “log” “net/http” “os” _ “github.com/go-sql-driver/mysql” “github.com/joho/godotenv” ) type User struct { FirstName string `json:”firstName”` LastName string `json:”lastName”` } var db *sql.DB func main() { err := godotenv.Load() if err != nil { log.Fatal(err) } db = Conn() defer db.Close() _, err = db.Exec(“CREATE TABLE IF NOT EXISTS users(id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, firstname VARCHAR(255) NOT NULL, lastname VARCHAR(255) NOT NULL)”) if err != nil { log.Fatal(err) } http.HandleFunc(“/users”, users) err = http.ListenAndServe(os.Getenv(“LISTEN_PORT”), nil) if err != nil { log.Fatal(err) } } func Conn() *sql.DB { db, err …

no image

DockerでGoのWebサーバーを起動する

ミニマムにやっておかないと忘れがちなのでメモ サンプル用Goサーバを作成する package main import ( “encoding/json” “net/http” ) type User struct { FirstName string `json:”firstName”` LastName string `json:”lastName”` } func users(w http.ResponseWriter, req *http.Request) { w.Header().Set(“Content-Type”, “application/json”) user := User{ FirstName: “John”, LastName: “Doe”, } var users []User users = append(users, user) json.NewEncoder(w).Encode(users) } func main() { http.HandleFunc(“/users”, users) http.ListenAndServe(“:8002”, nil) } 普通に起動して動作確認をします。 $ go run main.go http://localhost:8002/usersにアクセスするとJSON結果が出力されます。 [ { “firstName”: “John”, “lastName”: “Doe” } ] Dockerfile Dockerfileを作成します。alpineをつけるとよりミニマムなイメージができます。参考(https://hub.docker.com/_/golang?tab=description) FROM golang:1.14 #FROM golang:1.14-alpine # コンテナログイン時のディレクトリ指定 WORKDIR /opt/sandbox-docker-go # ホストのファイルをコンテナの作業ディレクトリにコピー COPY . . # ADD . . # ビルド RUN go build -o app main.go # 起動 CMD [“/opt/sandbox-docker-go/app”] 構成 $ …