github.com/influx6/npkg@v0.8.8/nenv/envfile.go (about) 1 package nenv 2 3 import ( 4 "bufio" 5 "errors" 6 "io" 7 "os" 8 "regexp" 9 "strings" 10 ) 11 12 const doubleQuoteSpecialChars = "\\\n\r\"!$`" 13 14 func ReadDotEnvFile(filename string) (envMap map[string]string, err error) { 15 file, err := os.Open(filename) 16 if err != nil { 17 return 18 } 19 defer file.Close() 20 21 return ParseDotEnvReader(file) 22 } 23 24 // Parse reads an env file from io.Reader, returning a map of keys and values. 25 func ParseDotEnvReader(r io.Reader) (envMap map[string]string, err error) { 26 envMap = make(map[string]string) 27 28 var lines []string 29 scanner := bufio.NewScanner(r) 30 for scanner.Scan() { 31 lines = append(lines, scanner.Text()) 32 } 33 34 if err = scanner.Err(); err != nil { 35 return 36 } 37 38 for _, fullLine := range lines { 39 if !isIgnoredLine(fullLine) { 40 var key, value string 41 key, value, err = parseLine(fullLine, envMap) 42 43 if err != nil { 44 return 45 } 46 envMap[key] = value 47 } 48 } 49 return 50 } 51 52 var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`) 53 54 func parseLine(line string, envMap map[string]string) (key string, value string, err error) { 55 if len(line) == 0 { 56 err = errors.New("zero length string") 57 return 58 } 59 60 // ditch the comments (but keep quoted hashes) 61 if strings.Contains(line, "#") { 62 segmentsBetweenHashes := strings.Split(line, "#") 63 quotesAreOpen := false 64 var segmentsToKeep []string 65 for _, segment := range segmentsBetweenHashes { 66 if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 { 67 if quotesAreOpen { 68 quotesAreOpen = false 69 segmentsToKeep = append(segmentsToKeep, segment) 70 } else { 71 quotesAreOpen = true 72 } 73 } 74 75 if len(segmentsToKeep) == 0 || quotesAreOpen { 76 segmentsToKeep = append(segmentsToKeep, segment) 77 } 78 } 79 80 line = strings.Join(segmentsToKeep, "#") 81 } 82 83 firstEquals := strings.Index(line, "=") 84 firstColon := strings.Index(line, ":") 85 splitString := strings.SplitN(line, "=", 2) 86 if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) { 87 // this is a yaml-style line 88 splitString = strings.SplitN(line, ":", 2) 89 } 90 91 if len(splitString) != 2 { 92 err = errors.New("Can't separate key from value") 93 return 94 } 95 96 // Parse the key 97 key = splitString[0] 98 if strings.HasPrefix(key, "export") { 99 key = strings.TrimPrefix(key, "export") 100 } 101 key = strings.TrimSpace(key) 102 103 key = exportRegex.ReplaceAllString(splitString[0], "$1") 104 105 // Parse the value 106 value = parseValue(splitString[1], envMap) 107 return 108 } 109 110 var ( 111 singleQuotesRegex = regexp.MustCompile(`\A'(.*)'\z`) 112 doubleQuotesRegex = regexp.MustCompile(`\A"(.*)"\z`) 113 escapeRegex = regexp.MustCompile(`\\.`) 114 unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) 115 ) 116 117 func parseValue(value string, envMap map[string]string) string { 118 119 // trim 120 value = strings.Trim(value, " ") 121 122 // check if we've got quoted values or possible escapes 123 if len(value) > 1 { 124 singleQuotes := singleQuotesRegex.FindStringSubmatch(value) 125 126 doubleQuotes := doubleQuotesRegex.FindStringSubmatch(value) 127 128 if singleQuotes != nil || doubleQuotes != nil { 129 // pull the quotes off the edges 130 value = value[1 : len(value)-1] 131 } 132 133 if doubleQuotes != nil { 134 // expand newlines 135 value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string { 136 c := strings.TrimPrefix(match, `\`) 137 switch c { 138 case "n": 139 return "\n" 140 case "r": 141 return "\r" 142 default: 143 return match 144 } 145 }) 146 // unescape characters 147 value = unescapeCharsRegex.ReplaceAllString(value, "$1") 148 } 149 150 if singleQuotes == nil { 151 value = expandVariables(value, envMap) 152 } 153 } 154 155 return value 156 } 157 158 var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) 159 160 func expandVariables(v string, m map[string]string) string { 161 return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { 162 submatch := expandVarRegex.FindStringSubmatch(s) 163 164 if submatch == nil { 165 return s 166 } 167 if submatch[1] == "\\" || submatch[2] == "(" { 168 return submatch[0][1:] 169 } else if submatch[4] != "" { 170 return m[submatch[4]] 171 } 172 return s 173 }) 174 } 175 176 func isIgnoredLine(line string) bool { 177 trimmedLine := strings.TrimSpace(line) 178 return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") 179 } 180 181 func doubleQuoteEscape(line string) string { 182 for _, c := range doubleQuoteSpecialChars { 183 toReplace := "\\" + string(c) 184 if c == '\n' { 185 toReplace = `\n` 186 } 187 if c == '\r' { 188 toReplace = `\r` 189 } 190 line = strings.Replace(line, string(c), toReplace, -1) 191 } 192 return line 193 }