Go言語のコマンドライン引数の使い方(サブコマンド等)

go言語でのコマンドライン引数の使い方でやや混乱したのでまとめる。

  • まとめること
    • flagの仕様
    • flagでgit push -fのようにオプション引数より前に非オプションの引数を置く方法・サブコマンドの例
    • go-flagパッケージのサブコマンドを試す

flagの仕様

Golang標準のflagパッケージは以下のように使う。

package main

import (
  "flag"
  "fmt"
  "os"
)

func main() {
  // -hオプション用文言
  flag.Usage = func() {
    fmt.Fprintf(os.Stderr, `
Usage of %s:
   %s [OPTIONS] ARGS...
Options\n`, os.Args[0],os.Args[0])
    flag.PrintDefaults()
  }

  var (
    opt1 = flag.String("opt1", "default-value", "First string option")
    opt2 = flag.String("opt2", "default-value", "Second string option")
  )
  flag.Parse()
  fmt.Println("opt1:", *opt1)
  fmt.Println("opt2:", *opt2)
  fmt.Println("args:", flag.Args())
}
$ ./argtest -h
Usage of ./argtest:
   ./argtest [OPTIONS] ARGS...
Options
  -opt1="default-value": First string option
  -opt2="default-value": Second string option

$ ./argtest -opt1 aaa -opt2 bbb AAA BBB 
opt1: aaa
opt2: bbb
args: [AAA BBB]

flag.Parse()関数は、"-"で始まらない(非フラグな)コマンドライン引数に到達すると、未解釈の引数をflag.Args()に格納し、パースをストップしてしまう。

なので、例:./argtest -opt1 aaa -opt2 bbb FILE1 FILE2...のようにオプション群を非オプション引数より前に置くのなら簡単にパースできる

flagでコマンドラインオプションをコマンドライン引数の後ろに置きたい

では、例:./argtest FILE1 -opt1 aaa -opt2 bbbという風にオプションを引数の後ろには書けないのだろうか。たとえばgit push -fみたいなサブコマンドをやりたいときに必要になる。

そういうときは、FlagSet型を使う。

これは複数のFlag設定を切り替えられるようにする仕組みだけど、FlagSetのParse()メソッドはパースする範囲をスライスとして渡せるので、これを利用して先頭に非フラグの引数を置いたり、サブコマンドを実現したりできる。

  fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
  var (
    opt1 = fs.String("opt1", "default-value", "First string option")
    opt2 = fs.String("opt2", "default-value", "Second string option")
  ) 

  arg1 := os.Args[1]
  fs.Parse(os.Args[2:])
  fmt.Println("arg1:", arg1)
  fmt.Println("opt1:", *opt1)
  fmt.Println("opt2:", *opt2)
  fmt.Println("args:", fs.Args())
$ ./argtest XXX -opt1 AAA -opt2 BBB YYY ZZZ
arg1: XXX
opt1: AAA
opt2: BBB
args: [YYY ZZZ]

サブコマンドの実装についてはVegetaが参考になった。各サブコマンド内で別々のフラグを設定している。See vegeta.

一応、こんなかんじになる。

subcommand.go

go-flagsを試してみる

調査の一環で非標準パッケージのgo-flagsを試してみたので感想

  • フラグ・非フラグの順序関係なくすべてパースできる。便利だけど、逆に位置を固定したいときにどうすればいいのか分からなかった。
  • サブコマンドをサポートしているようなので試してみた(下記)けど、コマンドがgo-flagsに依存してしまうしちょっと大げさだった
  • 使わなくていいかも。
package main

import (
  "fmt"
  "github.com/jessevdk/go-flags"
  "log"
  "os"
)

type Options struct {
  Option1 string     `short:"1" long:"opt1" description:"First string option"`
  Option2 string     `short:"2" long:"opt2" description:"Second string option"`
  Put     PutCommand `description:"Command to put something" command:"put" subcommands-optional:"true"`
  Get     GetCommand `description:"Command to put something" command:"get" subcommands-optional:"true"`
}

var opts Options
var parser = flags.NewParser(&opts, flags.Default)

type PutCommand struct {
  PutOption string `short:"p" long:"put-opt" description:"First string option"`
}
type GetCommand struct {
  GetOption string `short:"g" long:"get-opt" description:"First string option"`
}

func (x *PutCommand) Execute(args []string) error {
  fmt.Printf("Putting someting (opt=%v): %#v\n", x.PutOption, args)
  return nil 
}
func (x *GetCommand) Execute(args []string) error {
  fmt.Printf("Getting someting (opt=%v): %#v\n", x.GetOption, args)
  return nil 
}

func main() {
  if _, err := parser.Parse(); err != nil {
    os.Exit(1)
 }
}
$ ./command --opt1=AAA put XXX YYY ZZZ --put-opt=OPT
Putting someting (opt=OPT): []string{"XXX", "YYY", "ZZZ"}

$ ./command --opt1=AAA get XXX YYY ZZZ --get-opt=OPT 
Getting someting (opt=OPT): []string{"XXX", "YYY", "ZZZ"}

$ ./command --opt1=AAA
Please specify one command of: get or put

./command VVV --opt1=AAA get XXX YYY ZZZ --get-opt=OPT
Getting someting (opt=OPT): []string{"VVV", "XXX", "YYY", "ZZZ"} // <- argsにgetより前の引数が含まれている

参考