├── LICENSE.md ├── README.md ├── go.mod ├── parse.go └── parse_test.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 zimbatm and 4 | [contributors](https://github.com/direnv/go-dotenv/graphs/contributors) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the 'Software'), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-dotenv 2 | 3 | STATUS: deprecated. Use github.com/direnv/direnv/pkg/dotenv instead. 4 | 5 | Go parsing library for the dotenv format. 6 | 7 | There is no formal definition of the dotenv format but it has been introduced 8 | by https://github.com/bkeepers/dotenv which is thus canonical. This library is a port of that. 9 | 10 | This library was developed specifically for [direnv](https://direnv.net). 11 | 12 | ## Features 13 | 14 | * `k=v` format 15 | * bash `export k=v` format 16 | * yaml `k: v` format 17 | * variable expansion, including default values as in `${FOO:-default}` 18 | * comments 19 | 20 | ## Missing 21 | 22 | * probably needs API breakage 23 | 24 | ## Alternatives 25 | 26 | Some other good alternatives with various variations. 27 | 28 | * https://github.com/joho/godotenv 29 | * https://github.com/lazureykis/dotenv 30 | * https://github.com/subosito/gotenv 31 | 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/direnv/go-dotenv 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | // Package dotenv implements the parsing of the .env format. 2 | // 3 | // There is no formal definition of the format but it has been introduced by 4 | // https://github.com/bkeepers/dotenv which is thus canonical. 5 | package dotenv 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | // LINE is the regexp matching a single line 15 | const LINE = ` 16 | \A 17 | \s* 18 | (?:|#.*| # comment line 19 | (?:export\s+)? # optional export 20 | ([\w\.]+) # key 21 | (?:\s*=\s*|:\s+?) # separator 22 | ( # optional value begin 23 | '(?:\'|[^'])*' # single quoted value 24 | | # or 25 | "(?:\"|[^"])*" # double quoted value 26 | | # or 27 | [^\s#\n]+ # unquoted value 28 | )? # value end 29 | \s* 30 | (?:\#.*)? # optional comment 31 | ) 32 | \z 33 | ` 34 | 35 | var linesRe = regexp.MustCompile("[\\r\\n]+") 36 | var lineRe = regexp.MustCompile( 37 | regexp.MustCompile("\\s+").ReplaceAllLiteralString( 38 | regexp.MustCompile("\\s+# .*").ReplaceAllLiteralString(LINE, ""), "")) 39 | 40 | // Parse reads a string in the .env format and returns a map of the extracted key=values. 41 | // 42 | // Ported from https://github.com/bkeepers/dotenv/blob/84f33f48107c492c3a99bd41c1059e7b4c1bb67a/lib/dotenv/parser.rb 43 | func Parse(data string) (map[string]string, error) { 44 | var dotenv = make(map[string]string) 45 | 46 | for _, line := range linesRe.Split(data, -1) { 47 | if !lineRe.MatchString(line) { 48 | return nil, fmt.Errorf("invalid line: %s", line) 49 | } 50 | 51 | match := lineRe.FindStringSubmatch(line) 52 | // commented or empty line 53 | if len(match) == 0 { 54 | continue 55 | } 56 | if len(match[1]) == 0 { 57 | continue 58 | } 59 | 60 | key := match[1] 61 | value := match[2] 62 | 63 | err := parseValue(key, value, dotenv) 64 | 65 | if err != nil { 66 | return nil, fmt.Errorf("unable to parse %s, %s: %s", key, value, err) 67 | } 68 | } 69 | 70 | return dotenv, nil 71 | } 72 | 73 | // MustParse works the same as Parse but panics on error 74 | func MustParse(data string) map[string]string { 75 | env, err := Parse(data) 76 | if err != nil { 77 | panic(err) 78 | } 79 | return env 80 | } 81 | 82 | func parseValue(key string, value string, dotenv map[string]string) error { 83 | if len(value) <= 1 { 84 | dotenv[key] = value 85 | return nil 86 | } 87 | 88 | singleQuoted := false 89 | 90 | if value[0:1] == "'" && value[len(value)-1:] == "'" { 91 | // single-quoted string, do not expand 92 | singleQuoted = true 93 | value = value[1 : len(value)-1] 94 | } else if value[0:1] == `"` && value[len(value)-1:] == `"` { 95 | value = value[1 : len(value)-1] 96 | value = expandNewLines(value) 97 | value = unescapeCharacters(value) 98 | } 99 | 100 | if !singleQuoted { 101 | value = expandEnv(value, dotenv) 102 | } 103 | 104 | dotenv[key] = value 105 | return nil 106 | } 107 | 108 | var escRe = regexp.MustCompile("\\\\([^$])") 109 | 110 | func unescapeCharacters(value string) string { 111 | return escRe.ReplaceAllString(value, "$1") 112 | } 113 | 114 | func expandNewLines(value string) string { 115 | value = strings.Replace(value, "\\n", "\n", -1) 116 | value = strings.Replace(value, "\\r", "\r", -1) 117 | return value 118 | } 119 | 120 | func expandEnv(value string, dotenv map[string]string) string { 121 | expander := func(value string) string { 122 | envKey, defaultValue, hasDefault := splitKeyAndDefault(value, ":-") 123 | expanded, found := lookupDotenv(envKey, dotenv) 124 | 125 | if found { 126 | return expanded 127 | } else { 128 | return getFromEnvOrDefault(envKey, defaultValue, hasDefault) 129 | } 130 | } 131 | 132 | return os.Expand(value, expander) 133 | } 134 | 135 | func splitKeyAndDefault(value string, sep string) (string, string, bool) { 136 | var i = strings.Index(value, sep) 137 | 138 | if i == -1 { 139 | return value, "", false 140 | } else { 141 | return value[0:i], value[i+len(sep):], true 142 | } 143 | } 144 | 145 | func lookupDotenv(value string, dotenv map[string]string) (string, bool) { 146 | retval, ok := dotenv[value] 147 | return retval, ok 148 | } 149 | 150 | func getFromEnvOrDefault(envKey string, defaultValue string, hasDefault bool) string { 151 | var envValue = os.Getenv(envKey) 152 | 153 | if len(envValue) == 0 && hasDefault { 154 | return defaultValue 155 | } else { 156 | return envValue 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package dotenv_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | dotenv "github.com/direnv/go-dotenv" 8 | ) 9 | 10 | func shouldNotHaveEmptyKey(t *testing.T, env map[string]string) { 11 | if _, ok := env[""]; ok { 12 | t.Error("should not have empty key") 13 | } 14 | } 15 | 16 | func envShouldContain(t *testing.T, env map[string]string, key string, value string) { 17 | if env[key] != value { 18 | t.Errorf("%s: %s, expected %s", key, env[key], value) 19 | } 20 | } 21 | 22 | // See the reference implementation: 23 | // https://github.com/bkeepers/dotenv/blob/master/lib/dotenv/environment.rb 24 | // TODO: support shell variable expansions 25 | 26 | const TEST_EXPORTED = `export OPTION_A=2 27 | export OPTION_B='\n' # foo 28 | #export OPTION_C=3 29 | export OPTION_D= 30 | export OPTION_E="foo" 31 | ` 32 | 33 | func TestDotEnvExported(t *testing.T) { 34 | env := dotenv.MustParse(TEST_EXPORTED) 35 | shouldNotHaveEmptyKey(t, env) 36 | 37 | if env["OPTION_A"] != "2" { 38 | t.Error("OPTION_A") 39 | } 40 | if env["OPTION_B"] != "\\n" { 41 | t.Error("OPTION_B") 42 | } 43 | if env["OPTION_C"] != "" { 44 | t.Error("OPTION_C", env["OPTION_C"]) 45 | } 46 | if v, ok := env["OPTION_D"]; !(v == "" && ok) { 47 | t.Error("OPTION_D") 48 | } 49 | if env["OPTION_E"] != "foo" { 50 | t.Error("OPTION_E") 51 | } 52 | } 53 | 54 | const TEST_PLAIN = `OPTION_A=1 55 | OPTION_B=2 56 | OPTION_C= 3 57 | OPTION_D =4 58 | OPTION_E = 5 59 | OPTION_F= 60 | OPTION_G = 61 | SMTP_ADDRESS=smtp # This is a comment 62 | ` 63 | 64 | func TestDotEnvPlain(t *testing.T) { 65 | env := dotenv.MustParse(TEST_PLAIN) 66 | shouldNotHaveEmptyKey(t, env) 67 | 68 | if env["OPTION_A"] != "1" { 69 | t.Error("OPTION_A") 70 | } 71 | if env["OPTION_B"] != "2" { 72 | t.Error("OPTION_B") 73 | } 74 | if env["OPTION_C"] != "3" { 75 | t.Error("OPTION_C") 76 | } 77 | if env["OPTION_D"] != "4" { 78 | t.Error("OPTION_D") 79 | } 80 | if env["OPTION_E"] != "5" { 81 | t.Error("OPTION_E") 82 | } 83 | if v, ok := env["OPTION_F"]; !(v == "" && ok) { 84 | t.Error("OPTION_F") 85 | } 86 | if v, ok := env["OPTION_G"]; !(v == "" && ok) { 87 | t.Error("OPTION_G") 88 | } 89 | if env["SMTP_ADDRESS"] != "smtp" { 90 | t.Error("SMTP_ADDRESS") 91 | } 92 | } 93 | 94 | const TEST_SOLO_EMPTY = "SOME_VAR=" 95 | 96 | func TestSoloEmpty(t *testing.T) { 97 | env := dotenv.MustParse(TEST_SOLO_EMPTY) 98 | shouldNotHaveEmptyKey(t, env) 99 | 100 | v, ok := env["SOME_VAR"] 101 | if !ok { 102 | t.Error("SOME_VAR missing") 103 | } 104 | if v != "" { 105 | t.Error("SOME_VAR should be empty") 106 | } 107 | } 108 | 109 | const TEST_QUOTED = `OPTION_A='1' 110 | OPTION_B='2' 111 | OPTION_C='' 112 | OPTION_D='\n' 113 | OPTION_E="1" 114 | OPTION_F="2" 115 | OPTION_G="" 116 | OPTION_H="\n" 117 | #OPTION_I="3" 118 | ` 119 | 120 | func TestDotEnvQuoted(t *testing.T) { 121 | env := dotenv.MustParse(TEST_QUOTED) 122 | shouldNotHaveEmptyKey(t, env) 123 | 124 | if env["OPTION_A"] != "1" { 125 | t.Error("OPTION_A") 126 | } 127 | if env["OPTION_B"] != "2" { 128 | t.Error("OPTION_B") 129 | } 130 | if env["OPTION_C"] != "" { 131 | t.Error("OPTION_C") 132 | } 133 | if env["OPTION_D"] != "\\n" { 134 | t.Error("OPTION_D") 135 | } 136 | if env["OPTION_E"] != "1" { 137 | t.Error("OPTION_E") 138 | } 139 | if env["OPTION_F"] != "2" { 140 | t.Error("OPTION_F") 141 | } 142 | if env["OPTION_G"] != "" { 143 | t.Error("OPTION_G") 144 | } 145 | if env["OPTION_H"] != "\n" { 146 | t.Error("OPTION_H") 147 | } 148 | if env["OPTION_I"] != "" { 149 | t.Error("OPTION_I") 150 | } 151 | } 152 | 153 | const TEST_YAML = `OPTION_A: 1 154 | OPTION_B: '2' 155 | OPTION_C: '' 156 | OPTION_D: '\n' 157 | #OPTION_E: '333' 158 | OPTION_F: 159 | ` 160 | 161 | func TestDotEnvYAML(t *testing.T) { 162 | env := dotenv.MustParse(TEST_YAML) 163 | shouldNotHaveEmptyKey(t, env) 164 | 165 | if env["OPTION_A"] != "1" { 166 | t.Error("OPTION_A") 167 | } 168 | if env["OPTION_B"] != "2" { 169 | t.Error("OPTION_B") 170 | } 171 | if env["OPTION_C"] != "" { 172 | t.Error("OPTION_C") 173 | } 174 | if env["OPTION_D"] != "\\n" { 175 | t.Error("OPTION_D") 176 | } 177 | if env["OPTION_E"] != "" { 178 | t.Error("OPTION_E") 179 | } 180 | if v, ok := env["OPTION_F"]; !(v == "" && ok) { 181 | t.Error("OPTION_F") 182 | } 183 | } 184 | 185 | func TestFailingMustParse(t *testing.T) { 186 | defer func() { 187 | r := recover() 188 | if r == nil { 189 | t.Error("should panic") 190 | } 191 | }() 192 | dotenv.MustParse("...") 193 | } 194 | 195 | const TEST_COMMENT_OVERRIDE = ` 196 | VARIABLE=value 197 | #VARIABLE=disabled_value 198 | ` 199 | 200 | func TestCommentOverride(t *testing.T) { 201 | env := dotenv.MustParse(TEST_COMMENT_OVERRIDE) 202 | shouldNotHaveEmptyKey(t, env) 203 | 204 | if env["VARIABLE"] != "value" { 205 | t.Error("VARIABLE should == value, not", env["VARIABLE"]) 206 | } 207 | } 208 | 209 | const TEST_VARIABLE_EXPANSION = ` 210 | OPTION_A=$FOO 211 | OPTION_B="$FOO" 212 | OPTION_C=${FOO} 213 | OPTION_D="${FOO}" 214 | OPTION_E='$FOO' 215 | OPTION_F=$FOO/bar 216 | OPTION_G="$FOO/bar" 217 | OPTION_H=${FOO}/bar 218 | OPTION_I="${FOO}/bar" 219 | OPTION_J='$FOO/bar' 220 | OPTION_K=$BAR 221 | OPTION_L="$BAR" 222 | OPTION_M=${BAR} 223 | OPTION_N="${BAR}" 224 | OPTION_O='$BAR' 225 | OPTION_P=$BAR/baz 226 | OPTION_Q="$BAR/baz" 227 | OPTION_R=${BAR}/baz 228 | OPTION_S="${BAR}/baz" 229 | OPTION_T='$BAR/baz' 230 | OPTION_U="$OPTION_A/bar" 231 | OPTION_V=$OPTION_A/bar 232 | OPTION_W="$OPTION_A/bar" 233 | OPTION_X=${OPTION_A}/bar 234 | OPTION_Y="${OPTION_A}/bar" 235 | OPTION_Z='$OPTION_A/bar' 236 | OPTION_A1="$OPTION_A/bar/${OPTION_H}/$FOO" 237 | ` 238 | 239 | func TestVariableExpansion(t *testing.T) { 240 | err := os.Setenv("FOO", "foo") 241 | if err != nil { 242 | t.Fatalf("unable to set environment variable for testing: %s", err) 243 | } 244 | 245 | env := dotenv.MustParse(TEST_VARIABLE_EXPANSION) 246 | shouldNotHaveEmptyKey(t, env) 247 | 248 | envShouldContain(t, env, "OPTION_A", "foo") 249 | envShouldContain(t, env, "OPTION_B", "foo") 250 | envShouldContain(t, env, "OPTION_C", "foo") 251 | envShouldContain(t, env, "OPTION_D", "foo") 252 | envShouldContain(t, env, "OPTION_E", "$FOO") 253 | envShouldContain(t, env, "OPTION_F", "foo/bar") 254 | envShouldContain(t, env, "OPTION_G", "foo/bar") 255 | envShouldContain(t, env, "OPTION_H", "foo/bar") 256 | envShouldContain(t, env, "OPTION_I", "foo/bar") 257 | envShouldContain(t, env, "OPTION_J", "$FOO/bar") 258 | envShouldContain(t, env, "OPTION_K", "") 259 | envShouldContain(t, env, "OPTION_L", "") 260 | envShouldContain(t, env, "OPTION_M", "") 261 | envShouldContain(t, env, "OPTION_N", "") 262 | envShouldContain(t, env, "OPTION_O", "$BAR") 263 | envShouldContain(t, env, "OPTION_P", "/baz") 264 | envShouldContain(t, env, "OPTION_Q", "/baz") 265 | envShouldContain(t, env, "OPTION_R", "/baz") 266 | envShouldContain(t, env, "OPTION_S", "/baz") 267 | envShouldContain(t, env, "OPTION_T", "$BAR/baz") 268 | envShouldContain(t, env, "OPTION_U", "foo/bar") 269 | envShouldContain(t, env, "OPTION_V", "foo/bar") 270 | envShouldContain(t, env, "OPTION_W", "foo/bar") 271 | envShouldContain(t, env, "OPTION_X", "foo/bar") 272 | envShouldContain(t, env, "OPTION_Y", "foo/bar") 273 | envShouldContain(t, env, "OPTION_Z", "$OPTION_A/bar") 274 | envShouldContain(t, env, "OPTION_A1", "foo/bar/foo/bar/foo") 275 | } 276 | 277 | const TEST_VARIABLE_EXPANSION_WITH_DEFAULTS = ` 278 | OPTION_A="${FOO:-}" 279 | OPTION_B="${FOO:-default}" 280 | OPTION_C='${FOO:-default}' 281 | OPTION_D="${FOO:-default}/bar" 282 | OPTION_E='${FOO:-default}/bar' 283 | OPTION_F="$FOO:-default" 284 | OPTION_G="$BAR:-default" 285 | OPTION_H="${BAR:-}" 286 | OPTION_I="${BAR:-default}" 287 | OPTION_J='${BAR:-default}' 288 | OPTION_K="${BAR:-default}/bar" 289 | OPTION_L='${BAR:-default}/bar' 290 | OPTION_M="${OPTION_A:-}" 291 | OPTION_N="${OPTION_A:-default}" 292 | OPTION_O='${OPTION_A:-default}' 293 | OPTION_P="${OPTION_A:-default}/bar" 294 | OPTION_Q='${OPTION_A:-default}/bar' 295 | OPTION_R="${:-}" 296 | OPTION_S="${BAR:-:-}" 297 | ` 298 | 299 | func TestVariableExpansionWithDefaults(t *testing.T) { 300 | err := os.Setenv("FOO", "foo") 301 | if err != nil { 302 | t.Fatalf("unable to set environment variable for testing: %s", err) 303 | } 304 | 305 | env := dotenv.MustParse(TEST_VARIABLE_EXPANSION_WITH_DEFAULTS) 306 | shouldNotHaveEmptyKey(t, env) 307 | 308 | envShouldContain(t, env, "OPTION_A", "foo") 309 | envShouldContain(t, env, "OPTION_B", "foo") 310 | envShouldContain(t, env, "OPTION_C", "${FOO:-default}") 311 | envShouldContain(t, env, "OPTION_D", "foo/bar") 312 | envShouldContain(t, env, "OPTION_E", "${FOO:-default}/bar") 313 | envShouldContain(t, env, "OPTION_F", "foo:-default") 314 | envShouldContain(t, env, "OPTION_G", ":-default") 315 | envShouldContain(t, env, "OPTION_H", "") 316 | envShouldContain(t, env, "OPTION_I", "default") 317 | envShouldContain(t, env, "OPTION_J", "${BAR:-default}") 318 | envShouldContain(t, env, "OPTION_K", "default/bar") 319 | envShouldContain(t, env, "OPTION_L", "${BAR:-default}/bar") 320 | envShouldContain(t, env, "OPTION_M", "foo") 321 | envShouldContain(t, env, "OPTION_N", "foo") 322 | envShouldContain(t, env, "OPTION_O", "${OPTION_A:-default}") 323 | envShouldContain(t, env, "OPTION_P", "foo/bar") 324 | envShouldContain(t, env, "OPTION_Q", "${OPTION_A:-default}/bar") 325 | envShouldContain(t, env, "OPTION_R", "") // this is actually invalid in bash, but what to do here? 326 | envShouldContain(t, env, "OPTION_S", ":-") 327 | } 328 | --------------------------------------------------------------------------------