urfave/cliで選択型の引数解析を実現する
選択型の引数解析
Goのcliアプリにおける引数解析にurfave/cliを利用している。このライブラリを利用しているときに、引数の指定において選択型の引数解析を実現したい。
引数で--format json
のように指定可能にしつつ、サポートしていない値が指定されたら引数エラーにしたい。他言語でいえば、pythonのargparseにおけるchoicesのようなもの。
よくありそうなユースケースだけれど、urfave/cliではこのような引数解析はサポートされていなかった。これを実現する方法として大きく以下の3つがありそう。
サンプルコードの全体はこちら。
アプリ本体で解析する
urfave/cliのライブラリ側で引数解析にするのではなく、アプリ側でチェックする方法がわかりやすい。この方法であれば実装量少なく簡単に実現できる。
func main() {
app := &cli.App{
Action: sample,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "lang",
Value: "english",
Usage: "selected language [english, japanese]",
},
},
}
err := app.Run(os.Args)
if err != nil {
fmt.Println(err)
}
}
func sample(ctx *cli.Context) error {
lang := ctx.String("lang")
switch lang {
case "english", "japanese":
fmt.Printf("lang: %s\n", lang)
default:
cli.ShowAppHelp(ctx)
return fmt.Errorf("invalid lang specified: %s\n", lang)
}
return nil
}
実行例は以下の通り。
引数エラー時にヘルプメッセージを表示するなど、やりたいことが実現できる。
一方で、引数解析をアプリ側で実装しなくてはいけない点は気になる。引数解析の問題なのでライブラリ側で完結して欲しい。また、上記実装では選択可能な引数をハードコーディングしている。解消することは容易だが、見通し良く実装したい。
$ ./urfavecli --lang ja
NAME:
urfavecli - A new cli application
USAGE:
urfavecli [global options] command [command options] [arguments...]
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--lang value selected language [english, japanese] (default: "english")
--help, -h show help
invalid lang specified: ja
urfave/cliで解析する
Flag Actionsでバリデーションする
対象引数においてFlag Actionsを利用することで、指定の関数をコールバックすることができる。
これを用いてバリデーションを行い、意図しない入力の場合はエラーを返すようにすればよい。
func main() {
app := &cli.App{
Action: sample,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "protocol",
Value: "https",
Action: func(ctx *cli.Context, v string) error {
valid := []string{"http", "https"}
if !slices.Contains(valid, v) {
return fmt.Errorf("Flag protocol value %v must be one of %v", v, valid)
}
return nil
},
},
},
}
err := app.Run(os.Args)
if err != nil {
fmt.Println(err)
}
}
func sample(ctx *cli.Context) error {
protocol := ctx.String("protocol")
fmt.Printf("protocol: %s\n", protocol)
return nil
}
実行例は以下の通り。
この方法であれば簡単に実現できる上にライブラリ側で引数解析を処理してくれるのでうれしい。
一方で、この方法だと入力が不正なときにヘルプメッセージを表示することができなかった。引数指定を間違えたときには、該当引数だけでなくcli全体のヘルプの表示まで実現したい。ライブラリ側のオプションで実現できそうだが、それらしきオプションは見付けられなかった。
$ ./urfavecli --protocol tcp
invalid protocol: tcp (must be one of [http https])
GenericFlagでEnum指定する
GenericFlagおよびEnumを利用することで選択型の引数解析でやりたいことは実現できる。
ヘルプメッセージなども自動で生成してくれるのでこの方法がよさそう。これはEmum Support#602というIssueで紹介されていた。Enumの定義やGeneric interfaceを実装するためのSet関数およびString関数を実装する必要はあるが見通しよく実現できる。
type EnumValue struct {
Enum []string
Default string
selected string
}
func (e *EnumValue) Set(value string) error {
for _, enum := range e.Enum {
if enum == value {
e.selected = value
return nil
}
}
return fmt.Errorf("allowed values are %s", strings.Join(e.Enum, ", "))
}
func (e EnumValue) String() string {
if e.selected == "" {
return e.Default
}
return e.selected
}
func main() {
app := &cli.App{
Action: sample,
Flags: []cli.Flag{
&cli.GenericFlag{
Name: "format, f",
Value: &EnumValue{
Enum: []string{"json", "plist", "xml"},
Default: "json",
},
},
},
}
err := app.Run(os.Args)
if err != nil {
fmt.Println(err)
}
}
func sample(ctx *cli.Context) error {
format := ctx.String("format")
fmt.Printf("format: %s\n", format)
return nil
}
実行例は以下の通り。
意図した通り動作してくれているが、ヘルプメッセージの前後で2回エラーメッセージが表示されている点が気になる。
$ ./urfavecli --format txt
Incorrect Usage: invalid value "txt" for flag -format: allowed values are json, plist, xml
NAME:
urfavecli - A new cli application
USAGE:
urfavecli [global options] command [command options] [arguments...]
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--format value (default: json)
--help, -h show help
invalid value "txt" for flag -format: allowed values are json, plist, xml
結論
アプリの規模が小さい内はアプリ側で実装、アプリの規模が大きくなったらGenericFlag+Enumで実装するのがよさそう。
Discussion