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
-,

執筆者:


comment

Your email address will not be published.

関連記事

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

前回の「Go言語 ORMライブラリ GORMの使い方」に続いて「GORM+Gin」でTODOリストを作ってみました。使い方は「GitHubのREADME」を参考にしました。できたものは下記URLから確認できます。装飾は別途やれればと。 http://gin.taisablog.com/todo 事前情報 Webフレームワーク:Gin (https://github.com/gin-gonic/gin) ORM:GORM (https://gorm.io/docs) DB:MySQL ルーティングは通常のフォームだとPUT/DELETEが使えないので以下のようにしました。 [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] POST /todo/edit/:id –> main.main.func4 (3 handlers) // 編集 [GIN-debug] POST /todo/delete/:id –> main.main.func5 (3 handlers) // 削除 main.goだけで作成した場合 main.goに全ての処理を記述しています。 package main import ( “github.com/gin-gonic/gin” “github.com/jinzhu/gorm” _ “github.com/jinzhu/gorm/dialects/mysql” “net/http” “strconv” ) type Todo struct { gorm.Model Text string Status uint64 } func main() { db, err := gorm.Open(“mysql”, “gorm:gorm@/sandbox_gin?charset=utf8mb4&parseTime=True&loc=Local”) if err != nil { panic(“データベースへの接続に失敗しました”) } defer db.Close() db.LogMode(true) db.AutoMigrate(&Todo{}) r := …

[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” …

Golangを使ってJWTを15分で理解する

JWTとは JWT(ジョットと言うらしい)はJSON Web Tokenの略で、JSONをベースとしたアクセストークンのためのオープン標準 (RFC 7519) です。色々記事を見ましたが、最終的にWikipediaが分かりやすく一番参考にしました。https://ja.wikipedia.org/wiki/JSON_Web_Token JWTの構造 JWTは以下の3つの要素をピリオドで区切った文字列で構成されます。 ヘッダー 署名生成に使用するアルゴリズムを格納します。下記のHS256は、このトークンがHMAC-SHA256で署名されていることを示しています。署名アルゴリズムとしては、SHA-256を使用したHMAC (HS256) や、SHA-256を使用したRSA署名 (RS256) がよく用いられます。 { “alg” : “HS256”, “typ” : “JWT” } ペイロード 認証情報などのクレームを格納します。クレームとはペイロードに含める以下のような標準フィールド(クレーム)を指します。JWTの仕様では、トークンに一般的に含まれる7つの標準フィールドが定義されています。また用途に応じた独自のカスタムフィールドを含むこともできます。下記の例では、トークン発行日時を示す標準のクレーム (iat) と、カスタムクレーム (loggedInAs) を格納しています。 { “loggedInAs” : “admin”, “iat” : 1422779638 } 7つのペイロードの標準クレーム 署名 トークン検証用の署名です。署名はヘッダーとペイロードをBase64urlエンコーディングしてピリオドで結合したものから生成します。署名はヘッダーで指定された暗号化アルゴリズムにより生成されます。下記はHMAC-SHA256形式でのコード例です。 HMAC-SHA256(base64urlEncoding(header) + ‘.’ + base64urlEncoding(payload), ‘secret key’) JWTを使用するにあたって JWTはトークンが返され、それをローカルに保存して利用します(主にlocal storageやsession storageが用いられますが、セッションIDのようにCookieを用いる場合もあります。) 認証時にはAuthorizationヘッダーでBearerスキーマを利用します。またサーバー上に認証状態を保持しないステートレスな認証方式です。その為JWT単体ではトークンを無効にすることが出来ません。サーバーに状態を保持すれば可能ですが、その場合ステートレスの利点は失われることになります。さて、ここまではほぼ Wikipedia に書いてある内容そのままです。ここから実際にGo/GinのJWT Middlewareを使って実際の動作を確認してみます。 Go/GinのJWT Middlewareを使った動作確認 利用するJWT Middlewareについて ここでは、「https://github.com/gin-gonic/gin」 を使う前提で、次のMiddlewareを利用します。「https://github.com/appleboy/gin-jwt」。このMiddlewareは、auth_jwt.goの1ファイルでで構成されていて、「https://github.com/dgrijalva/jwt-go」 をGin用に薄くラップしたものです。jwt-goはトークンを作成したりパースしたり様々な機能が用意されています。 サンプルソース サンプルソースは、https://github.com/appleboy/gin-jwt/blob/master/README.md に載っているのでこれを元に確認します。処理は大きく「ログイン時にToken発行する」と「トークン認証&処理実行する」の2種類あります。 ログイン時にToken発行する ログイン時にTokenを発行する処理は、LoginHandlerです。Routerでは次のように定義しています。LoginHandlerではAuthenticatorとPayloadFuncが呼ばれる為、Middlewareにてこれらを実装する必要があります。 r.POST(“/login”, authMiddleware.LoginHandler) Authenticatorはログイン認証の為の関数です。例では固定値が設定されていますが、実際は主にDBから値を取得することになると思います。PayloadFuncはペイロードに含めるクレームを設定します。ペイロードには任意のクレームを追加可能なので、ログインIDとなるuserIDをセットしています。 // ログインに基づいたユーザの認証振る舞いをするコールバック Authenticator: func(c *gin.Context) (interface{}, error) { var loginVals login if err := c.ShouldBind(&loginVals); err != nil { return “”, jwt.ErrMissingLoginValues } userID := loginVals.Username password := loginVals.Password // …

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’ …

GORMでよく使うSQLの書き方

GORMでよく使うSQLの書き方をメモとして残しておきます。詳細は記事最後の参考リンクの公式ドキュメントで確認できます。 CREATE insertするだけであれば create を利用します。 db.Create(&models.User{Name: “user”}) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:22:00′,’2020-04-25 11:22:00′,’user’,0) UPDATE 特定のフィールドだけ更新したい場合、 Update と Updates を使います。 update 一つのフィールドだけアップデートします。 user := &models.User{Name: “user”} db.Create(user) db.Model(user).Update(“name”, “user2”) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:34:39′,’2020-04-25 11:34:39′,’user’,”) UPDATE “users” SET “name” = ‘user2’, “updated_at” = ‘2020-04-25 11:34:39’ WHERE “users”.”id” = 5 updates updates は map を利用して複数のフィールドをアップデートします。 user := &models.User{Name: “user”} db.Create(user) db.Model(user).Updates(map[string]interface{}{“name”: “user3”, “email”: “g5.taisa831@gmail.com”}) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:39:52′,’2020-04-25 11:39:52′,’user’,”) UPDATE “users” SET “email” = ‘g5.taisa831@gmail.com’, “name” = ‘user3’, “updated_at” = ‘2020-04-25 11:39:52’ WHERE “users”.”id” = 6 SAVE Save は SQL を実行する際にすべてのフィールドを含みます。フィールドを指定しなくても空にはなりません。 user := &models.User{Name: …