今川館

都内勤務の地味OLです

Goのflagパッケージを使ってサブコマンドを作る

GoでPythonのargparseみたいなサブコマンドを処理したい場合はflag.NewFlagSetというやつを使えば良いらしい。

Goでのやり方はこのページでドン!なんだけど。忘れないようにメモ。

Golang: Implementing subcommands for command line applications · Software adventures and thoughts

FlagSetを別々に作ってParseする

サブコマンドを作る場合はflag.NewFlagSetを呼び出すとFlagSetが返ってくるのでそれぞれ引数、オプションの解析をすれば良い。

今回は

  • help: ヘルプを表示する. -vオプションでより詳しく表示する.
  • money: 入力したふたつの通貨の為替レートを表示する.

上記2つのサブコマンドを持つコマンドを作る。

helpコマンドにはオプションを付けるので、以下のようにする。

help := flag.NewFlagSet("help", flag.ExitOnError)
verbosity := help.Int("verbosity", 0, "")
// または
// var verbosity int
// help.IntVar(&verbosity, "verbosity", 0, "")

money := flag.NewFlagSet("money", flag.ExitOnError)

必須引数はFlagSet.Args()で参照できる

moneyコマンドは通貨をふたつ入力するので、以下のようにする。

if len(money.Args()) < 2 {
	// e.g. $ <cmd> money USD
	fmt.Println("エラー: 通貨をふたつ入力してください")
	return
}

verbosityの数でメッセージの詳細度を上げたい

例えば「-v」よりも「-vv」、「-vvv」の方が詳細なメッセージを表示可能としたい。

最初、安易に以下のようにやってしまったが、

help := flag.NewFlagSet("help", flag.ExitOnError)
v1 := help.Bool("v", false, "")
v2 := help.Bool("vv", false, "")
v3 := help.Bool("vvv", false, "")

これだと、「-vvvv」などが解析エラーになってしまう。

なので、help.Args()をループで回して最大値を取るよう書き直した(以下)。

// 引数からメッセージ詳細度を算出する。算出に使わなかった引数は戻り値のスライスに返却する。
var regex = regexp.MustCompile("^-v+$")

func Verbosity(xs []string) (int, []string) {
	var v float64
	others := make([]string, 0, len(xs))
	for _, x := range xs {
		if regex.MatchString(x) {
			v = math.Max(v, float64(strings.Count(x, "v")))
		} else {
			others = append(others, x)
		}
	}
	return int(v), others
}

help := flag.NewFlagSet("help", flag.ExitOnError)
var verbosity int
help.IntVar(&verbosity, "verbosity", 0, "")

v, others := Verbosity(os.Args[2:])
help.Parse(others)
if verbosity < v {
	// e.g. <cmd> help -verbosity=0 -vvv
	help.Set("verbosity", strconv.Itoa(v))
}

サンプルコード

プログラム全体は以下のようになった。

package main

import (
	"flag"
	"fmt"
	"math"
	"os"
	"regexp"
	"strconv"
	"strings"
)

var regex = regexp.MustCompile("^-v+$")

// 引数からメッセージ詳細度を算出する。算出に使わなかった引数は戻り値のスライスに返却する。
func Verbosity(xs []string) (int, []string) {
	var v float64
	others := make([]string, 0, len(xs))
	for _, x := range xs {
		if regex.MatchString(x) {
			v = math.Max(v, float64(strings.Count(x, "v")))
		} else {
			others = append(others, x)
		}
	}
	return int(v), others
}

func main() {
	help := flag.NewFlagSet(	"help", flag.ExitOnError)
	var verbosity int
	help.IntVar(&verbosity, "verbosity", 0, "")

	money := flag.NewFlagSet("money", flag.ExitOnError)
	if len(os.Args) == 1 {
		fmt.Println("usage: argparse <command> [<args>]")
		fmt.Println("\thelp: ヘルプをプリントします")
		fmt.Println("\tmoney: 為替レートを調べます")
		return
	}

	switch os.Args[1] {
	case "help":
		v, others := Verbosity(os.Args[2:])
		help.Parse(others)
		if verbosity < v {
			// e.g. <cmd> help -verbosity=0 -vvv
			help.Set("verbosity", strconv.Itoa(v))
		}
	case "money":
		money.Parse(os.Args[2:])
	default:
		fmt.Printf("%q is not valid command.\n", os.Args[1])
		os.Exit(2)
	}

	if help.Parsed() {
		message := "Help me."
		if verbosity == 1 {
			message = "Help me!"
		}
		if verbosity == 2 {
			message = "Help me!!"
		}
		if verbosity >= 3 {
			message = "HELP ME!!"
		}
		fmt.Println(message)
	}

	if money.Parsed() {
		if len(money.Args()) < 2 {
			// e.g. $ <cmd> money USD
			fmt.Println("エラー: 通貨をふたつ入力してください")
			return
		}

		from, to := money.Arg(0), money.Arg(1)
		if from == "" || to == "" {
			// e.g. $ <cmd> money USD ''
			fmt.Println("エラー: 通貨をふたつ入力してください")
			return
		}
		fmt.Printf("%v/%v: 100円/$\n", from, to) // 適当
	}
}

動かすとこのような結果が出る。

$ go run foo.go help -verbosity=0 -vvv
# => HELP ME!!

$ go run foo.go help -vv
# => Help me!!

$ go run foo.go money USD JPY
# => USD/JPY: 100円/$