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

執筆者:


  1. Thanks a lot for the article post.Much thanks again. Fantastic.

comment

Your email address will not be published.

関連記事

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

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で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”] 構成 $ …

[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] Goを始めたらまずはioパッケージを知るべし

Goを書いているとio.writerとio.readerを扱うケースが頻繁に出てきますが、これはioパッケージが多くの他のパッケージのインターフェースになっているからなのでioパッケージを知っておくことで開発が楽になります。 参考書籍 本書はGoのio.Writer、io.Readerからはじまりシステムの深いところまで丁寧に説明されているのでとてもおすすめです。ioパッケージから始まっているのも納得です。Webで無料で見れますし書籍版、PDF版で購入も可能です。 Web版 https://ascii.jp/elem/000/001/235/1235262/ 書籍版 Goならわかるシステムプログラミング ioパッケージのインターフェース一覧 ioパッケージのインターフェース一覧です。Goのインターフェースの実装は、明示的にインターフェースを明示的にimplementsせず、インターフェースを満たしていたらimplementsしていることになります。 Reader(インターフェース)Writer(インターフェース)Seeker(インターフェース)Closer(インターフェース)ReadWriter(複合インターフェース)ReadCloser(複合インターフェース)WriteCloser(複合インターフェース)ReadSeeker(複合インターフェース)WriteSeeker(複合インターフェース)ReadWriteCloser(複合インターフェース)ReadWriteSeeker(複合インターフェース)ReaderFrom(インターフェース)WriterTo(インターフェース)ReaderAt(インターフェース)WriterAt(インターフェース)ByteReader(インターフェース)ByteScanner(インターフェース)ByteWriter(インターフェース)RuneScanner(インターフェース)StringWriter(インターフェース) 複合インターフェース表 Goではインターフェースにインターフェースを食わせることができ、ioパッケージで作られている複合インターフェースは以下となります。 インターフェースio.Readerio.Writerio.Seekerio.Closerio.ReadWriter◯◯  io.ReadSeeker◯ ◯ io.ReadCloser◯  ◯io.WriteSeeker ◯◯ io.WriteCloser ◯ ◯io.ReadWriteSeeker◯◯◯ io.ReadWriteCloser◯◯ ◯ インターフェースを満たしている一覧を確認する方法 以下のコマンドを叩くと対象のインターフェースを満たしているものの一覧が確認できます。 $ GOPATH=/ godoc -http “:6060” -analysis type ## 実行後以下のURLにアクセスすると`io`パッケージが確認できる http://localhost:6060/pkg/io/ Readerのimplements一覧 テストも含んでいますが沢山あります pointer type *archive/tar.Reader implements Readerpointer type *archive/tar.regFileReader implements Readerpointer type *archive/tar.sparseFileReader implements Readerpointer type *archive/tar.testFile implements Readerpointer type *archive/zip.checksumReader implements Readerpointer type *archive/zip.pooledFlateReader implements Readerpointer type *bufio.Reader implements Readerpointer type *bufio_test.StringReader implements Readerpointer type *bufio_test.emptyThenNonEmptyReader implements Readerpointer type *bufio_test.errorThenGoodReader implements Readerpointer type *bufio_test.negativeReader implements Readerpointer type *bufio_test.rot13Reader implements Readerpointer type *bufio_test.scriptedReader implements Readerpointer type *bufio_test.slowReader implements Readerpointer type *bufio_test.testReader implements Readerpointer type *bytes.Buffer implements Readerpointer type *bytes.Reader implements Readerpointer type *bytes_test.negativeReader implements Readerpointer …