想定読者
-
Go で CLI 作成時の引数処理に困っている人
- 特に、コンマ区切りの配列や特定のjsonを受け取りたいケース
- Go の標準ライブラリに慣れ親しみたい人
- 特に flag を使ったフラグ付きの引数処理
要約
Go の標準ライブラリ(標準パッケージ) flag を用いて Value
インターフェースを実装することで、時間や配列、カスタム構造体を引数として受け取る実装をシンプルに実現できます。
日本語読むより、こちらのレポジトリを見ていただくのが早いかもしれません。
flag パッケージ
flag パッケージとは、Go の標準ライブラリでコマンドラインのフラグをパースするツールです。 viper という有名パッケージもありますが、そちらに比べてシンプルなものになっています。
Value インターフェース
さて、こちらの flag では、文字列や整数のパースはパッケージに含まれるメソッド IntVar
や StringVar
で行うことができますが、日付や時間、カスタム構造体のパースはどうやったら良いのでしょうか?
公式ドキュメントを読むと、以下のようになります。
Or you can create custom flags that satisfy the Value interface (with pointer receivers) and couple them to flag parsing by
flag.Var(&flagVal, "name", "help message for flagname")
Value
インターフェースなるものを実装することで Var
メソッドを使って実現することができそうです。
Value
インターフェースはどういうインターフェースかについてもこちらに記載されていました。以下のようなインターフェースで、String
は値を表示するためのメソッドで、Set
がフラグを受け取ったときに値を解釈して入れるためのメソッドのようです。
type Value interface {
String() string
Set(string) error
}
以下で具体的な実装例を通して、利用方法を確認していきます。
各種実装例
URL
flag パッケージの例にも登場しますが、ほんの少しだけ修正を加えています。
Value
インターフェースの実装
以下のような形で Value
インターフェースを実装することで、URLの型としてパースすることができます。
package main
import (
"errors"
"net/url"
)
// URL example from https://golang.org/pkg/flag/#Value
type URLValue struct {
URL *url.URL
}
func (v URLValue) String() string {
if v.URL != nil {
return v.URL.String()
}
return ""
}
func (v URLValue) Set(s string) error {
if u, err := url.Parse(s); err != nil {
return errors.New(`Format is not acceptable`)
} else {
*v.URL = *u
}
return nil
}
Var
メソッド呼び出し
Value
インターフェースが実装できたら、以下のように Var
メソッドを呼び出して URL をパースします。
// URL のみバージョン
package main
import (
"flag"
"fmt"
"net/url"
)
var u = &url.URL{}
func main() {
fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
// URL argument. Check `url.go` for details
fs.Var(&URLValue{u}, "url", "URL to parse")
fs.Parse([]string{
"-url",
"https://golang.org/pkg/flag/",
})
// URL
fmt.Printf("url: {scheme: %q, host: %q, path: %q}\n", u.Scheme, u.Host, u.Path)
}
時刻
Value
インターフェースの実装
package main
import (
"errors"
"time"
)
const defaultLayout = time.RFC3339
// define acceptable layouts
var expectedLayouts = []string{
defaultLayout,
time.RFC1123,
time.RFC1123Z,
}
// Time example
type TimeValue struct {
Time *time.Time
}
func (v TimeValue) String() string {
if v.Time != nil {
return v.Time.Format(defaultLayout)
}
return ``
}
func (v TimeValue) Set(s string) error {
success := false
for _, layout := range expectedLayouts {
t, err := time.Parse(layout, s)
if err != nil {
continue
}
*v.Time = t
success = true
}
if !success {
return errors.New(`Format is not acceptable`)
}
return nil
}
Var
メソッド呼び出し
// 時刻 のみバージョン
package main
import (
"flag"
"fmt"
"time"
)
var t = &time.Time{}
func main() {
fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
// Time argument. Check `time.go` for details
fs.Var(&TimeValue{t}, "time", "Time to parse (RFC3339, RFC1123, RFC1123Z)")
fs.Parse([]string{
"-time",
"Mon, 02 Jan 2006 15:04:05 MST",
})
// Time
fmt.Printf("time: %q\n", t.Format(defaultLayout))
}
時間
Value
インターフェースの実装
package main
import (
"errors"
"time"
)
// Duration example
type DurationValue struct {
Duration *time.Duration
}
func (v DurationValue) String() string {
if v.Duration != nil {
return v.Duration.String()
}
return ``
}
func (v DurationValue) Set(s string) error {
d, err := time.ParseDuration(s)
if err != nil {
return errors.New(`Format is not acceptable`)
}
*v.Duration = d
return nil
}
Var
メソッド呼び出し
// 時間 のみのバージョン
package main
import (
"flag"
"fmt"
"time"
)
var d time.Duration
func main() {
fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
// Duration argument. Check `duration.go` for details
fs.Var(&DurationValue{&d}, "duration", "Duration to parse (supported units: 'ns', 'ms', 's', 'm', 'h') ")
fs.Parse([]string{
"-duration",
"75s",
})
// Duration
fmt.Printf("duration: %d\n", d)
}
配列
Value
インターフェースの実装
package main
import (
"fmt"
"strings"
)
const arraySeparator = `,`
// Array example
type ArrayValue struct {
Array *[]string
}
func (v ArrayValue) String() string {
if v.Array != nil {
return fmt.Sprintf("%#v", *v.Array)
}
return ``
}
func (v ArrayValue) Set(s string) error {
*v.Array = strings.Split(s, arraySeparator)
return nil
}
Var
メソッド呼び出し
// 配列 のみのバージョン
package main
import (
"flag"
"fmt"
)
var a []string
func main() {
fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
// Array argument. Check `array.go` for details
fs.Var(&ArrayValue{&a}, "array", "Array to parse (spearator: ',') ")
fs.Parse([]string{
"-array",
"a,b,c",
})
// Array
fmt.Printf("array: %q\n", a)
}
カスタム構造体
Value
インターフェースの実装
package main
import (
"encoding/json"
"errors"
"fmt"
)
type Hoge struct {
ID int `json:"id"`
Name string `json:"name"`
}
func (h *Hoge) String() string {
return fmt.Sprintf("Hoge{id: %d, name: %s}", h.ID, h.Name)
}
// Hoge (Custom struct) example
type HogeValue struct {
Hoge *Hoge
}
func (v HogeValue) String() string {
if v.Hoge != nil {
return v.Hoge.String()
}
return ``
}
func (v HogeValue) Set(s string) error {
var hoge Hoge
if err := json.Unmarshal([]byte(s), &hoge); err != nil {
return errors.New(`Format is not acceptable`)
}
*v.Hoge = hoge
return nil
}
Var
メソッド呼び出し
// カスタム構造体 のみのバージョン
package main
import (
"flag"
"fmt"
)
var h = &Hoge{}
func main() {
fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
// Hoge argument. Check `hoge.go` for details
fs.Var(&HogeValue{h}, "hoge", "Hoge to parse (json format)")
fs.Parse([]string{
"-hoge",
`{"id": 352, "name": "Diego"}`,
})
// Hoge (Custom struct)
fmt.Printf("hoge: %#v\n", h)
}
レポジトリ作成しました
上記の例を github のレポジトリに上げています。
https://github.com/nrnrk/go-flag-example
以下のコマンドで実行できます。
git clone git@github.com:nrnrk/go-flag-example.git
cd go-flag-example
go run .