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