Go言語

【Go言語】flagパッケージで独自の型のオプションを定義する

flagパッケージを使うと、string型やint型、bool型のオプションを設定することができますが、オプションをsliceとして受け取るする場合などは独自の型を定義する必要があります。

flagパッケージの中身を読みながら独自の型を定義する方法をまとめていきます。

flagパッケージのドキュメント、中身をみる

flag.Var

flagパッケージのドキュメントをみると次の関数があります。

// Var defines a flag with the specified name and usage string. The type and
// value of the flag are represented by the first argument, of type Value, which
// typically holds a user-defined implementation of Value. For instance, the
// caller could create a flag that turns a comma-separated string into a slice
// of strings by giving the slice the methods of Value; in particular, Set would
// decompose the comma-separated string into the slice.
func Var(value Value, name string, usage string) {
    CommandLine.Var(value, name, usage)
}

https://golang.org/pkg/flag/#Var

コメントを読むと

Varは、指定された名前と使い方のフラグを定義します。フラグの型と値は第一引数のValueで表現されます(Valueはインターフェース)。通常、このValueは、ユーザーが定義するValueインターフェースの実装です。 例えば、スライスにValueインターフェースを実装することにより、カンマで区切られた文字列を文字列のスライスに変換するフラグを作ることができます。このとき、Setはカンマ区切りの文字列をスライスに分割します。

ふーん?。といった感じですが、

Value型を定義して、Var関数を呼び出せば独自の型のオプションを定義できそうです。

Value インターフェース

Valueが何なのか見てみると、インターフェースになっていて、String() stringSet(string) errorを定義していれば良いっぽいです。

// Value is the interface to the dynamic value stored in a flag.
// (The default value is represented as a string.)
//
// If a Value has an IsBoolFlag() bool method returning true,
// the command-line parser makes -name equivalent to -name=true
// rather than using the next command-line argument.
//
// Set is called once, in command line order, for each flag present.
// The flag package may call the String method with a zero-valued receiver,
// such as a nil pointer.
type Value interface {
    String() string
    Set(string) error
}

https://golang.org/pkg/flag/#Value

Setとは?

まず、Setが何をするものなのか調べます。

// bool の例
type boolValue bool

func (b *boolValue) Set(s string) error {
    v, err := strconv.ParseBool(s)
    if err != nil {
        err = errParse
    }
    *b = boolValue(v)
    return err
}

// intの例
type intValue int

func (i *intValue) Set(s string) error {
    v, err := strconv.ParseInt(s, 0, strconv.IntSize)
    if err != nil {
        err = numError(err)
    }
    *i = intValue(v)
    return err
}

https://golang.org/src/flag/flag.go#L116

文字列型の値をフラグの型に変換しています。先程のVar関数のコメントにあった

Set would decompose the comma-separated string into the slice.

(Setはカンマ区切りの文字列をスライスに分割します。)

の意味がわかりましたね。

Setメソッドは型で渡されるコマンドライン引数の値を、各フラグの型に変換する処理のようです。

String()とは?

次にString()が何なのかを調べます。 そのために、もう一度Var関数の実装を追っていきましょう。

func Var(value Value, name string, usage string) {
    CommandLine.Var(value, name, usage)
}

このCommandLineはパッケージ変数で*flag.FlagSet型の変数です。

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

と定義されています。

*flag.FlagSetVarメソッドを見てみます。

// Var defines a flag with the specified name and usage string. The type and
// value of the flag are represented by the first argument, of type Value, which
// typically holds a user-defined implementation of Value. For instance, the
// caller could create a flag that turns a comma-separated string into a slice
// of strings by giving the slice the methods of Value; in particular, Set would
// decompose the comma-separated string into the slice.
func (f *FlagSet) Var(value Value, name string, usage string) {
    // Remember the default value as a string; it won't change.
    flag := &Flag{name, usage, value, value.String()}
    _, alreadythere := f.formal[name]
    if alreadythere {
        var msg string
        if f.name == "" {
            msg = fmt.Sprintf("flag redefined: %s", name)
        } else {
            msg = fmt.Sprintf("%s flag redefined: %s", f.name, name)
        }
        fmt.Fprintln(f.Output(), msg)
        panic(msg) // Happens only if flags are declared with identical names
    }
    if f.formal == nil {
        f.formal = make(map[string]*Flag)
    }
  // flagが未定義なら登録
    f.formal[name] = flag
}

https://golang.org/pkg/flag/#FlagSet.Var

引数のValue型の値とフラグの名前、使い方から*flag.Flag型の値を作り、未登録、登録済みならエラーを返す処理になっています。

この*flag.Flag型の値を生成するときの4つ目のフィールドの値にString()が使われていますね。

type Flag struct {
    Name     string // name as it appears on command line
    Usage    string // help message
    Value    Value  // value as set
    DefValue string // default value (as text); for usage message
}

https://golang.org/pkg/flag/#Flag

どうやらString() は、使い方メッセージ内のデフォルト値の表示に使われるようです。

ここまでの整理

  • Var関数を使えば独自の型のオプションを定義できそう
  • Var関数にはValueインターフェースを満たした値を渡す必要がある
  • Valueインターフェースを満たすには、String()Set(string) errorを実装する必要がある
  • String()はヘルプメッセージ内のデフォルト値(文字列)を返す処理
  • Set(string) errorはコマンドライン引数の値をフラグの型に変換する処理

独自の型のオプションを定義してみる

カンマ区切りの数値をintのスライスとして受け取れるオプションを作ってみます。
やることは、

  • 独自の型を定義(intSliceValue型とする)
  • intSliceValue型がValueインターフェースを満たすようにSet, Stringメソッドを定義
  • flag.Var()を使ってオプションを設定

package main

import (
    "errors"
    "flag"
    "fmt"
    "strconv"
    "strings"
)

func main() {
    var numbers []int
    flag.Var((*intSliceValue)(&numbers), "numbers", "usage") // numbersを独自に定義したフラグの型に変換ƒ
    flag.Parse()
    fmt.Println(numbers)
}

type intSliceValue []int

func (v *intSliceValue) Set(s string) error {
    strs := strings.Split(s, ",")
    ints := make([]int, len(strs))
    var err error
    for i, v := range strs {
        ints[i], err = strconv.Atoi(v)
        if err != nil {
            return errors.New("parse error")
        }
    }
    *v = append(*v, ints...)
    return nil
}

func (v *intSliceValue) String() string {
    return ""
}

実行結果

flagパッケージ独自の型実行結果

 

イチオシ記事

1

自己紹介 フリーランスエンジニアをしているヨノと申します。 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエンド中 ...

2

はじめまして、フリーランスエンジニアのヨノと申します。 自己紹介 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエ ...

3

ネット上で色々言われているフリーランスエンジニア....。「本当はどうなの?」と思っている人は多いでしょう。 そこで本記事ではフリーランスエンジニア5年生の私が、ネット上の意見も引用しながら実態を解説 ...

-Go言語