godotenv.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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 avaiable through os.Getenv("SOME_ENV_VAR")
  14. package godotenv
  15. import (
  16. "bufio"
  17. "errors"
  18. "os"
  19. "os/exec"
  20. "strings"
  21. )
  22. // Load will read your env file(s) and load them into ENV for this process.
  23. //
  24. // Call this function as close as possible to the start of your program (ideally in main)
  25. //
  26. // If you call Load without any args it will default to loading .env in the current path
  27. //
  28. // You can otherwise tell it which files to load (there can be more than one) like
  29. //
  30. // godotenv.Load("fileone", "filetwo")
  31. //
  32. // 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
  33. func Load(filenames ...string) (err error) {
  34. filenames = filenamesOrDefault(filenames)
  35. for _, filename := range filenames {
  36. err = loadFile(filename, false)
  37. if err != nil {
  38. return // return early on a spazout
  39. }
  40. }
  41. return
  42. }
  43. // Overload will read your env file(s) and load them into ENV for this process.
  44. //
  45. // Call this function as close as possible to the start of your program (ideally in main)
  46. //
  47. // If you call Overload without any args it will default to loading .env in the current path
  48. //
  49. // You can otherwise tell it which files to load (there can be more than one) like
  50. //
  51. // godotenv.Overload("fileone", "filetwo")
  52. //
  53. // It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
  54. func Overload(filenames ...string) (err error) {
  55. filenames = filenamesOrDefault(filenames)
  56. for _, filename := range filenames {
  57. err = loadFile(filename, true)
  58. if err != nil {
  59. return // return early on a spazout
  60. }
  61. }
  62. return
  63. }
  64. // Read all env (with same file loading semantics as Load) but return values as
  65. // a map rather than automatically writing values into env
  66. func Read(filenames ...string) (envMap map[string]string, err error) {
  67. filenames = filenamesOrDefault(filenames)
  68. envMap = make(map[string]string)
  69. for _, filename := range filenames {
  70. individualEnvMap, individualErr := readFile(filename)
  71. if individualErr != nil {
  72. err = individualErr
  73. return // return early on a spazout
  74. }
  75. for key, value := range individualEnvMap {
  76. envMap[key] = value
  77. }
  78. }
  79. return
  80. }
  81. // Exec loads env vars from the specified filenames (empty map falls back to default)
  82. // then executes the cmd specified.
  83. //
  84. // Simply hooks up os.Stdin/err/out to the command and calls Run()
  85. //
  86. // If you want more fine grained control over your command it's recommended
  87. // that you use `Load()` or `Read()` and the `os/exec` package yourself.
  88. func Exec(filenames []string, cmd string, cmdArgs []string) error {
  89. Load(filenames...)
  90. command := exec.Command(cmd, cmdArgs...)
  91. command.Stdin = os.Stdin
  92. command.Stdout = os.Stdout
  93. command.Stderr = os.Stderr
  94. return command.Run()
  95. }
  96. func filenamesOrDefault(filenames []string) []string {
  97. if len(filenames) == 0 {
  98. return []string{".env"}
  99. }
  100. return filenames
  101. }
  102. func loadFile(filename string, overload bool) error {
  103. envMap, err := readFile(filename)
  104. if err != nil {
  105. return err
  106. }
  107. for key, value := range envMap {
  108. if os.Getenv(key) == "" || overload {
  109. os.Setenv(key, value)
  110. }
  111. }
  112. return nil
  113. }
  114. func readFile(filename string) (envMap map[string]string, err error) {
  115. file, err := os.Open(filename)
  116. if err != nil {
  117. return
  118. }
  119. defer file.Close()
  120. envMap = make(map[string]string)
  121. var lines []string
  122. scanner := bufio.NewScanner(file)
  123. for scanner.Scan() {
  124. lines = append(lines, scanner.Text())
  125. }
  126. if err = scanner.Err(); err != nil {
  127. return
  128. }
  129. for _, fullLine := range lines {
  130. if !isIgnoredLine(fullLine) {
  131. var key, value string
  132. key, value, err = parseLine(fullLine)
  133. if err != nil {
  134. return
  135. }
  136. envMap[key] = value
  137. }
  138. }
  139. return
  140. }
  141. func parseLine(line string) (key string, value string, err error) {
  142. if len(line) == 0 {
  143. err = errors.New("zero length string")
  144. return
  145. }
  146. // ditch the comments (but keep quoted hashes)
  147. if strings.Contains(line, "#") {
  148. segmentsBetweenHashes := strings.Split(line, "#")
  149. quotesAreOpen := false
  150. var segmentsToKeep []string
  151. for _, segment := range segmentsBetweenHashes {
  152. if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
  153. if quotesAreOpen {
  154. quotesAreOpen = false
  155. segmentsToKeep = append(segmentsToKeep, segment)
  156. } else {
  157. quotesAreOpen = true
  158. }
  159. }
  160. if len(segmentsToKeep) == 0 || quotesAreOpen {
  161. segmentsToKeep = append(segmentsToKeep, segment)
  162. }
  163. }
  164. line = strings.Join(segmentsToKeep, "#")
  165. }
  166. // now split key from value
  167. splitString := strings.SplitN(line, "=", 2)
  168. if len(splitString) != 2 {
  169. // try yaml mode!
  170. splitString = strings.SplitN(line, ":", 2)
  171. }
  172. if len(splitString) != 2 {
  173. err = errors.New("Can't separate key from value")
  174. return
  175. }
  176. // Parse the key
  177. key = splitString[0]
  178. if strings.HasPrefix(key, "export") {
  179. key = strings.TrimPrefix(key, "export")
  180. }
  181. key = strings.Trim(key, " ")
  182. // Parse the value
  183. value = splitString[1]
  184. // trim
  185. value = strings.Trim(value, " ")
  186. // check if we've got quoted values
  187. if strings.Count(value, "\"") == 2 || strings.Count(value, "'") == 2 {
  188. // pull the quotes off the edges
  189. value = strings.Trim(value, "\"'")
  190. // expand quotes
  191. value = strings.Replace(value, "\\\"", "\"", -1)
  192. // expand newlines
  193. value = strings.Replace(value, "\\n", "\n", -1)
  194. }
  195. return
  196. }
  197. func isIgnoredLine(line string) bool {
  198. trimmedLine := strings.Trim(line, " \n\t")
  199. return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
  200. }