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>
のような形式にも対応できそうですね。