1// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
2//
3// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv
4//
5// The TL;DR is that you make a .env file that looks something like
6//
7// SOME_ENV_VAR=somevalue
8//
9// and then in your go code you can call
10//
11// godotenv.Load()
12//
13// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
14package godotenv
15
16import (
17 "bytes"
18 "fmt"
19 "io"
20 "os"
21 "os/exec"
22 "sort"
23 "strconv"
24 "strings"
25)
26
27const doubleQuoteSpecialChars = "\\\n\r\"!$`"
28
29// Parse reads an env file from io.Reader, returning a map of keys and values.
30func Parse(r io.Reader) (map[string]string, error) {
31 var buf bytes.Buffer
32 _, err := io.Copy(&buf, r)
33 if err != nil {
34 return nil, err
35 }
36
37 return UnmarshalBytes(buf.Bytes())
38}
39
40// Load will read your env file(s) and load them into ENV for this process.
41//
42// Call this function as close as possible to the start of your program (ideally in main).
43//
44// If you call Load without any args it will default to loading .env in the current path.
45//
46// You can otherwise tell it which files to load (there can be more than one) like:
47//
48// godotenv.Load("fileone", "filetwo")
49//
50// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults.
51func Load(filenames ...string) (err error) {
52 filenames = filenamesOrDefault(filenames)
53
54 for _, filename := range filenames {
55 err = loadFile(filename, false)
56 if err != nil {
57 return // return early on a spazout
58 }
59 }
60 return
61}
62
63// Overload will read your env file(s) and load them into ENV for this process.
64//
65// Call this function as close as possible to the start of your program (ideally in main).
66//
67// If you call Overload without any args it will default to loading .env in the current path.
68//
69// You can otherwise tell it which files to load (there can be more than one) like:
70//
71// godotenv.Overload("fileone", "filetwo")
72//
73// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars.
74func Overload(filenames ...string) (err error) {
75 filenames = filenamesOrDefault(filenames)
76
77 for _, filename := range filenames {
78 err = loadFile(filename, true)
79 if err != nil {
80 return // return early on a spazout
81 }
82 }
83 return
84}
85
86// Read all env (with same file loading semantics as Load) but return values as
87// a map rather than automatically writing values into env
88func Read(filenames ...string) (envMap map[string]string, err error) {
89 filenames = filenamesOrDefault(filenames)
90 envMap = make(map[string]string)
91
92 for _, filename := range filenames {
93 individualEnvMap, individualErr := readFile(filename)
94
95 if individualErr != nil {
96 err = individualErr
97 return // return early on a spazout
98 }
99
100 for key, value := range individualEnvMap {
101 envMap[key] = value
102 }
103 }
104
105 return
106}
107
108// Unmarshal reads an env file from a string, returning a map of keys and values.
109func Unmarshal(str string) (envMap map[string]string, err error) {
110 return UnmarshalBytes([]byte(str))
111}
112
113// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values.
114func UnmarshalBytes(src []byte) (map[string]string, error) {
115 out := make(map[string]string)
116 err := parseBytes(src, out)
117
118 return out, err
119}
120
121// Exec loads env vars from the specified filenames (empty map falls back to default)
122// then executes the cmd specified.
123//
124// Simply hooks up os.Stdin/err/out to the command and calls Run().
125//
126// If you want more fine grained control over your command it's recommended
127// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself.
128func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error {
129 op := Load
130 if overload {
131 op = Overload
132 }
133 if err := op(filenames...); err != nil {
134 return err
135 }
136
137 command := exec.Command(cmd, cmdArgs...)
138 command.Stdin = os.Stdin
139 command.Stdout = os.Stdout
140 command.Stderr = os.Stderr
141 return command.Run()
142}
143
144// Write serializes the given environment and writes it to a file.
145func Write(envMap map[string]string, filename string) error {
146 content, err := Marshal(envMap)
147 if err != nil {
148 return err
149 }
150 file, err := os.Create(filename)
151 if err != nil {
152 return err
153 }
154 defer file.Close()
155 _, err = file.WriteString(content + "\n")
156 if err != nil {
157 return err
158 }
159 return file.Sync()
160}
161
162// Marshal outputs the given environment as a dotenv-formatted environment file.
163// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
164func Marshal(envMap map[string]string) (string, error) {
165 lines := make([]string, 0, len(envMap))
166 for k, v := range envMap {
167 if d, err := strconv.Atoi(v); err == nil {
168 lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
169 } else {
170 lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
171 }
172 }
173 sort.Strings(lines)
174 return strings.Join(lines, "\n"), nil
175}
176
177func filenamesOrDefault(filenames []string) []string {
178 if len(filenames) == 0 {
179 return []string{".env"}
180 }
181 return filenames
182}
183
184func loadFile(filename string, overload bool) error {
185 envMap, err := readFile(filename)
186 if err != nil {
187 return err
188 }
189
190 currentEnv := map[string]bool{}
191 rawEnv := os.Environ()
192 for _, rawEnvLine := range rawEnv {
193 key := strings.Split(rawEnvLine, "=")[0]
194 currentEnv[key] = true
195 }
196
197 for key, value := range envMap {
198 if !currentEnv[key] || overload {
199 _ = os.Setenv(key, value)
200 }
201 }
202
203 return nil
204}
205
206func readFile(filename string) (envMap map[string]string, err error) {
207 file, err := os.Open(filename)
208 if err != nil {
209 return
210 }
211 defer file.Close()
212
213 return Parse(file)
214}
215
216func doubleQuoteEscape(line string) string {
217 for _, c := range doubleQuoteSpecialChars {
218 toReplace := "\\" + string(c)
219 if c == '\n' {
220 toReplace = `\n`
221 }
222 if c == '\r' {
223 toReplace = `\r`
224 }
225 line = strings.Replace(line, string(c), toReplace, -1)
226 }
227 return line
228}