diff --git a/README.md b/README.md index 24ae10c..794cff4 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,58 @@ func main() { 検証は `go-flags` によるパース成功後、`Run` メソッドの呼び出し前に実行されます。`flagrun.Go` / `flagrun.Check` / `flagrun.Ship` のいずれでも利用できます。 +## カスタムフラグ型 + +### `flagrun.Bytes` — 人間可読なバイト数 + +`flagrun.Bytes` は `go-flags` 向けのカスタム型で、`10MB` や `2GiB` のような人間可読な文字列を数値に変換します。内部では [go-humanize](https://github.com/dustin/go-humanize) の `ParseBytes` を利用しています。 + +```go +package main + +import ( + "fmt" + + "github.com/monitoring-forge/flagrun" +) + +type Opt struct { + MaxSize flagrun.Bytes `short:"s" long:"max-size" default:"1MB" description:"Maximum size"` +} + +func (p *Opt) Run(args []string) (string, int) { + fmt.Printf("max size: %d bytes\n", p.MaxSize) + return "ok\t1", flagrun.OK +} +``` + +### `flagrun.Re` — 正規表現フラグ + +`flagrun.Re` は `go-flags` 向けのカスタム型で、フラグ値を `regexp.Regexp` としてパースします。未指定時や空文字の場合は `Present()` が `false` を返し、正規表現のコンパイルに失敗した場合はエラーを返します。 + +```go +package main + +import ( + "fmt" + + "github.com/monitoring-forge/flagrun" +) + +type Opt struct { + Pattern flagrun.Re `short:"p" long:"pattern" description:"Filter pattern"` +} + +func (p *Opt) Run(args []string) (string, int) { + if p.Pattern.Present() { + fmt.Printf("pattern: %s\n", p.Pattern.String()) + matched := p.Pattern.Regexp().MatchString("example") + fmt.Printf("matched: %v\n", matched) + } + return "ok\t1", flagrun.OK +} +``` + ## オプション | `flagrun.Go` / `flagrun.Check` / `flagrun.Ship` では、以下の関数を使って動作をカスタマイズできます。 diff --git a/flaghuman.go b/flaghuman.go new file mode 100644 index 0000000..bb1e145 --- /dev/null +++ b/flaghuman.go @@ -0,0 +1,16 @@ +package flagrun + +import "github.com/dustin/go-humanize" + +// Bytes is a custom type to handle human-readable byte inputs +type Bytes uint64 + +// UnmarshalFlag parses human inputs like "10MB" or "2GiB" using go-humanize +func (hb *Bytes) UnmarshalFlag(value string) error { + bytes, err := humanize.ParseBytes(value) + if err != nil { + return err + } + *hb = Bytes(bytes) + return nil +} diff --git a/flaghuman_test.go b/flaghuman_test.go new file mode 100644 index 0000000..234f709 --- /dev/null +++ b/flaghuman_test.go @@ -0,0 +1,20 @@ +package flagrun + +import "testing" + +func TestHumanBytesUnmarshalFlag(t *testing.T) { + var hb Bytes + if err := hb.UnmarshalFlag("10MB"); err != nil { + t.Fatalf("UnmarshalFlag error: %v", err) + } + if hb != 10*1000*1000 { + t.Errorf("Bytes = %d; want 10000000", hb) + } +} + +func TestHumanBytesUnmarshalFlagInvalid(t *testing.T) { + var hb Bytes + if err := hb.UnmarshalFlag("invalid"); err == nil { + t.Error("UnmarshalFlag should return error for invalid input") + } +} diff --git a/flagregexp.go b/flagregexp.go new file mode 100644 index 0000000..4cea79a --- /dev/null +++ b/flagregexp.go @@ -0,0 +1,35 @@ +package flagrun + +import "regexp" + +type Re struct { + value string + regexp *regexp.Regexp +} + +func (fr *Re) UnmarshalFlag(value string) error { + if value == "" { + fr.value = "" + fr.regexp = nil + return nil + } + re, err := regexp.Compile(value) + if err != nil { + return err + } + fr.value = value + fr.regexp = re + return nil +} + +func (fr *Re) String() string { + return fr.value +} + +func (fr *Re) Regexp() *regexp.Regexp { + return fr.regexp +} + +func (fr *Re) Present() bool { + return fr.regexp != nil +} diff --git a/flagregexp_test.go b/flagregexp_test.go new file mode 100644 index 0000000..fa18665 --- /dev/null +++ b/flagregexp_test.go @@ -0,0 +1,28 @@ +package flagrun + +import "testing" + +func TestFlagRegexp(t *testing.T) { + testCases := []struct { + input string + err bool + expected bool + }{ + {"", false, false}, + {"^example", false, true}, + {"invalid[", true, false}, + } + for _, tc := range testCases { + fr := &Re{} + err := fr.UnmarshalFlag(tc.input) + if !tc.err && err != nil { + t.Errorf("unexpected error for input %q: %v", tc.input, err) + } + if tc.err && err == nil { + t.Errorf("expected error for input %q, but got none", tc.input) + } + if fr.Present() != tc.expected { + t.Errorf("input: %q, expected Present: %v, got: %v", tc.input, tc.expected, fr.Present()) + } + } +} diff --git a/flagrun.go b/flagrun.go index c953ee5..72c9478 100644 --- a/flagrun.go +++ b/flagrun.go @@ -159,10 +159,6 @@ func buildFlagrun(options ...FlagrunOptions) *Flagrun { return f } -func nullint(i int) *int { - return &i -} - func (f *Flagrun) parseArgs(argv []string, stdout, stderr io.Writer, opt any) ([]string, *int) { psr := flags.NewParser(opt, flags.HelpFlag|flags.PassDoubleDash) if f.Usage != "" { @@ -183,23 +179,23 @@ func (f *Flagrun) parseArgs(argv []string, stdout, stderr io.Writer, opt any) ([ runtime.GOARCH, runtime.Version(), f.Commit) - return nil, nullint(OK) + return nil, new(OK) } else if flags.WroteHelp(err) { fmt.Fprintf(stdout, "%v\n", err) - return nil, nullint(OK) + return nil, new(OK) } else if err != nil { fmt.Fprintf(stderr, "%v\n", err) - return nil, nullint(UNKNOWN) + return nil, new(UNKNOWN) } else if f.ArgsRequired && len(args) == 0 { fmt.Fprintf(stderr, "command is required\n") psr.WriteHelp(stderr) - return nil, nullint(UNKNOWN) + return nil, new(UNKNOWN) } if f.Validator != nil { if err := f.Validator(args); err != nil { fmt.Fprintf(stderr, "%v\n", err) - return nil, nullint(UNKNOWN) + return nil, new(UNKNOWN) } } diff --git a/go.mod b/go.mod index 1013eed..9799c92 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( ) require ( + github.com/dustin/go-humanize v1.1.0 go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/sys v0.49.0 // indirect ) diff --git a/go.sum b/go.sum index e36cd09..9e969b4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg= +github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/mackerelio/checkers v0.2.1 h1:vRXzAT+CEyaLjSrHVxKUBjyWq1kfo8QcI8ZoSKZATQs=