title: 【転載】Go 言語標準ライブラリのコマンドライン引数の解析:flag ライブラリの詳細解説
date: 2021-09-07 13:12:00
comment: false
toc: true
category:
- Golang
tags: - 転載
- Golang
- コマンドライン
- 引数
- 解析
- flag
- 詳細解説
- 標準ライブラリ
この記事は、Go 言語標準ライブラリのコマンドライン引数の解析:flag ライブラリの詳細解説から転載されています。
Golangプログラムには、コマンドライン引数を処理するための多くの方法があります。
簡単な場合は、ライブラリを使用せずに直接os.Args
を使用できます。
package main
import (
"fmt"
"os"
)
func main() {
// os.Argsは[]stringです
if len(os.Args) > 0 {
for index, arg := range os.Args {
fmt.Printf("args[%d]=%v\n", index, arg)
}
}
}
実行してみてください。最初の引数は実行ファイルのパスです。
$ go run demo.go hello world hello golang
args[0]=/var/folders/72/lkr7ltfd27lcf36d75jdyjr40000gp/T/go-build187785213/b001/exe/demo
args[1]=hello
args[2]=world
args[3]=hello
args[4]=golang
上記からわかるように、os.Args
は単純な引数しか処理できず、引数の位置に厳密な要求があります。複雑なシナリオでは、自分で解析ルールを定義する必要があり、非常に面倒です。
本当に複雑なシナリオに直面した場合、Golang の標準ライブラリ flag パッケージを使用してコマンドライン引数を処理できます。
この記事では、Golang 標準ライブラリの flag パッケージの使い方を紹介します。
引数の種類#
引数がブール型かどうかに応じて、2 種類に分けられます:
- ブール型引数:
--debug
のように、後に具体的な値を指定する必要はなく、指定すれば True、指定しなければ False です。 - 非ブール型引数:非ブール型で、int、string などの他の型の可能性があります。例えば、
--name jack
のように、後に具体的な引数値を指定できます。
引数名の長さに応じて、さらに分けることができます:
- 長引数:例えば、
--name jack
は長引数で、引数名の前に 2 つの-
があります。 - 短引数:通常は 1 文字または 2 文字(長引数の省略形)で、例えば
-n
のように、引数名の前に 1 つの-
があります。
入門例#
まず、文字列型の引数の例を示します。
package main
import (
"flag"
"fmt"
)
func main(){
var name string
flag.StringVar(&name, "name", "jack", "あなたの名前")
flag.Parse() // 引数を解析
fmt.Println(name)
}
flag.StringVar
は文字列引数を定義し、いくつかの引数を受け取ります。
- 最初の引数:受け取った値をどの変数に格納するか、ポインタで指定する必要があります。
- 2 番目の引数:コマンドラインで使用する引数名、例えば
--name jack
の中の name。 - 3 番目の引数:コマンドラインでこの引数値が指定されていない場合のデフォルト値、ここでは
jack
。 - 4 番目の引数:この引数の用途や意味を記録します。
上記のプログラムを実行すると、出力は以下のようになります。
$ go run demo.go
jack
$ go run demo.go --name wangbm
wangbm
改善点#
プログラムが受け取る引数が非常に少ない場合、上記のように書いても問題ありません。
しかし、引数の数が多くなると、メイン関数内に引数解析のコードが大量に積み重なり、コードの可読性や美観に影響を与えます。
引数解析のコードをinit
関数に置くことをお勧めします。init
関数はmain
関数よりも先に実行されます。
package main
import (
"flag"
"fmt"
)
var name string
func init() {
flag.StringVar(&name, "name", "jack", "あなたの名前")
}
func main(){
flag.Parse()
fmt.Println(name)
}
引数の型#
コマンドラインで引数を指定すると、Go はその引数をどのように解析し、どの型に変換するかを事前に定義する必要があります。
異なる引数は、flag
の異なるメソッドに対応しています。
以下に、異なる引数の型をどのように定義するかを説明します。
ブール型#
実装効果:--debug
を指定しない場合、debug のデフォルト値は false で、--debug
を指定すると debug は true に設定されます。
var debug bool
func init() {
flag.BoolVar(&debug, "debug", false, "DEBUGモードを有効にするか")
}
func main(){
flag.Parse()
fmt.Println(debug)
}
実行後、結果は以下のようになります。
$ go run main.go
false
$ go run main.go --debug
true
数値型#
age 引数を定義し、指定しない場合はデフォルトで 18 に設定します。
var age int
func init() {
flag.IntVar(&age, "age", 18, "あなたの年齢")
}
func main(){
flag.Parse()
fmt.Println(age)
}
実行後、結果は以下のようになります。
$ go run main.go
18
$ go run main.go --age 20
20
int64
、uint
、float64
型はそれぞれ Int64Var、UintVar、Float64Var メソッドに対応し、同様に扱います。
文字列#
name 引数を定義し、指定しない場合はデフォルトで jack に設定します。
var name string
func init() {
flag.StringVar(&name, "name", "jack", "あなたの名前")
}
func main(){
flag.Parse()
fmt.Println(name)
}
実行後、結果は以下のようになります。
$ go run main.go
jack
$ go run main.go --name wangbm
wangbm
時間型#
interval 引数を定義し、指定しない場合はデフォルトで 1s に設定します。
var interval time.Duration
func init() {
flag.DurationVar(&interval, "interval", 1 * time.Second, "ループ間隔")
}
func main(){
flag.Parse()
fmt.Println(interval)
}
効果を確認すると、以下のようになります。
$ go run main.go
1s
$ go run main.go --interval 2s
2s
カスタム型#
flag パッケージがサポートする型には Bool、Duration、Float64、Int、Int64、String、Uint、Uint64 があります。
これらの型の引数は、それぞれのバックエンド型にカプセル化されます。例えば、Int 型の引数は intValue にカプセル化され、String 型の引数は stringValue にカプセル化されます。
これらのバックエンド型はすべて flag.Value インターフェースを実装しているため、コマンドライン引数を Flag 型のインスタンスとして抽象化できます。以下は Value インターフェースと Flag 型のコードです:
type Value interface {
String() string
Set(string) error
}
// Flag型
type Flag struct {
Name string // コマンドラインに表示される名前
Usage string // ヘルプメッセージ
Value Value // 設定された値(インターフェースなので、異なる型のインスタンスになる可能性があります)
DefValue string // デフォルト値(テキストとして);使用メッセージ用
}
func Var(value Value, name string, usage string) {
CommandLine.Var(value, name, usage)
}
カスタム型の引数を実装するには、Var 関数の最初の引数オブジェクトが flag.Value インターフェースを実装するだけで済みます。
type sliceValue []string
func newSliceValue(vals []string, p *[]string) *sliceValue {
*p = vals
return (*sliceValue)(p)
}
func (s *sliceValue) Set(val string) error {
// 引数値の解析方法
*s = sliceValue(strings.Split(val, ","))
return nil
}
func (s *sliceValue) String() string {
return strings.Join([]string(*s), ",")
}
例えば、次のような効果を実現したい場合、引数はカンマで区切られた文字列として渡され、flag の解析時にスライスに変換されます。
$ go run demo.go -members "Jack,Tom"
[Jack Tom]
このようにコードを書くことができます。
var members []string
type sliceValue []string
func newSliceValue(vals []string, p *[]string) *sliceValue {
*p = vals
return (*sliceValue)(p)
}
func (s *sliceValue) Set(val string) error {
// 引数値の解析方法
*s = sliceValue(strings.Split(val, ","))
return nil
}
func (s *sliceValue) String() string {
return strings.Join([]string(*s), ",")
}
func init() {
flag.Var(newSliceValue([]string{}, &members), "members", "メンバーリスト")
}
func main(){
flag.Parse()
fmt.Println(members)
}
友人の中には、(*sliceValue)(p)
という行のコードについて疑問を持つかもしれません。これは何を意味するのでしょうか?
実際には、以前の【2.9 詳細図解:静的型と動的型】で説明されており、忘れた場合は復習に行くことができます。
長短オプション#
flag パッケージでは、使用上、長短オプションの区別はありません。以下の例を見てください。
package main
import (
"flag"
"fmt"
)
var name string
func init() {
flag.StringVar(&name, "name", "明哥", "あなたの名前")
}
func main(){
flag.Parse()
fmt.Println(name)
}
次のようにいくつかの引数形式を指定することができます。
$ go run main.go
明哥
$ go run main.go --name jack
jack
$ go run main.go -name jack
jack
1 つの-
と 2 つの-
の実行結果は同じです。
では、もう 1 つ追加してみましょう。
ついにエラーが発生しました。つまり、最大で 2 つの-
しか指定できません。
$ go run main.go ---name jack
bad flag syntax: ---name
Usage of /tmp/go-build245956022/b001/exe/main:
-name string
あなたの名前 (default "明哥")
exit status 2