はじめに
軽微なプログラムを書く機会があり、せっかくなのでGolangでコマンドラインツールの開発に挑戦してみました。
簡単なものであれば、私のような初学者でも抵抗なく取り組めそうなことと、こういうツール欲しいなぁというシーンが私の周りではよくあったのでこの機会に学習することにしました。
作成したものはこちらのリポジトリにあげています。
今回作成するもの
Firebase Authentication ユーザーのCRUDコマンド
認証にFirebase Authenticationを利用しているWebアプリの開発に現在携わっているのですが、ユーザーの確認、作成や削除などをする際、周りのメンバー含めコンソールからポチポチしていました。
どうしてもユーザーの作成や削除などを頻繁に行う必要があったのですが、公式から提供されているCLIではCSVベースでのエクスポートとインポートしかないため、これをコマンドラインツールにしてみました。
使用するライブラリ
ビュアなGolangでもコマンドラインツールは書けそうでしたが、ライブラリを利用することで実装がより簡単にできそうでしたので利用することにしました。
候補となったのは以下の二つです。
「urfave/cli」というライブラリも良さそうでしたが、cobraはdockerやKubernetesでも採用されているとのことだったので、今回はcobraを利用することにしました。
雛形の作成
まずは 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
雛形の内容を確認
func main() {
//main.goからはcmd/root.goのExecuteファンクションを呼び出している
cmd.Execute()
}
コマンドライン引数の設定(spf13/pflag)や、設定ファイルの読み込み(spf13/viper)が実装されています
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")
}
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())
}
}
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がありますので、そちらを利用します。
今回はこちらの詳細については省略しますが、以下のようにスニペットが用意されているため簡単に実装ができます。
サブコマンドの実装
上記で追加した get
コマンドに処理を実装していきます。
パラメータでUID指定と、一覧取得を切り替えるようにしています。
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からユーザー情報を取得する以下のファンクションをそれぞれ実装します。
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のサブコマンドを実装しました。
作成したものはこちらのリポジトリにあげています。
おわりに
今回初めてcobraを利用してみましたが、私のような初学者にはちょうどよかったですまた、利用シーンも多そうなのでこれからも色々作っていきたいです。今回できなかった以下の点については今後更新していきたいと思います。
- エラーハンドリングを意識した実装
- テストの実装