2時間くらいで最低限使える感じになりました。 リポジトリはこちら。 github.com
できたもの
jsto
というコマンドができました。
例えばUTCの現在時刻を知りたいときに本コマンドを使うと結果がすぐわかります。
jsto <主要標準時の名前>
という形式です。
bash-3.2$ date Sat Apr 23 00:05:47 JST 2022 bash-3.2$ bash-3.2$ jsto utc 'UTC' The time is: 2022/04/22 15:05:53 bash-3.2$
UTCの他にも個人的なセレクトで利用する標準時も対応しました。
Usage: jsto [command] Available Commands: completion Generate the autocompletion script for the specified shell edt show EDT time (UTC-4, JST-13) help Help about any command ist show IST time (UTC+5:30, JST-3:30) pdt show PDT time (UTC-7, JST-15) utc show UTC time (UTC+0, JST-9)
目的
どうしてこれをやろうと思ったのかを並べると以下です。
- Goを書く機会が日常的に全くないが、書く機会を作りたい
- UTCとかEDTとかの世界の標準時をCLIでシュッと表示できたら楽かも
- JST在住の自分が世界の標準時を知るためのコマンドで "JST to hoge" すなわち
jsto
ってコマンド名イケてるのでは *1
課程をメモ
spf13/cobra を選ぶまで
私は A Tour of go を一度はざっとやったことはある程度で、普段はGoを書く生活をしていないため初心者です。 とりあえずCLIコマンドを作りたいという目的で "go cli tool" と探したところ、公式ページの Command-line Interfaces (CLIs) に辿り着きました。 そのページでは CLI Libraries というセクションがあり、いくつかのCLIのためのライブラリが紹介されていました。
正直言ってどれが良いかの判断をするほどの情報を持っておらず、先頭4つのライブラリで一番星の多いものを選択することにしました。 調べたところ2022-04-23 時点では以下の結果だったため、最も多い spf13/cobra を使ってみることにしました。
- https://github.com/spf13/cobra -> 26.2k
- https://github.com/urfave/cli -> 17.8k
- https://github.com/spf13/viper -> 19k
- https://github.com/go-delve/delve -> 18.3k
spf13/cobra を書き始める
はじめに spf13/cobra のREADMEをざっと眺めました。理解するというよりは雰囲気をざっとみています。
おそらく意図的にREADME自体は短めに作られており必要なページへリンクが貼られているようでした。*2
Usageセクションに以下の説明があり、さっそく cobra-cli
をインストールしました。
cobra-cli is a command line program to generate cobra applications and command files. It will bootstrap your application scaffolding to rapidly develop a Cobra-based application. It is the easiest way to incorporate Cobra into your application.
go install github.com/spf13/cobra-cli@latest
cobra-cli
のインストール後、そのドキュメントに従って初期セットアップを行なっていきます。
具体的にはディレクトリとテンプレート的なコードが生成されます。
$ go mod init -> go.mod が作成される $ cobra-cli init -> 以下の状態になる --- . ├── LICENSE ├── cmd │ └── root.go ├── go.mod ├── go.sum └── main.go
さらにサブコマンドを追加するため以下を行いました。
$ cobra-cli add utc -> cmd/utc.go が作成されます --- . ├── LICENSE ├── cmd │ ├── root.go │ └── utc.go ├── go.mod ├── go.sum └── main.go
この段階でhelpやcompletionなども揃った状態のCLIコマンドが実行可能となっています。フレームワークすごいとなりました。
$ 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. Usage: jsto [command] Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command utc A brief description of your command Flags: -h, --help help for jsto -t, --toggle Help message for toggle Use "jsto [command] --help" for more information about a command.
サブコマンドutcの実装を進める
ここから先は実装に入ります。サブコマンドである cmd/utc.go の初期状態は以下のようになっています。
/* Copyright © 2022 NAME HERE <EMAIL ADDRESS> */ package cmd import ( "fmt" "github.com/spf13/cobra" ) // utcCmd represents the utc command var utcCmd = &cobra.Command{ Use: "utc", Short: "A brief description of your command", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your command. 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) { fmt.Println("utc called") }, } func init() { rootCmd.AddCommand(utcCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // utcCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: // utcCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") }
何も変更せずに実行すると以下のような結果となります。
$ go run main.go utc utc called
これを以下のようにtime package を使ってUTCを出力するように変えました。
/* Copyright © 2022 NAME HERE <EMAIL ADDRESS> */ package cmd import ( "fmt" "time" "github.com/spf13/cobra" ) // utcCmd represents the utc command var utcCmd = &cobra.Command{ Use: "utc", Short: "show UTC time (UTC+0, JST-9)", Long: `Displays the time in UTC. This is -9 hours from Japan time. ex) 'UTC' time is: 2022/04/22 13:12:45 `, Run: func(cmd *cobra.Command, args []string) { t := time.Now().UTC() fmt.Println("'UTC' The time is:\n", t.Format("2006/01/02 15:04:05")) }, } func init() { rootCmd.AddCommand(utcCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // utcCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: // utcCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") }
すると以下のようになりました!細かいツッコミどころはたくさんありますが、最低限の機能を達成です。
$ go run main.go utc 'UTC' The time is: 2022/04/22 15:35:06
UTC以外の主要な国際標準時を追加していく
UTCの次はEDTを追加しました。海外のカンファレンスなどでちょくちょく見かける標準時です。
ここは少し苦労しました。UTCは time.Now().UTC()
で簡単に取得できましたので time.Now().UTC(-4)
などのように書けると期待していましたができなかったためです。以下のドキュメントを見ても確かに引数などは与えられないようでした。
探し回ると LoadLocation のサンプルがやりたいことに近いことがわかりました。
以下が記載されていたサンプルです。ポイントとしては以下でしょうか。
time.LoadLocation()
で指定した地域のタイムゾーンを取得できるtime.In(location)
でロケーションを指定できる
package main import ( "fmt" "time" ) func main() { location, err := time.LoadLocation("America/Los_Angeles") if err != nil { panic(err) } timeInUTC := time.Date(2018, 8, 30, 12, 0, 0, 0, time.UTC) fmt.Println(timeInUTC.In(location)) }
このサンプルを受けて edt
サブコマンドは以下のようになりました。
/* Copyright © 2022 NAME HERE <EMAIL ADDRESS> */ package cmd import ( "fmt" "time" "github.com/spf13/cobra" ) // estCmd represents the edt command var estCmd = &cobra.Command{ Use: "edt", Short: "show EDT time (UTC-4, JST-13)", Long: `Displays the time in edt. This is -13 hours from Japan time. ex) 'EDT' time is: 2022/04/22 13:12:45 `, Run: func(cmd *cobra.Command, args []string) { loc, err := time.LoadLocation("America/New_York") if err != nil { panic(err) } t := time.Now().In(loc) fmt.Println("'EDT' time is (UTC-4, JST-13):\n", t.Format("2006/01/02 15:04:05")) }, } func init() { rootCmd.AddCommand(estCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // estCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: // estCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") }
今後の課題(他の標準時を追加しながら重複コードに気づく)
edtのサブコマンドを作る課程で地域を指定できることがわかったため、あらゆる世界の標準時のコマンドを実装する準備が整いました。 こうして pdt, ist なども追加していったのですが、各サブコマンドに重複するコードが多いことに気づきました。 具体的には以下の差しかないのです。
- 指定するタイムゾーンの地域
- 表示される一部のテキスト("UTC-4"など)
これらを共通の関数に切り出し、引数として渡すようにするのが次のステップかなと考えています。 他にも以下くらいは勉強がてら楽しもうと思います。
- テストコードを追加
- GitHub ActionsでCI
- TagとかReleaseを利用したバージョン管理
以上、ちょっとした気分転換でGo言語で簡単なコマンドを作って楽しんだ話でした。