godotenv.go

  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}