From fac61148206aad772920750da240aa9c890911ee Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 7 Aug 2024 09:46:29 -0300 Subject: [PATCH] docs: cleanup readme (#322) * docs: cleanup readme Signed-off-by: Carlos Alexandro Becker * docs: more improvements --------- Signed-off-by: Carlos Alexandro Becker --- README.md | 715 ++++++------------------------------------------ env.go | 29 +- example_test.go | 2 +- 3 files changed, 104 insertions(+), 642 deletions(-) diff --git a/README.md b/README.md index a3f27b3..7a25749 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,26 @@

A simple, zero-dependencies library to parse environment variables into structs.

+A simple and zero-dependencies library to parse environment variables into `struct`s. + +###### Getting started + +```go +type config struct { + Home string `env:"HOME"` +} + +// parse +var cfg config +err := env.Parse(&cfg) + +// parse with generics +cfg, err := env.ParseAs[config]() +``` + +You can see the full documentation and list of examples at +[pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11). + --- ## Used and supported by @@ -17,88 +37,49 @@

-## Example - -Get the module with: - -```sh -go get github.com/caarlos0/env/v11 -``` - -The usage looks like this: - -```go -package main +## Usage -import ( - "fmt" - "time" - - "github.com/caarlos0/env/v11" -) - -type config struct { - Home string `env:"HOME"` - Port int `env:"PORT" envDefault:"3000"` - Password string `env:"PASSWORD,unset"` - IsProduction bool `env:"PRODUCTION"` - Duration time.Duration `env:"DURATION"` - Hosts []string `env:"HOSTS" envSeparator:":"` - TempFolder string `env:"TEMP_FOLDER,expand" envDefault:"${HOME}/tmp"` - StringInts map[string]int `env:"MAP_STRING_INT"` -} - -func main() { - cfg := config{} - if err := env.Parse(&cfg); err != nil { - fmt.Printf("%+v\n", err) - } - - // or you can use generics - cfg, err := env.ParseAs[config]() - if err != nil { - fmt.Printf("%+v\n", err) - } - - fmt.Printf("%+v\n", cfg) -} -``` - -You can run it like this: - -```sh -$ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s MAP_STRING_INT=k1:1,k2:2 go run main.go -{Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s StringInts:map[k1:1 k2:2]} -``` - -## Caveats +### Caveats > [!CAUTION] > > _Unexported fields_ will be **ignored** by `env`. > This is by design and will not change. -## Supported types and defaults +### Methods + +- `Parse`: parse the current environment into a type +- `ParseAs`: parse the current environment into a type using generics +- `ParseWithOptions`: parse the current environment into a type with custom + options +- `ParseAsithOptions`: parse the current environment into a type with custom + options and using generics +- `Must`: can be used to wrap `Parse.*` calls to panic on error +- `GetFieldParams`: get the `env` parsed options for a type +- `GetFieldParamsWithOptions`: get the `env` parsed options for a type with + custom options + +### Supported types and defaults Out of the box all built-in types are supported, plus a few others that are commonly used. Complete list: -- `string` - `bool` -- `int` -- `int8` +- `float32` +- `float64` - `int16` - `int32` - `int64` -- `uint` -- `uint8` +- `int8` +- `int` +- `string` - `uint16` - `uint32` - `uint64` -- `float32` -- `float64` +- `uint8` +- `uint` - `time.Duration` - `encoding.TextUnmarshaler` - `url.URL` @@ -106,589 +87,53 @@ Complete list: Pointers, slices and slices of pointers, and maps of those types are also supported. -You can also use/define a [custom parser func](#custom-parser-funcs) for any -other type you want. - -You can also use custom keys and values in your maps, as long as you provide a -parser function for them. - -If you set the `envDefault` tag for something, this value will be used in the -case of absence of it in the environment. - -By default, slice types will split the environment value on `,`; you can change -this behavior by setting the `envSeparator` tag. For map types, the default -separator between key and value is `:` and `,` for key-value pairs. -The behavior can be changed by setting the `envKeyValSeparator` and -`envSeparator` tags accordingly. - -## Custom Parser Funcs - -If you have a type that is not supported out of the box by the lib, you are able -to use (or define) and pass custom parsers (and their associated `reflect.Type`) -to the `env.ParseWithOptions()` function. - -In addition to accepting a struct pointer (same as `Parse()`), this function -also accepts a `Options{}`, and you can set your custom parsers in the `FuncMap` -field. - -If you add a custom parser for, say `Foo`, it will also be used to parse -`*Foo` and `[]Foo` types. - -Check the examples in the [go doc](http://pkg.go.dev/github.com/caarlos0/env/v11) -for more info. - -### A note about `TextUnmarshaler` and `time.Time` - -Env supports by default anything that implements the `TextUnmarshaler` interface. -That includes things like `time.Time` for example. -The upside is that depending on the format you need, you don't need to change -anything. -The downside is that if you do need time in another format, you'll need to -create your own type. - -Its fairly straightforward: - -```go -type MyTime time.Time - -func (t *MyTime) UnmarshalText(text []byte) error { - tt, err := time.Parse("2006-01-02", string(text)) - *t = MyTime(tt) - return err -} - -type Config struct { - SomeTime MyTime `env:"SOME_TIME"` -} -``` - -And then you can parse `Config` with `env.Parse`. - -## Required fields +You may also add custom parsers for your types. -The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added to -ensure that some environment variable is set. In the example above, an error is -returned if the `config` struct is changed to: - -```go -type config struct { - SecretKey string `env:"SECRET_KEY,required"` -} -``` +### Tags -> [!NOTE] -> -> Note that being set is not the same as being empty. -> If the variable is set, but empty, the field will have its type's default -> value. -> This also means that custom parser funcs will not be invoked. +The following tags are provided: -## Expand vars +- `env`: sets the environment variable name and optionally takes the tag options + described below +- `envDefault`: sets the default value for the field +- `envPrefix`: can be used in a field that is a complex type to set a prefix to + all environment variables used in it +- `envSeparator`: sets the character to be used to separate items in slices and + maps (default: `,`) +- `envKeyValSeparator`: sets the character to be used to separate keys and their + values in maps (default: `:`) -If you set the `expand` option, environment variables (either in `${var}` or -`$var` format) in the string will be replaced according with the actual value -of the variable. For example: +### `env` tag options -```go -type config struct { - SecretKey string `env:"SECRET_KEY,expand"` -} -``` +Here are all the options available for the `env` tag: -This also works with `envDefault`: +- `,expand`: expands environment variables, e.g. `FOO_${BAR}` +- `,file`: instructs that the content of the variable is a path to a file that should be read +- `,init`: initialize nil pointers +- `,notEmpty`: make the field errors if the environment variable is empty +- `,required`: make the field errors if the environment variable is not set +- `,unset`: unset the environment variable after use -```go -import ( - "fmt" - "github.com/caarlos0/env/v11" -) +### Parse Options -type config struct { - Host string `env:"HOST" envDefault:"localhost"` - Port int `env:"PORT" envDefault:"3000"` - Address string `env:"ADDRESS,expand" envDefault:"$HOST:${PORT}"` -} - -func main() { - cfg := config{} - if err := env.Parse(&cfg); err != nil { - fmt.Printf("%+v\n", err) - } - fmt.Printf("%+v\n", cfg) -} -``` - -results in this: - -```sh -$ PORT=8080 go run main.go -{Host:localhost Port:8080 Address:localhost:8080} -``` - -## Init `nil` pointers - -You can automatically initialize `nil` pointers regardless of if a variable is -set for them or not. -This behavior can be enabled by using the `init` tag option. - -Example: - -```go -type config struct { - URL *url.URL `env:"URL,init"` -} -``` +There are a few options available in the functions that end with `WithOptions`: -## Not Empty fields +- `Environment`: keys and values to be used instead of `os.Environ()` +- `TagName`: specifies another tag name to use rather than the default `env` +- `RequiredIfNoDef`: set all `env` fields as required if they do not declare + `envDefault` +- `OnSet`: allows to hook into the `env` parsing and do something when a value + is set +- `Prefix`: prefix to be used in all environment variables +- `UseFieldNameByDefault`: defines whether or not `env` should use the field name by default if the `env` key is missing +- `FuncMap`: custom parse functions for custom types -While `required` demands the environment variable to be set, it doesn't check -its value. If you want to make sure the environment is set and not empty, you -need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`). - -Example: - -```go -type config struct { - SecretKey string `env:"SECRET_KEY,notEmpty"` -} -``` - -## Unset environment variable after reading it - -The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added -to ensure that some environment variable is unset after reading it. - -Example: - -```go -type config struct { - SecretKey string `env:"SECRET_KEY,unset"` -} -``` - -## From file - -The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added -in order to indicate that the value of the variable shall be loaded from a -file. -The path of that file is given by the environment variable associated with it: - -```go -package main - -import ( - "fmt" - "time" - - "github.com/caarlos0/env/v11" -) - -type config struct { - Secret string `env:"SECRET,file"` - Password string `env:"PASSWORD,file" envDefault:"/tmp/password"` - Certificate string `env:"CERTIFICATE,file,expand" envDefault:"${CERTIFICATE_FILE}"` -} - -func main() { - cfg := config{} - if err := env.Parse(&cfg); err != nil { - fmt.Printf("%+v\n", err) - } - - fmt.Printf("%+v\n", cfg) -} -``` - -```sh -$ echo qwerty > /tmp/secret -$ echo dvorak > /tmp/password -$ echo coleman > /tmp/certificate - -$ SECRET=/tmp/secret \ - CERTIFICATE_FILE=/tmp/certificate \ - go run main.go -{Secret:qwerty Password:dvorak Certificate:coleman} -``` +### Documentation and examples -## Options - -### Use field names as environment variables by default - -If you don't want to set the `env` tag on every field, you can use the -`UseFieldNameByDefault` option. - -It will use the field name to define the environment variable name. -So, `Foo` becomes `FOO`, `FooBar` becomes `FOO_BAR`, and so on. - -Here's an example: - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Config struct { - Username string // will use $USERNAME - Password string // will use $PASSWORD - UserFullName string // will use $USER_FULL_NAME -} - -func main() { - cfg := &Config{} - opts := env.Options{UseFieldNameByDefault: true} - - // Load env vars. - if err := env.ParseWithOptions(cfg, opts); err != nil { - log.Fatal(err) - } - - // Print the loaded data. - fmt.Printf("%+v\n", cfg) -} -``` - -### Environment - -By setting the `Options.Environment` map you can tell `Parse` to add those -`keys` and `values` as `env` vars before parsing is done. -These `envs` are stored in the map and never actually set by `os.Setenv`. -This option effectively makes `env` ignore the OS environment variables: only -the ones provided in the option are used. - -This can make your testing scenarios a bit more clean and easy to handle. - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Config struct { - Password string `env:"PASSWORD"` -} - -func main() { - cfg := &Config{} - opts := env.Options{Environment: map[string]string{ - "PASSWORD": "MY_PASSWORD", - }} - - // Load env vars. - if err := env.ParseWithOptions(cfg, opts); err != nil { - log.Fatal(err) - } - - // Print the loaded data. - fmt.Printf("%+v\n", cfg) -} -``` - -### Changing default tag name - -You can change what tag name to use for setting the env vars by setting the -`Options.TagName` variable. - -For example - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Config struct { - Password string `json:"PASSWORD"` -} - -func main() { - cfg := &Config{} - opts := env.Options{TagName: "json"} - - // Load env vars. - if err := env.ParseWithOptions(cfg, opts); err != nil { - log.Fatal(err) - } - - // Print the loaded data. - fmt.Printf("%+v\n", cfg) -} -``` - -### Prefixes - -You can prefix sub-structs env tags, as well as a whole `env.Parse` call. - -Here's an example flexing it a bit: - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Config struct { - Home string `env:"HOME"` -} - -type ComplexConfig struct { - Foo Config `envPrefix:"FOO_"` - Clean Config - Bar Config `envPrefix:"BAR_"` - Blah string `env:"BLAH"` -} - -func main() { - cfg := &ComplexConfig{} - opts := env.Options{ - Prefix: "T_", - Environment: map[string]string{ - "T_FOO_HOME": "/foo", - "T_BAR_HOME": "/bar", - "T_BLAH": "blahhh", - "T_HOME": "/clean", - }, - } - - // Load env vars. - if err := env.ParseWithOptions(cfg, opts); err != nil { - log.Fatal(err) - } - - // Print the loaded data. - fmt.Printf("%+v\n", cfg) -} -``` - -### Complex objects inside array (slice) - -You can set sub-struct field values inside a slice by naming the environment variables with sequential numbers starting from 0 (without omitting numbers in between) and an underscore. -It is possible to use prefix tag too. - -Here's an example with and without prefix tag: - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Test struct { - Str string `env:"STR"` - Num int `env:"NUM"` -} -type ComplexConfig struct { - Baz []Test `env:",init"` - Bar []Test `envPrefix:"BAR"` - Foo *[]Test `envPrefix:"FOO_"` -} - -func main() { - cfg := &ComplexConfig{} - opts := env.Options{ - Environment: map[string]string{ - "0_STR": "bt", - "1_NUM": "10", - - "FOO_0_STR": "b0t", - "FOO_1_STR": "b1t", - "FOO_1_NUM": "212", - - "BAR_0_STR": "f0t", - "BAR_0_NUM": "101", - "BAR_1_STR": "f1t", - "BAR_1_NUM": "111", - }, - } - - // Load env vars. - if err := env.ParseWithOptions(cfg, opts); err != nil { - log.Fatal(err) - } - - // Print the loaded data. - fmt.Printf("%+v\n", cfg) -} -``` - -### On set hooks - -You might want to listen to value sets and, for example, log something or do -some other kind of logic. -You can do this by passing a `OnSet` option: - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Config struct { - Username string `env:"USERNAME" envDefault:"admin"` - Password string `env:"PASSWORD"` -} - -func main() { - cfg := &Config{} - opts := env.Options{ - OnSet: func(tag string, value interface{}, isDefault bool) { - fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) - }, - } - - // Load env vars. - if err := env.ParseWithOptions(cfg, opts); err != nil { - log.Fatal(err) - } - - // Print the loaded data. - fmt.Printf("%+v\n", cfg) -} -``` - -## Making all fields to required - -You can make all fields that don't have a default value be required by setting -the `RequiredIfNoDef: true` in the `Options`. - -For example - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Config struct { - Username string `env:"USERNAME" envDefault:"admin"` - Password string `env:"PASSWORD"` -} - -func main() { - cfg := &Config{} - opts := env.Options{RequiredIfNoDef: true} - - // Load env vars. - if err := env.ParseWithOptions(cfg, opts); err != nil { - log.Fatal(err) - } - - // Print the loaded data. - fmt.Printf("%+v\n", cfg) -} -``` - -## Defaults from code - -You may define default value also in code, by initialising the config data -before it's filled by `env.Parse`. -Default values defined as struct tags will overwrite existing values during -Parse. - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Config struct { - Username string `env:"USERNAME" envDefault:"admin"` - Password string `env:"PASSWORD"` -} - -func main() { - cfg := Config{ - Username: "test", - Password: "123456", - } - - if err := env.Parse(&cfg); err != nil { - fmt.Println("failed:", err) - } - - fmt.Printf("%+v", cfg) // {Username:admin Password:123456} -} -``` - -## Error handling - -You can handle the errors the library throws like so: - -```go -package main - -import ( - "fmt" - "log" - - "github.com/caarlos0/env/v11" -) - -type Config struct { - Username string `env:"USERNAME" envDefault:"admin"` - Password string `env:"PASSWORD"` -} - -func main() { - var cfg Config - err := env.Parse(&cfg) - if e, ok := err.(*env.AggregateError); ok { - for _, er := range e.Errors { - switch v := er.(type) { - case env.ParseError: - // handle it - case env.NotStructPtrError: - // handle it - case env.NoParserError: - // handle it - case env.NoSupportedTagOptionError: - // handle it - default: - fmt.Printf("Unknown error type %v", v) - } - } - } - - fmt.Printf("%+v", cfg) // {Username:admin Password:123456} -} -``` - -> **Info** -> -> If you want to check if an specific error is in the chain, you can also use -> `errors.Is()`. +Examples are live in +[pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11), +and also in the +[example test file](./example_test.go). ## Badges @@ -696,7 +141,7 @@ func main() { [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](/LICENSE.md) [![Build status](https://img.shields.io/github/actions/workflow/status/caarlos0/env/build.yml?style=for-the-badge&branch=main)](https://github.com/caarlos0/env/actions?workflow=build) [![Codecov branch](https://img.shields.io/codecov/c/github/caarlos0/env/main.svg?style=for-the-badge)](https://codecov.io/gh/caarlos0/env) -[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](http://godoc.org/github.com/caarlos0/env/v11) +[![Go docs](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](http://godoc.org/github.com/caarlos0/env/v11) [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?style=for-the-badge)](https://github.com/goreleaser) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=for-the-badge)](https://conventionalcommits.org) diff --git a/env.go b/env.go index 5b0be18..27776b8 100644 --- a/env.go +++ b/env.go @@ -1,3 +1,18 @@ +// Package env is a simple, zero-dependencies library to parse environment +// variables into structs. +// +// Example: +// +// type config struct { +// Home string `env:"HOME"` +// } +// // parse +// var cfg config +// err := env.Parse(&cfg) +// // or parse with generics +// cfg, err := env.ParseAs[config]() +// +// Check the examples and README for more detailed usage. package env import ( @@ -89,7 +104,8 @@ func defaultTypeParsers() map[reflect.Type]ParserFunc { } } -// ParserFunc defines the signature of a function that can be used within `CustomParsers`. +// ParserFunc defines the signature of a function that can be used within +// `Options`' `FuncMap`. type ParserFunc func(v string) (interface{}, error) // OnSetFn is a hook that can be run when a value is set. @@ -103,20 +119,20 @@ type Options struct { // Environment keys and values that will be accessible for the service. Environment map[string]string - // TagName specifies another tagname to use rather than the default env. + // TagName specifies another tag name to use rather than the default 'env'. TagName string - // RequiredIfNoDef automatically sets all env as required if they do not + // RequiredIfNoDef automatically sets all fields as required if they do not // declare 'envDefault'. RequiredIfNoDef bool // OnSet allows to run a function when a value is set. OnSet OnSetFn - // Prefix define a prefix for each key. + // Prefix define a prefix for every key. Prefix string - // UseFieldNameByDefault defines whether or not env should use the field + // UseFieldNameByDefault defines whether or not `env` should use the field // name by default if the `env` key is missing. // Note that the field name will be "converted" to conform with environment // variable names conventions. @@ -125,7 +141,8 @@ type Options struct { // Custom parse functions for different types. FuncMap map[reflect.Type]ParserFunc - // Used internally. maps the env variable key to its resolved string value. (for env var expansion) + // Used internally. maps the env variable key to its resolved string value. + // (for env var expansion) rawEnvVars map[string]string } diff --git a/example_test.go b/example_test.go index 3a3ea93..ae8cc06 100644 --- a/example_test.go +++ b/example_test.go @@ -105,7 +105,7 @@ func ExampleParse_unset() { // Similarly, you can use `envKeyValSeparator` to define which character should // be used to separate a key from a value in a map. // The defaults are `,` and `:`, respectively. -func ExampleParse_seprator() { +func ExampleParse_separator() { type Config struct { Map map[string]string `env:"CUSTOM_MAP" envSeparator:"-" envKeyValSeparator:"|"` }