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. 5package dotenv 6 7import ( 8 "fmt" 9 "os" 10 "regexp" 11 "strings" 12) 13 14// LINE is the regexp matching a single line 15const 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 [^#\n]+ # unquoted value 28)? # value end 29\s* 30(?:\#.*)? # optional comment 31) 32\z 33` 34 35var linesRe = regexp.MustCompile("[\\r\\n]+") 36var 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 43func 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 74func MustParse(data string) map[string]string { 75 env, err := Parse(data) 76 if err != nil { 77 panic(err) 78 } 79 return env 80} 81 82func 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 108var escRe = regexp.MustCompile("\\\\([^$])") 109 110func unescapeCharacters(value string) string { 111 return escRe.ReplaceAllString(value, "$1") 112} 113 114func 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 120func expandEnv(value string, dotenv map[string]string) string { 121 expander := func(value string) string { 122 expanded, found := lookupDotenv(value, dotenv) 123 124 if found { 125 return expanded 126 } else { 127 return os.Getenv(value) 128 } 129 } 130 131 return os.Expand(value, expander) 132} 133 134func lookupDotenv(value string, dotenv map[string]string) (string, bool) { 135 retval, ok := dotenv[value] 136 return retval, ok 137} 138