Golang - cobraを使ったコマンドラインツール作成

## はじめに
軽微なプログラムを書く機会があり、せっかくなのでGolangでコマンドラインツールの開発に挑戦してみました。
簡単なものであれば、私のような初学者でも抵抗なく取り組めそうなことと、こういうツール欲しいなぁというシーンが私の周りではよくあったのでこの機会に学習することにしました。

作成したものは[こちら](https://github.com/so-heee/firebase-admin-sdk-cmd)のリポジトリにあげています。

## 今回作成するもの
##### Firebase Authentication ユーザーのCRUDコマンド

認証にFirebase Authenticationを利用しているWebアプリの開発に現在携わっているのですが、ユーザーの確認、作成や削除などをする際、周りのメンバー含めコンソールからポチポチしていました。
どうしてもユーザーの作成や削除などを頻繁に行う必要があったのですが、公式から提供されているCLIではCSVベースでのエクスポートとインポートしかないため、これをコマンドラインツールにしてみました。

https://firebase.google.com/docs/cli?hl=ja

## 使用するライブラリ
ビュアなGolangでもコマンドラインツールは書けそうでしたが、ライブラリを利用することで実装がより簡単にできそうでしたので利用することにしました。
候補となったのは以下の二つです。

- [urfave/cli](https://github.com/urfave/cli)
- [spf13/cobra](https://github.com/spf13/cobra)


「urfave/cli」というライブラリも良さそうでしたが、cobraはdockerやKubernetesでも採用されているとのことだったので、今回はcobraを利用することにしました。

![cobra.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/50d05bcb-e3a5-0ec9-09a6-cf77b95f6bea.png)

https://github.com/spf13/cobra

- [dockerでのcobra利用](https://github.com/docker/cli)
- [kubernetesでのcobra利用](https://github.com/kubernetes/kubernetes/tree/master/cmd)

## 雛形の作成

まずは `go mod init` でGoModuleの初期化をします。

```
$ go mod init github.com/so-heee/golang-example/firebase-admin-sdk-cmd 
```

以下のコマンドでコマンドラインツールの雛形を作成することができます。

```
$ cobra init --pkg-name github.com/so-heee/golang-example/firebase-admin-sdk-cmd
```

作成すると今のような構成が作成されます。

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

## 雛形の内容を確認

```go:main.go
func main() {
    //main.goからはcmd/root.goのExecuteファンクションを呼び出している
    cmd.Execute()
}
```

コマンドライン引数の設定（[spf13/pflag](https://github.com/spf13/pflag)）や、設定ファイルの読み込み（[spf13/viper](https://github.com/spf13/viper)）が実装されています

```go:cmd/root.go
func init() {
    // 各コマンドのExecuteメソッドが呼び出されたときに実行される渡された関数を設定 
    cobra.OnInitialize(initConfig)
    
    // 現在のコマンドで特に設定された永続的なFlagSetを返します 
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.firebase-admin-sdk-cmd.yaml)")
    
    // このコマンドに適用される完全なFlagSetを返します
    rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
```

```go:cmd/root.go
func initConfig() {
    if cfgFile != "" {
        // init処理で定義されているyamlファイルを読み込んでいる
        viper.SetConfigFile(cfgFile)
    } else {
        // Find home directory.
        home, err := homedir.Dir()
        if err != nil {
          fmt.Println(err)
          os.Exit(1)
        }
    
        // 「.cmdline-cobra-example」（拡張子なし）という名前のホームディレクトリで構成を検索します
        viper.AddConfigPath(home)
        viper.SetConfigName(".cmdline-cobra-example")
        }
        viper.AutomaticEnv() // read in environment variables that match
        
        // 構成ファイルが見つかった場合のプリント
        if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    }
}
```

```go:cmd/root.go
var rootCmd = &cobra.Command{
    // 1行の使用法メッセージ
    Use:   "firebase-admin-sdk-cmd",
    // 「ヘルプ」出力に表示される短い説明
    Short: "A brief description of your application",
    // 'help <this-command>'出力に表示される長いメッセージ
    Long: `A longer description that spans multiple lines and likely contains
    examples and usage of using your application. For example:
    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.`,
    // 実行用の関数
    Run: func(cmd *cobra.Command, args []string) {
    },
}
```

実行してみます。

```
$ go run main.go
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

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.
```

## サブコマンドの追加
以下のコマンドでサブコマンドを追加することができます。

```
$ cobra add get

.
├── LICENSE
├── cmd
│   ├── get.go
│   └── root.go
├── go.mod
├── go.sum
└── main.go
```

メインのヘルプにサブコマンドが追加されました。

```
$ go run main.go -h
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

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:
  firebase-admin-sdk-cmd [command]

Available Commands:
  get         A brief description of your command
  help        Help about any command

Flags:
      --config string   config file (default is $HOME/.command-test.yaml)
  -h, --help            help for command-test
  -t, --toggle          Help message for toggle

Use "firebase-admin-sdk-cmd [command] --help" for more information about a command.
```

サブコマンドを実行してみると `get.go` の Runファンクション内のプリント処理が実行されます。

```
$ go run main.go get   
get called
```

## firebaseとの連携
Firebase公式から提供されている[Firebase Admin Go SDK](https://github.com/firebase/firebase-admin-go)がありますので、そちらを利用します。
今回はこちらの詳細については省略しますが、以下のようにスニペットが用意されているため簡単に実装ができます。

https://github.com/firebase/firebase-admin-go/tree/master/snippets

## サブコマンドの実装
上記で追加した `get` コマンドに処理を実装していきます。
パラメータでUID指定と、一覧取得を切り替えるようにしています。

```go:cmd/get.go
type getOptions struct {
	id   string
	list bool
}

func NewCmdGet() *cobra.Command {
	var (
		o = &getOptions{}
	)
	cmd := &cobra.Command{
		Use:   "get",
		Short: "Get firebase authentication user",
		Run: func(cmd *cobra.Command, args []string) {
			runCmdGet(o)
		},
	}
	cmd.Flags().BoolVarP(&o.list, "list", "l", false, "Get user list")
	cmd.Flags().StringVarP(&o.id, "id", "i", "", "Get user UID")
	return cmd
}

func runCmdGet(opt *getOptions) {
	if opt.list {
		getUsers()
	} else {
		if len(opt.id) != 0 {
			getUser(opt.id)
		} else {
			fmt.Println("Required firebase authentication UID")
		}
	}
}
```

firebaseからユーザー情報を取得する以下のファンクションをそれぞれ実装します。

```go:cmd/get.go
func getUser(uid string) *auth.UserRecord {
	ctx, client := getFirebaseClient()
	u, err := client.GetUser(ctx, uid)
	if err != nil {
		log.Fatalf("error getting user %s: %v\n", uid, err)
	}
	log.Printf("Successfully fetched user data: %v\n", u.UserInfo.Email)
	return u
}

func getUsers() {
	ctx, client := getFirebaseClient()
	iter := client.Users(ctx, "")
	for {
		user, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			log.Fatalf("Error listing users: %s\n", err)
		}
		log.Printf("Success user user: %v\n", user.UserInfo)
	}

	pager := iterator.NewPager(client.Users(ctx, ""), 100, "")
	for {
		var users []*auth.ExportedUserRecord
		nextPageToken, err := pager.NextPage(&users)
		if err != nil {
			log.Fatalf("paging error %v\n", err)
		}
		for _, u := range users {
			log.Printf("read user user: %v\n", u)
		}
		if nextPageToken == "" {
			break
		}
	}
}
```

上記と同様にcreate, update, deleteのサブコマンドを実装しました。

作成したものは[こちら](https://github.com/so-heee/firebase-admin-sdk-cmd)のリポジトリにあげています。


## おわりに
今回初めてcobraを利用してみましたが、私のような初学者にはちょうどよかったです:relaxed:また、利用シーンも多そうなのでこれからも色々作っていきたいです。今回できなかった以下の点については今後更新していきたいと思います。

- エラーハンドリングを意識した実装
- テストの実装



## 参考
- [Golangのコマンドライブラリcobraを使って少しうまく実装する](https://qiita.com/tkit/items/3cdeafcde2bd98612428)
- [GolangでFirebase Admin SDKの認証周りを試しました](https://qiita.com/nisitanisubaru/items/3ff4e0b08b20700f408c)