New upstream version 1.2.0+git20190917.de67a66
Andreas Henriksson
4 years ago
0 | lol$wut |
0 | name: Go workflow | |
1 | on: [push] | |
2 | jobs: | |
3 | test: | |
4 | name: Test on ${{ matrix.os }} | |
5 | runs-on: ${{ matrix.os }} | |
6 | strategy: | |
7 | matrix: | |
8 | os: [ubuntu-latest, windows-latest, macos-latest] | |
9 | go: ['1.12', '1.13'] | |
10 | steps: | |
11 | - name: Go ${{ matrix.go }} | |
12 | uses: actions/setup-go@v1 | |
13 | with: | |
14 | version: ${{ matrix.go }} | |
15 | - name: Checkout source code | |
16 | uses: actions/checkout@master | |
17 | - name: Get dependencies | |
18 | run: go get -t -v | |
19 | - name: Run test | |
20 | run: go test -test.v -coverprofile=coverage.out -covermode=count | |
21 | - name: Publish coverage | |
22 | if: matrix.os != 'windows-latest' | |
23 | env: | |
24 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
25 | run: bash <(curl -s https://codecov.io/bash) |
0 | # Changelog | |
1 | ||
2 | ## [1.2.0] - 2019-08-03 | |
3 | ||
4 | ### Added | |
5 | ||
6 | - Add `Must` helper to raise an error as panic. It can be used with `Load` and `OverLoad`. | |
7 | - Add more tests to be 100% coverage. | |
8 | - Add CHANGELOG | |
9 | - Add more OS for the test: OSX and Windows | |
10 | ||
11 | ### Changed | |
12 | ||
13 | - Reduce complexity and improve source code for having `A+` score in [goreportcard](https://goreportcard.com/report/github.com/subosito/gotenv). | |
14 | - Updated README with mentions to all available functions | |
15 | ||
16 | ### Removed | |
17 | ||
18 | - Remove `ErrFormat` | |
19 | - Remove `MustLoad` and `MustOverload`, replaced with `Must` helper. | |
20 | ||
21 | ## [1.1.1] - 2018-06-05 | |
22 | ||
23 | ### Changed | |
24 | ||
25 | - Replace `os.Getenv` with `os.LookupEnv` to ensure that the environment variable is not set, by [radding](https://github.com/radding) | |
26 | ||
27 | ## [1.1.0] - 2017-03-20 | |
28 | ||
29 | ### Added | |
30 | ||
31 | - Supports carriage return in env | |
32 | - Handle files with UTF-8 BOM | |
33 | ||
34 | ### Changed | |
35 | ||
36 | - Whitespace handling | |
37 | ||
38 | ### Fixed | |
39 | ||
40 | - Incorrect variable expansion | |
41 | - Handling escaped '$' characters | |
42 | ||
43 | ## [1.0.0] - 2014-10-05 | |
44 | ||
45 | First stable release. | |
46 |
0 | The MIT License (MIT) | |
1 | ||
2 | Copyright (c) 2013 Alif Rachmawadi | |
3 | ||
4 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
5 | of this software and associated documentation files (the "Software"), to deal | |
6 | in the Software without restriction, including without limitation the rights | |
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
8 | copies of the Software, and to permit persons to whom the Software is | |
9 | furnished to do so, subject to the following conditions: | |
10 | ||
11 | The above copyright notice and this permission notice shall be included in | |
12 | all copies or substantial portions of the Software. | |
13 | ||
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
20 | THE SOFTWARE. |
0 | # gotenv | |
1 | ||
2 | [![Build Status](https://github.com/subosito/gotenv/workflows/Go%20workflow/badge.svg)](https://github.com/subosito/gotenv/actions) | |
3 | [![Coverage Status](https://badgen.net/codecov/c/github/subosito/gotenv)](https://codecov.io/gh/subosito/gotenv) | |
4 | [![Go Report Card](https://goreportcard.com/badge/github.com/subosito/gotenv)](https://goreportcard.com/report/github.com/subosito/gotenv) | |
5 | [![GoDoc](https://godoc.org/github.com/subosito/gotenv?status.svg)](https://godoc.org/github.com/subosito/gotenv) | |
6 | ||
7 | Load environment variables from `.env` or `io.Reader` in Go. | |
8 | ||
9 | ## Usage | |
10 | ||
11 | Put the gotenv package on your `import` statement: | |
12 | ||
13 | ```go | |
14 | import "github.com/subosito/gotenv" | |
15 | ``` | |
16 | ||
17 | To modify your app environment variables, `gotenv` expose 2 main functions: | |
18 | ||
19 | - `gotenv.Load` | |
20 | - `gotenv.Apply` | |
21 | ||
22 | By default, `gotenv.Load` will look for a file called `.env` in the current working directory. | |
23 | ||
24 | 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. | |
25 | ||
26 | Once loaded you can use `os.Getenv()` to get the value of the variable. | |
27 | ||
28 | Let's say you have `.env` file: | |
29 | ||
30 | ``` | |
31 | APP_ID=1234567 | |
32 | APP_SECRET=abcdef | |
33 | ``` | |
34 | ||
35 | Here's the example of your app: | |
36 | ||
37 | ```go | |
38 | package main | |
39 | ||
40 | import ( | |
41 | "github.com/subosito/gotenv" | |
42 | "log" | |
43 | "os" | |
44 | ) | |
45 | ||
46 | func init() { | |
47 | gotenv.Load() | |
48 | } | |
49 | ||
50 | func main() { | |
51 | log.Println(os.Getenv("APP_ID")) // "1234567" | |
52 | log.Println(os.Getenv("APP_SECRET")) // "abcdef" | |
53 | } | |
54 | ``` | |
55 | ||
56 | 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.: | |
57 | ||
58 | ```go | |
59 | gotenv.Load(".env.production", "credentials") | |
60 | ``` | |
61 | ||
62 | While `gotenv.Load` loads entries from `.env` file, `gotenv.Apply` allows you to use any `io.Reader`: | |
63 | ||
64 | ```go | |
65 | gotenv.Apply(strings.NewReader("APP_ID=1234567")) | |
66 | ||
67 | log.Println(os.Getenv("APP_ID")) | |
68 | // Output: "1234567" | |
69 | ``` | |
70 | ||
71 | Both `gotenv.Load` and `gotenv.Apply` **DO NOT** overrides existing environment variables. If you want to override existing ones, you can see section below. | |
72 | ||
73 | ### Environment Overrides | |
74 | ||
75 | Besides above functions, `gotenv` also provides another functions that overrides existing: | |
76 | ||
77 | - `gotenv.OverLoad` | |
78 | - `gotenv.OverApply` | |
79 | ||
80 | ||
81 | Here's the example of this overrides behavior: | |
82 | ||
83 | ```go | |
84 | os.Setenv("HELLO", "world") | |
85 | ||
86 | // NOTE: using Apply existing value will be reserved | |
87 | gotenv.Apply(strings.NewReader("HELLO=universe")) | |
88 | fmt.Println(os.Getenv("HELLO")) | |
89 | // Output: "world" | |
90 | ||
91 | // NOTE: using OverApply existing value will be overridden | |
92 | gotenv.OverApply(strings.NewReader("HELLO=universe")) | |
93 | fmt.Println(os.Getenv("HELLO")) | |
94 | // Output: "universe" | |
95 | ``` | |
96 | ||
97 | ### Throw a Panic | |
98 | ||
99 | 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. | |
100 | ||
101 | ```go | |
102 | err := gotenv.Load(".env-is-not-exist") | |
103 | fmt.Println("error", err) | |
104 | // error: open .env-is-not-exist: no such file or directory | |
105 | ||
106 | gotenv.Must(gotenv.Load, ".env-is-not-exist") | |
107 | // it will throw a panic | |
108 | // panic: open .env-is-not-exist: no such file or directory | |
109 | ``` | |
110 | ||
111 | ### Another Scenario | |
112 | ||
113 | 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. | |
114 | ||
115 | ```go | |
116 | // import "strings" | |
117 | ||
118 | pairs := gotenv.Parse(strings.NewReader("FOO=test\nBAR=$FOO")) | |
119 | // gotenv.Env{"FOO": "test", "BAR": "test"} | |
120 | ||
121 | err, pairs = gotenv.StrictParse(strings.NewReader(`FOO="bar"`)) | |
122 | // gotenv.Env{"FOO": "bar"} | |
123 | ``` | |
124 | ||
125 | `Parse` ignores invalid lines and returns `Env` of valid environment variables, while `StrictParse` returns an error for invalid lines. | |
126 | ||
127 | ## Notes | |
128 | ||
129 | 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. |
0 | BOM=UTF-8⏎ |
0 | OPTION_A='1' | |
1 | OPTION_B='2' | |
2 | OPTION_C='' | |
3 | OPTION_D='\n' | |
4 | OPTION_E="1" | |
5 | OPTION_F="2" | |
6 | OPTION_G="" | |
7 | OPTION_H="\n" |
0 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | |
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
2 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |
3 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |
4 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | |
5 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | |
6 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | |
7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | |
8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |
9 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= | |
10 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
0 | // Package gotenv provides functionality to dynamically load the environment variables | |
1 | package gotenv | |
2 | ||
3 | import ( | |
4 | "bufio" | |
5 | "fmt" | |
6 | "io" | |
7 | "os" | |
8 | "regexp" | |
9 | "strings" | |
10 | ) | |
11 | ||
12 | const ( | |
13 | // Pattern for detecting valid line format | |
14 | linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z` | |
15 | ||
16 | // Pattern for detecting valid variable within a value | |
17 | variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)` | |
18 | ) | |
19 | ||
20 | // Env holds key/value pair of valid environment variable | |
21 | type Env map[string]string | |
22 | ||
23 | /* | |
24 | 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. | |
25 | When it's called with no argument, it will load `.env` file on the current path and set the environment variables. | |
26 | Otherwise, it will loop over the filenames parameter and set the proper environment variables. | |
27 | */ | |
28 | func Load(filenames ...string) error { | |
29 | return loadenv(false, filenames...) | |
30 | } | |
31 | ||
32 | /* | |
33 | OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables. | |
34 | */ | |
35 | func OverLoad(filenames ...string) error { | |
36 | return loadenv(true, filenames...) | |
37 | } | |
38 | ||
39 | /* | |
40 | Must is wrapper function that will panic when supplied function returns an error. | |
41 | */ | |
42 | func Must(fn func(filenames ...string) error, filenames ...string) { | |
43 | if err := fn(filenames...); err != nil { | |
44 | panic(err.Error()) | |
45 | } | |
46 | } | |
47 | ||
48 | /* | |
49 | Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist. | |
50 | */ | |
51 | func Apply(r io.Reader) error { | |
52 | return parset(r, false) | |
53 | } | |
54 | ||
55 | /* | |
56 | OverApply is a function to load an io Reader then export and override the valid variables into environment variables. | |
57 | */ | |
58 | func OverApply(r io.Reader) error { | |
59 | return parset(r, true) | |
60 | } | |
61 | ||
62 | func loadenv(override bool, filenames ...string) error { | |
63 | if len(filenames) == 0 { | |
64 | filenames = []string{".env"} | |
65 | } | |
66 | ||
67 | for _, filename := range filenames { | |
68 | f, err := os.Open(filename) | |
69 | if err != nil { | |
70 | return err | |
71 | } | |
72 | ||
73 | err = parset(f, override) | |
74 | if err != nil { | |
75 | return err | |
76 | } | |
77 | ||
78 | f.Close() | |
79 | } | |
80 | ||
81 | return nil | |
82 | } | |
83 | ||
84 | // parse and set :) | |
85 | func parset(r io.Reader, override bool) error { | |
86 | env, err := StrictParse(r) | |
87 | if err != nil { | |
88 | return err | |
89 | } | |
90 | ||
91 | for key, val := range env { | |
92 | setenv(key, val, override) | |
93 | } | |
94 | ||
95 | return nil | |
96 | } | |
97 | ||
98 | func setenv(key, val string, override bool) { | |
99 | if override { | |
100 | os.Setenv(key, val) | |
101 | } else { | |
102 | if _, present := os.LookupEnv(key); !present { | |
103 | os.Setenv(key, val) | |
104 | } | |
105 | } | |
106 | } | |
107 | ||
108 | // Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables. | |
109 | // It expands the value of a variable from the environment variable but does not set the value to the environment itself. | |
110 | // This function is skipping any invalid lines and only processing the valid one. | |
111 | func Parse(r io.Reader) Env { | |
112 | env, _ := StrictParse(r) | |
113 | return env | |
114 | } | |
115 | ||
116 | // StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables. | |
117 | // It expands the value of a variable from the environment variable but does not set the value to the environment itself. | |
118 | // This function is returning an error if there are any invalid lines. | |
119 | func StrictParse(r io.Reader) (Env, error) { | |
120 | env := make(Env) | |
121 | scanner := bufio.NewScanner(r) | |
122 | ||
123 | i := 1 | |
124 | bom := string([]byte{239, 187, 191}) | |
125 | ||
126 | for scanner.Scan() { | |
127 | line := scanner.Text() | |
128 | ||
129 | if i == 1 { | |
130 | line = strings.TrimPrefix(line, bom) | |
131 | } | |
132 | ||
133 | i++ | |
134 | ||
135 | err := parseLine(line, env) | |
136 | if err != nil { | |
137 | return env, err | |
138 | } | |
139 | } | |
140 | ||
141 | return env, nil | |
142 | } | |
143 | ||
144 | func parseLine(s string, env Env) error { | |
145 | rl := regexp.MustCompile(linePattern) | |
146 | rm := rl.FindStringSubmatch(s) | |
147 | ||
148 | if len(rm) == 0 { | |
149 | return checkFormat(s, env) | |
150 | } | |
151 | ||
152 | key := rm[1] | |
153 | val := rm[2] | |
154 | ||
155 | // determine if string has quote prefix | |
156 | hdq := strings.HasPrefix(val, `"`) | |
157 | ||
158 | // determine if string has single quote prefix | |
159 | hsq := strings.HasPrefix(val, `'`) | |
160 | ||
161 | // trim whitespace | |
162 | val = strings.Trim(val, " ") | |
163 | ||
164 | // remove quotes '' or "" | |
165 | rq := regexp.MustCompile(`\A(['"])(.*)(['"])\z`) | |
166 | val = rq.ReplaceAllString(val, "$2") | |
167 | ||
168 | if hdq { | |
169 | val = strings.Replace(val, `\n`, "\n", -1) | |
170 | val = strings.Replace(val, `\r`, "\r", -1) | |
171 | ||
172 | // Unescape all characters except $ so variables can be escaped properly | |
173 | re := regexp.MustCompile(`\\([^$])`) | |
174 | val = re.ReplaceAllString(val, "$1") | |
175 | } | |
176 | ||
177 | rv := regexp.MustCompile(variablePattern) | |
178 | fv := func(s string) string { | |
179 | return varReplacement(s, hsq, env) | |
180 | } | |
181 | ||
182 | val = rv.ReplaceAllStringFunc(val, fv) | |
183 | val = parseVal(val, env) | |
184 | ||
185 | env[key] = val | |
186 | return nil | |
187 | } | |
188 | ||
189 | func parseExport(st string, env Env) error { | |
190 | if strings.HasPrefix(st, "export") { | |
191 | vs := strings.SplitN(st, " ", 2) | |
192 | ||
193 | if len(vs) > 1 { | |
194 | if _, ok := env[vs[1]]; !ok { | |
195 | return fmt.Errorf("line `%s` has an unset variable", st) | |
196 | } | |
197 | } | |
198 | } | |
199 | ||
200 | return nil | |
201 | } | |
202 | ||
203 | func varReplacement(s string, hsq bool, env Env) string { | |
204 | if strings.HasPrefix(s, "\\") { | |
205 | return strings.TrimPrefix(s, "\\") | |
206 | } | |
207 | ||
208 | if hsq { | |
209 | return s | |
210 | } | |
211 | ||
212 | sn := `(\$)(\{?([A-Z0-9_]+)\}?)` | |
213 | rn := regexp.MustCompile(sn) | |
214 | mn := rn.FindStringSubmatch(s) | |
215 | ||
216 | if len(mn) == 0 { | |
217 | return s | |
218 | } | |
219 | ||
220 | v := mn[3] | |
221 | ||
222 | replace, ok := env[v] | |
223 | if !ok { | |
224 | replace = os.Getenv(v) | |
225 | } | |
226 | ||
227 | return replace | |
228 | } | |
229 | ||
230 | func checkFormat(s string, env Env) error { | |
231 | st := strings.TrimSpace(s) | |
232 | ||
233 | if (st == "") || strings.HasPrefix(st, "#") { | |
234 | return nil | |
235 | } | |
236 | ||
237 | if err := parseExport(st, env); err != nil { | |
238 | return err | |
239 | } | |
240 | ||
241 | return fmt.Errorf("line `%s` doesn't match format", s) | |
242 | } | |
243 | ||
244 | func parseVal(val string, env Env) string { | |
245 | if strings.Contains(val, "=") { | |
246 | if !(val == "\n" || val == "\r") { | |
247 | kv := strings.Split(val, "\n") | |
248 | ||
249 | if len(kv) == 1 { | |
250 | kv = strings.Split(val, "\r") | |
251 | } | |
252 | ||
253 | if len(kv) > 1 { | |
254 | val = kv[0] | |
255 | ||
256 | for i := 1; i < len(kv); i++ { | |
257 | parseLine(kv[i], env) | |
258 | } | |
259 | } | |
260 | } | |
261 | } | |
262 | ||
263 | return val | |
264 | } |
0 | package gotenv_test | |
1 | ||
2 | import ( | |
3 | "bufio" | |
4 | "os" | |
5 | "strings" | |
6 | "testing" | |
7 | ||
8 | "github.com/stretchr/testify/assert" | |
9 | "github.com/subosito/gotenv" | |
10 | ) | |
11 | ||
12 | var formats = []struct { | |
13 | in string | |
14 | out gotenv.Env | |
15 | preset bool | |
16 | }{ | |
17 | // parses unquoted values | |
18 | {`FOO=bar`, gotenv.Env{"FOO": "bar"}, false}, | |
19 | ||
20 | // parses values with spaces around equal sign | |
21 | {`FOO =bar`, gotenv.Env{"FOO": "bar"}, false}, | |
22 | {`FOO= bar`, gotenv.Env{"FOO": "bar"}, false}, | |
23 | ||
24 | // parses values with leading spaces | |
25 | {` FOO=bar`, gotenv.Env{"FOO": "bar"}, false}, | |
26 | ||
27 | // parses values with following spaces | |
28 | {`FOO=bar `, gotenv.Env{"FOO": "bar"}, false}, | |
29 | ||
30 | // parses double quoted values | |
31 | {`FOO="bar"`, gotenv.Env{"FOO": "bar"}, false}, | |
32 | ||
33 | // parses double quoted values with following spaces | |
34 | {`FOO="bar" `, gotenv.Env{"FOO": "bar"}, false}, | |
35 | ||
36 | // parses single quoted values | |
37 | {`FOO='bar'`, gotenv.Env{"FOO": "bar"}, false}, | |
38 | ||
39 | // parses single quoted values with following spaces | |
40 | {`FOO='bar' `, gotenv.Env{"FOO": "bar"}, false}, | |
41 | ||
42 | // parses escaped double quotes | |
43 | {`FOO="escaped\"bar"`, gotenv.Env{"FOO": `escaped"bar`}, false}, | |
44 | ||
45 | // parses empty values | |
46 | {`FOO=`, gotenv.Env{"FOO": ""}, false}, | |
47 | ||
48 | // expands variables found in values | |
49 | {"FOO=test\nBAR=$FOO", gotenv.Env{"FOO": "test", "BAR": "test"}, false}, | |
50 | ||
51 | // parses variables wrapped in brackets | |
52 | {"FOO=test\nBAR=${FOO}bar", gotenv.Env{"FOO": "test", "BAR": "testbar"}, false}, | |
53 | ||
54 | // reads variables from ENV when expanding if not found in local env | |
55 | {`BAR=$FOO`, gotenv.Env{"BAR": "test"}, true}, | |
56 | ||
57 | // expands undefined variables to an empty string | |
58 | {`BAR=$FOO`, gotenv.Env{"BAR": ""}, false}, | |
59 | ||
60 | // expands variables in quoted strings | |
61 | {"FOO=test\nBAR=\"quote $FOO\"", gotenv.Env{"FOO": "test", "BAR": "quote test"}, false}, | |
62 | ||
63 | // does not expand variables in single quoted strings | |
64 | {"BAR='quote $FOO'", gotenv.Env{"BAR": "quote $FOO"}, false}, | |
65 | ||
66 | // does not expand escaped variables | |
67 | {`FOO="foo\$BAR"`, gotenv.Env{"FOO": "foo$BAR"}, false}, | |
68 | {`FOO="foo\${BAR}"`, gotenv.Env{"FOO": "foo${BAR}"}, false}, | |
69 | {"FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"", gotenv.Env{"FOO": "test", "BAR": "foo${FOO} test"}, false}, | |
70 | ||
71 | // parses yaml style options | |
72 | {"OPTION_A: 1", gotenv.Env{"OPTION_A": "1"}, false}, | |
73 | ||
74 | // parses export keyword | |
75 | {"export OPTION_A=2", gotenv.Env{"OPTION_A": "2"}, false}, | |
76 | ||
77 | // allows export line if you want to do it that way | |
78 | {"OPTION_A=2\nexport OPTION_A", gotenv.Env{"OPTION_A": "2"}, false}, | |
79 | ||
80 | // expands newlines in quoted strings | |
81 | {`FOO="bar\nbaz"`, gotenv.Env{"FOO": "bar\nbaz"}, false}, | |
82 | ||
83 | // parses variables with "." in the name | |
84 | {`FOO.BAR=foobar`, gotenv.Env{"FOO.BAR": "foobar"}, false}, | |
85 | ||
86 | // strips unquoted values | |
87 | {`foo=bar `, gotenv.Env{"foo": "bar"}, false}, // not 'bar ' | |
88 | ||
89 | // ignores empty lines | |
90 | {"\n \t \nfoo=bar\n \nfizz=buzz", gotenv.Env{"foo": "bar", "fizz": "buzz"}, false}, | |
91 | ||
92 | // ignores inline comments | |
93 | {"foo=bar # this is foo", gotenv.Env{"foo": "bar"}, false}, | |
94 | ||
95 | // allows # in quoted value | |
96 | {`foo="bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false}, | |
97 | ||
98 | // ignores comment lines | |
99 | {"\n\n\n # HERE GOES FOO \nfoo=bar", gotenv.Env{"foo": "bar"}, false}, | |
100 | ||
101 | // parses # in quoted values | |
102 | {`foo="ba#r"`, gotenv.Env{"foo": "ba#r"}, false}, | |
103 | {"foo='ba#r'", gotenv.Env{"foo": "ba#r"}, false}, | |
104 | ||
105 | // parses # in quoted values with following spaces | |
106 | {`foo="ba#r" `, gotenv.Env{"foo": "ba#r"}, false}, | |
107 | {`foo='ba#r' `, gotenv.Env{"foo": "ba#r"}, false}, | |
108 | ||
109 | // supports carriage return | |
110 | {"FOO=bar\rbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false}, | |
111 | ||
112 | // supports carriage return combine with new line | |
113 | {"FOO=bar\r\nbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false}, | |
114 | ||
115 | // expands carriage return in quoted strings | |
116 | {`FOO="bar\rbaz"`, gotenv.Env{"FOO": "bar\rbaz"}, false}, | |
117 | ||
118 | // escape $ properly when no alphabets/numbers/_ are followed by it | |
119 | {`FOO="bar\\$ \\$\\$"`, gotenv.Env{"FOO": "bar$ $$"}, false}, | |
120 | ||
121 | // ignore $ when it is not escaped and no variable is followed by it | |
122 | {`FOO="bar $ "`, gotenv.Env{"FOO": "bar $ "}, false}, | |
123 | ||
124 | // parses unquoted values with spaces after separator | |
125 | {`FOO= bar`, gotenv.Env{"FOO": "bar"}, false}, | |
126 | ||
127 | // allows # in quoted value with spaces after separator | |
128 | {`foo= "bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false}, | |
129 | } | |
130 | ||
131 | var errorFormats = []struct { | |
132 | in string | |
133 | out gotenv.Env | |
134 | err string | |
135 | }{ | |
136 | // allows export line if you want to do it that way and checks for unset variables | |
137 | {"OPTION_A=2\nexport OH_NO_NOT_SET", gotenv.Env{"OPTION_A": "2"}, "line `export OH_NO_NOT_SET` has an unset variable"}, | |
138 | ||
139 | // throws an error if line format is incorrect | |
140 | {`lol$wut`, gotenv.Env{}, "line `lol$wut` doesn't match format"}, | |
141 | } | |
142 | ||
143 | var fixtures = []struct { | |
144 | filename string | |
145 | results gotenv.Env | |
146 | }{ | |
147 | { | |
148 | "fixtures/exported.env", | |
149 | gotenv.Env{ | |
150 | "OPTION_A": "2", | |
151 | "OPTION_B": `\n`, | |
152 | }, | |
153 | }, | |
154 | { | |
155 | "fixtures/plain.env", | |
156 | gotenv.Env{ | |
157 | "OPTION_A": "1", | |
158 | "OPTION_B": "2", | |
159 | "OPTION_C": "3", | |
160 | "OPTION_D": "4", | |
161 | "OPTION_E": "5", | |
162 | }, | |
163 | }, | |
164 | { | |
165 | "fixtures/quoted.env", | |
166 | gotenv.Env{ | |
167 | "OPTION_A": "1", | |
168 | "OPTION_B": "2", | |
169 | "OPTION_C": "", | |
170 | "OPTION_D": `\n`, | |
171 | "OPTION_E": "1", | |
172 | "OPTION_F": "2", | |
173 | "OPTION_G": "", | |
174 | "OPTION_H": "\n", | |
175 | }, | |
176 | }, | |
177 | { | |
178 | "fixtures/yaml.env", | |
179 | gotenv.Env{ | |
180 | "OPTION_A": "1", | |
181 | "OPTION_B": "2", | |
182 | "OPTION_C": "", | |
183 | "OPTION_D": `\n`, | |
184 | }, | |
185 | }, | |
186 | } | |
187 | ||
188 | func TestParse(t *testing.T) { | |
189 | for _, tt := range formats { | |
190 | if tt.preset { | |
191 | os.Setenv("FOO", "test") | |
192 | } | |
193 | ||
194 | exp := gotenv.Parse(strings.NewReader(tt.in)) | |
195 | assert.Equal(t, tt.out, exp) | |
196 | os.Clearenv() | |
197 | } | |
198 | } | |
199 | ||
200 | func TestStrictParse(t *testing.T) { | |
201 | for _, tt := range errorFormats { | |
202 | env, err := gotenv.StrictParse(strings.NewReader(tt.in)) | |
203 | assert.Equal(t, tt.err, err.Error()) | |
204 | assert.Equal(t, tt.out, env) | |
205 | } | |
206 | } | |
207 | ||
208 | func TestLoad(t *testing.T) { | |
209 | for _, tt := range fixtures { | |
210 | err := gotenv.Load(tt.filename) | |
211 | assert.Nil(t, err) | |
212 | ||
213 | for key, val := range tt.results { | |
214 | assert.Equal(t, val, os.Getenv(key)) | |
215 | } | |
216 | ||
217 | os.Clearenv() | |
218 | } | |
219 | } | |
220 | ||
221 | func TestLoad_default(t *testing.T) { | |
222 | k := "HELLO" | |
223 | v := "world" | |
224 | ||
225 | err := gotenv.Load() | |
226 | assert.Nil(t, err) | |
227 | assert.Equal(t, v, os.Getenv(k)) | |
228 | os.Clearenv() | |
229 | } | |
230 | ||
231 | func TestLoad_overriding(t *testing.T) { | |
232 | k := "HELLO" | |
233 | v := "universe" | |
234 | ||
235 | os.Setenv(k, v) | |
236 | err := gotenv.Load() | |
237 | assert.Nil(t, err) | |
238 | assert.Equal(t, v, os.Getenv(k)) | |
239 | os.Clearenv() | |
240 | } | |
241 | ||
242 | func TestLoad_Env(t *testing.T) { | |
243 | err := gotenv.Load(".env.invalid") | |
244 | assert.NotNil(t, err) | |
245 | } | |
246 | ||
247 | func TestLoad_nonExist(t *testing.T) { | |
248 | file := ".env.not.exist" | |
249 | ||
250 | err := gotenv.Load(file) | |
251 | if err == nil { | |
252 | t.Errorf("gotenv.Load(`%s`) => error: `no such file or directory` != nil", file) | |
253 | } | |
254 | } | |
255 | ||
256 | func TestLoad_unicodeBOMFixture(t *testing.T) { | |
257 | file := "fixtures/bom.env" | |
258 | ||
259 | f, err := os.Open(file) | |
260 | assert.Nil(t, err) | |
261 | ||
262 | scanner := bufio.NewScanner(f) | |
263 | ||
264 | i := 1 | |
265 | bom := string([]byte{239, 187, 191}) | |
266 | ||
267 | for scanner.Scan() { | |
268 | if i == 1 { | |
269 | line := scanner.Text() | |
270 | assert.True(t, strings.HasPrefix(line, bom)) | |
271 | } | |
272 | } | |
273 | } | |
274 | ||
275 | func TestLoad_unicodeBOM(t *testing.T) { | |
276 | file := "fixtures/bom.env" | |
277 | ||
278 | err := gotenv.Load(file) | |
279 | assert.Nil(t, err) | |
280 | assert.Equal(t, "UTF-8", os.Getenv("BOM")) | |
281 | os.Clearenv() | |
282 | } | |
283 | ||
284 | func TestMust_Load(t *testing.T) { | |
285 | for _, tt := range fixtures { | |
286 | assert.NotPanics(t, func() { | |
287 | gotenv.Must(gotenv.Load, tt.filename) | |
288 | ||
289 | for key, val := range tt.results { | |
290 | assert.Equal(t, val, os.Getenv(key)) | |
291 | } | |
292 | ||
293 | os.Clearenv() | |
294 | }, "Caling gotenv.Must with gotenv.Load should NOT panic") | |
295 | } | |
296 | } | |
297 | ||
298 | func TestMust_Load_default(t *testing.T) { | |
299 | assert.NotPanics(t, func() { | |
300 | gotenv.Must(gotenv.Load) | |
301 | ||
302 | tkey := "HELLO" | |
303 | val := "world" | |
304 | ||
305 | assert.Equal(t, val, os.Getenv(tkey)) | |
306 | os.Clearenv() | |
307 | }, "Caling gotenv.Must with gotenv.Load without arguments should NOT panic") | |
308 | } | |
309 | ||
310 | func TestMust_Load_nonExist(t *testing.T) { | |
311 | assert.Panics(t, func() { gotenv.Must(gotenv.Load, ".env.not.exist") }, "Caling gotenv.Must with gotenv.Load and non exist file SHOULD panic") | |
312 | } | |
313 | ||
314 | func TestOverLoad_overriding(t *testing.T) { | |
315 | k := "HELLO" | |
316 | v := "universe" | |
317 | ||
318 | os.Setenv(k, v) | |
319 | err := gotenv.OverLoad() | |
320 | assert.Nil(t, err) | |
321 | assert.Equal(t, "world", os.Getenv(k)) | |
322 | os.Clearenv() | |
323 | } | |
324 | ||
325 | func TestMustOverLoad_nonExist(t *testing.T) { | |
326 | assert.Panics(t, func() { gotenv.Must(gotenv.OverLoad, ".env.not.exist") }, "Caling gotenv.Must with Overgotenv.Load and non exist file SHOULD panic") | |
327 | } | |
328 | ||
329 | func TestApply(t *testing.T) { | |
330 | os.Setenv("HELLO", "world") | |
331 | r := strings.NewReader("HELLO=universe") | |
332 | err := gotenv.Apply(r) | |
333 | assert.Nil(t, err) | |
334 | assert.Equal(t, "world", os.Getenv("HELLO")) | |
335 | os.Clearenv() | |
336 | } | |
337 | ||
338 | func TestOverApply(t *testing.T) { | |
339 | os.Setenv("HELLO", "world") | |
340 | r := strings.NewReader("HELLO=universe") | |
341 | err := gotenv.OverApply(r) | |
342 | assert.Nil(t, err) | |
343 | assert.Equal(t, "universe", os.Getenv("HELLO")) | |
344 | os.Clearenv() | |
345 | } |