Compare commits

..

48 Commits

Author SHA1 Message Date
a79fa1e548 Merge pull request #41 from alexquick/document-write-dotenv
Document Marshal, Unmarshal, and Write
2017-09-18 16:32:59 +10:00
144189c1ed Merge pull request #42 from alexquick/feature-sorted-output
Sort output of Marshal/Write
2017-09-18 16:32:10 +10:00
3dd2dbe832 sort output of Write/Marshal 2017-09-16 18:02:27 -04:00
9f04f40640 Be more careful with TestRoundtrip 2017-09-16 17:55:04 -04:00
e6264cf869 document Marshal, Unmarshal, and Write 2017-09-16 17:12:55 -04:00
9739509bea Merge pull request #35 from alexquick/feature-write-dotenv
support for writing envs out in dotenv format
2017-09-13 22:32:30 -07:00
b1bb9d9fc3 rename WriteString/ReadString to Marshal/Unmarshal 2017-09-14 00:24:22 -04:00
5d289f4405 escape some other bash-y special chars ($!) 2017-09-13 23:13:08 -04:00
88e7c8bd35 support for writing envs out in dotenv format 2017-09-13 23:13:08 -04:00
c9360df4d1 Merge pull request #34 from alexquick/fix-parsing-issues
Fix some small parsing bugs
2017-08-22 14:21:26 +10:00
59f20222da Merge pull request #33 from crash7/go-report-card
Add Go Report Card badge and fix spelling error
2017-08-22 14:17:51 +10:00
9d9ddadf44 Merge pull request #36 from pda/parse-from-reader
Parse(reader) as alternative to Read(filenames)
2017-08-06 18:28:30 +10:00
390de3704e README.md mentions Parse(io.Reader) 2017-08-06 17:34:10 +10:00
ebf1036af6 Parse(io.Reader) => map[string]string 2017-08-06 17:34:10 +10:00
a905e99577 fix panic with " as the value 2017-07-16 18:43:49 -04:00
6f30f0c011 support for equals in yaml-style lines 2017-07-16 17:25:28 -04:00
84bf91f40e rudimentry support for nested quotes 2017-07-16 17:24:36 -04:00
b9324c6f3c handle escaping more comprehensively 2017-07-16 17:15:29 -04:00
12b7e03247 Add Go Report Card badge and fix spelling error 2017-07-14 21:33:04 -03:00
3ddb2792f3 README housekeeping 2017-07-05 14:31:29 +10:00
325433c502 Merge pull request #29 from joho/respect_empty_external_env_vars
Respect preset empty external env vars
2017-03-29 07:01:54 +11:00
034acc2190 Change check of existing env to respect empty (but set) vars. 2017-03-28 11:54:56 +11:00
cd1272609d Add failing test for override of empty var 2017-03-28 11:39:40 +11:00
eaf676fc03 Merge pull request #27 from goenning/empty_var
allow usage of empty var on .env
2017-03-23 07:07:31 +11:00
a42a65518c allow empty_var 2017-03-22 13:05:44 +00:00
b01826f956 Merge pull request #25 from matiasanaya/master
Fix quoted values check
2017-03-21 20:56:48 +11:00
6a1233b2f6 Fix quoted values check 2017-03-21 19:04:19 +11:00
d10b3fbe00 Merge pull request #24 from joho/setup_travis
Move CI
2017-02-22 08:49:41 +11:00
0a959c8d8f Add a badge for the windows build too. 2017-02-22 08:43:40 +11:00
bcaccd4f68 Apparently this file is meant to be hidden? 2017-02-22 08:29:43 +11:00
22e45bfff4 Switch build badge over to travis 2017-02-22 08:27:57 +11:00
2fc79dff51 Replace wercker.yml with travis.yml 2017-02-22 08:23:25 +11:00
726cc8b906 Merge pull request #22 from mmilata/dont-swallow-errors
Improve error handling
2016-12-17 10:05:37 +11:00
861984c215 Don't hide line parsing errors 2016-12-12 14:43:30 +01:00
0ff0c0fc7a Propagate errors encountered when reading file 2016-12-12 14:41:36 +01:00
4ed13390c0 Merge pull request #13 from jmervine/master
Add Overload methods.
2015-09-07 11:02:28 +10:00
008304c688 adding Overload method 2015-09-05 08:59:08 -07:00
443e926da0 Merge pull request #11 from buddhamagnet/master
Remove unecessary assignment in autoloader
2015-06-10 07:30:23 +10:00
2ed25fcb28 remove unecessary assignment in autoloader 2015-06-09 18:24:15 +01:00
f6581828bb outdent else because golint said so. 2015-03-23 12:17:14 +11:00
d29c003c20 Still trying to please golint with package comments. 2015-03-23 12:15:55 +11:00
19b5c2bf30 Some golint feedback from http://goreportcard.com/report/joho/godotenv 2015-03-23 12:15:01 +11:00
e1c92610d7 run gofmt -w -s ./.. 2015-03-23 12:06:31 +11:00
ead2e75027 Merge pull request #8 from calavera/add_values_to_envmap
Fix issue reading file.
2015-01-02 15:46:44 +11:00
dc9cc93c4e Add values to the envMap when reading the file.
But do not override values in the global environment.
2014-12-23 17:57:02 -08:00
a01a834e16 Update docs for the bin command. 2014-10-12 10:38:08 +11:00
d2ce5befea Move cmd.go so it installs as "godotenv" 2014-10-12 10:27:22 +11:00
a86c254d7d Merge pull request #6 from joho/add_bin_command
Add a bin command
2014-10-12 09:58:57 +11:00
9 changed files with 421 additions and 97 deletions

