From e29fc92d9b347c6c9156d506b36298f69d86bfd0 Mon Sep 17 00:00:00 2001 From: Spiegel Date: Tue, 26 May 2026 15:33:09 +0900 Subject: [PATCH 1/5] Update Taskfile to replace nancy task with govulncheck and regenerate dependency graph --- Taskfile.yml | 8 ++++---- dependency.png | Bin 15768 -> 16020 bytes 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index c70e045..a0b44b8 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,7 +5,7 @@ tasks: cmds: - task: prepare - task: test - # - task: nancy + - task: govulncheck - task: graph test: @@ -18,10 +18,10 @@ tasks: - ./go.mod - '**/*.go' - nancy: - desc: Check vulnerability of external packages with Nancy. + govulncheck: + desc: Check reachable vulnerabilities with latest govulncheck. cmds: - - depm list -j | nancy sleuth -n + - go run golang.org/x/vuln/cmd/govulncheck@latest ./... sources: - ./go.mod - '**/*.go' diff --git a/dependency.png b/dependency.png index 82d12ea25a134294866faa74500b0f32b6f74f3a..0bcec86e9d9b5f87faaef4789a4d68ba4d03a97c 100644 GIT binary patch literal 16020 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 literal 15768 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 From b4d6379345ceef018a3a612c665b8362ca683786 Mon Sep 17 00:00:00 2001 From: Spiegel Date: Tue, 26 May 2026 16:27:22 +0900 Subject: [PATCH 2/5] chore: align docs and workflows --- .github/copilot-instructions.md | 26 +++++++++++++++++ .github/workflows/ci.yml | 44 +++++++++++++++++++++++++++++ .github/workflows/codeql.yml | 35 +++++++++++++++++++++++ .github/workflows/lint.yml | 50 --------------------------------- README.md | 22 +++++++++++++-- examples/example3.go | 4 ++- 6 files changed, 128 insertions(+), 53 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/lint.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..74b5497 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,26 @@ +# Copilot Instructions + +Project purpose +- Provide a small utility to bind struct fields to pflag flags for CLI programs. + +Design principles +- Small, dependency-minimal, and well-tested. +- Keep public API stable and backward-compatible. + +Error handling policy +- Use sentinel errors where appropriate and ensure `errors.Is` works. +- Guard nil-sensitive inputs and return clear errors. + +Testing / validation +- Local validation command: `task test` +- Prefer deterministic tests using `httptest` or mocked `http.RoundTripper`. + +Release process +- Tag from `main`/`master` with semver `vMAJOR.MINOR.PATCH`. +- Annotated tags: `git tag -a vX.Y.Z -m "Release vX.Y.Z"`. +- Push tag and create GitHub release with `gh release create`. + +Operational pattern +1. Create focused branch per topic. +2. Make small, scoped changes and run `task test`. +3. Push branch, open PR, and merge after CI passes. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dc0d6f1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: ci + +on: + push: + branches: + - master + pull_request: + +permissions: + contents: read + +jobs: + test-and-lint: + name: lint and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: latest + args: --enable gosec + + - name: Test module + run: go test -shuffle on ./... + + govulncheck: + name: govulncheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Run govulncheck + uses: golang/govulncheck-action@v1 + with: + go-version-file: go.mod + go-package: ./... + repo-checkout: false diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..28b32f6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +name: CodeQL + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + - cron: "0 20 * * 0" + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: go + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 26b63ce..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: lint -on: - push: - branches: - - master - pull_request: - -permissions: - contents: read - # Optional: allow read access to pull request. Use with `only-new-issues` option. - # pull-requests: read -jobs: - golangci: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-go@v6 - with: - go-version-file: 'go.mod' - - name: golangci-lint - uses: golangci/golangci-lint-action@v9 - with: - # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: latest - - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. - args: --enable gosec - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true - - # Optional: if set to true then the all caching functionality will be complete disabled, - # takes precedence over all other caching options. - # skip-cache: true - - # Optional: if set to true then the action don't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - # skip-build-cache: true - - name: testing - run: go test -shuffle on ./... - - name: install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest - - name: running govulncheck - run: govulncheck ./... diff --git a/README.md b/README.md index b4b51b0..709479a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,30 @@ # [struct2pflag] -[![lint status](https://github.com/goark/struct2pflag/workflows/lint/badge.svg)](https://github.com/goark/struct2pflag/actions) +[![ci status](https://github.com/goark/struct2pflag/actions/workflows/ci.yml/badge.svg)](https://github.com/goark/struct2pflag/actions/workflows/ci.yml) +[![codeql status](https://github.com/goark/struct2pflag/actions/workflows/codeql.yml/badge.svg)](https://github.com/goark/struct2pflag/actions/workflows/codeql.yml) [![GitHub license](http://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/goark/struct2pflag/master/LICENSE) -[![GitHub release](http://img.shields.io/github/release/goark/struct2pflag.svg)](https://github.com/goark/mt/releases/latest) +[![GitHub release](http://img.shields.io/github/release/goark/struct2pflag.svg)](https://github.com/goark/struct2pflag/releases/latest) [![Go reference](https://pkg.go.dev/badge/github.com/goark/struct2pflag.svg)](https://pkg.go.dev/github.com/goark/struct2pflag) +Design goals + +- Provide a minimal, dependency-light helper to bind struct fields to pflag flags. +- Maintain backwards compatibility for public API and use clear sentinel errors. + +Development + +- Local validation: `task test` (preferred). Include `gofmt`, `go vet`, and `go test` in CI. +- Keep changes small and topic-focused; open one PR per change. + +CI Workflows + +- Keep two primary workflows in `.github/workflows/`: `ci.yml` and `codeql.yml`. +- Update README badges if workflow names change. + +Usage + [`struct2pflag`][struct2pflag] automatically registers struct fields as flags for your Go command-line programs. (forked from [hymkor/struct2flag](https://github.com/hymkor/struct2flag)) diff --git a/examples/example3.go b/examples/example3.go index 390ba67..1672be5 100644 --- a/examples/example3.go +++ b/examples/example3.go @@ -28,7 +28,9 @@ func main() { var env Env if data, err := os.ReadFile("example3.json"); err == nil { - _ = json.Unmarshal(data, &env) + if err := json.Unmarshal(data, &env); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to parse example3.json: %v\n", err) + } } struct2pflag.BindDefault(&env) pflag.Parse() From d397f0e8bbf90ba447531f1a9ed66150702ebdaa Mon Sep 17 00:00:00 2001 From: Spiegel Date: Tue, 26 May 2026 16:34:03 +0900 Subject: [PATCH 3/5] feat: add environment variable binding API --- README.md | 26 ++++++++ env.go | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++ env_test.go | 156 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 env.go create mode 100644 env_test.go diff --git a/README.md b/README.md index 709479a..0c98e59 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,32 @@ N=1 S="foo" ``` +Environment variable binding +---------------------------- + +`struct2pflag.BindEnv` can apply environment variables to your config struct before CLI parsing. + +```go +type Env struct { + TopN int `env:"TOP_N" pflag:"top-n,n,number of top tags"` + Wait time.Duration `env:"WAIT" pflag:"wait,w,wait duration"` +} + +func main() { + env := Env{TopN: 10, Wait: time.Second} + + _ = struct2pflag.BindEnv(&env) + struct2pflag.BindDefault(&env) + pflag.Parse() +} +``` + +Recommended precedence is: + +1. CLI flags +2. Environment variables +3. Struct default values + Reading default values from JSON and overriding them with command-line flags ---------------------------------------------------------------------------- diff --git a/env.go b/env.go new file mode 100644 index 0000000..afeae88 --- /dev/null +++ b/env.go @@ -0,0 +1,189 @@ +package struct2pflag + +import ( + "errors" + "fmt" + "os" + "reflect" + "strconv" + "time" +) + +var ( + // ErrInvalidEnvTarget indicates BindEnv received a non-struct pointer target. + ErrInvalidEnvTarget = errors.New("invalid env bind target") + // ErrEnvParse indicates a conversion error while applying an environment variable. + ErrEnvParse = errors.New("failed to parse environment variable") + // ErrUnsupportedEnvType indicates the target field type is not supported by BindEnv. + ErrUnsupportedEnvType = errors.New("unsupported env field type") +) + +// EnvOpt configures BindEnv behavior. +type EnvOpt func(*envOptions) + +type envOptions struct { + prefix string + tag string + strict bool +} + +var durationType = reflect.TypeOf(time.Duration(0)) + +func defaultEnvOptions() envOptions { + return envOptions{tag: "env"} +} + +// WithEnvPrefix adds a prefix to environment variable names, such as APP_. +func WithEnvPrefix(prefix string) EnvOpt { + return func(o *envOptions) { + o.prefix = prefix + } +} + +// WithEnvTag changes the struct tag key used to resolve environment names. +func WithEnvTag(tag string) EnvOpt { + return func(o *envOptions) { + if tag != "" { + o.tag = tag + } + } +} + +// WithStrict enables errors for unsupported tagged field types. +func WithStrict(strict bool) EnvOpt { + return func(o *envOptions) { + o.strict = strict + } +} + +// BindEnv applies environment variables to exported fields of a struct pointer. +// +// By default, BindEnv reads the env tag (for example, env:"TOP_N"). If the +// environment variable exists, it is converted and assigned to the field. +// Unset environment variables are ignored. +func BindEnv(v any, opts ...EnvOpt) error { + if v == nil { + return fmt.Errorf("%w: expected pointer to struct", ErrInvalidEnvTarget) + } + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() || rv.Elem().Kind() != reflect.Struct { + return fmt.Errorf("%w: expected pointer to struct", ErrInvalidEnvTarget) + } + + config := defaultEnvOptions() + for _, opt := range opts { + if opt != nil { + opt(&config) + } + } + + return bindEnvStruct(rv.Elem(), config) +} + +func bindEnvStruct(v reflect.Value, opts envOptions) error { + t := v.Type() + for i := range v.NumField() { + f := v.Field(i) + field := t.Field(i) + if !field.IsExported() { + continue + } + + switch f.Kind() { + case reflect.Struct: + if err := bindEnvStruct(f, opts); err != nil { + return err + } + case reflect.Pointer: + if f.Type().Elem().Kind() == reflect.Struct && !f.IsNil() { + if err := bindEnvStruct(f.Elem(), opts); err != nil { + return err + } + } + } + + envName, ok := field.Tag.Lookup(opts.tag) + if !ok || envName == "" { + continue + } + envName = opts.prefix + envName + + raw, found := os.LookupEnv(envName) + if !found { + continue + } + if err := assignFromEnv(f, field.Name, envName, raw, opts.strict); err != nil { + return err + } + } + return nil +} + +func assignFromEnv(f reflect.Value, fieldName, envName, raw string, strict bool) error { + if !f.CanSet() { + if strict { + return fmt.Errorf("%w: env %q for field %q cannot be assigned", ErrUnsupportedEnvType, envName, fieldName) + } + return nil + } + + if f.Type() == durationType { + d, err := time.ParseDuration(raw) + if err != nil { + return parseError(envName, fieldName, "time.Duration", raw, err) + } + f.SetInt(int64(d)) + return nil + } + + switch f.Kind() { + case reflect.String: + f.SetString(raw) + return nil + case reflect.Bool: + value, err := strconv.ParseBool(raw) + if err != nil { + return parseError(envName, fieldName, "bool", raw, err) + } + f.SetBool(value) + return nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + value, err := strconv.ParseInt(raw, 10, f.Type().Bits()) + if err != nil { + return parseError(envName, fieldName, f.Type().String(), raw, err) + } + f.SetInt(value) + return nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + value, err := strconv.ParseUint(raw, 10, f.Type().Bits()) + if err != nil { + return parseError(envName, fieldName, f.Type().String(), raw, err) + } + f.SetUint(value) + return nil + case reflect.Float32, reflect.Float64: + value, err := strconv.ParseFloat(raw, f.Type().Bits()) + if err != nil { + return parseError(envName, fieldName, f.Type().String(), raw, err) + } + f.SetFloat(value) + return nil + default: + if strict { + return fmt.Errorf("%w: env %q for field %q has unsupported type %s", ErrUnsupportedEnvType, envName, fieldName, f.Type()) + } + return nil + } +} + +func parseError(envName, fieldName, expected, raw string, err error) error { + return fmt.Errorf( + "%w: env %q for field %q expects %s (value=%q): %v", + ErrEnvParse, + envName, + fieldName, + expected, + raw, + err, + ) +} diff --git a/env_test.go b/env_test.go new file mode 100644 index 0000000..d44b9b8 --- /dev/null +++ b/env_test.go @@ -0,0 +1,156 @@ +package struct2pflag_test + +import ( + "errors" + "testing" + "time" + + "github.com/goark/struct2pflag" +) + +type envNested struct { + Name string `env:"APP_NAME"` +} + +type envConfig struct { + Enabled bool `env:"ENABLED"` + Count int `env:"COUNT"` + Rate float64 `env:"RATE"` + Wait time.Duration `env:"WAIT"` + Label string `env:"LABEL"` + Nested envNested +} + +func TestBindEnvBasic(t *testing.T) { + t.Setenv("ENABLED", "true") + t.Setenv("COUNT", "12") + t.Setenv("RATE", "1.25") + t.Setenv("WAIT", "3s") + t.Setenv("LABEL", "alpha") + t.Setenv("APP_NAME", "demo") + + cfg := envConfig{} + if err := struct2pflag.BindEnv(&cfg); err != nil { + t.Fatalf("BindEnv() error = %v", err) + } + + if cfg.Enabled != true { + t.Fatalf("Enabled = %v, want true", cfg.Enabled) + } + if cfg.Count != 12 { + t.Fatalf("Count = %v, want 12", cfg.Count) + } + if cfg.Rate != 1.25 { + t.Fatalf("Rate = %v, want 1.25", cfg.Rate) + } + if cfg.Wait != 3*time.Second { + t.Fatalf("Wait = %v, want 3s", cfg.Wait) + } + if cfg.Label != "alpha" { + t.Fatalf("Label = %q, want alpha", cfg.Label) + } + if cfg.Nested.Name != "demo" { + t.Fatalf("Nested.Name = %q, want demo", cfg.Nested.Name) + } +} + +func TestBindEnvUnsetPreservesDefaults(t *testing.T) { + cfg := envConfig{ + Enabled: true, + Count: 5, + Rate: 2.0, + Wait: time.Minute, + Label: "default", + } + if err := struct2pflag.BindEnv(&cfg); err != nil { + t.Fatalf("BindEnv() error = %v", err) + } + if cfg.Enabled != true || cfg.Count != 5 || cfg.Rate != 2.0 || cfg.Wait != time.Minute || cfg.Label != "default" { + t.Fatalf("defaults were unexpectedly changed: %#v", cfg) + } +} + +func TestBindEnvOptions(t *testing.T) { + t.Setenv("APP_PORT", "8080") + t.Setenv("APP_DEBUG", "true") + type cfg struct { + Port int `conf:"PORT"` + Debug bool `conf:"DEBUG"` + } + + c := cfg{} + err := struct2pflag.BindEnv(&c, + struct2pflag.WithEnvTag("conf"), + struct2pflag.WithEnvPrefix("APP_"), + ) + if err != nil { + t.Fatalf("BindEnv() error = %v", err) + } + if c.Port != 8080 || c.Debug != true { + t.Fatalf("BindEnv() applied unexpected values: %#v", c) + } +} + +func TestBindEnvParseErrors(t *testing.T) { + type cfg struct { + N int `env:"N"` + B bool `env:"B"` + D time.Duration `env:"D"` + } + + t.Run("int", func(t *testing.T) { + t.Setenv("N", "oops") + c := cfg{} + err := struct2pflag.BindEnv(&c) + if err == nil || !errors.Is(err, struct2pflag.ErrEnvParse) { + t.Fatalf("BindEnv() error = %v, want ErrEnvParse", err) + } + }) + + t.Run("bool", func(t *testing.T) { + t.Setenv("B", "oops") + c := cfg{} + err := struct2pflag.BindEnv(&c) + if err == nil || !errors.Is(err, struct2pflag.ErrEnvParse) { + t.Fatalf("BindEnv() error = %v, want ErrEnvParse", err) + } + }) + + t.Run("duration", func(t *testing.T) { + t.Setenv("D", "oops") + c := cfg{} + err := struct2pflag.BindEnv(&c) + if err == nil || !errors.Is(err, struct2pflag.ErrEnvParse) { + t.Fatalf("BindEnv() error = %v, want ErrEnvParse", err) + } + }) +} + +func TestBindEnvInvalidTarget(t *testing.T) { + type cfg struct { + Value int `env:"VALUE"` + } + var c cfg + err := struct2pflag.BindEnv(c) + if err == nil || !errors.Is(err, struct2pflag.ErrInvalidEnvTarget) { + t.Fatalf("BindEnv() error = %v, want ErrInvalidEnvTarget", err) + } +} + +func TestBindEnvStrictUnsupportedType(t *testing.T) { + t.Setenv("ITEMS", "a,b,c") + type cfg struct { + Items []string `env:"ITEMS"` + } + + c1 := cfg{} + if err := struct2pflag.BindEnv(&c1); err != nil { + t.Fatalf("BindEnv() strict=false error = %v", err) + } + + c2 := cfg{} + err := struct2pflag.BindEnv(&c2, struct2pflag.WithStrict(true)) + if err == nil || !errors.Is(err, struct2pflag.ErrUnsupportedEnvType) { + t.Fatalf("BindEnv() strict=true error = %v, want ErrUnsupportedEnvType", err) + } +} From f58585244336c79cbb7622326da1e8166c7b06b5 Mon Sep 17 00:00:00 2001 From: Spiegel Date: Tue, 26 May 2026 16:52:30 +0900 Subject: [PATCH 4/5] feat: extend Bind type support and translate env spec --- docs/struct2pflag-env-bind-spec.md | 139 +++++++++++++++++++++++++++++ flag_test.go | 29 ++++++ main.go | 12 ++- pflag_test.go | 43 +++++++++ 4 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 docs/struct2pflag-env-bind-spec.md diff --git a/docs/struct2pflag-env-bind-spec.md b/docs/struct2pflag-env-bind-spec.md new file mode 100644 index 0000000..79937dc --- /dev/null +++ b/docs/struct2pflag-env-bind-spec.md @@ -0,0 +1,139 @@ +# struct2pflag: Additional Env Binding Specification + +## Background + +- We want to support environment variable-based configuration such as `TOP_N` in `tagtools`. +- Currently, `github.com/goark/struct2pflag` binds struct fields to CLI flags. +- To enable reuse across multiple tools, environment variable binding should also be implemented in `struct2pflag`. + +## Goals + +- Add support in `struct2pflag` for binding environment variables to struct fields. +- Standardize conversion rules and unify the behavior among flags, environment variables, and struct defaults. +- Include `time.Duration` conversion in the initial implementation. + +## Scope + +- Add functionality to `struct2pflag` itself: + - API to apply environment variables to struct fields + - Conversion error handling + - Implementation of supported types + - Unit tests +- Integrating this in `tagtools` will be handled in a separate PR/session. + +## Out of Scope + +- Full application on the `tagtools` side (not implemented in this session) +- Breaking compatibility of the existing `Bind` API +- Expanding to complex nested struct rules or custom tag specifications beyond the minimum needed + +## Precedence Rules (Consumer Guide) + +Recommended precedence: + +1. CLI flags +2. Environment variables +3. Struct default values + +Note: env binding in `struct2pflag` is responsible only for applying values to the struct. Final precedence is controlled by call order. +Example: `default -> env bind -> flag parse` + +## Proposed API + +### Minimal API + +- `func BindEnv(v any, opts ...EnvOpt) error` + +`v` must be a pointer to a struct. + +### Option Proposals + +- `WithEnvPrefix(prefix string)` + - Example: prepend `APP_` +- `WithEnvTag(tag string)` + - Default tag key: `env` +- `WithStrict(bool)` + - When `strict=true`, unsupported types and invalid values become errors + +## Tag Specification Proposal + +- Explicit declaration such as `env:"TOP_N"` +- When no tag is specified, either: + - A: Ignore the field (safe) + - B: Auto-generate from the field name (`TopN` -> `TOP_N`) + +Start with A (explicit tags) as the recommended policy. + +## Supported Types (Initial) + +- `string` +- `bool` +- `int`, `int8`, `int16`, `int32`, `int64` +- `uint`, `uint8`, `uint16`, `uint32`, `uint64` +- `float32`, `float64` +- `time.Duration` + +### Duration Conversion + +- Use `time.ParseDuration` +- Examples: `100ms`, `5s`, `3m`, `1h30m` + +## Error Handling + +- Environment variable is unset: skip (no error) +- Environment variable is set but conversion fails: return error +- Error messages should include at least: + - Environment variable name + - Field name + - Expected type + - Original raw value + +## Implementation Strategy (Recommended) + +- Iterate struct fields with reflection +- Process only settable/exported fields +- Organize per-type conversion handlers in an internal table-like structure +- Keep room for future converter-registration API extensions + +## Test Cases (struct2pflag side) + +### Success Cases + +- Correctly set values for each supported type +- Preserve defaults when env variables are unset +- Correct duration conversion behavior + +### Failure Cases + +- Error when assigning non-numeric string to int +- Error for invalid bool value +- Error for invalid duration value +- Error when input is not a struct pointer + +### Boundary Cases + +- Behavior for empty-string env values (documented per type) +- Behavior differences with and without strict option + +## tagtools Integration Image (Next Session) + +- Target: `TopN` in `toptags` +- Example: + - `TopN int `env:"TOP_N" pflag:"top-n,n,number of top tags"`` + - `default -> BindEnv(&cfg) -> struct2pflag.Bind(fs, &cfg) -> fs.Parse(args)` +- Expected behavior: + - `TOP_N` overrides defaults when set + - `--top-n` takes precedence over `TOP_N` when provided + +## Compatibility Considerations + +- No impact on existing `Bind` call sites +- New API is additive only +- Existing users see no behavior change unless env binding is explicitly used + +## Definition of Done + +- Implement env binding API and duration conversion in `struct2pflag` +- Unit tests pass +- Add usage notes to README/package docs +- Verify PoC integration (`TOP_N`) in `tagtools` with a separate PR diff --git a/flag_test.go b/flag_test.go index f650fb0..1bfd588 100644 --- a/flag_test.go +++ b/flag_test.go @@ -3,6 +3,7 @@ package struct2pflag_test import ( "os" "testing" + "time" "github.com/goark/struct2pflag" "github.com/spf13/pflag" @@ -20,6 +21,12 @@ type ts struct { B1 bool `flag:"boolean option(1)"` B2 bool `flag:"B2,boolean option(2)"` B3 bool `flag:"B3,boolean option(3)"` + + F1 float32 `flag:"float option(1)"` + F2 float64 `flag:"F2,float option(2)"` + + D1 time.Duration `flag:"duration option(1)"` + D2 time.Duration `flag:"D2,duration option(2)"` } func TestBind(t *testing.T) { @@ -34,6 +41,10 @@ func TestBind(t *testing.T) { "--I2", "8", "--b1", "--B2", + "--f1", "1.5", + "--F2", "2.5", + "--d1", "3s", + "--D2", "4s", }, ) @@ -71,6 +82,20 @@ func TestBind(t *testing.T) { t.Fatalf("expect %#v,but %#v", ts1.B3, expect) } + if expect := float32(1.5); ts1.F1 != expect { + t.Fatalf("expect %#v,but %#v", ts1.F1, expect) + } + if expect := 2.5; ts1.F2 != expect { + t.Fatalf("expect %#v,but %#v", ts1.F2, expect) + } + + if expect := 3 * time.Second; ts1.D1 != expect { + t.Fatalf("expect %#v,but %#v", ts1.D1, expect) + } + if expect := 4 * time.Second; ts1.D2 != expect { + t.Fatalf("expect %#v,but %#v", ts1.D2, expect) + } + ts1 = &ts{} flagSet = pflag.NewFlagSet("", pflag.ContinueOnError) struct2pflag.Bind(flagSet, ts1) @@ -81,6 +106,10 @@ func TestBind(t *testing.T) { {"--i2", "should be upper case"}, {"--B1"}, {"--b2"}, + {"--F1", "should be lower case"}, + {"--f2", "should be upper case"}, + {"--D1", "should be lower case"}, + {"--d2", "should be upper case"}, } stderrSaved := os.Stderr devnull, err := os.Create(os.DevNull) diff --git a/main.go b/main.go index 22d73b8..356da5f 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package struct2pflag import ( "reflect" "strings" + "time" "github.com/spf13/pflag" ) @@ -36,7 +37,7 @@ const ( // pointer is non-nil (nil pointers are skipped). // // Supported field kinds: -// - bool, int, uint, string +// - bool, int, uint, float32, float64, string, time.Duration // // For each supported field Bind registers the corresponding fs.*VarP binding using the field's // address and the field's current value as the flag default. @@ -95,6 +96,11 @@ func Bind(fs *pflag.FlagSet, cfg interface{}) { } } + if f.Type() == durationType { + fs.DurationVarP(f.Addr().Interface().(*time.Duration), longname, shortname, time.Duration(f.Int()), usage) + continue + } + switch f.Kind() { case reflect.Bool: fs.BoolVarP(f.Addr().Interface().(*bool), longname, shortname, f.Bool(), usage) @@ -102,6 +108,10 @@ func Bind(fs *pflag.FlagSet, cfg interface{}) { fs.IntVarP(f.Addr().Interface().(*int), longname, shortname, int(f.Int()), usage) case reflect.Uint: fs.UintVarP(f.Addr().Interface().(*uint), longname, shortname, uint(f.Uint()), usage) + case reflect.Float32: + fs.Float32VarP(f.Addr().Interface().(*float32), longname, shortname, float32(f.Float()), usage) + case reflect.Float64: + fs.Float64VarP(f.Addr().Interface().(*float64), longname, shortname, f.Float(), usage) case reflect.String: fs.StringVarP(f.Addr().Interface().(*string), longname, shortname, f.String(), usage) } diff --git a/pflag_test.go b/pflag_test.go index 4fe9b02..46c169d 100644 --- a/pflag_test.go +++ b/pflag_test.go @@ -3,6 +3,7 @@ package struct2pflag_test import ( "os" "testing" + "time" "github.com/goark/struct2pflag" "github.com/spf13/pflag" @@ -22,6 +23,12 @@ type tsPflag struct { B1 bool `pflag:"boolean option(1)"` // name omitted B2 bool `pflag:"B2,boolean option(2)"` // long name only B3 bool `pflag:"B3,b,boolean option(3)"` // long and short names + + F1 float32 `pflag:"F1,f,float option(1)"` // long and short names + F2 float64 `pflag:"F2,float option(2)"` // long name only + + D1 time.Duration `pflag:"D1,d,duration option(1)"` // long and short names + D2 time.Duration `pflag:"D2,duration option(2)"` // long name only } func TestPflagBindLongname(t *testing.T) { @@ -36,6 +43,10 @@ func TestPflagBindLongname(t *testing.T) { "--UI2", "8", "--b1", "--B2", + "--F1", "1.25", + "--F2", "2.5", + "--D1", "1500ms", + "--D2", "2s", }, ) if err != nil { @@ -74,6 +85,18 @@ func TestPflagBindLongname(t *testing.T) { if expect := false; ts1.B3 != expect { t.Errorf("expect %#v,but %#v", ts1.B3, expect) } + if expect := float32(1.25); ts1.F1 != expect { + t.Errorf("expect %#v,but %#v", ts1.F1, expect) + } + if expect := 2.5; ts1.F2 != expect { + t.Errorf("expect %#v,but %#v", ts1.F2, expect) + } + if expect := 1500 * time.Millisecond; ts1.D1 != expect { + t.Errorf("expect %#v,but %#v", ts1.D1, expect) + } + if expect := 2 * time.Second; ts1.D2 != expect { + t.Errorf("expect %#v,but %#v", ts1.D2, expect) + } } func TestPflagBindShortname(t *testing.T) { @@ -85,6 +108,8 @@ func TestPflagBindShortname(t *testing.T) { "-s", "foo", "-i", "9", "-b", + "-f", "1.5", + "-d", "750ms", }, ) if err != nil { @@ -123,6 +148,18 @@ func TestPflagBindShortname(t *testing.T) { if expect := true; ts2.B3 != expect { t.Errorf("expect %#v,but %#v", ts2.B3, expect) } + if expect := float32(1.5); ts2.F1 != expect { + t.Errorf("expect %#v,but %#v", ts2.F1, expect) + } + if expect := 0.0; ts2.F2 != expect { + t.Errorf("expect %#v,but %#v", ts2.F2, expect) + } + if expect := 750 * time.Millisecond; ts2.D1 != expect { + t.Errorf("expect %#v,but %#v", ts2.D1, expect) + } + if expect := time.Duration(0); ts2.D2 != expect { + t.Errorf("expect %#v,but %#v", ts2.D2, expect) + } } func TestPflagBindErr(t *testing.T) { @@ -141,6 +178,12 @@ func TestPflagBindErr(t *testing.T) { {"--B1"}, {"--b2"}, {"-B"}, + {"--f1", "should be upper case"}, + {"--f2", "should be upper case"}, + {"--d1", "should be upper case"}, + {"--d2", "should be upper case"}, + {"-F", "should be lower case"}, + {"-D", "should be lower case"}, } stderrSaved := os.Stderr devnull, err := os.Create(os.DevNull) From 35967faddc758b28fc5511eb7553c9697b90d207 Mon Sep 17 00:00:00 2001 From: Spiegel Date: Tue, 26 May 2026 17:00:21 +0900 Subject: [PATCH 5/5] docs: clarify duration handling and update README --- README.md | 2 ++ env.go | 2 ++ main.go | 2 ++ 3 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 0c98e59..387c0c3 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Usage [`struct2pflag`][struct2pflag] automatically registers struct fields as flags for your Go command-line programs. +Supported types in `Bind` include `bool`, `int`, `uint`, `float32`, `float64`, `string`, and `time.Duration`. + (forked from [hymkor/struct2flag](https://github.com/hymkor/struct2flag)) Minimal example diff --git a/env.go b/env.go index afeae88..69ba0e7 100644 --- a/env.go +++ b/env.go @@ -127,6 +127,8 @@ func assignFromEnv(f reflect.Value, fieldName, envName, raw string, strict bool) return nil } + // time.Duration has underlying kind int64, so we must detect it by exact type + // before the generic integer conversion path. if f.Type() == durationType { d, err := time.ParseDuration(raw) if err != nil { diff --git a/main.go b/main.go index 356da5f..a15d038 100644 --- a/main.go +++ b/main.go @@ -96,6 +96,8 @@ func Bind(fs *pflag.FlagSet, cfg interface{}) { } } + // time.Duration has underlying kind int64, so we must detect it by exact type + // and bind with DurationVarP before the generic integer path. if f.Type() == durationType { fs.DurationVarP(f.Addr().Interface().(*time.Duration), longname, shortname, time.Duration(f.Int()), usage) continue