gotenv.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. // Package gotenv provides functionality to dynamically load the environment variables
  2. package gotenv
  3. import (
  4. "bufio"
  5. "bytes"
  6. "fmt"
  7. "io"
  8. "os"
  9. "path/filepath"
  10. "regexp"
  11. "sort"
  12. "strconv"
  13. "strings"
  14. )
  15. const (
  16. // Pattern for detecting valid line format
  17. linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`
  18. // Pattern for detecting valid variable within a value
  19. variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`
  20. // Byte order mark character
  21. bom = "\xef\xbb\xbf"
  22. )
  23. // Env holds key/value pair of valid environment variable
  24. type Env map[string]string
  25. // 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.
  26. // When it's called with no argument, it will load `.env` file on the current path and set the environment variables.
  27. // Otherwise, it will loop over the filenames parameter and set the proper environment variables.
  28. func Load(filenames ...string) error {
  29. return loadenv(false, filenames...)
  30. }
  31. // OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.
  32. func OverLoad(filenames ...string) error {
  33. return loadenv(true, filenames...)
  34. }
  35. // Must is wrapper function that will panic when supplied function returns an error.
  36. func Must(fn func(filenames ...string) error, filenames ...string) {
  37. if err := fn(filenames...); err != nil {
  38. panic(err.Error())
  39. }
  40. }
  41. // Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.
  42. func Apply(r io.Reader) error {
  43. return parset(r, false)
  44. }
  45. // OverApply is a function to load an io Reader then export and override the valid variables into environment variables.
  46. func OverApply(r io.Reader) error {
  47. return parset(r, true)
  48. }
  49. func loadenv(override bool, filenames ...string) error {
  50. if len(filenames) == 0 {
  51. filenames = []string{".env"}
  52. }
  53. for _, filename := range filenames {
  54. f, err := os.Open(filename)
  55. if err != nil {
  56. return err
  57. }
  58. err = parset(f, override)
  59. f.Close()
  60. if err != nil {
  61. return err
  62. }
  63. }
  64. return nil
  65. }
  66. // parse and set :)
  67. func parset(r io.Reader, override bool) error {
  68. env, err := strictParse(r, override)
  69. if err != nil {
  70. return err
  71. }
  72. for key, val := range env {
  73. setenv(key, val, override)
  74. }
  75. return nil
  76. }
  77. func setenv(key, val string, override bool) {
  78. if override {
  79. os.Setenv(key, val)
  80. } else {
  81. if _, present := os.LookupEnv(key); !present {
  82. os.Setenv(key, val)
  83. }
  84. }
  85. }
  86. // Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
  87. // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
  88. // This function is skipping any invalid lines and only processing the valid one.
  89. func Parse(r io.Reader) Env {
  90. env, _ := strictParse(r, false)
  91. return env
  92. }
  93. // StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
  94. // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
  95. // This function is returning an error if there are any invalid lines.
  96. func StrictParse(r io.Reader) (Env, error) {
  97. return strictParse(r, false)
  98. }
  99. // Read is a function to parse a file line by line and returns the valid Env key/value pair of valid variables.
  100. // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
  101. // This function is skipping any invalid lines and only processing the valid one.
  102. func Read(filename string) (Env, error) {
  103. f, err := os.Open(filename)
  104. if err != nil {
  105. return nil, err
  106. }
  107. defer f.Close()
  108. return strictParse(f, false)
  109. }
  110. // Unmarshal reads a string line by line and returns the valid Env key/value pair of valid variables.
  111. // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
  112. // This function is returning an error if there are any invalid lines.
  113. func Unmarshal(str string) (Env, error) {
  114. return strictParse(strings.NewReader(str), false)
  115. }
  116. // Marshal outputs the given environment as a env file.
  117. // Variables will be sorted by name.
  118. func Marshal(env Env) (string, error) {
  119. lines := make([]string, 0, len(env))
  120. for k, v := range env {
  121. if d, err := strconv.Atoi(v); err == nil {
  122. lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
  123. } else {
  124. lines = append(lines, fmt.Sprintf(`%s=%q`, k, v))
  125. }
  126. }
  127. sort.Strings(lines)
  128. return strings.Join(lines, "\n"), nil
  129. }
  130. // Write serializes the given environment and writes it to a file
  131. func Write(env Env, filename string) error {
  132. content, err := Marshal(env)
  133. if err != nil {
  134. return err
  135. }
  136. // ensure the path exists
  137. if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {
  138. return err
  139. }
  140. // create or truncate the file
  141. file, err := os.Create(filename)
  142. if err != nil {
  143. return err
  144. }
  145. defer file.Close()
  146. _, err = file.WriteString(content + "\n")
  147. if err != nil {
  148. return err
  149. }
  150. return file.Sync()
  151. }
  152. // splitLines is a valid SplitFunc for a bufio.Scanner. It will split lines on CR ('\r'), LF ('\n') or CRLF (any of the three sequences).
  153. // If a CR is immediately followed by a LF, it is treated as a CRLF (one single line break).
  154. func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
  155. if atEOF && len(data) == 0 {
  156. return 0, nil, bufio.ErrFinalToken
  157. }
  158. idx := bytes.IndexAny(data, "\r\n")
  159. switch {
  160. case atEOF && idx < 0:
  161. return len(data), data, bufio.ErrFinalToken
  162. case idx < 0:
  163. return 0, nil, nil
  164. }
  165. // consume CR or LF
  166. eol := idx + 1
  167. // detect CRLF
  168. if len(data) > eol && data[eol-1] == '\r' && data[eol] == '\n' {
  169. eol++
  170. }
  171. return eol, data[:idx], nil
  172. }
  173. func strictParse(r io.Reader, override bool) (Env, error) {
  174. env := make(Env)
  175. scanner := bufio.NewScanner(r)
  176. scanner.Split(splitLines)
  177. firstLine := true
  178. for scanner.Scan() {
  179. line := strings.TrimSpace(scanner.Text())
  180. if firstLine {
  181. line = strings.TrimPrefix(line, bom)
  182. firstLine = false
  183. }
  184. if line == "" || line[0] == '#' {
  185. continue
  186. }
  187. quote := ""
  188. // look for the delimiter character
  189. idx := strings.Index(line, "=")
  190. if idx == -1 {
  191. idx = strings.Index(line, ":")
  192. }
  193. // look for a quote character
  194. if idx > 0 && idx < len(line)-1 {
  195. val := strings.TrimSpace(line[idx+1:])
  196. if val[0] == '"' || val[0] == '\'' {
  197. quote = val[:1]
  198. // look for the closing quote character within the same line
  199. idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)
  200. if idx >= 0 && val[idx] != '\\' {
  201. quote = ""
  202. }
  203. }
  204. }
  205. // look for the closing quote character
  206. for quote != "" && scanner.Scan() {
  207. l := scanner.Text()
  208. line += "\n" + l
  209. idx := strings.LastIndex(l, quote)
  210. if idx > 0 && l[idx-1] == '\\' {
  211. // foud a matching quote character but it's escaped
  212. continue
  213. }
  214. if idx >= 0 {
  215. // foud a matching quote
  216. quote = ""
  217. }
  218. }
  219. if quote != "" {
  220. return env, fmt.Errorf("missing quotes")
  221. }
  222. err := parseLine(line, env, override)
  223. if err != nil {
  224. return env, err
  225. }
  226. }
  227. return env, nil
  228. }
  229. var (
  230. lineRgx = regexp.MustCompile(linePattern)
  231. unescapeRgx = regexp.MustCompile(`\\([^$])`)
  232. varRgx = regexp.MustCompile(variablePattern)
  233. )
  234. func parseLine(s string, env Env, override bool) error {
  235. rm := lineRgx.FindStringSubmatch(s)
  236. if len(rm) == 0 {
  237. return checkFormat(s, env)
  238. }
  239. key := strings.TrimSpace(rm[1])
  240. val := strings.TrimSpace(rm[2])
  241. var hsq, hdq bool
  242. // check if the value is quoted
  243. if l := len(val); l >= 2 {
  244. l -= 1
  245. // has double quotes
  246. hdq = val[0] == '"' && val[l] == '"'
  247. // has single quotes
  248. hsq = val[0] == '\'' && val[l] == '\''
  249. // remove quotes '' or ""
  250. if hsq || hdq {
  251. val = val[1:l]
  252. }
  253. }
  254. if hdq {
  255. val = strings.ReplaceAll(val, `\n`, "\n")
  256. val = strings.ReplaceAll(val, `\r`, "\r")
  257. // Unescape all characters except $ so variables can be escaped properly
  258. val = unescapeRgx.ReplaceAllString(val, "$1")
  259. }
  260. if !hsq {
  261. fv := func(s string) string {
  262. return varReplacement(s, hsq, env, override)
  263. }
  264. val = varRgx.ReplaceAllStringFunc(val, fv)
  265. }
  266. env[key] = val
  267. return nil
  268. }
  269. func parseExport(st string, env Env) error {
  270. if strings.HasPrefix(st, "export") {
  271. vs := strings.SplitN(st, " ", 2)
  272. if len(vs) > 1 {
  273. if _, ok := env[vs[1]]; !ok {
  274. return fmt.Errorf("line `%s` has an unset variable", st)
  275. }
  276. }
  277. }
  278. return nil
  279. }
  280. var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)
  281. func varReplacement(s string, hsq bool, env Env, override bool) string {
  282. if s == "" {
  283. return s
  284. }
  285. if s[0] == '\\' {
  286. // the dollar sign is escaped
  287. return s[1:]
  288. }
  289. if hsq {
  290. return s
  291. }
  292. mn := varNameRgx.FindStringSubmatch(s)
  293. if len(mn) == 0 {
  294. return s
  295. }
  296. v := mn[3]
  297. if replace, ok := os.LookupEnv(v); ok && !override {
  298. return replace
  299. }
  300. if replace, ok := env[v]; ok {
  301. return replace
  302. }
  303. return os.Getenv(v)
  304. }
  305. func checkFormat(s string, env Env) error {
  306. st := strings.TrimSpace(s)
  307. if st == "" || st[0] == '#' {
  308. return nil
  309. }
  310. if err := parseExport(st, env); err != nil {
  311. return err
  312. }
  313. return fmt.Errorf("line `%s` doesn't match format", s)
  314. }