8
.travis.yml Normal file
View File

@ -0,0 +1,8 @@
language: go
go:
- 1.8
os:
- linux
- osx

View File

@ -1,4 +1,4 @@
# GoDotEnv [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78 "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) # GoDotEnv [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4?svg=true)](https://ci.appveyor.com/project/joho/godotenv) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv)
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file) A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
@ -8,12 +8,23 @@ From the original Library:
> >
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. > But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
It can be used as a library (for loading in env for your own daemons etc) or as a bin command.
There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows.
## Installation ## Installation
As a library
```shell ```shell
go get github.com/joho/godotenv go get github.com/joho/godotenv
``` ```
or if you want to use it as a bin command
```shell
go get github.com/joho/godotenv/cmd/godotenv
```
## Usage ## Usage
Add your application configuration to your `.env` file in the root of your project: Add your application configuration to your `.env` file in the root of your project:
@ -85,7 +96,45 @@ myEnv, err := godotenv.Read()
s3Bucket := myEnv["S3_BUCKET"] s3Bucket := myEnv["S3_BUCKET"]
``` ```
end ... or from an `io.Reader` instead of a local file
```go
reader := getRemoteFile()
myEnv, err := godotenv.Parse(reader)
```
... or from a `string` if you so desire
```go
content := getRemoteFileContent()
myEnv, err := godotenv.Unmarshal(content)
```
### Command Mode
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
```
godotenv -f /some/path/to/.env some_command with some args
```
If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
### Writing Env Files
Godotenv can also write a map representing the environment to a correctly-formatted and escaped file
```go
env, err := godotenv.Unmarshal("KEY=value")
err := godotenv.Write(env, "./.env")
```
... or to a string
```go
env, err := godotenv.Unmarshal("KEY=value")
content, err := godotenv.Marshal(env)
```
## Contributing ## Contributing
@ -99,9 +148,15 @@ Contributions are most welcome! The parser itself is pretty stupidly naive and I
4. Push to the branch (`git push origin my-new-feature`) 4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request 5. Create new Pull Request
## Releases
Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.
Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`
## CI ## CI
Linux: [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78/m "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv) Linux: [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv)
## Who? ## Who?

View File

@ -11,5 +11,5 @@ package autoload
import "github.com/joho/godotenv" import "github.com/joho/godotenv"
func init() { func init() {
_ = godotenv.Load() godotenv.Load()
} }

View File

@ -45,7 +45,7 @@ example
// take rest of args and "exec" them // take rest of args and "exec" them
cmd := args[0] cmd := args[0]
cmdArgs := args[1:len(args)] cmdArgs := args[1:]
err := godotenv.Exec(envFilenames, cmd, cmdArgs) err := godotenv.Exec(envFilenames, cmd, cmdArgs)
if err != nil { if err != nil {

2
fixtures/invalid1.env Normal file
View File

@ -0,0 +1,2 @@
INVALID LINE
foo=bar

View File

@ -3,3 +3,5 @@ OPTION_B=2
OPTION_C= 3 OPTION_C= 3
OPTION_D =4 OPTION_D =4
OPTION_E = 5 OPTION_E = 5
OPTION_F =
OPTION_G=

View File

@ -1,44 +1,48 @@
/* // Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
A go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) //
// Examples/readme can be found on the github page at https://github.com/joho/godotenv
Examples/readme can be found on the github page at https://github.com/joho/godotenv //
// The TL;DR is that you make a .env file that looks something like
The TL;DR is that you make a .env file that looks something like //
// SOME_ENV_VAR=somevalue
SOME_ENV_VAR=somevalue //
// and then in your go code you can call
and then in your go code you can call //
// godotenv.Load()
godotenv.Load() //
// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
and all the env vars declared in .env will be avaiable through os.Getenv("SOME_ENV_VAR")
*/
package godotenv package godotenv
import ( import (
"bufio" "bufio"
"errors" "errors"
"fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"regexp"
"sort"
"strings" "strings"
) )
/* const doubleQuoteSpecialChars = "\\\n\r\"!$`"
Call this function as close as possible to the start of your program (ideally in main)
If you call Load without any args it will default to loading .env in the current path // Load will read your env file(s) and load them into ENV for this process.
//
You can otherwise tell it which files to load (there can be more than one) like // Call this function as close as possible to the start of your program (ideally in main)
//
godotenv.Load("fileone", "filetwo") // If you call Load without any args it will default to loading .env in the current path
//
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 // You can otherwise tell it which files to load (there can be more than one) like
*/ //
// godotenv.Load("fileone", "filetwo")
//
// 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
func Load(filenames ...string) (err error) { func Load(filenames ...string) (err error) {
filenames = filenamesOrDefault(filenames) filenames = filenamesOrDefault(filenames)
for _, filename := range filenames { for _, filename := range filenames {
err = loadFile(filename) err = loadFile(filename, false)
if err != nil { if err != nil {
return // return early on a spazout return // return early on a spazout
} }
@ -46,10 +50,31 @@ func Load(filenames ...string) (err error) {
return return
} }
/* // Overload will read your env file(s) and load them into ENV for this process.
Read all env (with same file loading semantics as Load) but return values as //
a map rather than automatically writing values into env // Call this function as close as possible to the start of your program (ideally in main)
*/ //
// If you call Overload without any args it will default to loading .env in the current path
//
// You can otherwise tell it which files to load (there can be more than one) like
//
// godotenv.Overload("fileone", "filetwo")
//
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
func Overload(filenames ...string) (err error) {
filenames = filenamesOrDefault(filenames)
for _, filename := range filenames {
err = loadFile(filename, true)
if err != nil {
return // return early on a spazout
}
}
return
}
// Read all env (with same file loading semantics as Load) but return values as
// a map rather than automatically writing values into env
func Read(filenames ...string) (envMap map[string]string, err error) { func Read(filenames ...string) (envMap map[string]string, err error) {
filenames = filenamesOrDefault(filenames) filenames = filenamesOrDefault(filenames)
envMap = make(map[string]string) envMap = make(map[string]string)
@ -70,15 +95,46 @@ func Read(filenames ...string) (envMap map[string]string, err error) {
return return
} }
/* // Parse reads an env file from io.Reader, returning a map of keys and values.
Loads env vars from the specified filenames (empty map falls back to default) func Parse(r io.Reader) (envMap map[string]string, err error) {
then executes the cmd specified. envMap = make(map[string]string)
Simply hooks up os.Stdin/err/out to the command and calls Run() var lines []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
If you want more fine grained control over your command it's recommended if err = scanner.Err(); err != nil {
that you use `Load()` or `Read()` and the `os/exec` package yourself. return
*/ }
for _, fullLine := range lines {
if !isIgnoredLine(fullLine) {
var key, value string
key, value, err = parseLine(fullLine)
if err != nil {
return
}
envMap[key] = value
}
}
return
}
//Unmarshal reads an env file from a string, returning a map of keys and values.
func Unmarshal(str string) (envMap map[string]string, err error) {
return Parse(strings.NewReader(str))
}
// Exec loads env vars from the specified filenames (empty map falls back to default)
// then executes the cmd specified.
//
// Simply hooks up os.Stdin/err/out to the command and calls Run()
//
// If you want more fine grained control over your command it's recommended
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
func Exec(filenames []string, cmd string, cmdArgs []string) error { func Exec(filenames []string, cmd string, cmdArgs []string) error {
Load(filenames...) Load(filenames...)
@ -89,25 +145,58 @@ func Exec(filenames []string, cmd string, cmdArgs []string) error {
return command.Run() return command.Run()
} }
// Write serializes the given environment and writes it to a file
func Write(envMap map[string]string, filename string) error {
content, error := Marshal(envMap)
if error != nil {
return error
}
file, error := os.Create(filename)
if error != nil {
return error
}
_, err := file.WriteString(content)
return err
}
// Marshal outputs the given environment as a dotenv-formatted environment file.
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
func Marshal(envMap map[string]string) (string, error) {
lines := make([]string, 0, len(envMap))
for k, v := range envMap {
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
}
sort.Strings(lines)
return strings.Join(lines, "\n"), nil
}
func filenamesOrDefault(filenames []string) []string { func filenamesOrDefault(filenames []string) []string {
if len(filenames) == 0 { if len(filenames) == 0 {
return []string{".env"} return []string{".env"}
} else {
return filenames
} }
return filenames
} }
func loadFile(filename string) (err error) { func loadFile(filename string, overload bool) error {
envMap, err := readFile(filename) envMap, err := readFile(filename)
if err != nil { if err != nil {
return return err
}
currentEnv := map[string]bool{}
rawEnv := os.Environ()
for _, rawEnvLine := range rawEnv {
key := strings.Split(rawEnvLine, "=")[0]
currentEnv[key] = true
} }
for key, value := range envMap { for key, value := range envMap {
os.Setenv(key, value) if !currentEnv[key] || overload {
os.Setenv(key, value)
}
} }
return return nil
} }
func readFile(filename string) (envMap map[string]string, err error) { func readFile(filename string) (envMap map[string]string, err error) {
@ -117,24 +206,7 @@ func readFile(filename string) (envMap map[string]string, err error) {
} }
defer file.Close() defer file.Close()
envMap = make(map[string]string) return Parse(file)
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
for _, fullLine := range lines {
if !isIgnoredLine(fullLine) {
key, value, err := parseLine(fullLine)
if err == nil && os.Getenv(key) == "" {
envMap[key] = value
}
}
}
return
} }
func parseLine(line string) (key string, value string, err error) { func parseLine(line string) (key string, value string, err error) {
@ -147,7 +219,7 @@ func parseLine(line string) (key string, value string, err error) {
if strings.Contains(line, "#") { if strings.Contains(line, "#") {
segmentsBetweenHashes := strings.Split(line, "#") segmentsBetweenHashes := strings.Split(line, "#")
quotesAreOpen := false quotesAreOpen := false
segmentsToKeep := make([]string, 0) var segmentsToKeep []string
for _, segment := range segmentsBetweenHashes { for _, segment := range segmentsBetweenHashes {
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 { if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
if quotesAreOpen { if quotesAreOpen {
@ -166,11 +238,11 @@ func parseLine(line string) (key string, value string, err error) {
line = strings.Join(segmentsToKeep, "#") line = strings.Join(segmentsToKeep, "#")
} }
// now split key from value firstEquals := strings.Index(line, "=")
firstColon := strings.Index(line, ":")
splitString := strings.SplitN(line, "=", 2) splitString := strings.SplitN(line, "=", 2)
if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
if len(splitString) != 2 { //this is a yaml-style line
// try yaml mode!
splitString = strings.SplitN(line, ":", 2) splitString = strings.SplitN(line, ":", 2)
} }
@ -187,25 +259,56 @@ func parseLine(line string) (key string, value string, err error) {
key = strings.Trim(key, " ") key = strings.Trim(key, " ")
// Parse the value // Parse the value
value = splitString[1] value = parseValue(splitString[1])
return
}
func parseValue(value string) string {
// trim // trim
value = strings.Trim(value, " ") value = strings.Trim(value, " ")
// check if we've got quoted values // check if we've got quoted values or possible escapes
if strings.Count(value, "\"") == 2 || strings.Count(value, "'") == 2 { if len(value) > 1 {
// pull the quotes off the edges first := string(value[0:1])
value = strings.Trim(value, "\"'") last := string(value[len(value)-1:])
if first == last && strings.ContainsAny(first, `"'`) {
// expand quotes // pull the quotes off the edges
value = strings.Replace(value, "\\\"", "\"", -1) value = value[1 : len(value)-1]
// expand newlines // handle escapes
value = strings.Replace(value, "\\n", "\n", -1) escapeRegex := regexp.MustCompile(`\\.`)
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
c := strings.TrimPrefix(match, `\`)
switch c {
case "n":
return "\n"
case "r":
return "\r"
default:
return c
}
})
}
} }
return return value
} }
func isIgnoredLine(line string) bool { func isIgnoredLine(line string) bool {
trimmedLine := strings.Trim(line, " \n\t") trimmedLine := strings.Trim(line, " \n\t")
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
} }
func doubleQuoteEscape(line string) string {
for _, c := range doubleQuoteSpecialChars {
toReplace := "\\" + string(c)
if c == '\n' {
toReplace = `\n`
}
if c == '\r' {
toReplace = `\r`
}
line = strings.Replace(line, string(c), toReplace, -1)
}
return line
}

View File

@ -1,10 +1,15 @@
package godotenv package godotenv
import ( import (
"bytes"
"fmt"
"os" "os"
"reflect"
"testing" "testing"
) )
var noopPresets = make(map[string]string)
func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) { func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) {
key, value, _ := parseLine(rawEnvLine) key, value, _ := parseLine(rawEnvLine)
if key != expectedKey || value != expectedValue { if key != expectedKey || value != expectedValue {
@ -12,11 +17,15 @@ func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expect
} }
} }
func loadEnvAndCompareValues(t *testing.T, envFileName string, expectedValues map[string]string) { func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, envFileName string, expectedValues map[string]string, presets map[string]string) {
// first up, clear the env // first up, clear the env
os.Clearenv() os.Clearenv()
err := Load(envFileName) for k, v := range presets {
os.Setenv(k, v)
}
err := loader(envFileName)
if err != nil { if err != nil {
t.Fatalf("Error loading %v", envFileName) t.Fatalf("Error loading %v", envFileName)
} }
@ -38,6 +47,14 @@ func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) {
} }
} }
func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {
err := Overload()
pathError := err.(*os.PathError)
if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
t.Errorf("Didn't try and open .env by default")
}
}
func TestLoadFileNotFound(t *testing.T) { func TestLoadFileNotFound(t *testing.T) {
err := Load("somefilethatwillneverexistever.env") err := Load("somefilethatwillneverexistever.env")
if err == nil { if err == nil {
@ -45,6 +62,13 @@ func TestLoadFileNotFound(t *testing.T) {
} }
} }
func TestOverloadFileNotFound(t *testing.T) {
err := Overload("somefilethatwillneverexistever.env")
if err == nil {
t.Error("File wasn't found but Overload didn't return an error")
}
}
func TestReadPlainEnv(t *testing.T) { func TestReadPlainEnv(t *testing.T) {
envFileName := "fixtures/plain.env" envFileName := "fixtures/plain.env"
expectedValues := map[string]string{ expectedValues := map[string]string{
@ -53,6 +77,8 @@ func TestReadPlainEnv(t *testing.T) {
"OPTION_C": "3", "OPTION_C": "3",
"OPTION_D": "4", "OPTION_D": "4",
"OPTION_E": "5", "OPTION_E": "5",
"OPTION_F": "",
"OPTION_G": "",
} }
envMap, err := Read(envFileName) envMap, err := Read(envFileName)
@ -71,6 +97,53 @@ func TestReadPlainEnv(t *testing.T) {
} }
} }
func TestParse(t *testing.T) {
envMap, err := Parse(bytes.NewReader([]byte("ONE=1\nTWO='2'\nTHREE = \"3\"")))
expectedValues := map[string]string{
"ONE": "1",
"TWO": "2",
"THREE": "3",
}
if err != nil {
t.Fatalf("error parsing env: %v", err)
}
for key, value := range expectedValues {
if envMap[key] != value {
t.Errorf("expected %s to be %s, got %s", key, value, envMap[key])
}
}
}
func TestLoadDoesNotOverride(t *testing.T) {
envFileName := "fixtures/plain.env"
// ensure NO overload
presets := map[string]string{
"OPTION_A": "do_not_override",
"OPTION_B": "",
}
expectedValues := map[string]string{
"OPTION_A": "do_not_override",
"OPTION_B": "",
}
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)
}
func TestOveroadDoesOverride(t *testing.T) {
envFileName := "fixtures/plain.env"
// ensure NO overload
presets := map[string]string{
"OPTION_A": "do_not_override",
}
expectedValues := map[string]string{
"OPTION_A": "1",
}
loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets)
}
func TestLoadPlainEnv(t *testing.T) { func TestLoadPlainEnv(t *testing.T) {
envFileName := "fixtures/plain.env" envFileName := "fixtures/plain.env"
expectedValues := map[string]string{ expectedValues := map[string]string{
@ -81,7 +154,7 @@ func TestLoadPlainEnv(t *testing.T) {
"OPTION_E": "5", "OPTION_E": "5",
} }
loadEnvAndCompareValues(t, envFileName, expectedValues) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
} }
func TestLoadExportedEnv(t *testing.T) { func TestLoadExportedEnv(t *testing.T) {
@ -91,7 +164,7 @@ func TestLoadExportedEnv(t *testing.T) {
"OPTION_B": "\n", "OPTION_B": "\n",
} }
loadEnvAndCompareValues(t, envFileName, expectedValues) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
} }
func TestLoadEqualsEnv(t *testing.T) { func TestLoadEqualsEnv(t *testing.T) {
@ -100,7 +173,7 @@ func TestLoadEqualsEnv(t *testing.T) {
"OPTION_A": "postgres://localhost:5432/database?sslmode=disable", "OPTION_A": "postgres://localhost:5432/database?sslmode=disable",
} }
loadEnvAndCompareValues(t, envFileName, expectedValues) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
} }
func TestLoadQuotedEnv(t *testing.T) { func TestLoadQuotedEnv(t *testing.T) {
@ -116,7 +189,7 @@ func TestLoadQuotedEnv(t *testing.T) {
"OPTION_H": "\n", "OPTION_H": "\n",
} }
loadEnvAndCompareValues(t, envFileName, expectedValues) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
} }
func TestActualEnvVarsAreLeftAlone(t *testing.T) { func TestActualEnvVarsAreLeftAlone(t *testing.T) {
@ -138,24 +211,33 @@ func TestParsing(t *testing.T) {
parseAndCompare(t, "FOO= bar", "FOO", "bar") parseAndCompare(t, "FOO= bar", "FOO", "bar")
// parses double quoted values // parses double quoted values
parseAndCompare(t, "FOO=\"bar\"", "FOO", "bar") parseAndCompare(t, `FOO="bar"`, "FOO", "bar")
// parses single quoted values // parses single quoted values
parseAndCompare(t, "FOO='bar'", "FOO", "bar") parseAndCompare(t, "FOO='bar'", "FOO", "bar")
// parses escaped double quotes // parses escaped double quotes
parseAndCompare(t, "FOO=escaped\\\"bar\"", "FOO", "escaped\"bar") parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`)
// parses single quotes inside double quotes
parseAndCompare(t, `FOO="'d'"`, "FOO", `'d'`)
// parses yaml style options // parses yaml style options
parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1") parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1")
//parses yaml values with equal signs
parseAndCompare(t, "OPTION_A: Foo=bar", "OPTION_A", "Foo=bar")
// parses non-yaml options with colons
parseAndCompare(t, "OPTION_A=1:B", "OPTION_A", "1:B")
// parses export keyword // parses export keyword
parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2") parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2")
parseAndCompare(t, "export OPTION_B='\\n'", "OPTION_B", "\n") parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\n")
// it 'expands newlines in quoted strings' do // it 'expands newlines in quoted strings' do
// expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz") // expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz")
parseAndCompare(t, "FOO=\"bar\\nbaz\"", "FOO", "bar\nbaz") parseAndCompare(t, `FOO="bar\nbaz"`, "FOO", "bar\nbaz")
// it 'parses varibales with "." in the name' do // it 'parses varibales with "." in the name' do
// expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar') // expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar')
@ -175,16 +257,25 @@ func TestParsing(t *testing.T) {
// it 'allows # in quoted value' do // it 'allows # in quoted value' do
// expect(env('foo="bar#baz" # comment')).to eql('foo' => 'bar#baz') // expect(env('foo="bar#baz" # comment')).to eql('foo' => 'bar#baz')
parseAndCompare(t, "FOO=\"bar#baz\" # comment", "FOO", "bar#baz") parseAndCompare(t, `FOO="bar#baz" # comment`, "FOO", "bar#baz")
parseAndCompare(t, "FOO='bar#baz' # comment", "FOO", "bar#baz") parseAndCompare(t, "FOO='bar#baz' # comment", "FOO", "bar#baz")
parseAndCompare(t, "FOO=\"bar#baz#bang\" # comment", "FOO", "bar#baz#bang") parseAndCompare(t, `FOO="bar#baz#bang" # comment`, "FOO", "bar#baz#bang")
// it 'parses # in quoted values' do // it 'parses # in quoted values' do
// expect(env('foo="ba#r"')).to eql('foo' => 'ba#r') // expect(env('foo="ba#r"')).to eql('foo' => 'ba#r')
// expect(env("foo='ba#r'")).to eql('foo' => 'ba#r') // expect(env("foo='ba#r'")).to eql('foo' => 'ba#r')
parseAndCompare(t, "FOO=\"ba#r\"", "FOO", "ba#r") parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r")
parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r") parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r")
//newlines and backslashes should be escaped
parseAndCompare(t, `FOO="bar\n\ b\az"`, "FOO", "bar\n baz")
parseAndCompare(t, `FOO="bar\\\n\ b\az"`, "FOO", "bar\\\n baz")
parseAndCompare(t, `FOO="bar\\r\ b\az"`, "FOO", "bar\\r baz")
parseAndCompare(t, `="value"`, "", "value")
parseAndCompare(t, `KEY="`, "KEY", "\"")
parseAndCompare(t, `KEY="value`, "KEY", "\"value")
// it 'throws an error if line format is incorrect' do // it 'throws an error if line format is incorrect' do
// expect{env('lol$wut')}.to raise_error(Dotenv::FormatError) // expect{env('lol$wut')}.to raise_error(Dotenv::FormatError)
badlyFormattedLine := "lol$wut" badlyFormattedLine := "lol$wut"
@ -216,7 +307,71 @@ func TestLinesToIgnore(t *testing.T) {
} }
// make sure we're not getting false positives // make sure we're not getting false positives
if isIgnoredLine("export OPTION_B='\\n'") { if isIgnoredLine(`export OPTION_B='\n'`) {
t.Error("ignoring a perfectly valid line to parse") t.Error("ignoring a perfectly valid line to parse")
} }
} }
func TestErrorReadDirectory(t *testing.T) {
envFileName := "fixtures/"
envMap, err := Read(envFileName)
if err == nil {
t.Errorf("Expected error, got %v", envMap)
}
}
func TestErrorParsing(t *testing.T) {
envFileName := "fixtures/invalid1.env"
envMap, err := Read(envFileName)
if err == nil {
t.Errorf("Expected error, got %v", envMap)
}
}
func TestWrite(t *testing.T) {
writeAndCompare := func(env string, expected string) {
envMap, _ := Unmarshal(env)
actual, _ := Marshal(envMap)
if expected != actual {
t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual)
}
}
//just test some single lines to show the general idea
//TestRoundtrip makes most of the good assertions
//values are always double-quoted
writeAndCompare(`key=value`, `key="value"`)
//double-quotes are escaped
writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`)
//but single quotes are left alone
writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`)
// newlines, backslashes, and some other special chars are escaped
writeAndCompare(`foo="$ba\n\r\\r!"`, `foo="\$ba\n\r\\r\!"`)
// lines should be sorted
writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"")
}
func TestRoundtrip(t *testing.T) {
fixtures := []string{"equals.env", "exported.env", "plain.env", "quoted.env"}
for _, fixture := range fixtures {
fixtureFilename := fmt.Sprintf("fixtures/%s", fixture)
env, err := readFile(fixtureFilename)
if err != nil {
t.Errorf("Expected '%s' to read without error (%v)", fixtureFilename, err)
}
rep, err := Marshal(env)
if err != nil {
t.Errorf("Expected '%s' to Marshal (%v)", fixtureFilename, err)
}
roundtripped, err := Unmarshal(rep)
if err != nil {
t.Errorf("Expected '%s' to Mashal and Unmarshal (%v)", fixtureFilename, err)
}
if !reflect.DeepEqual(env, roundtripped) {
t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped)
}
}
}

View File

@ -1 +0,0 @@
box: pjvds/golang