goのflagで@<filename>と言う形式ならファイルの中身を利用するValueを作ってみる

ちょっとだけメモ。

最近は、標準ライブラリのflagだけで生活できるような気がしている。

まぁそれはおいておいて、例えば、curlなどで使われている@<filename>と言う表記で、ファイルの中身を取り出すvalueが欲しくなった。その実装のメモ。

flag.Value

flagパッケージでオプションの取扱いかたを変えたいときには flag.Value を実装した型をつくる。これは以下のようなインターフェイスのもの。

type Value interface {
    String() string
    Set(string) error
}

Set()で良い感じに値を更新してくれれば良い。作ったValueは、flag.FlagSet.Var()経由で利用できるようだ。

boolValueを例に実装を覗いてみる

flag.Valueの実装例として、flag.boolValueを覗いてみる。boolValueはboolのnew typeで作られていたらしい。

// -- bool Value
type boolValue bool

func newBoolValue(val bool, p *bool) *boolValue {
    *p = val
    return (*boolValue)(p)
}

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

func (b *boolValue) Get() interface{} { return bool(*b) }

func (b *boolValue) String() string { return strconv.FormatBool(bool(*b)) }

コレを通常使う場合は以下のようなコードになる。FlagSet.BoolVar() (か FlagSet.Bool()) が使われる。

var option struct {
    Verbose bool
}

fs := flag.NewFlagSet("app", flag.ExitOnError)
fs.BoolVar(&option.Verbose, "verbose", false, "verbose output")

fs.Parse()
fmt.Println(option.Verbose)

この実装は以下の様になっている。

// BoolVar defines a bool flag with specified name, default value, and usage string.
// The argument p points to a bool variable in which to store the value of the flag.
func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string) {
    f.Var(newBoolValue(value, p), name, usage)
}

まぁそんなわけでいい感じにやっていけば良い。

@付きの実装

基本的には、Set()をどうやって実装するかというだけの話になる。以下のような実装になった。

type FileContenOrLiteralValue string

func (v *FileContenOrLiteralValue) String() string {
    return string(*v)
}

func (v *FileContenOrLiteralValue) Set(s string) error {
    if !strings.HasPrefix(s, "@") {
        *v = FileContenOrLiteralValue(s)
        return nil
    }

    b, err := ioutil.ReadFile(strings.TrimPrefix(s, "@"))
    if err != nil {
        return err
    }
    *v = FileContenOrLiteralValue(string(b))
    return nil
}

こんな感じで使う。

func main() {
    var options struct {
        Target string
    }
    fs := flag.NewFlagSet("app", flag.ExitOnError)
    fs.Var((*FileContenOrLiteralValue)(&options.Target), "target", "literal or @<targetname>")

    fs.Parse(os.Args[1:])
    fmt.Println(options.Target)
}

実際の実行例

$ cat todo.json
{
  "todo": {
    "title": "foo"
  }
}

$ go run 01*/main.go --help
Usage of app:
  -target value
        literal or @<targetname>

ファイルの中身を取り出す

$ go run 01*/main.go --target @todo.json
{
  "todo": {
    "title": "foo"
  }
}

直接JSON文字列として

$ go run 01*/main.go --target '{"status": "ok"}'
{"status": "ok"}

プロセス置換も使える

$ go run 01*/main.go --target @<(echo "{\"ans\": \"$(echo 1 + 1 | bc -l)\"")
{"ans": "2"

はい。

それが良いかは別として、同じような理屈で、file://<path> のような形式にも対応できそうですね。

gist