pax_global_header00006660000000000000000000000064141154144430014513gustar00rootroot0000000000000052 comment=ba5c1ad5e1fa08e6090de78dab1254da32f35c9f env-6.7.1/000077500000000000000000000000001411541444300123165ustar00rootroot00000000000000env-6.7.1/.github/000077500000000000000000000000001411541444300136565ustar00rootroot00000000000000env-6.7.1/.github/FUNDING.yml000066400000000000000000000000231411541444300154660ustar00rootroot00000000000000github: [caarlos0] env-6.7.1/.github/dependabot.yml000066400000000000000000000005431411541444300165100ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" time: "08:00" labels: - "dependencies" - "automerge" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" time: "08:00" labels: - "dependencies" - "automerge" env-6.7.1/.github/workflows/000077500000000000000000000000001411541444300157135ustar00rootroot00000000000000env-6.7.1/.github/workflows/build.yml000066400000000000000000000032451411541444300175410ustar00rootroot00000000000000name: build on: push: branches: - 'master' tags: - 'v*' pull_request: jobs: build: strategy: matrix: go-version: [~1.17] os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Cache Go modules uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: CI run: make setup ci - name: Upload coverage uses: codecov/codecov-action@v2 if: matrix.os == 'ubuntu-latest' with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 if: success() && startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-latest' with: version: latest distribution: goreleaser-pro args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} env-6.7.1/.github/workflows/lint.yml000066400000000000000000000006421411541444300174060ustar00rootroot00000000000000name: golangci-lint on: push: tags: - v* branches: - master - main pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/setup-go@v2 with: go-version: ~1.16 - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: skip-go-installation: true env-6.7.1/.gitignore000066400000000000000000000000371411541444300143060ustar00rootroot00000000000000coverage.txt bin card.png dist env-6.7.1/.golangci.yml000066400000000000000000000001601411541444300146770ustar00rootroot00000000000000linters: enable: - thelper - gofumpt - tparallel - unconvert - unparam - wastedassign env-6.7.1/.goreleaser.yml000066400000000000000000000001541411541444300152470ustar00rootroot00000000000000includes: - from_url: url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml env-6.7.1/LICENSE.md000066400000000000000000000021071411541444300137220ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2019 Carlos Alexandro Becker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. env-6.7.1/Makefile000066400000000000000000000013731411541444300137620ustar00rootroot00000000000000SOURCE_FILES?=./... TEST_PATTERN?=. export GO111MODULE := on setup: go mod tidy .PHONY: setup build: go build .PHONY: build test: go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m .PHONY: test cover: test go tool cover -html=coverage.txt .PHONY: cover fmt: find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done .PHONY: fmt lint: ./bin/golangci-lint run ./... .PHONY: lint ci: build test .PHONY: ci card: wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png" .PHONY: card .DEFAULT_GOAL := ci env-6.7.1/README.md000066400000000000000000000206071411541444300136020ustar00rootroot00000000000000# env [![Build Status](https://img.shields.io/github/workflow/status/caarlos0/env/build?style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build) [![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env) [![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v6) Simple lib to parse envs to structs in Go. ## Example Get the module with: ```sh go get github.com/caarlos0/env/v6 ``` The usage looks like this: ```go package main import ( "fmt" "time" "github.com/caarlos0/env/v6" ) type config struct { Home string `env:"HOME"` Port int `env:"PORT" envDefault:"3000"` Password string `env:"PASSWORD,unset"` IsProduction bool `env:"PRODUCTION"` Hosts []string `env:"HOSTS" envSeparator:":"` Duration time.Duration `env:"DURATION"` TempFolder string `env:"TEMP_FOLDER" envDefault:"${HOME}/tmp" envExpand:"true"` } func main() { cfg := config{} if err := env.Parse(&cfg); 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 go run main.go {Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s} ``` ⚠️⚠️⚠️ **Attention:** _unexported fields_ will be **ignored**. ## 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` - `int16` - `int32` - `int64` - `uint` - `uint8` - `uint16` - `uint32` - `uint64` - `float32` - `float64` - `string` - `time.Duration` - `encoding.TextUnmarshaler` - `url.URL` Pointers, slices and slices of pointers of those types are also supported. You can also use/define a [custom parser func](#custom-parser-funcs) for any other type you want. 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. If you set the `envExpand` tag, environment variables (either in `${var}` or `$var` format) in the string will be replaced according with the actual value of the variable. ## 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.ParseWithFuncs()` function. In addition to accepting a struct pointer (same as `Parse()`), this function also accepts a `map[reflect.Type]env.ParserFunc`. `env` also ships with some pre-built custom parser funcs for common types. You can check them out [here](parsers/). If you add a custom parser for, say `Foo`, it will also be used to parse `*Foo` and `[]Foo` types. This directory contains pre-built, custom parsers that can be used with `env.ParseWithFuncs` to facilitate the parsing of envs that are not basic types. Check the example in the [go doc](http://godoc.org/github.com/caarlos0/env) 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 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"` } ``` ## Not Empty fields While `required` demands the environment variable to be check, 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 to 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 Example below ```go package main import ( "fmt" "time" "github.com/caarlos0/env/v6" ) type config struct { Secret string `env:"SECRET,file"` Password string `env:"PASSWORD,file" envDefault:"/tmp/password"` Certificate string `env:"CERTIFICATE,file" envDefault:"${CERTIFICATE_FILE}" envExpand:"true"` } 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} ``` ## Options ### 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/v6" ) 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.Parse(cfg, opts); err != nil { log.Fatal(err) } // Print the loaded data. fmt.Printf("%+v\n", cfg.envData) } ``` ### 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/v6" ) type Config struct { Password string `json:"PASSWORD"` } func main() { cfg := &Config{} opts := &env.Options{TagName: "json"} // Load env vars. if err := env.Parse(cfg, opts); err != nil { log.Fatal(err) } // Print the loaded data. fmt.Printf("%+v\n", cfg.envData) } ``` ### 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/v6" ) 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.Parse(cfg, opts); err != nil { log.Fatal(err) } // Print the loaded data. fmt.Printf("%+v\n", cfg.envData) } ``` ## 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/v6" ) 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.Parse(cfg, opts); err != nil { log.Fatal(err) } // Print the loaded data. fmt.Printf("%+v\n", cfg.envData) } ``` ## Stargazers over time [![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) env-6.7.1/env.go000066400000000000000000000263051411541444300134430ustar00rootroot00000000000000package env import ( "encoding" "errors" "fmt" "io/ioutil" "net/url" "os" "reflect" "strconv" "strings" "time" ) // nolint: gochecknoglobals var ( // ErrNotAStructPtr is returned if you pass something that is not a pointer to a // Struct to Parse. ErrNotAStructPtr = errors.New("env: expected a pointer to a Struct") defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ reflect.Bool: func(v string) (interface{}, error) { return strconv.ParseBool(v) }, reflect.String: func(v string) (interface{}, error) { return v, nil }, reflect.Int: func(v string) (interface{}, error) { i, err := strconv.ParseInt(v, 10, 32) return int(i), err }, reflect.Int16: func(v string) (interface{}, error) { i, err := strconv.ParseInt(v, 10, 16) return int16(i), err }, reflect.Int32: func(v string) (interface{}, error) { i, err := strconv.ParseInt(v, 10, 32) return int32(i), err }, reflect.Int64: func(v string) (interface{}, error) { return strconv.ParseInt(v, 10, 64) }, reflect.Int8: func(v string) (interface{}, error) { i, err := strconv.ParseInt(v, 10, 8) return int8(i), err }, reflect.Uint: func(v string) (interface{}, error) { i, err := strconv.ParseUint(v, 10, 32) return uint(i), err }, reflect.Uint16: func(v string) (interface{}, error) { i, err := strconv.ParseUint(v, 10, 16) return uint16(i), err }, reflect.Uint32: func(v string) (interface{}, error) { i, err := strconv.ParseUint(v, 10, 32) return uint32(i), err }, reflect.Uint64: func(v string) (interface{}, error) { i, err := strconv.ParseUint(v, 10, 64) return i, err }, reflect.Uint8: func(v string) (interface{}, error) { i, err := strconv.ParseUint(v, 10, 8) return uint8(i), err }, reflect.Float64: func(v string) (interface{}, error) { return strconv.ParseFloat(v, 64) }, reflect.Float32: func(v string) (interface{}, error) { f, err := strconv.ParseFloat(v, 32) return float32(f), err }, } defaultTypeParsers = map[reflect.Type]ParserFunc{ reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) { u, err := url.Parse(v) if err != nil { return nil, fmt.Errorf("unable to parse URL: %v", err) } return *u, nil }, reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) { s, err := time.ParseDuration(v) if err != nil { return nil, fmt.Errorf("unable to parse duration: %v", err) } return s, err }, } ) // ParserFunc defines the signature of a function that can be used within `CustomParsers`. type ParserFunc func(v string) (interface{}, error) // OnSetFn is a hook that can be run when a value is set. type OnSetFn func(tag string, value interface{}, isDefault bool) // Options for the parser. 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 string // RequiredIfNoDef automatically sets all env as required if they do not declare 'envDefault' RequiredIfNoDef bool // OnSet allows to run a function when a value is set OnSet OnSetFn // Sets to true if we have already configured once. configured bool } // configure will do the basic configurations and defaults. func configure(opts []Options) []Options { // If we have already configured the first item // of options will have been configured set to true. if len(opts) > 0 && opts[0].configured { return opts } // Created options with defaults. opt := Options{ TagName: "env", Environment: toMap(os.Environ()), configured: true, } // Loop over all opts structs and set // to opt if value is not default/empty. for _, item := range opts { if item.Environment != nil { opt.Environment = item.Environment } if item.TagName != "" { opt.TagName = item.TagName } if item.OnSet != nil { opt.OnSet = item.OnSet } opt.RequiredIfNoDef = item.RequiredIfNoDef } return []Options{opt} } func getOnSetFn(opts []Options) OnSetFn { return opts[0].OnSet } // getTagName returns the tag name. func getTagName(opts []Options) string { return opts[0].TagName } // getEnvironment returns the environment map. func getEnvironment(opts []Options) map[string]string { return opts[0].Environment } // Parse parses a struct containing `env` tags and loads its values from // environment variables. func Parse(v interface{}, opts ...Options) error { return ParseWithFuncs(v, map[reflect.Type]ParserFunc{}, opts...) } // ParseWithFuncs is the same as `Parse` except it also allows the user to pass // in custom parsers. func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...Options) error { opts = configure(opts) ptrRef := reflect.ValueOf(v) if ptrRef.Kind() != reflect.Ptr { return ErrNotAStructPtr } ref := ptrRef.Elem() if ref.Kind() != reflect.Struct { return ErrNotAStructPtr } parsers := defaultTypeParsers for k, v := range funcMap { parsers[k] = v } return doParse(ref, parsers, opts) } func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Options) error { refType := ref.Type() for i := 0; i < refType.NumField(); i++ { refField := ref.Field(i) if !refField.CanSet() { continue } if reflect.Ptr == refField.Kind() && !refField.IsNil() { err := ParseWithFuncs(refField.Interface(), funcMap, opts...) if err != nil { return err } continue } if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" { err := Parse(refField.Addr().Interface(), opts...) if err != nil { return err } continue } refTypeField := refType.Field(i) value, err := get(refTypeField, opts) if err != nil { return err } if value == "" { if reflect.Struct == refField.Kind() { if err := doParse(refField, funcMap, opts); err != nil { return err } } continue } if err := set(refField, refTypeField, value, funcMap); err != nil { return err } } return nil } func get(field reflect.StructField, opts []Options) (val string, err error) { var exists bool var isDefault bool var loadFile bool var unset bool var notEmpty bool required := opts[0].RequiredIfNoDef key, tags := parseKeyForOption(field.Tag.Get(getTagName(opts))) for _, tag := range tags { switch tag { case "": continue case "file": loadFile = true case "required": required = true case "unset": unset = true case "notEmpty": notEmpty = true default: return "", fmt.Errorf("env: tag option %q not supported", tag) } } expand := strings.EqualFold(field.Tag.Get("envExpand"), "true") defaultValue, defExists := field.Tag.Lookup("envDefault") val, exists, isDefault = getOr(key, defaultValue, defExists, getEnvironment(opts)) if expand { val = os.ExpandEnv(val) } if unset { defer os.Unsetenv(key) } if required && !exists && len(key) > 0 { return "", fmt.Errorf(`env: required environment variable %q is not set`, key) } if notEmpty && val == "" { return "", fmt.Errorf("env: environment variable %q should not be empty", key) } if loadFile && val != "" { filename := val val, err = getFromFile(filename) if err != nil { return "", fmt.Errorf(`env: could not load content of file "%s" from variable %s: %v`, filename, key, err) } } if onSetFn := getOnSetFn(opts); onSetFn != nil { onSetFn(key, val, isDefault) } return val, err } // split the env tag's key into the expected key and desired option, if any. func parseKeyForOption(key string) (string, []string) { opts := strings.Split(key, ",") return opts[0], opts[1:] } func getFromFile(filename string) (value string, err error) { b, err := ioutil.ReadFile(filename) return string(b), err } func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) { value, exists := envs[key] switch { case (!exists || key == "") && defExists: return defaultValue, true, true case !exists: return "", false, false } return value, true, false } func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error { if tm := asTextUnmarshaler(field); tm != nil { if err := tm.UnmarshalText([]byte(value)); err != nil { return newParseError(sf, err) } return nil } typee := sf.Type fieldee := field if typee.Kind() == reflect.Ptr { typee = typee.Elem() fieldee = field.Elem() } parserFunc, ok := funcMap[typee] if ok { val, err := parserFunc(value) if err != nil { return newParseError(sf, err) } fieldee.Set(reflect.ValueOf(val)) return nil } parserFunc, ok = defaultBuiltInParsers[typee.Kind()] if ok { val, err := parserFunc(value) if err != nil { return newParseError(sf, err) } fieldee.Set(reflect.ValueOf(val).Convert(typee)) return nil } if field.Kind() == reflect.Slice { return handleSlice(field, value, sf, funcMap) } return newNoParserError(sf) } func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { separator := sf.Tag.Get("envSeparator") if separator == "" { separator = "," } parts := strings.Split(value, separator) typee := sf.Type.Elem() if typee.Kind() == reflect.Ptr { typee = typee.Elem() } if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok { return parseTextUnmarshalers(field, parts, sf) } parserFunc, ok := funcMap[typee] if !ok { parserFunc, ok = defaultBuiltInParsers[typee.Kind()] if !ok { return newNoParserError(sf) } } result := reflect.MakeSlice(sf.Type, 0, len(parts)) for _, part := range parts { r, err := parserFunc(part) if err != nil { return newParseError(sf, err) } v := reflect.ValueOf(r).Convert(typee) if sf.Type.Elem().Kind() == reflect.Ptr { v = reflect.New(typee) v.Elem().Set(reflect.ValueOf(r).Convert(typee)) } result = reflect.Append(result, v) } field.Set(result) return nil } func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler { if reflect.Ptr == field.Kind() { if field.IsNil() { field.Set(reflect.New(field.Type().Elem())) } } else if field.CanAddr() { field = field.Addr() } tm, ok := field.Interface().(encoding.TextUnmarshaler) if !ok { return nil } return tm } func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error { s := len(data) elemType := field.Type().Elem() slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s) for i, v := range data { sv := slice.Index(i) kind := sv.Kind() if kind == reflect.Ptr { sv = reflect.New(elemType.Elem()) } else { sv = sv.Addr() } tm := sv.Interface().(encoding.TextUnmarshaler) if err := tm.UnmarshalText([]byte(v)); err != nil { return newParseError(sf, err) } if kind == reflect.Ptr { slice.Index(i).Set(sv) } } field.Set(slice) return nil } func newParseError(sf reflect.StructField, err error) error { if err == nil { return nil } return parseError{ sf: sf, err: err, } } type parseError struct { sf reflect.StructField err error } func (e parseError) Error() string { return fmt.Sprintf(`env: parse error on field "%s" of type "%s": %v`, e.sf.Name, e.sf.Type, e.err) } func newNoParserError(sf reflect.StructField) error { return fmt.Errorf(`env: no parser found for field "%s" of type "%s"`, sf.Name, sf.Type) } env-6.7.1/env_test.go000066400000000000000000001051221411541444300144750ustar00rootroot00000000000000package env import ( "errors" "fmt" "io/ioutil" "net/http" "net/url" "os" "path/filepath" "reflect" "runtime" "strconv" "strings" "testing" "time" "github.com/matryer/is" ) type unmarshaler struct { time.Duration } // TextUnmarshaler implements encoding.TextUnmarshaler. func (d *unmarshaler) UnmarshalText(data []byte) (err error) { if len(data) != 0 { d.Duration, err = time.ParseDuration(string(data)) } else { d.Duration = 0 } return err } // nolint: maligned type Config struct { String string `env:"STRING"` StringPtr *string `env:"STRING"` Strings []string `env:"STRINGS"` StringPtrs []*string `env:"STRINGS"` Bool bool `env:"BOOL"` BoolPtr *bool `env:"BOOL"` Bools []bool `env:"BOOLS"` BoolPtrs []*bool `env:"BOOLS"` Int int `env:"INT"` IntPtr *int `env:"INT"` Ints []int `env:"INTS"` IntPtrs []*int `env:"INTS"` Int8 int8 `env:"INT8"` Int8Ptr *int8 `env:"INT8"` Int8s []int8 `env:"INT8S"` Int8Ptrs []*int8 `env:"INT8S"` Int16 int16 `env:"INT16"` Int16Ptr *int16 `env:"INT16"` Int16s []int16 `env:"INT16S"` Int16Ptrs []*int16 `env:"INT16S"` Int32 int32 `env:"INT32"` Int32Ptr *int32 `env:"INT32"` Int32s []int32 `env:"INT32S"` Int32Ptrs []*int32 `env:"INT32S"` Int64 int64 `env:"INT64"` Int64Ptr *int64 `env:"INT64"` Int64s []int64 `env:"INT64S"` Int64Ptrs []*int64 `env:"INT64S"` Uint uint `env:"UINT"` UintPtr *uint `env:"UINT"` Uints []uint `env:"UINTS"` UintPtrs []*uint `env:"UINTS"` Uint8 uint8 `env:"UINT8"` Uint8Ptr *uint8 `env:"UINT8"` Uint8s []uint8 `env:"UINT8S"` Uint8Ptrs []*uint8 `env:"UINT8S"` Uint16 uint16 `env:"UINT16"` Uint16Ptr *uint16 `env:"UINT16"` Uint16s []uint16 `env:"UINT16S"` Uint16Ptrs []*uint16 `env:"UINT16S"` Uint32 uint32 `env:"UINT32"` Uint32Ptr *uint32 `env:"UINT32"` Uint32s []uint32 `env:"UINT32S"` Uint32Ptrs []*uint32 `env:"UINT32S"` Uint64 uint64 `env:"UINT64"` Uint64Ptr *uint64 `env:"UINT64"` Uint64s []uint64 `env:"UINT64S"` Uint64Ptrs []*uint64 `env:"UINT64S"` Float32 float32 `env:"FLOAT32"` Float32Ptr *float32 `env:"FLOAT32"` Float32s []float32 `env:"FLOAT32S"` Float32Ptrs []*float32 `env:"FLOAT32S"` Float64 float64 `env:"FLOAT64"` Float64Ptr *float64 `env:"FLOAT64"` Float64s []float64 `env:"FLOAT64S"` Float64Ptrs []*float64 `env:"FLOAT64S"` Duration time.Duration `env:"DURATION"` Durations []time.Duration `env:"DURATIONS"` DurationPtr *time.Duration `env:"DURATION"` DurationPtrs []*time.Duration `env:"DURATIONS"` Unmarshaler unmarshaler `env:"UNMARSHALER"` UnmarshalerPtr *unmarshaler `env:"UNMARSHALER"` Unmarshalers []unmarshaler `env:"UNMARSHALERS"` UnmarshalerPtrs []*unmarshaler `env:"UNMARSHALERS"` URL url.URL `env:"URL"` URLPtr *url.URL `env:"URL"` URLs []url.URL `env:"URLS"` URLPtrs []*url.URL `env:"URLS"` StringWithdefault string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/db"` CustomSeparator []string `env:"SEPSTRINGS" envSeparator:":"` NonDefined struct { String string `env:"NONDEFINED_STR"` } NotAnEnv string unexported string `env:"FOO"` } type ParentStruct struct { InnerStruct *InnerStruct unexported *InnerStruct Ignored *http.Client } type InnerStruct struct { Inner string `env:"innervar"` Number uint `env:"innernum"` } type ForNestedStruct struct { NestedStruct } type NestedStruct struct { NestedVar string `env:"nestedvar"` } func TestParsesEnv(t *testing.T) { is := is.New(t) defer os.Clearenv() tos := func(v interface{}) string { return fmt.Sprintf("%v", v) } toss := func(v ...interface{}) string { ss := []string{} for _, s := range v { ss = append(ss, tos(s)) } return strings.Join(ss, ",") } str1 := "str1" str2 := "str2" os.Setenv("STRING", str1) os.Setenv("STRINGS", toss(str1, str2)) bool1 := true bool2 := false os.Setenv("BOOL", tos(bool1)) os.Setenv("BOOLS", toss(bool1, bool2)) int1 := -1 int2 := 2 os.Setenv("INT", tos(int1)) os.Setenv("INTS", toss(int1, int2)) var int81 int8 = -2 var int82 int8 = 5 os.Setenv("INT8", tos(int81)) os.Setenv("INT8S", toss(int81, int82)) var int161 int16 = -24 var int162 int16 = 15 os.Setenv("INT16", tos(int161)) os.Setenv("INT16S", toss(int161, int162)) var int321 int32 = -14 var int322 int32 = 154 os.Setenv("INT32", tos(int321)) os.Setenv("INT32S", toss(int321, int322)) var int641 int64 = -12 var int642 int64 = 150 os.Setenv("INT64", tos(int641)) os.Setenv("INT64S", toss(int641, int642)) var uint1 uint = 1 var uint2 uint = 2 os.Setenv("UINT", tos(uint1)) os.Setenv("UINTS", toss(uint1, uint2)) var uint81 uint8 = 15 var uint82 uint8 = 51 os.Setenv("UINT8", tos(uint81)) os.Setenv("UINT8S", toss(uint81, uint82)) var uint161 uint16 = 532 var uint162 uint16 = 123 os.Setenv("UINT16", tos(uint161)) os.Setenv("UINT16S", toss(uint161, uint162)) var uint321 uint32 = 93 var uint322 uint32 = 14 os.Setenv("UINT32", tos(uint321)) os.Setenv("UINT32S", toss(uint321, uint322)) var uint641 uint64 = 5 var uint642 uint64 = 43 os.Setenv("UINT64", tos(uint641)) os.Setenv("UINT64S", toss(uint641, uint642)) var float321 float32 = 9.3 var float322 float32 = 1.1 os.Setenv("FLOAT32", tos(float321)) os.Setenv("FLOAT32S", toss(float321, float322)) float641 := 1.53 float642 := 0.5 os.Setenv("FLOAT64", tos(float641)) os.Setenv("FLOAT64S", toss(float641, float642)) duration1 := time.Second duration2 := time.Second * 4 os.Setenv("DURATION", tos(duration1)) os.Setenv("DURATIONS", toss(duration1, duration2)) unmarshaler1 := unmarshaler{time.Minute} unmarshaler2 := unmarshaler{time.Millisecond * 1232} os.Setenv("UNMARSHALER", tos(unmarshaler1.Duration)) os.Setenv("UNMARSHALERS", toss(unmarshaler1.Duration, unmarshaler2.Duration)) url1 := "https://goreleaser.com" url2 := "https://caarlos0.dev" os.Setenv("URL", tos(url1)) os.Setenv("URLS", toss(url1, url2)) os.Setenv("SEPSTRINGS", strings.Join([]string{str1, str2}, ":")) nonDefinedStr := "nonDefinedStr" os.Setenv("NONDEFINED_STR", nonDefinedStr) cfg := Config{} is.NoErr(Parse(&cfg)) is.Equal(str1, cfg.String) is.Equal(&str1, cfg.StringPtr) is.Equal(str1, cfg.Strings[0]) is.Equal(str2, cfg.Strings[1]) is.Equal(&str1, cfg.StringPtrs[0]) is.Equal(&str2, cfg.StringPtrs[1]) is.Equal(bool1, cfg.Bool) is.Equal(&bool1, cfg.BoolPtr) is.Equal(bool1, cfg.Bools[0]) is.Equal(bool2, cfg.Bools[1]) is.Equal(&bool1, cfg.BoolPtrs[0]) is.Equal(&bool2, cfg.BoolPtrs[1]) is.Equal(int1, cfg.Int) is.Equal(&int1, cfg.IntPtr) is.Equal(int1, cfg.Ints[0]) is.Equal(int2, cfg.Ints[1]) is.Equal(&int1, cfg.IntPtrs[0]) is.Equal(&int2, cfg.IntPtrs[1]) is.Equal(int81, cfg.Int8) is.Equal(&int81, cfg.Int8Ptr) is.Equal(int81, cfg.Int8s[0]) is.Equal(int82, cfg.Int8s[1]) is.Equal(&int81, cfg.Int8Ptrs[0]) is.Equal(&int82, cfg.Int8Ptrs[1]) is.Equal(int161, cfg.Int16) is.Equal(&int161, cfg.Int16Ptr) is.Equal(int161, cfg.Int16s[0]) is.Equal(int162, cfg.Int16s[1]) is.Equal(&int161, cfg.Int16Ptrs[0]) is.Equal(&int162, cfg.Int16Ptrs[1]) is.Equal(int321, cfg.Int32) is.Equal(&int321, cfg.Int32Ptr) is.Equal(int321, cfg.Int32s[0]) is.Equal(int322, cfg.Int32s[1]) is.Equal(&int321, cfg.Int32Ptrs[0]) is.Equal(&int322, cfg.Int32Ptrs[1]) is.Equal(int641, cfg.Int64) is.Equal(&int641, cfg.Int64Ptr) is.Equal(int641, cfg.Int64s[0]) is.Equal(int642, cfg.Int64s[1]) is.Equal(&int641, cfg.Int64Ptrs[0]) is.Equal(&int642, cfg.Int64Ptrs[1]) is.Equal(uint1, cfg.Uint) is.Equal(&uint1, cfg.UintPtr) is.Equal(uint1, cfg.Uints[0]) is.Equal(uint2, cfg.Uints[1]) is.Equal(&uint1, cfg.UintPtrs[0]) is.Equal(&uint2, cfg.UintPtrs[1]) is.Equal(uint81, cfg.Uint8) is.Equal(&uint81, cfg.Uint8Ptr) is.Equal(uint81, cfg.Uint8s[0]) is.Equal(uint82, cfg.Uint8s[1]) is.Equal(&uint81, cfg.Uint8Ptrs[0]) is.Equal(&uint82, cfg.Uint8Ptrs[1]) is.Equal(uint161, cfg.Uint16) is.Equal(&uint161, cfg.Uint16Ptr) is.Equal(uint161, cfg.Uint16s[0]) is.Equal(uint162, cfg.Uint16s[1]) is.Equal(&uint161, cfg.Uint16Ptrs[0]) is.Equal(&uint162, cfg.Uint16Ptrs[1]) is.Equal(uint321, cfg.Uint32) is.Equal(&uint321, cfg.Uint32Ptr) is.Equal(uint321, cfg.Uint32s[0]) is.Equal(uint322, cfg.Uint32s[1]) is.Equal(&uint321, cfg.Uint32Ptrs[0]) is.Equal(&uint322, cfg.Uint32Ptrs[1]) is.Equal(uint641, cfg.Uint64) is.Equal(&uint641, cfg.Uint64Ptr) is.Equal(uint641, cfg.Uint64s[0]) is.Equal(uint642, cfg.Uint64s[1]) is.Equal(&uint641, cfg.Uint64Ptrs[0]) is.Equal(&uint642, cfg.Uint64Ptrs[1]) is.Equal(float321, cfg.Float32) is.Equal(&float321, cfg.Float32Ptr) is.Equal(float321, cfg.Float32s[0]) is.Equal(float322, cfg.Float32s[1]) is.Equal(&float321, cfg.Float32Ptrs[0]) is.Equal(&float322, cfg.Float32Ptrs[1]) is.Equal(float641, cfg.Float64) is.Equal(&float641, cfg.Float64Ptr) is.Equal(float641, cfg.Float64s[0]) is.Equal(float642, cfg.Float64s[1]) is.Equal(&float641, cfg.Float64Ptrs[0]) is.Equal(&float642, cfg.Float64Ptrs[1]) is.Equal(duration1, cfg.Duration) is.Equal(&duration1, cfg.DurationPtr) is.Equal(duration1, cfg.Durations[0]) is.Equal(duration2, cfg.Durations[1]) is.Equal(&duration1, cfg.DurationPtrs[0]) is.Equal(&duration2, cfg.DurationPtrs[1]) is.Equal(unmarshaler1, cfg.Unmarshaler) is.Equal(&unmarshaler1, cfg.UnmarshalerPtr) is.Equal(unmarshaler1, cfg.Unmarshalers[0]) is.Equal(unmarshaler2, cfg.Unmarshalers[1]) is.Equal(&unmarshaler1, cfg.UnmarshalerPtrs[0]) is.Equal(&unmarshaler2, cfg.UnmarshalerPtrs[1]) is.Equal(url1, cfg.URL.String()) is.Equal(url1, cfg.URLPtr.String()) is.Equal(url1, cfg.URLs[0].String()) is.Equal(url2, cfg.URLs[1].String()) is.Equal(url1, cfg.URLPtrs[0].String()) is.Equal(url2, cfg.URLPtrs[1].String()) is.Equal("postgres://localhost:5432/db", cfg.StringWithdefault) is.Equal(nonDefinedStr, cfg.NonDefined.String) is.Equal(str1, cfg.CustomSeparator[0]) is.Equal(str2, cfg.CustomSeparator[1]) is.Equal(cfg.NotAnEnv, "") is.Equal(cfg.unexported, "") } func TestSetEnvAndTagOptsChain(t *testing.T) { is := is.New(t) defer os.Clearenv() type config struct { Key1 string `mytag:"KEY1,required"` Key2 int `mytag:"KEY2,required"` } envs := map[string]string{ "KEY1": "VALUE1", "KEY2": "3", } cfg := config{} is.NoErr(Parse(&cfg, Options{TagName: "mytag"}, Options{Environment: envs})) is.Equal("VALUE1", cfg.Key1) is.Equal(3, cfg.Key2) } func TestJSONTag(t *testing.T) { is := is.New(t) defer os.Clearenv() type config struct { Key1 string `json:"KEY1"` Key2 int `json:"KEY2"` } os.Setenv("KEY1", "VALUE7") os.Setenv("KEY2", "5") cfg := config{} is.NoErr(Parse(&cfg, Options{TagName: "json"})) is.Equal("VALUE7", cfg.Key1) is.Equal(5, cfg.Key2) } func TestParsesEnvInner(t *testing.T) { is := is.New(t) os.Setenv("innervar", "someinnervalue") os.Setenv("innernum", "8") defer os.Clearenv() cfg := ParentStruct{ InnerStruct: &InnerStruct{}, unexported: &InnerStruct{}, } is.NoErr(Parse(&cfg)) is.Equal("someinnervalue", cfg.InnerStruct.Inner) is.Equal(uint(8), cfg.InnerStruct.Number) } func TestParsesEnvInnerFails(t *testing.T) { defer os.Clearenv() type config struct { Foo struct { Number int `env:"NUMBER"` } } os.Setenv("NUMBER", "not-a-number") isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Number" of type "int": strconv.ParseInt: parsing "not-a-number": invalid syntax`) } func TestParsesEnvInnerNil(t *testing.T) { is := is.New(t) os.Setenv("innervar", "someinnervalue") defer os.Clearenv() cfg := ParentStruct{} is.NoErr(Parse(&cfg)) } func TestParsesEnvInnerInvalid(t *testing.T) { os.Setenv("innernum", "-547") defer os.Clearenv() cfg := ParentStruct{ InnerStruct: &InnerStruct{}, } isErrorWithMessage(t, Parse(&cfg), `env: parse error on field "Number" of type "uint": strconv.ParseUint: parsing "-547": invalid syntax`) } func TestParsesEnvNested(t *testing.T) { is := is.New(t) os.Setenv("nestedvar", "somenestedvalue") defer os.Clearenv() var cfg ForNestedStruct is.NoErr(Parse(&cfg)) is.Equal("somenestedvalue", cfg.NestedVar) } func TestEmptyVars(t *testing.T) { is := is.New(t) os.Clearenv() cfg := Config{} is.NoErr(Parse(&cfg)) is.Equal("", cfg.String) is.Equal(false, cfg.Bool) is.Equal(0, cfg.Int) is.Equal(uint(0), cfg.Uint) is.Equal(uint64(0), cfg.Uint64) is.Equal(int64(0), cfg.Int64) is.Equal(0, len(cfg.Strings)) is.Equal(0, len(cfg.CustomSeparator)) is.Equal(0, len(cfg.Ints)) is.Equal(0, len(cfg.Bools)) } func TestPassAnInvalidPtr(t *testing.T) { var thisShouldBreak int isErrorWithMessage(t, Parse(&thisShouldBreak), "env: expected a pointer to a Struct") } func TestPassReference(t *testing.T) { cfg := Config{} isErrorWithMessage(t, Parse(cfg), "env: expected a pointer to a Struct") } func TestInvalidBool(t *testing.T) { os.Setenv("BOOL", "should-be-a-bool") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Bool" of type "bool": strconv.ParseBool: parsing "should-be-a-bool": invalid syntax`) } func TestInvalidInt(t *testing.T) { os.Setenv("INT", "should-be-an-int") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int" of type "int": strconv.ParseInt: parsing "should-be-an-int": invalid syntax`) } func TestInvalidUint(t *testing.T) { os.Setenv("UINT", "-44") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint" of type "uint": strconv.ParseUint: parsing "-44": invalid syntax`) } func TestInvalidFloat32(t *testing.T) { os.Setenv("FLOAT32", "AAA") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float32" of type "float32": strconv.ParseFloat: parsing "AAA": invalid syntax`) } func TestInvalidFloat64(t *testing.T) { os.Setenv("FLOAT64", "AAA") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float64" of type "float64": strconv.ParseFloat: parsing "AAA": invalid syntax`) } func TestInvalidUint64(t *testing.T) { os.Setenv("UINT64", "AAA") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint64" of type "uint64": strconv.ParseUint: parsing "AAA": invalid syntax`) } func TestInvalidInt64(t *testing.T) { os.Setenv("INT64", "AAA") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int64" of type "int64": strconv.ParseInt: parsing "AAA": invalid syntax`) } func TestInvalidInt64Slice(t *testing.T) { os.Setenv("BADINTS", "A,2,3") defer os.Clearenv() type config struct { BadFloats []int64 `env:"BADINTS"` } isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]int64": strconv.ParseInt: parsing "A": invalid syntax`) } func TestInvalidUInt64Slice(t *testing.T) { os.Setenv("BADINTS", "A,2,3") defer os.Clearenv() type config struct { BadFloats []uint64 `env:"BADINTS"` } isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]uint64": strconv.ParseUint: parsing "A": invalid syntax`) } func TestInvalidFloat32Slice(t *testing.T) { os.Setenv("BADFLOATS", "A,2.0,3.0") defer os.Clearenv() type config struct { BadFloats []float32 `env:"BADFLOATS"` } isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]float32": strconv.ParseFloat: parsing "A": invalid syntax`) } func TestInvalidFloat64Slice(t *testing.T) { os.Setenv("BADFLOATS", "A,2.0,3.0") defer os.Clearenv() type config struct { BadFloats []float64 `env:"BADFLOATS"` } isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]float64": strconv.ParseFloat: parsing "A": invalid syntax`) } func TestInvalidBoolsSlice(t *testing.T) { os.Setenv("BADBOOLS", "t,f,TRUE,faaaalse") defer os.Clearenv() type config struct { BadBools []bool `env:"BADBOOLS"` } isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadBools" of type "[]bool": strconv.ParseBool: parsing "faaaalse": invalid syntax`) } func TestInvalidDuration(t *testing.T) { os.Setenv("DURATION", "should-be-a-valid-duration") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Duration" of type "time.Duration": unable to parse duration: time: invalid duration "should-be-a-valid-duration"`) } func TestInvalidDurations(t *testing.T) { os.Setenv("DURATIONS", "1s,contains-an-invalid-duration,3s") defer os.Clearenv() isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Durations" of type "[]time.Duration": unable to parse duration: time: invalid duration "contains-an-invalid-duration"`) } func TestParseStructWithoutEnvTag(t *testing.T) { is := is.New(t) cfg := Config{} is.NoErr(Parse(&cfg)) is.Equal(cfg.NotAnEnv, "") } func TestParseStructWithInvalidFieldKind(t *testing.T) { type config struct { WontWorkByte byte `env:"BLAH"` } os.Setenv("BLAH", "a") isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "WontWorkByte" of type "uint8": strconv.ParseUint: parsing "a": invalid syntax`) } func TestUnsupportedSliceType(t *testing.T) { type config struct { WontWork []map[int]int `env:"WONTWORK"` } os.Setenv("WONTWORK", "1,2,3") defer os.Clearenv() isErrorWithMessage(t, Parse(&config{}), `env: no parser found for field "WontWork" of type "[]map[int]int"`) } func TestBadSeparator(t *testing.T) { type config struct { WontWork []int `env:"WONTWORK" envSeparator:":"` } os.Setenv("WONTWORK", "1,2,3,4") defer os.Clearenv() isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "WontWork" of type "[]int": strconv.ParseInt: parsing "1,2,3,4": invalid syntax`) } func TestNoErrorRequiredSet(t *testing.T) { is := is.New(t) type config struct { IsRequired string `env:"IS_REQUIRED,required"` } cfg := &config{} os.Setenv("IS_REQUIRED", "") defer os.Clearenv() is.NoErr(Parse(cfg)) is.Equal("", cfg.IsRequired) } func TestHook(t *testing.T) { is := is.New(t) type config struct { Something string `env:"SOMETHING" envDefault:"important"` Another string `env:"ANOTHER"` } cfg := &config{} os.Setenv("ANOTHER", "1") defer os.Clearenv() type onSetArgs struct { tag string key interface{} isDefault bool } var onSetCalled []onSetArgs is.NoErr(Parse(cfg, Options{ OnSet: func(tag string, value interface{}, isDefault bool) { onSetCalled = append(onSetCalled, onSetArgs{tag, value, isDefault}) }, })) is.Equal("important", cfg.Something) is.Equal("1", cfg.Another) is.Equal(2, len(onSetCalled)) is.Equal(onSetArgs{"SOMETHING", "important", true}, onSetCalled[0]) is.Equal(onSetArgs{"ANOTHER", "1", false}, onSetCalled[1]) } func TestErrorRequiredWithDefault(t *testing.T) { is := is.New(t) type config struct { IsRequired string `env:"IS_REQUIRED,required" envDefault:"important"` } cfg := &config{} os.Setenv("IS_REQUIRED", "") defer os.Clearenv() is.NoErr(Parse(cfg)) is.Equal("", cfg.IsRequired) } func TestErrorRequiredNotSet(t *testing.T) { type config struct { IsRequired string `env:"IS_REQUIRED,required"` } isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "IS_REQUIRED" is not set`) } func TestNoErrorNotEmptySet(t *testing.T) { is := is.New(t) os.Setenv("IS_REQUIRED", "1") defer os.Clearenv() type config struct { IsRequired string `env:"IS_REQUIRED,notEmpty"` } is.NoErr(Parse(&config{})) } func TestNoErrorRequiredAndNotEmptySet(t *testing.T) { is := is.New(t) os.Setenv("IS_REQUIRED", "1") defer os.Clearenv() type config struct { IsRequired string `env:"IS_REQUIRED,required,notEmpty"` } is.NoErr(Parse(&config{})) } func TestErrorNotEmptySet(t *testing.T) { os.Setenv("IS_REQUIRED", "") defer os.Clearenv() type config struct { IsRequired string `env:"IS_REQUIRED,notEmpty"` } isErrorWithMessage(t, Parse(&config{}), `env: environment variable "IS_REQUIRED" should not be empty`) } func TestErrorRequiredAndNotEmptySet(t *testing.T) { os.Setenv("IS_REQUIRED", "") defer os.Clearenv() type config struct { IsRequired string `env:"IS_REQUIRED,notEmpty,required"` } isErrorWithMessage(t, Parse(&config{}), `env: environment variable "IS_REQUIRED" should not be empty`) } func TestErrorRequiredNotSetWithDefault(t *testing.T) { is := is.New(t) type config struct { IsRequired string `env:"IS_REQUIRED,required" envDefault:"important"` } cfg := &config{} is.NoErr(Parse(cfg)) is.Equal("important", cfg.IsRequired) } func TestParseExpandOption(t *testing.T) { is := is.New(t) type config struct { Host string `env:"HOST" envDefault:"localhost"` Port int `env:"PORT" envDefault:"3000" envExpand:"True"` SecretKey string `env:"SECRET_KEY" envExpand:"True"` ExpandKey string `env:"EXPAND_KEY"` CompoundKey string `env:"HOST_PORT" envDefault:"${HOST}:${PORT}" envExpand:"True"` Default string `env:"DEFAULT" envDefault:"def1" envExpand:"True"` } defer os.Clearenv() os.Setenv("HOST", "localhost") os.Setenv("PORT", "3000") os.Setenv("EXPAND_KEY", "qwerty12345") os.Setenv("SECRET_KEY", "${EXPAND_KEY}") cfg := config{} err := Parse(&cfg) is.NoErr(err) is.Equal("localhost", cfg.Host) is.Equal(3000, cfg.Port) is.Equal("qwerty12345", cfg.SecretKey) is.Equal("qwerty12345", cfg.ExpandKey) is.Equal("localhost:3000", cfg.CompoundKey) is.Equal("def1", cfg.Default) } func TestParseUnsetRequireOptions(t *testing.T) { is := is.New(t) type config struct { Password string `env:"PASSWORD,unset,required"` } defer os.Clearenv() cfg := config{} isErrorWithMessage(t, Parse(&cfg), `env: required environment variable "PASSWORD" is not set`) os.Setenv("PASSWORD", "superSecret") is.NoErr(Parse(&cfg)) is.Equal("superSecret", cfg.Password) unset, exists := os.LookupEnv("PASSWORD") is.Equal("", unset) is.Equal(false, exists) } func TestCustomParser(t *testing.T) { is := is.New(t) type foo struct { name string } type bar struct { Name string `env:"OTHER"` Foo *foo `env:"BLAH"` } type config struct { Var foo `env:"VAR"` Foo *foo `env:"BLAH"` Other *bar } os.Setenv("VAR", "test") defer os.Unsetenv("VAR") os.Setenv("OTHER", "test2") defer os.Unsetenv("OTHER") os.Setenv("BLAH", "test3") defer os.Unsetenv("BLAH") cfg := &config{ Other: &bar{}, } err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ reflect.TypeOf(foo{}): func(v string) (interface{}, error) { return foo{name: v}, nil }, }) is.NoErr(err) is.Equal(cfg.Var.name, "test") is.Equal(cfg.Foo.name, "test3") is.Equal(cfg.Other.Name, "test2") is.Equal(cfg.Other.Foo.name, "test3") } func TestParseWithFuncsNoPtr(t *testing.T) { type foo struct{} isErrorWithMessage(t, ParseWithFuncs(foo{}, nil), "env: expected a pointer to a Struct") } func TestParseWithFuncsInvalidType(t *testing.T) { var c int isErrorWithMessage(t, ParseWithFuncs(&c, nil), "env: expected a pointer to a Struct") } func TestCustomParserError(t *testing.T) { type foo struct { name string } customParserFunc := func(v string) (interface{}, error) { return nil, errors.New("something broke") } t.Run("single", func(t *testing.T) { is := is.New(t) type config struct { Var foo `env:"VAR"` } os.Setenv("VAR", "single") cfg := &config{} err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ reflect.TypeOf(foo{}): customParserFunc, }) is.Equal(cfg.Var.name, "") isErrorWithMessage(t, err, `env: parse error on field "Var" of type "env.foo": something broke`) }) t.Run("slice", func(t *testing.T) { is := is.New(t) type config struct { Var []foo `env:"VAR2"` } os.Setenv("VAR2", "slice,slace") cfg := &config{} err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ reflect.TypeOf(foo{}): customParserFunc, }) is.Equal(cfg.Var, nil) isErrorWithMessage(t, err, `env: parse error on field "Var" of type "[]env.foo": something broke`) }) } func TestCustomParserBasicType(t *testing.T) { is := is.New(t) type ConstT int32 type config struct { Const ConstT `env:"CONST_"` } exp := ConstT(123) os.Setenv("CONST_", fmt.Sprintf("%d", exp)) customParserFunc := func(v string) (interface{}, error) { i, err := strconv.Atoi(v) if err != nil { return nil, err } r := ConstT(i) return r, nil } cfg := &config{} err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ reflect.TypeOf(ConstT(0)): customParserFunc, }) is.NoErr(err) is.Equal(exp, cfg.Const) } func TestCustomParserUint64Alias(t *testing.T) { is := is.New(t) type T uint64 var one T = 1 type config struct { Val T `env:"" envDefault:"1x"` } parserCalled := false tParser := func(value string) (interface{}, error) { parserCalled = true trimmed := strings.TrimSuffix(value, "x") i, err := strconv.Atoi(trimmed) if err != nil { return nil, err } return T(i), nil } cfg := config{} err := ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{ reflect.TypeOf(one): tParser, }) is.True(parserCalled) // tParser should have been called is.NoErr(err) is.Equal(T(1), cfg.Val) } func TestTypeCustomParserBasicInvalid(t *testing.T) { is := is.New(t) type ConstT int32 type config struct { Const ConstT `env:"CONST_"` } os.Setenv("CONST_", "foobar") customParserFunc := func(_ string) (interface{}, error) { return nil, errors.New("random error") } cfg := &config{} err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ reflect.TypeOf(ConstT(0)): customParserFunc, }) is.Equal(cfg.Const, ConstT(0)) isErrorWithMessage(t, err, `env: parse error on field "Const" of type "env.ConstT": random error`) } func TestCustomParserNotCalledForNonAlias(t *testing.T) { is := is.New(t) type T uint64 type U uint64 type config struct { Val uint64 `env:"" envDefault:"33"` Other U `env:"OTHER" envDefault:"44"` } tParserCalled := false tParser := func(value string) (interface{}, error) { tParserCalled = true return T(99), nil } cfg := config{} err := ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{ reflect.TypeOf(T(0)): tParser, }) is.True(!tParserCalled) // tParser should not have been called is.NoErr(err) is.Equal(uint64(33), cfg.Val) is.Equal(U(44), cfg.Other) } func TestCustomParserBasicUnsupported(t *testing.T) { is := is.New(t) type ConstT struct { A int } type config struct { Const ConstT `env:"CONST_"` } os.Setenv("CONST_", "42") cfg := &config{} err := Parse(cfg) is.Equal(cfg.Const, ConstT{0}) isErrorWithMessage(t, err, `env: no parser found for field "Const" of type "env.ConstT"`) } func TestUnsupportedStructType(t *testing.T) { type config struct { Foo http.Client `env:"FOO"` } os.Setenv("FOO", "foo") defer os.Clearenv() isErrorWithMessage(t, Parse(&config{}), `env: no parser found for field "Foo" of type "http.Client"`) } func TestEmptyOption(t *testing.T) { is := is.New(t) type config struct { Var string `env:"VAR,"` } cfg := &config{} os.Setenv("VAR", "") defer os.Clearenv() is.NoErr(Parse(cfg)) is.Equal("", cfg.Var) } func TestErrorOptionNotRecognized(t *testing.T) { type config struct { Var string `env:"VAR,not_supported!"` } isErrorWithMessage(t, Parse(&config{}), `env: tag option "not_supported!" not supported`) } func TestTextUnmarshalerError(t *testing.T) { type config struct { Unmarshaler unmarshaler `env:"UNMARSHALER"` } os.Setenv("UNMARSHALER", "invalid") isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Unmarshaler" of type "env.unmarshaler": time: invalid duration "invalid"`) } func TestTextUnmarshalersError(t *testing.T) { type config struct { Unmarshalers []unmarshaler `env:"UNMARSHALERS"` } os.Setenv("UNMARSHALERS", "1s,invalid") isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Unmarshalers" of type "[]env.unmarshaler": time: invalid duration "invalid"`) } func TestParseURL(t *testing.T) { is := is.New(t) type config struct { ExampleURL url.URL `env:"EXAMPLE_URL" envDefault:"https://google.com"` } var cfg config is.NoErr(Parse(&cfg)) is.Equal("https://google.com", cfg.ExampleURL.String()) } func TestParseInvalidURL(t *testing.T) { type config struct { ExampleURL url.URL `env:"EXAMPLE_URL_2"` } os.Setenv("EXAMPLE_URL_2", "nope://s s/") isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "ExampleURL" of type "url.URL": unable to parse URL: parse "nope://s s/": invalid character " " in host name`) } func ExampleParse() { type inner struct { Foo string `env:"FOO" envDefault:"foobar"` } type config struct { Home string `env:"HOME,required"` Port int `env:"PORT" envDefault:"3000"` IsProduction bool `env:"PRODUCTION"` Inner inner } os.Setenv("HOME", "/tmp/fakehome") var cfg config if err := Parse(&cfg); err != nil { fmt.Println("failed:", err) } fmt.Printf("%+v", cfg) // Output: {Home:/tmp/fakehome Port:3000 IsProduction:false Inner:{Foo:foobar}} } func ExampleParse_onSet() { type config struct { Home string `env:"HOME,required"` Port int `env:"PORT" envDefault:"3000"` IsProduction bool `env:"PRODUCTION"` } os.Setenv("HOME", "/tmp/fakehome") var cfg config if err := Parse(&cfg, Options{ OnSet: func(tag string, value interface{}, isDefault bool) { fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) }, }); err != nil { fmt.Println("failed:", err) } fmt.Printf("%+v", cfg) // Output: Set HOME to /tmp/fakehome (default? false) // Set PORT to 3000 (default? true) // Set PRODUCTION to (default? false) // {Home:/tmp/fakehome Port:3000 IsProduction:false} } func TestIgnoresUnexported(t *testing.T) { is := is.New(t) type unexportedConfig struct { home string `env:"HOME"` Home2 string `env:"HOME"` } cfg := unexportedConfig{} os.Setenv("HOME", "/tmp/fakehome") is.NoErr(Parse(&cfg)) is.Equal(cfg.home, "") is.Equal("/tmp/fakehome", cfg.Home2) } type LogLevel int8 func (l *LogLevel) UnmarshalText(text []byte) error { txt := string(text) switch txt { case "debug": *l = DebugLevel case "info": *l = InfoLevel default: return fmt.Errorf("unknown level: %q", txt) } return nil } const ( DebugLevel LogLevel = iota - 1 InfoLevel ) func TestPrecedenceUnmarshalText(t *testing.T) { is := is.New(t) os.Setenv("LOG_LEVEL", "debug") os.Setenv("LOG_LEVELS", "debug,info") defer os.Unsetenv("LOG_LEVEL") defer os.Unsetenv("LOG_LEVELS") type config struct { LogLevel LogLevel `env:"LOG_LEVEL"` LogLevels []LogLevel `env:"LOG_LEVELS"` } var cfg config is.NoErr(Parse(&cfg)) is.Equal(DebugLevel, cfg.LogLevel) is.Equal([]LogLevel{DebugLevel, InfoLevel}, cfg.LogLevels) } func ExampleParseWithFuncs() { type thing struct { desc string } type conf struct { Thing thing `env:"THING"` } os.Setenv("THING", "my thing") c := conf{} err := ParseWithFuncs(&c, map[reflect.Type]ParserFunc{ reflect.TypeOf(thing{}): func(v string) (interface{}, error) { return thing{desc: v}, nil }, }) if err != nil { fmt.Println(err) } fmt.Println(c.Thing.desc) // Output: // my thing } func TestFile(t *testing.T) { is := is.New(t) type config struct { SecretKey string `env:"SECRET_KEY,file"` } dir := t.TempDir() file := filepath.Join(dir, "sec_key") is.NoErr(ioutil.WriteFile(file, []byte("secret"), 0o660)) defer os.Clearenv() os.Setenv("SECRET_KEY", file) cfg := config{} is.NoErr(Parse(&cfg)) is.Equal("secret", cfg.SecretKey) } func TestFileNoParam(t *testing.T) { is := is.New(t) type config struct { SecretKey string `env:"SECRET_KEY,file"` } defer os.Clearenv() cfg := config{} is.NoErr(Parse(&cfg)) } func TestFileNoParamRequired(t *testing.T) { type config struct { SecretKey string `env:"SECRET_KEY,file,required"` } isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "SECRET_KEY" is not set`) } func TestFileBadFile(t *testing.T) { type config struct { SecretKey string `env:"SECRET_KEY,file"` } filename := "not-a-real-file" defer os.Clearenv() os.Setenv("SECRET_KEY", filename) oserr := "no such file or directory" if runtime.GOOS == "windows" { oserr = "The system cannot find the file specified." } isErrorWithMessage(t, Parse(&config{}), fmt.Sprintf(`env: could not load content of file "%s" from variable SECRET_KEY: open %s: %s`, filename, filename, oserr)) } func TestFileWithDefault(t *testing.T) { is := is.New(t) type config struct { SecretKey string `env:"SECRET_KEY,file" envDefault:"${FILE}" envExpand:"true"` } defer os.Clearenv() dir := t.TempDir() file := filepath.Join(dir, "sec_key") is.NoErr(ioutil.WriteFile(file, []byte("secret"), 0o660)) defer os.Clearenv() os.Setenv("FILE", file) cfg := config{} is.NoErr(Parse(&cfg)) is.Equal("secret", cfg.SecretKey) } func TestCustomSliceType(t *testing.T) { is := is.New(t) type customslice []byte type config struct { SecretKey customslice `env:"SECRET_KEY"` } parsecustomsclice := func(value string) (interface{}, error) { return customslice(value), nil } defer os.Clearenv() os.Setenv("SECRET_KEY", "somesecretkey") var cfg config is.NoErr(ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{reflect.TypeOf(customslice{}): parsecustomsclice})) } func TestBlankKey(t *testing.T) { is := is.New(t) type testStruct struct { Blank string BlankWithTag string `env:""` } val := testStruct{} defer os.Clearenv() os.Setenv("", "You should not see this") is.NoErr(Parse(&val)) is.Equal("", val.Blank) is.Equal("", val.BlankWithTag) } 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 } func TestCustomTimeParser(t *testing.T) { is := is.New(t) type config struct { SomeTime MyTime `env:"SOME_TIME"` } os.Setenv("SOME_TIME", "2021-05-06") defer os.Unsetenv("SOME_TIME") var cfg config is.NoErr(Parse(&cfg)) is.Equal(2021, time.Time(cfg.SomeTime).Year()) is.Equal(time.Month(5), time.Time(cfg.SomeTime).Month()) is.Equal(6, time.Time(cfg.SomeTime).Day()) } func TestRequiredIfNoDefOption(t *testing.T) { type Tree struct { Fruit string `env:"FRUIT"` } type config struct { Name string `env:"NAME"` Genre string `env:"GENRE" envDefault:"Unknown"` Tree } var cfg config t.Run("missing", func(t *testing.T) { isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "NAME" is not set`) os.Setenv("NAME", "John") t.Cleanup(os.Clearenv) isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "FRUIT" is not set`) }) t.Run("all set", func(t *testing.T) { os.Setenv("NAME", "John") os.Setenv("FRUIT", "Apple") t.Cleanup(os.Clearenv) // should not trigger an error for the missing 'GENRE' env because it has a default value. is.New(t).NoErr(Parse(&cfg, Options{RequiredIfNoDef: true})) }) } func isErrorWithMessage(tb testing.TB, err error, msg string) { tb.Helper() is := is.New(tb) is.True(err != nil) // should have failed is.Equal(err.Error(), msg) // should have the expected message } env-6.7.1/env_unix.go000066400000000000000000000004011411541444300144730ustar00rootroot00000000000000// +build darwin dragonfly freebsd linux netbsd openbsd solaris package env import "strings" func toMap(env []string) map[string]string { r := map[string]string{} for _, e := range env { p := strings.SplitN(e, "=", 2) r[p[0]] = p[1] } return r } env-6.7.1/env_windows.go000066400000000000000000000011061411541444300152050ustar00rootroot00000000000000package env import "strings" func toMap(env []string) map[string]string { r := map[string]string{} for _, e := range env { p := strings.SplitN(e, "=", 2) // On Windows, environment variables can start with '='. If so, Split at next character. // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 prefixEqualSign := false if len(e) > 0 && e[0] == '=' { e = e[1:] prefixEqualSign = true } p = strings.SplitN(e, "=", 2) if prefixEqualSign { p[0] = "=" + p[0] } r[p[0]] = p[1] } return r } env-6.7.1/env_windows_test.go000066400000000000000000000010561411541444300162500ustar00rootroot00000000000000package env import ( "testing" "github.com/matryer/is" ) // On Windows, environment variables can start with '='. This test verifies this behavior without relying on a Windows environment. // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 func TestToMapWindows(t *testing.T) { is := is.New(t) envVars := []string{"=::=::\\", "=C:=C:\\test", "VAR=REGULARVAR"} result := toMap(envVars) is.Equal(map[string]string{ "=::": "::\\", "=C:": "C:\\test", "VAR": "REGULARVAR", }, result) } env-6.7.1/go.mod000066400000000000000000000001211411541444300134160ustar00rootroot00000000000000module github.com/caarlos0/env/v6 require github.com/matryer/is v1.4.0 go 1.17 env-6.7.1/go.sum000066400000000000000000000002411411541444300134460ustar00rootroot00000000000000github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=