もりはやメモφ(・ω・ )

ITとか読書感想文とか

Go言語で主要な標準時を表示するコマンドを作ってみた

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のためのライブラリが紹介されていました。

Go CLI Libraries

正直言ってどれが良いかの判断をするほどの情報を持っておらず、先頭4つのライブラリで一番星の多いものを選択することにしました。 調べたところ2022-04-23 時点では以下の結果だったため、最も多い spf13/cobra を使ってみることにしました。

spf13/cobra を書き始める

はじめに spf13/cobra のREADMEをざっと眺めました。理解するというよりは雰囲気をざっとみています。

github.com

おそらく意図的に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 のインストール後、そのドキュメントに従って初期セットアップを行なっていきます。

github.com

具体的にはディレクトリとテンプレート的なコードが生成されます。

$ 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) などのように書けると期待していましたができなかったためです。以下のドキュメントを見ても確かに引数などは与えられないようでした。

pkg.go.dev

探し回ると LoadLocation のサンプルがやりたいことに近いことがわかりました。

pkg.go.dev

以下が記載されていたサンプルです。ポイントとしては以下でしょうか。

  • 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言語で簡単なコマンドを作って楽しんだ話でした。

*1:正直酔ってたかもしれません

*2:おかげで抵抗少なく読みやすくて助かりました