pax_global_header00006660000000000000000000000064142427246720014524gustar00rootroot0000000000000052 comment=44bec41b0cd577f4858e86e5271dce8f5fea65b6 gotenv-1.3.0/000077500000000000000000000000001424272467200130275ustar00rootroot00000000000000gotenv-1.3.0/.env000066400000000000000000000000141424272467200136130ustar00rootroot00000000000000HELLO=world gotenv-1.3.0/.env.invalid000066400000000000000000000000101424272467200152340ustar00rootroot00000000000000lol$wut gotenv-1.3.0/.github/000077500000000000000000000000001424272467200143675ustar00rootroot00000000000000gotenv-1.3.0/.github/workflows/000077500000000000000000000000001424272467200164245ustar00rootroot00000000000000gotenv-1.3.0/.github/workflows/workflow.yml000066400000000000000000000017011424272467200210200ustar00rootroot00000000000000name: Go workflow on: [push] jobs: test: name: Test Go ${{ matrix.go }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] go: ['1.13', '1.14', '1.15', '1.16', '1.17', '1.18'] steps: - name: Go ${{ matrix.go }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go }} - name: Checkout source code uses: actions/checkout@master - name: Get dependencies run: go get -t -v - name: Run test if: matrix.os == 'windows-latest' run: go test -v - name: Run test coverage if: matrix.os != 'windows-latest' run: go test -v -coverprofile=coverage.out -covermode=count - name: Publish coverage if: matrix.os != 'windows-latest' env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: bash <(curl -s https://codecov.io/bash) gotenv-1.3.0/.gitignore000066400000000000000000000000331424272467200150130ustar00rootroot00000000000000*.test *.out annotate.json gotenv-1.3.0/CHANGELOG.md000066400000000000000000000022431424272467200146410ustar00rootroot00000000000000# Changelog ## [1.3.0] - 2022-05-23 ### Added - Support = within double-quoted strings - Add support for multiline values ### Changed - `OverLoad` prefer environment variables over local variables ## [1.2.0] - 2019-08-03 ### Added - Add `Must` helper to raise an error as panic. It can be used with `Load` and `OverLoad`. - Add more tests to be 100% coverage. - Add CHANGELOG - Add more OS for the test: OSX and Windows ### Changed - Reduce complexity and improve source code for having `A+` score in [goreportcard](https://goreportcard.com/report/github.com/subosito/gotenv). - Updated README with mentions to all available functions ### Removed - Remove `ErrFormat` - Remove `MustLoad` and `MustOverload`, replaced with `Must` helper. ## [1.1.1] - 2018-06-05 ### Changed - Replace `os.Getenv` with `os.LookupEnv` to ensure that the environment variable is not set, by [radding](https://github.com/radding) ## [1.1.0] - 2017-03-20 ### Added - Supports carriage return in env - Handle files with UTF-8 BOM ### Changed - Whitespace handling ### Fixed - Incorrect variable expansion - Handling escaped '$' characters ## [1.0.0] - 2014-10-05 First stable release. gotenv-1.3.0/LICENSE000066400000000000000000000020721424272467200140350ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Alif Rachmawadi 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. gotenv-1.3.0/README.md000066400000000000000000000076341424272467200143200ustar00rootroot00000000000000# gotenv [![Build Status](https://github.com/subosito/gotenv/workflows/Go%20workflow/badge.svg)](https://github.com/subosito/gotenv/actions) [![Coverage Status](https://badgen.net/codecov/c/github/subosito/gotenv)](https://codecov.io/gh/subosito/gotenv) [![Go Report Card](https://goreportcard.com/badge/github.com/subosito/gotenv)](https://goreportcard.com/report/github.com/subosito/gotenv) [![GoDoc](https://godoc.org/github.com/subosito/gotenv?status.svg)](https://godoc.org/github.com/subosito/gotenv) Load environment variables from `.env` or `io.Reader` in Go. ## Usage Put the gotenv package on your `import` statement: ```go import "github.com/subosito/gotenv" ``` To modify your app environment variables, `gotenv` expose 2 main functions: - `gotenv.Load` - `gotenv.Apply` By default, `gotenv.Load` will look for a file called `.env` in the current working directory. Behind the scene, it will then load `.env` file and export the valid variables to the environment variables. Make sure you call the method as soon as possible to ensure it loads all variables, say, put it on `init()` function. Once loaded you can use `os.Getenv()` to get the value of the variable. Let's say you have `.env` file: ``` APP_ID=1234567 APP_SECRET=abcdef ``` Here's the example of your app: ```go package main import ( "github.com/subosito/gotenv" "log" "os" ) func init() { gotenv.Load() } func main() { log.Println(os.Getenv("APP_ID")) // "1234567" log.Println(os.Getenv("APP_SECRET")) // "abcdef" } ``` You can also load other than `.env` file if you wish. Just supply filenames when calling `Load()`. It will load them in order and the first value set for a variable will win.: ```go gotenv.Load(".env.production", "credentials") ``` While `gotenv.Load` loads entries from `.env` file, `gotenv.Apply` allows you to use any `io.Reader`: ```go gotenv.Apply(strings.NewReader("APP_ID=1234567")) log.Println(os.Getenv("APP_ID")) // Output: "1234567" ``` Both `gotenv.Load` and `gotenv.Apply` **DO NOT** overrides existing environment variables. If you want to override existing ones, you can see section below. ### Environment Overrides Besides above functions, `gotenv` also provides another functions that overrides existing: - `gotenv.OverLoad` - `gotenv.OverApply` Here's the example of this overrides behavior: ```go os.Setenv("HELLO", "world") // NOTE: using Apply existing value will be reserved gotenv.Apply(strings.NewReader("HELLO=universe")) fmt.Println(os.Getenv("HELLO")) // Output: "world" // NOTE: using OverApply existing value will be overridden gotenv.OverApply(strings.NewReader("HELLO=universe")) fmt.Println(os.Getenv("HELLO")) // Output: "universe" ``` ### Throw a Panic Both `gotenv.Load` and `gotenv.OverLoad` returns an error on something wrong occurred, like your env file is not exist, and so on. To make it easier to use, `gotenv` also provides `gotenv.Must` helper, to let it panic when an error returned. ```go err := gotenv.Load(".env-is-not-exist") fmt.Println("error", err) // error: open .env-is-not-exist: no such file or directory gotenv.Must(gotenv.Load, ".env-is-not-exist") // it will throw a panic // panic: open .env-is-not-exist: no such file or directory ``` ### Another Scenario Just in case you want to parse environment variables from any `io.Reader`, gotenv keeps its `Parse` and `StrictParse` function as public API so you can use that. ```go // import "strings" pairs := gotenv.Parse(strings.NewReader("FOO=test\nBAR=$FOO")) // gotenv.Env{"FOO": "test", "BAR": "test"} pairs, err := gotenv.StrictParse(strings.NewReader(`FOO="bar"`)) // gotenv.Env{"FOO": "bar"} ``` `Parse` ignores invalid lines and returns `Env` of valid environment variables, while `StrictParse` returns an error for invalid lines. ## Notes The gotenv package is a Go port of [`dotenv`](https://github.com/bkeepers/dotenv) project with some additions made for Go. For general features, it aims to be compatible as close as possible. gotenv-1.3.0/fixtures/000077500000000000000000000000001424272467200147005ustar00rootroot00000000000000gotenv-1.3.0/fixtures/bom.env000066400000000000000000000000141424272467200161620ustar00rootroot00000000000000BOM=UTF-8gotenv-1.3.0/fixtures/exported.env000066400000000000000000000022101424272467200172370ustar00rootroot00000000000000export OPTION_A=2 export OPTION_B='\n' # This is a comment export OPTION_C='The MIT License (MIT) Copyright (c) 2013 Alif Rachmawadi 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.' gotenv-1.3.0/fixtures/plain.env000066400000000000000000000000731424272467200165150ustar00rootroot00000000000000OPTION_A=1 OPTION_B=2 OPTION_C= 3 OPTION_D =4 OPTION_E = 5 gotenv-1.3.0/fixtures/quoted.env000066400000000000000000000003511424272467200167120ustar00rootroot00000000000000OPTION_A='1' OPTION_B='2' OPTION_C='' OPTION_D='\n' OPTION_E="1" OPTION_F="2" OPTION_G="" OPTION_H="\n" OPTION_I="some multi-line text with \"escaped quotes\" and ${OPTION_A} variable" OPTION_J='some$pecial$1$2!*chars=qweq""e$$\$""' gotenv-1.3.0/fixtures/vars.env000066400000000000000000000000511424272467200163610ustar00rootroot00000000000000A="fromFile" B="$A" D="$C" C="fromFile" gotenv-1.3.0/fixtures/yaml.env000066400000000000000000000000661424272467200163560ustar00rootroot00000000000000OPTION_A: 1 OPTION_B: '2' OPTION_C: '' OPTION_D: '\n' gotenv-1.3.0/go.mod000066400000000000000000000004061424272467200141350ustar00rootroot00000000000000module github.com/subosito/gotenv go 1.18 require github.com/stretchr/testify v1.7.0 require ( github.com/davecgh/go-spew v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) gotenv-1.3.0/go.sum000066400000000000000000000020001424272467200141520ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotenv-1.3.0/gotenv.go000066400000000000000000000152651424272467200146710ustar00rootroot00000000000000// Package gotenv provides functionality to dynamically load the environment variables package gotenv import ( "bufio" "fmt" "io" "os" "regexp" "strings" ) const ( // Pattern for detecting valid line format linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z` // Pattern for detecting valid variable within a value variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)` // Byte order mark character bom = "\xef\xbb\xbf" ) // Env holds key/value pair of valid environment variable type Env map[string]string /* Load is a function to load a file or multiple files and then export the valid variables into environment variables if they do not exist. When it's called with no argument, it will load `.env` file on the current path and set the environment variables. Otherwise, it will loop over the filenames parameter and set the proper environment variables. */ func Load(filenames ...string) error { return loadenv(false, filenames...) } /* OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables. */ func OverLoad(filenames ...string) error { return loadenv(true, filenames...) } /* Must is wrapper function that will panic when supplied function returns an error. */ func Must(fn func(filenames ...string) error, filenames ...string) { if err := fn(filenames...); err != nil { panic(err.Error()) } } /* Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist. */ func Apply(r io.Reader) error { return parset(r, false) } /* OverApply is a function to load an io Reader then export and override the valid variables into environment variables. */ func OverApply(r io.Reader) error { return parset(r, true) } func loadenv(override bool, filenames ...string) error { if len(filenames) == 0 { filenames = []string{".env"} } for _, filename := range filenames { f, err := os.Open(filename) if err != nil { return err } err = parset(f, override) if err != nil { return err } f.Close() } return nil } // parse and set :) func parset(r io.Reader, override bool) error { env, err := strictParse(r, override) if err != nil { return err } for key, val := range env { setenv(key, val, override) } return nil } func setenv(key, val string, override bool) { if override { os.Setenv(key, val) } else { if _, present := os.LookupEnv(key); !present { os.Setenv(key, val) } } } // Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables. // It expands the value of a variable from the environment variable but does not set the value to the environment itself. // This function is skipping any invalid lines and only processing the valid one. func Parse(r io.Reader) Env { env, _ := strictParse(r, false) return env } // StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables. // It expands the value of a variable from the environment variable but does not set the value to the environment itself. // This function is returning an error if there are any invalid lines. func StrictParse(r io.Reader) (Env, error) { return strictParse(r, false) } func strictParse(r io.Reader, override bool) (Env, error) { env := make(Env) scanner := bufio.NewScanner(r) firstLine := true for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if firstLine { line = strings.TrimPrefix(line, bom) firstLine = false } if line == "" || line[0] == '#' { continue } quote := "" idx := strings.Index(line, "=") if idx == -1 { idx = strings.Index(line, ":") } if idx > 0 && idx < len(line)-1 { val := strings.TrimSpace(line[idx+1:]) if val[0] == '"' || val[0] == '\'' { quote = val[:1] idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote) if idx >= 0 && val[idx] != '\\' { quote = "" } } } for quote != "" && scanner.Scan() { l := scanner.Text() line += "\n" + l idx := strings.LastIndex(l, quote) if idx > 0 && l[idx-1] == '\\' { continue } if idx >= 0 { quote = "" } } if quote != "" { return env, fmt.Errorf("missing quotes") } err := parseLine(line, env, override) if err != nil { return env, err } } return env, nil } var ( lineRgx = regexp.MustCompile(linePattern) unescapeRgx = regexp.MustCompile(`\\([^$])`) varRgx = regexp.MustCompile(variablePattern) ) func parseLine(s string, env Env, override bool) error { rm := lineRgx.FindStringSubmatch(s) if len(rm) == 0 { return checkFormat(s, env) } key := rm[1] val := rm[2] // trim whitespace val = strings.TrimSpace(val) // determine if string has quote prefix hdq := strings.HasPrefix(val, `"`) // determine if string has single quote prefix hsq := strings.HasPrefix(val, `'`) // remove quotes '' or "" if l := len(val); (hsq || hdq) && l >= 2 { val = val[1 : l-1] } if hdq { val = strings.ReplaceAll(val, `\n`, "\n") val = strings.ReplaceAll(val, `\r`, "\r") // Unescape all characters except $ so variables can be escaped properly val = unescapeRgx.ReplaceAllString(val, "$1") } fv := func(s string) string { return varReplacement(s, hsq, env, override) } if !hsq { val = varRgx.ReplaceAllStringFunc(val, fv) val = parseVal(val, env, hdq, override) } env[key] = val return nil } func parseExport(st string, env Env) error { if strings.HasPrefix(st, "export") { vs := strings.SplitN(st, " ", 2) if len(vs) > 1 { if _, ok := env[vs[1]]; !ok { return fmt.Errorf("line `%s` has an unset variable", st) } } } return nil } var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`) func varReplacement(s string, hsq bool, env Env, override bool) string { if strings.HasPrefix(s, "\\") { return strings.TrimPrefix(s, "\\") } if hsq { return s } mn := varNameRgx.FindStringSubmatch(s) if len(mn) == 0 { return s } v := mn[3] if replace, ok := os.LookupEnv(v); ok && !override { return replace } replace, ok := env[v] if !ok { replace = os.Getenv(v) } return replace } func checkFormat(s string, env Env) error { st := strings.TrimSpace(s) if (st == "") || strings.HasPrefix(st, "#") { return nil } if err := parseExport(st, env); err != nil { return err } return fmt.Errorf("line `%s` doesn't match format", s) } func parseVal(val string, env Env, ignoreNewlines bool, override bool) string { if strings.Contains(val, "=") && !ignoreNewlines { kv := strings.Split(val, "\r") if len(kv) > 1 { val = kv[0] for _, l := range kv[1:] { _ = parseLine(l, env, override) } } } return val } gotenv-1.3.0/gotenv_test.go000066400000000000000000000252711424272467200157260ustar00rootroot00000000000000package gotenv_test import ( "bufio" "os" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/subosito/gotenv" ) var formats = []struct { in string out gotenv.Env preset bool }{ // parses unquoted values {`FOO=bar`, gotenv.Env{"FOO": "bar"}, false}, // parses values with spaces around equal sign {`FOO =bar`, gotenv.Env{"FOO": "bar"}, false}, {`FOO= bar`, gotenv.Env{"FOO": "bar"}, false}, // parses values with leading spaces {` FOO=bar`, gotenv.Env{"FOO": "bar"}, false}, // parses values with following spaces {`FOO=bar `, gotenv.Env{"FOO": "bar"}, false}, // parses double quoted values {`FOO="bar"`, gotenv.Env{"FOO": "bar"}, false}, // parses double quoted values with following spaces {`FOO="bar" `, gotenv.Env{"FOO": "bar"}, false}, // parses single quoted values {`FOO='bar'`, gotenv.Env{"FOO": "bar"}, false}, // parses single quoted values with following spaces {`FOO='bar' `, gotenv.Env{"FOO": "bar"}, false}, // parses escaped double quotes {`FOO="escaped\"bar"`, gotenv.Env{"FOO": `escaped"bar`}, false}, // parses empty values {`FOO=`, gotenv.Env{"FOO": ""}, false}, // expands variables found in values {"FOO=test\nBAR=$FOO", gotenv.Env{"FOO": "test", "BAR": "test"}, false}, // parses variables wrapped in brackets {"FOO=test\nBAR=${FOO}bar", gotenv.Env{"FOO": "test", "BAR": "testbar"}, false}, // reads variables from ENV when expanding if not found in local env {`BAR=$FOO`, gotenv.Env{"BAR": "test"}, true}, // expands undefined variables to an empty string {`BAR=$FOO`, gotenv.Env{"BAR": ""}, false}, // expands variables in quoted strings {"FOO=test\nBAR=\"quote $FOO\"", gotenv.Env{"FOO": "test", "BAR": "quote test"}, false}, // does not expand variables in single quoted strings {"BAR='quote $FOO'", gotenv.Env{"BAR": "quote $FOO"}, false}, // does not expand escaped variables {`FOO="foo\$BAR"`, gotenv.Env{"FOO": "foo$BAR"}, false}, {`FOO="foo\${BAR}"`, gotenv.Env{"FOO": "foo${BAR}"}, false}, {"FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"", gotenv.Env{"FOO": "test", "BAR": "foo${FOO} test"}, false}, // parses yaml style options {"OPTION_A: 1", gotenv.Env{"OPTION_A": "1"}, false}, // parses export keyword {"export OPTION_A=2", gotenv.Env{"OPTION_A": "2"}, false}, // allows export line if you want to do it that way {"OPTION_A=2\nexport OPTION_A", gotenv.Env{"OPTION_A": "2"}, false}, // expands newlines in quoted strings {`FOO="bar\nbaz"`, gotenv.Env{"FOO": "bar\nbaz"}, false}, // parses variables with "." in the name {`FOO.BAR=foobar`, gotenv.Env{"FOO.BAR": "foobar"}, false}, // strips unquoted values {`foo=bar `, gotenv.Env{"foo": "bar"}, false}, // not 'bar ' // ignores empty lines {"\n \t \nfoo=bar\n \nfizz=buzz", gotenv.Env{"foo": "bar", "fizz": "buzz"}, false}, // ignores inline comments {"foo=bar # this is foo", gotenv.Env{"foo": "bar"}, false}, // allows # in quoted value {`foo="bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false}, // ignores comment lines {"\n\n\n # HERE GOES FOO \nfoo=bar", gotenv.Env{"foo": "bar"}, false}, // parses # in quoted values {`foo="ba#r"`, gotenv.Env{"foo": "ba#r"}, false}, {"foo='ba#r'", gotenv.Env{"foo": "ba#r"}, false}, // parses # in quoted values with following spaces {`foo="ba#r" `, gotenv.Env{"foo": "ba#r"}, false}, {`foo='ba#r' `, gotenv.Env{"foo": "ba#r"}, false}, // supports carriage return {"FOO=bar\rbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false}, // supports carriage return combine with new line {"FOO=bar\r\nbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false}, // expands carriage return in quoted strings {`FOO="bar\rbaz"`, gotenv.Env{"FOO": "bar\rbaz"}, false}, // escape $ properly when no alphabets/numbers/_ are followed by it {`FOO="bar\\$ \\$\\$"`, gotenv.Env{"FOO": "bar$ $$"}, false}, // ignore $ when it is not escaped and no variable is followed by it {`FOO="bar $ "`, gotenv.Env{"FOO": "bar $ "}, false}, // parses unquoted values with spaces after separator {`FOO= bar`, gotenv.Env{"FOO": "bar"}, false}, // allows # in quoted value with spaces after separator {`foo= "bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false}, // allows = in double quoted values with newlines (typically base64 padding) {`foo="---\na==\n---"`, gotenv.Env{"foo": "---\na==\n---"}, false}, } var errorFormats = []struct { in string out gotenv.Env err string }{ // allows export line if you want to do it that way and checks for unset variables {"OPTION_A=2\nexport OH_NO_NOT_SET", gotenv.Env{"OPTION_A": "2"}, "line `export OH_NO_NOT_SET` has an unset variable"}, // throws an error if line format is incorrect {`lol$wut`, gotenv.Env{}, "line `lol$wut` doesn't match format"}, } var fixtures = []struct { filename string results gotenv.Env }{ { "fixtures/exported.env", gotenv.Env{ "OPTION_A": "2", "OPTION_B": `\n`, "OPTION_C": `The MIT License (MIT) Copyright (c) 2013 Alif Rachmawadi 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.`, }, }, { "fixtures/plain.env", gotenv.Env{ "OPTION_A": "1", "OPTION_B": "2", "OPTION_C": "3", "OPTION_D": "4", "OPTION_E": "5", }, }, { "fixtures/quoted.env", gotenv.Env{ "OPTION_A": "1", "OPTION_B": "2", "OPTION_C": "", "OPTION_D": `\n`, "OPTION_E": "1", "OPTION_F": "2", "OPTION_G": "", "OPTION_H": "\n", "OPTION_I": `some multi-line text with "escaped quotes" and 1 variable`, "OPTION_J": `some$pecial$1$2!*chars=qweq""e$$\$""`, }, }, { "fixtures/yaml.env", gotenv.Env{ "OPTION_A": "1", "OPTION_B": "2", "OPTION_C": "", "OPTION_D": `\n`, }, }, } func TestParse(t *testing.T) { for _, tt := range formats { if tt.preset { os.Setenv("FOO", "test") } exp := gotenv.Parse(strings.NewReader(tt.in)) assert.Equal(t, tt.out, exp) os.Clearenv() } } func TestStrictParse(t *testing.T) { for _, tt := range errorFormats { env, err := gotenv.StrictParse(strings.NewReader(tt.in)) assert.Equal(t, tt.err, err.Error()) assert.Equal(t, tt.out, env) } } func TestLoad(t *testing.T) { for _, tt := range fixtures { err := gotenv.Load(tt.filename) assert.Nil(t, err) for key, val := range tt.results { assert.Equal(t, val, os.Getenv(key)) } os.Clearenv() } } func TestLoad_default(t *testing.T) { k := "HELLO" v := "world" err := gotenv.Load() assert.Nil(t, err) assert.Equal(t, v, os.Getenv(k)) os.Clearenv() } func TestLoad_overriding(t *testing.T) { k := "HELLO" v := "universe" os.Setenv(k, v) err := gotenv.Load() assert.Nil(t, err) assert.Equal(t, v, os.Getenv(k)) os.Clearenv() } func TestLoad_overrideVars(t *testing.T) { os.Setenv("A", "fromEnv") err := gotenv.Load("fixtures/vars.env") assert.Nil(t, err) assert.Equal(t, "fromEnv", os.Getenv("B")) os.Clearenv() } func TestLoad_overrideVars2(t *testing.T) { os.Setenv("C", "fromEnv") err := gotenv.Load("fixtures/vars.env") assert.Nil(t, err) assert.Equal(t, "fromEnv", os.Getenv("D")) os.Clearenv() } func TestLoad_Env(t *testing.T) { err := gotenv.Load(".env.invalid") assert.NotNil(t, err) } func TestLoad_nonExist(t *testing.T) { file := ".env.not.exist" err := gotenv.Load(file) if err == nil { t.Errorf("gotenv.Load(`%s`) => error: `no such file or directory` != nil", file) } } func TestLoad_unicodeBOMFixture(t *testing.T) { file := "fixtures/bom.env" f, err := os.Open(file) assert.Nil(t, err) scanner := bufio.NewScanner(f) i := 1 bom := string([]byte{239, 187, 191}) for scanner.Scan() { if i == 1 { line := scanner.Text() assert.True(t, strings.HasPrefix(line, bom)) } } } func TestLoad_unicodeBOM(t *testing.T) { file := "fixtures/bom.env" err := gotenv.Load(file) assert.Nil(t, err) assert.Equal(t, "UTF-8", os.Getenv("BOM")) os.Clearenv() } func TestMust_Load(t *testing.T) { for _, tt := range fixtures { assert.NotPanics(t, func() { gotenv.Must(gotenv.Load, tt.filename) for key, val := range tt.results { assert.Equal(t, val, os.Getenv(key)) } os.Clearenv() }, "Caling gotenv.Must with gotenv.Load should NOT panic") } } func TestMust_Load_default(t *testing.T) { assert.NotPanics(t, func() { gotenv.Must(gotenv.Load) tkey := "HELLO" val := "world" assert.Equal(t, val, os.Getenv(tkey)) os.Clearenv() }, "Caling gotenv.Must with gotenv.Load without arguments should NOT panic") } func TestMust_Load_nonExist(t *testing.T) { assert.Panics(t, func() { gotenv.Must(gotenv.Load, ".env.not.exist") }, "Caling gotenv.Must with gotenv.Load and non exist file SHOULD panic") } func TestOverLoad_overriding(t *testing.T) { k := "HELLO" v := "universe" os.Setenv(k, v) err := gotenv.OverLoad() assert.Nil(t, err) assert.Equal(t, "world", os.Getenv(k)) os.Clearenv() } func TestOverLoad_overrideVars(t *testing.T) { os.Setenv("A", "fromEnv") err := gotenv.OverLoad("fixtures/vars.env") assert.Nil(t, err) assert.Equal(t, "fromFile", os.Getenv("B")) os.Clearenv() } func TestOverLoad_overrideVars2(t *testing.T) { os.Setenv("C", "fromEnv") err := gotenv.OverLoad("fixtures/vars.env") assert.Nil(t, err) // The value for D is not "fromFile" because C is defined after the // definition of D. assert.Equal(t, "fromEnv", os.Getenv("D"), "C defined after usage") os.Clearenv() } func TestMustOverLoad_nonExist(t *testing.T) { assert.Panics(t, func() { gotenv.Must(gotenv.OverLoad, ".env.not.exist") }, "Caling gotenv.Must with Overgotenv.Load and non exist file SHOULD panic") } func TestApply(t *testing.T) { os.Setenv("HELLO", "world") r := strings.NewReader("HELLO=universe") err := gotenv.Apply(r) assert.Nil(t, err) assert.Equal(t, "world", os.Getenv("HELLO")) os.Clearenv() } func TestOverApply(t *testing.T) { os.Setenv("HELLO", "world") r := strings.NewReader("HELLO=universe") err := gotenv.OverApply(r) assert.Nil(t, err) assert.Equal(t, "universe", os.Getenv("HELLO")) os.Clearenv() }