• Stars
    star
    4,722
  • Rank 8,960 (Top 0.2 %)
  • Language
    Go
  • License
    MIT License
  • Created over 9 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

env

Build Status Coverage Status

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

Used and supported by

encore icon

Encore – the platform for building Go-based cloud backends.

Example

Get the module with:

go get github.com/caarlos0/env/v11

The usage looks like this:

package main

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)
	}

	fmt.Printf("%+v\n", cfg)
}

You can run it like this:

$ 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

Caution

Unexported fields will be ignored by env. This is by design and will not change.

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
  • time.Duration
  • encoding.TextUnmarshaler
  • url.URL

Pointers, slices and slices of pointers, and maps of those types are also supported.

You can also use/define a custom parser func 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 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:

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:

type config struct {
	SecretKey string `env:"SECRET_KEY,required"`
}

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.

Expand vars

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:

type config struct {
	SecretKey string `env:"SECRET_KEY,expand"`
}

This also works with envDefault:

import (
	"fmt"
	"github.com/caarlos0/env/v11"
)

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:

$ PORT=8080 go run main.go
{Host:localhost Port:8080 Address:localhost:8080}

Not Empty fields

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:

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:

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:

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)
}
$ 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

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 as environment variable name.

Here's an example:

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.

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

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:

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)
}

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:

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

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.

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:

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().

Related projects

  • envdoc - generate documentation for environment variables from env tags

Stargazers over time

Stargazers over time

More Repositories

1

starcharts

Plot your repository stars over time.
Go
1,016
star
2

dotfiles.zsh

Config files for ZSH, Java, Ruby, Go, Editors, Terminals and more.
Shell
700
star
3

svu

Semantic Version Util
Go
529
star
4

timer

A `sleep` with progress
Go
406
star
5

tasktimer

Task Timer (tt) is a dead simple TUI task timer
Go
398
star
6

dotfiles.fish

my dotfiles
Lua
306
star
7

fork-cleaner

Quickly clean up unused forks on your github account.
Go
261
star
8

domain_exporter

Exports the expiration time of your domains as prometheus metrics.
Go
254
star
9

org-stats

Get the contributor stats summary from all repos of any given organization
Go
180
star
10

dotfiles

❄️ my configurations for home-manager, nixOS, and nix-darwin
Nix
105
star
11

twitter-cleaner

Automatically delete tweets, retweets, and favorites from your timeline, and, if provided, from your twitter archive as well.
Go
97
star
12

jsonfmt

Like gofmt, but for JSON files.
Go
78
star
13

clone-org

Clone all repos of a github organization
Go
65
star
14

parttysh

something really useful: a party parrot over ssh
Go
58
star
15

httperr

func(w http.ResponseWriter, r *http.Request) error
Go
55
star
16

log

Colorful CLI logger
Go
54
star
17

speedtest-exporter

Exports speedtest-cli metrics in the prometheus format
Go
51
star
18

timea.go

timea.go (did you see what I did there?) is a simple library to print given times in "time ago" manner.
Go
45
star
19

testfs

A simple fs.FS implementation to be used inside tests.
Go
33
star
20

name-generator

quick'n'dirty name generator using docker's namesgenerator pkg
Go
26
star
21

github_releases_exporter

Exports GitHub release metrics to the Prometheus format
Go
24
star
22

version_exporter

Monitor the versions of the things you run and care about
Go
24
star
23

discord-applemusic-rich-presence

Discord's Rich Presence from Apple Music
Go
22
star
24

go-version

Version library extracted from sigs.k8s.io/release-utils
Go
21
star
25

xdg-open-svc

xdg-open as a service
Go
19
star
26

awesome-twitter-communities

A list of awesome twitter communities
19
star
27

go-gumroad

Easily check licenses against Gumroad's API.
Go
19
star
28

karmahub

Compares the amount of issues and pull requests you created with the amount of comments and code reviews you did.
Go
17
star
29

go-sshagent

A simple SSH Agent implementation, written in Go, mainly to be used within tests.
Go
15
star
30

caarlos0

15
star
31

fastcom-exporter

Prometheus Fast.com exporter
Go
15
star
32

promfmt

format prometheus .rules files
Go
13
star
33

goreleaserfiles

my goreleaser.yml files
Go
13
star
34

duration

Copy of stdlib's time.Duration, but ParseDuration accepts other bigger units such as days, weeks, months and years
Go
13
star
35

nur

My Nix User Repository
Nix
13
star
36

sshmarshal

Code copied from x/crypto and golang/go#37132
Go
13
star
37

ctrlc

Go library that provides an easy way of having a task that is context-aware and deals with SIGINT and SIGTERM signals
Go
13
star
38

kube-shutdown-after

Shutdown pods in sandbox clusters when it's not work hours
Go
12
star
39

home

my home k3s cluster
12
star
40

mdtree

Convert markdown lists into ASCII trees
Go
12
star
41

homekit-amt8000

Homekit bridge for Intelbras AMT8000 Alarm Systems
Go
11
star
42

esp8266-garage-door

Homekit controller and firmware for an ESP8266-based automated garage door
Go
8
star
43

uhr

Zeichenorientierte Benutzerschnittstelle Uhr
Go
8
star
44

meta

reusable github actions workflows and other shareable configuration files 🫥
Go
7
star
45

solarman-exporter

Prometheus Exporter for SolarmanPV
Go
6
star
46

linktree

static linktree with tailwindcss and html
HTML
6
star
47

homekit-shelly

Homekit bridge for Shelly Flood and Smoke sensors
Go
5
star
48

homekit-solarman

Homekit bridge for Solarman Inverters
Go
5
star
49

sinkhole

docker image that just responds any path with 200 and does nothing, for testing
Go
5
star
50

advent-of-code-2022

Advent Of Code 2022 — In Rust, btw
Rust
5
star
51

teatest-example

example teatest app
Go
4
star
52

homebrew-tap

Homebrew Formulae to my binaries, powered by @goreleaser
Ruby
4
star
53

beckeros

Rust
3
star
54

ziglings

learning some zig :P
Zig
3
star
55

go-solarman

Go client for the SolarmanPV API
Go
2
star
56

sync

Simple sync primitives I use here and there, extracted so I can reuse them
Go
2
star
57

transactional

a sqlx transaction wrapper for http.Handler
Go
2
star
58

servers

ansible playbook for my servers
Jinja
2
star
59

.github

github config
1
star
60

dockerfiles

Dockerfile
1
star
61

fish-git-fetch-merge

git fetch && git merge in a breeze
Shell
1
star