Cobra+Afero で作る Golang 製 CLI
はじめに
Go 言語で日記を管理する CLI (今回は適当に mydiary
という名前にしました)を作りました。リポジトリはこちら。
元々のモチベは AWS Copilot CLI が Golang で実装されていて CLI 部分は Cobra で実装されているっぽいので挙動を把握したかったからです。題材に日記を管理する CLI を選んだのは、2017 年から日記をテキストファイルでつけていて今までは Python スクリプトを元日に叩いてテンプレートを生成していたところ、年一回しか実行しないのと型もないので挙動がわからなくなっていたので作り直そうと思ったからです。
例によって開発は Docker で行って VSCode Remote Containers にお世話になっています。
やってみる
作るものの詳細
個人的に使うものなので以下の二つの機能があれば十分です。
- 日記の特定のテンプレートを生成する。例えば 2022 年なら
2022/202201.txt
みたいな形で各月のテキストファイルを生成し中身が以下のようになっていればいい。コマンドはこんな感じ:mydiary init --year 2020
2020,January,01,Sat
2020,January,02,Sun
- 指定した日にちの日記を見る。コマンドはこんな感じ:
mydiary show --date 20200101
Cobra とは
Cobra は Golang の CLI を作るときの便利ライブラリで、 AWS Copilot CLI 以外にも静的サイトジェネレーターの Hugo や Kubernetes でも使われています。実は Cobra Generator という CLI アプリケーションの初期化を簡単にできるものがあるらしいことを後で知りました。これの存在を知らなかったので自分は AWS Copilot CLI のコードを読みながら挙動を読み解いていったのですが、そんなことをせずに普通にユーザーガイドを読むと使い方のイメージが掴みやすいと思います。
CLI を作る場合大元のコマンドがあってそれにサブコマンドが複数付属していてさらにフラグを指定できるみたいな作りが多いと思います。例えば Git なら git
という大元のコマンドがあって、それに add
や commit
のようなサブコマンドがあり、さらに commit
には -m
フラグがあります。Cobra の場合コマンドは cobra.Command
構造体で定義して AddCommand
でサブコマンドとして別のコマンドを追加できます。今回作った CLI だとここで大元のコマンドを定義しています。
func BuildRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "mydiary",
Short: "Manage your diary",
Long: `mydiary is a CLI tool for Golang that manages your diary.
This tool generates diary templates, shows diary of specified date.`,
}
cmd.AddCommand(BuildInitCmd())
cmd.AddCommand(BuildShowCmd())
return cmd
}
mydiary init
コマンド
このコマンドでは年を引数にとってその年の各月の日記テンプレートを生成します。テンプレートは日付と曜日を含んでおり日付を入れるのは簡単ですが曜日の場合はそんなに簡単にはいきません。といってもツェラーの公式を実装するだけで実現できます。曜日を計算する処理はここに書いておいてテストも書くことにしました。
init
コマンドには year
フラグがついています。フラグについてはユーザーガイドのここにまとまっています。ざっくりいうとこんな感じで使えます。
var year int64
cmd := &cobra.Command{
// 省略
}
cmd.Flags().Int64Var(&year, "year", 0, "year of diary")
cmd.MarkFlagRequired("year")
int64 型の値を year
変数に代入するフラグとして year
をもうけてヘルプコマンドに対する説明としては "year of diary" を返すようにしており、 init
コマンドの必須フラグに year を指定しています。他にも String や Boolean の値をフラグに代入することもできます。
そしてこの init
コマンドはテンプレートファイルを生成するわけなのでファイル I/O が発生します。やってることはディレクトリを作成してファイルを書き込むだけなので os.Mkdir
してもいいのですが、それだと開発している最中にディレクトリが何度も作られて微妙だなと思いました。色々調べているうちにすぐあとで述べる afero
を使うことにしました。
Afero を使うとテストが便利
Afero は Golang のファイルシステムフレームワークです。以下のように標準の os
パッケージと同じ感じで使えます。
var Fs = afero.NewOsFs()
err *Fs.MkdirAll("2020", 0755)
README によれば Afero をテストに使うことができます。 afero.NewMemMapFs()
を使うと実際にファイルの読み書きをするのではなくメモリ上のファイルシステムを使って読み書きを行うので、例えばテストのたびにテスト用のディレクトリを作ったり削除したりする必要がありません。
今回作った CLI だと日記のテンプレートファイルを書き込む WriteMonthTemplate
では ws.Fs.WriteFile(filename, template, 0755)
のように引数に Afero のファイルシステムを持たせて、テストの時には MemMapFs
を渡すことで単体テストをやりやすくしています。
(2023/05/28 追記)
mattn さんの「Go 言語プログラミングエッセンス」に触発されてテストケースを Table Driven Test にする・GoReleaser と GitHub Actions を使って自動でリリースする・テストを並行で実行する、などの更新を加えました。
おわりに
Cobra を使ってコードを書いたことでだいぶ AWS Copilot CLI のコードが読みやすく感じるようになりました。ちょっとした CLI を作る時には Cobra はだいぶ便利だと思います。
Discussion