Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ fmt.Println(cfg.Port) // 8080
### Usage message

The `Usage` function prints a usage message documenting all defined environment variables.
An optional usage string can be added for each environment variable via the `usage:"STRING"` struct tag:
An optional usage string can be added to environment variables using the `usage:"STRING"` struct tag:

```go
os.Unsetenv("DB_HOST")
Expand All @@ -227,7 +227,7 @@ var cfg struct {
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
fmt.Println("Usage:")
env.Usage(&cfg, os.Stdout)
env.Usage(&cfg, os.Stdout, nil)
}
```

Expand All @@ -238,12 +238,12 @@ Usage:
HTTP_PORT int default 8080 http server port
```

The format of the message can be customized by implementing the `Usage([]env.Var, io.Writer)` method:
The format of the message can be customized by implementing the `Usage([]env.Var, io.Writer, *env.Options)` method:

```go
type config struct{ ... }
type Config struct{ ... }

func (config) Usage(vars []env.Var, w io.Writer) {
func (Config) Usage(vars []env.Var, w io.Writer, opts *env.Options) {
for v := range vars {
// write to w.
}
Expand Down
2 changes: 1 addition & 1 deletion env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestLoad(t *testing.T) {
load := func() { _ = env.Load(cfg, &env.Options{Source: env.Map{}}) }
assert.Panics[E](t, load, panicMsg)

usage := func() { env.Usage(cfg, io.Discard) }
usage := func() { env.Usage(cfg, io.Discard, nil) }
assert.Panics[E](t, usage, panicMsg)
})
}
Expand Down
2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func ExampleUsage() {
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
fmt.Println("Usage:")
env.Usage(&cfg, os.Stdout)
env.Usage(&cfg, os.Stdout, nil)
}

// Output: env: DB_HOST DB_PORT are required but not set
Expand Down
27 changes: 16 additions & 11 deletions usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import (
"text/tabwriter"
)

// cache maps the struct type to the [Var] slice parsed from it.
// cache maps a struct type to the [Var] slice parsed from it.
// It is primarily needed to fix the following bug:
//
// var cfg struct {
// Port int `env:"PORT"`
// }
// env.Load(&cfg, nil) // 1. sets cfg.Port to 8080
// env.Usage(&cfg, os.Stdout) // 2. prints cfg.Port's default == 8080 (instead of 0)
// env.Load(&cfg, nil) // 1. sets cfg.Port to 8080
// env.Usage(&cfg, os.Stdout, nil) // 2. prints cfg.Port's default == 8080 (instead of 0)
//
// It also speeds up [Usage], since there is no need to parse the struct again.
var cache = make(map[reflect.Type][]Var)

// Var holds the information about an environment variable parsed from the struct field.
// Var holds the information about the environment variable parsed from a struct field.
type Var struct {
Name string // The name of the variable.
Type reflect.Type // The type of the variable.
Expand All @@ -33,9 +33,10 @@ type Var struct {
}

// Usage writes a usage message documenting all defined environment variables to the given [io.Writer].
// An optional usage string can be added for each environment variable via the `usage:"STRING"` struct tag.
// The format of the message can be customized by implementing the Usage([]env.Var, io.Writer) method on the cfg's type.
func Usage(cfg any, w io.Writer) {
// The caller must pass the same [Options] to both [Load] and [Usage], or nil.
// An optional usage string can be added to environment variables using the `usage:"STRING"` struct tag.
// The format of the message can be customized by implementing the Usage([]env.Var, io.Writer, *env.Options) method on the cfg's type.
func Usage(cfg any, w io.Writer, opts *Options) {
pv := reflect.ValueOf(cfg)
if !structPtr(pv) {
panic("env: cfg must be a non-nil struct pointer")
Expand All @@ -47,14 +48,18 @@ func Usage(cfg any, w io.Writer) {
vars = parseVars(v)
}

if u, ok := cfg.(interface{ Usage([]Var, io.Writer) }); ok {
u.Usage(vars, w)
if u, ok := cfg.(interface {
Usage([]Var, io.Writer, *Options)
}); ok {
u.Usage(vars, w, opts)
} else {
defaultUsage(vars, w)
defaultUsage(vars, w, opts)
}
}

func defaultUsage(vars []Var, w io.Writer) {
func defaultUsage(vars []Var, w io.Writer, _ *Options) {
// TODO: use opts.SliceSep to parse slice values.

tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
defer tw.Flush()

Expand Down
12 changes: 6 additions & 6 deletions usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ func TestUsage(t *testing.T) {
var cfg struct {
Foo string `env:"FOO" default:""`
}
env.Usage(&cfg, &buf)
env.Usage(&cfg, &buf, nil)
assert.Equal[E](t, buf.String(), " FOO string default <empty>\n")
})

t.Run("custom usage message", func(t *testing.T) {
var buf bytes.Buffer
var cfg config
env.Usage(&cfg, &buf)
var cfg Config
env.Usage(&cfg, &buf, nil)
assert.Equal[E](t, buf.String(), "custom")
})

Expand All @@ -38,13 +38,13 @@ func TestUsage(t *testing.T) {
assert.Equal[E](t, cfg.Foo, 1)

var buf bytes.Buffer
env.Usage(&cfg, &buf)
env.Usage(&cfg, &buf, nil)
assert.Equal[E](t, buf.String(), " FOO int default 0\n")
})
}

type config struct{}
type Config struct{}

func (config) Usage(_ []env.Var, w io.Writer) {
func (Config) Usage(_ []env.Var, w io.Writer, _ *env.Options) {
_, _ = w.Write([]byte("custom"))
}