Compare commits

...

24 Commits

Author SHA1 Message Date
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
9 changed files with 192 additions and 75 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)
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)

View File

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

View File

@ -45,7 +45,7 @@ example
// take rest of args and "exec" them
cmd := args[0]
cmdArgs := args[1:len(args)]
cmdArgs := args[1:]
err := godotenv.Exec(envFilenames, cmd, cmdArgs)
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_D =4
OPTION_E = 5
OPTION_F =
OPTION_G=

View File

@ -1,18 +1,16 @@
/*
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
The TL;DR is that you make a .env file that looks something like
SOME_ENV_VAR=somevalue
and then in your go code you can call
godotenv.Load()
and all the env vars declared in .env will be avaiable through os.Getenv("SOME_ENV_VAR")
*/
// Package godotenv is 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
//
// The TL;DR is that you make a .env file that looks something like
//
// SOME_ENV_VAR=somevalue
//
// and then in your go code you can call
//
// godotenv.Load()
//
// and all the env vars declared in .env will be avaiable through os.Getenv("SOME_ENV_VAR")
package godotenv
import (
@ -23,22 +21,22 @@ import (
"strings"
)
/*
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
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
*/
// Load will read your env file(s) and load them into ENV for this process.
//
// 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
//
// 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) {
filenames = filenamesOrDefault(filenames)
for _, filename := range filenames {
err = loadFile(filename)
err = loadFile(filename, false)
if err != nil {
return // return early on a spazout
}
@ -46,10 +44,31 @@ func Load(filenames ...string) (err error) {
return
}
/*
Read all env (with same file loading semantics as Load) but return values as
a map rather than automatically writing values into env
*/
// Overload will read your env file(s) and load them into ENV for this process.
//
// 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) {
filenames = filenamesOrDefault(filenames)
envMap = make(map[string]string)
@ -70,15 +89,13 @@ func Read(filenames ...string) (envMap map[string]string, err error) {
return
}
/*
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.
*/
// 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 {
Load(filenames...)
@ -92,22 +109,30 @@ func Exec(filenames []string, cmd string, cmdArgs []string) error {
func filenamesOrDefault(filenames []string) []string {
if len(filenames) == 0 {
return []string{".env"}
} else {
}
return filenames
}
}
func loadFile(filename string) (err error) {
func loadFile(filename string, overload bool) error {
envMap, err := readFile(filename)
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 {
if !currentEnv[key] || overload {
os.Setenv(key, value)
}
}
return
return nil
}
func readFile(filename string) (envMap map[string]string, err error) {
@ -125,13 +150,19 @@ func readFile(filename string) (envMap map[string]string, err error) {
lines = append(lines, scanner.Text())
}
if err = scanner.Err(); err != nil {
return
}
for _, fullLine := range lines {
if !isIgnoredLine(fullLine) {
key, value, err := parseLine(fullLine)
var key, value string
key, value, err = parseLine(fullLine)
if err == nil && os.Getenv(key) == "" {
envMap[key] = value
if err != nil {
return
}
envMap[key] = value
}
}
return
@ -147,7 +178,7 @@ func parseLine(line string) (key string, value string, err error) {
if strings.Contains(line, "#") {
segmentsBetweenHashes := strings.Split(line, "#")
quotesAreOpen := false
segmentsToKeep := make([]string, 0)
var segmentsToKeep []string
for _, segment := range segmentsBetweenHashes {
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
if quotesAreOpen {
@ -188,18 +219,23 @@ func parseLine(line string) (key string, value string, err error) {
// Parse the value
value = splitString[1]
// trim
value = strings.Trim(value, " ")
// check if we've got quoted values
if strings.Count(value, "\"") == 2 || strings.Count(value, "'") == 2 {
if value != "" {
first := string(value[0:1])
last := string(value[len(value)-1:])
if first == last && strings.ContainsAny(first, `"'`) {
// pull the quotes off the edges
value = strings.Trim(value, "\"'")
value = strings.Trim(value, `"'`)
// expand quotes
value = strings.Replace(value, "\\\"", "\"", -1)
value = strings.Replace(value, `\"`, `"`, -1)
// expand newlines
value = strings.Replace(value, "\\n", "\n", -1)
value = strings.Replace(value, `\n`, "\n", -1)
}
}
return

View File

@ -5,6 +5,8 @@ import (
"testing"
)
var noopPresets = make(map[string]string)
func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) {
key, value, _ := parseLine(rawEnvLine)
if key != expectedKey || value != expectedValue {
@ -12,11 +14,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
os.Clearenv()
err := Load(envFileName)
for k, v := range presets {
os.Setenv(k, v)
}
err := loader(envFileName)
if err != nil {
t.Fatalf("Error loading %v", envFileName)
}
@ -38,6 +44,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) {
err := Load("somefilethatwillneverexistever.env")
if err == nil {
@ -45,6 +59,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) {
envFileName := "fixtures/plain.env"
expectedValues := map[string]string{
@ -53,6 +74,8 @@ func TestReadPlainEnv(t *testing.T) {
"OPTION_C": "3",
"OPTION_D": "4",
"OPTION_E": "5",
"OPTION_F": "",
"OPTION_G": "",
}
envMap, err := Read(envFileName)
@ -71,6 +94,36 @@ func TestReadPlainEnv(t *testing.T) {
}
}
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) {
envFileName := "fixtures/plain.env"
expectedValues := map[string]string{
@ -81,7 +134,7 @@ func TestLoadPlainEnv(t *testing.T) {
"OPTION_E": "5",
}
loadEnvAndCompareValues(t, envFileName, expectedValues)
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
func TestLoadExportedEnv(t *testing.T) {
@ -91,7 +144,7 @@ func TestLoadExportedEnv(t *testing.T) {
"OPTION_B": "\n",
}
loadEnvAndCompareValues(t, envFileName, expectedValues)
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
func TestLoadEqualsEnv(t *testing.T) {
@ -100,7 +153,7 @@ func TestLoadEqualsEnv(t *testing.T) {
"OPTION_A": "postgres://localhost:5432/database?sslmode=disable",
}
loadEnvAndCompareValues(t, envFileName, expectedValues)
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
func TestLoadQuotedEnv(t *testing.T) {
@ -116,7 +169,7 @@ func TestLoadQuotedEnv(t *testing.T) {
"OPTION_H": "\n",
}
loadEnvAndCompareValues(t, envFileName, expectedValues)
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
func TestActualEnvVarsAreLeftAlone(t *testing.T) {
@ -138,24 +191,24 @@ func TestParsing(t *testing.T) {
parseAndCompare(t, "FOO= bar", "FOO", "bar")
// parses double quoted values
parseAndCompare(t, "FOO=\"bar\"", "FOO", "bar")
parseAndCompare(t, `FOO="bar"`, "FOO", "bar")
// parses single quoted values
parseAndCompare(t, "FOO='bar'", "FOO", "bar")
// parses escaped double quotes
parseAndCompare(t, "FOO=escaped\\\"bar\"", "FOO", "escaped\"bar")
parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`)
// parses yaml style options
parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1")
// parses export keyword
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
// 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
// expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar')
@ -175,14 +228,14 @@ func TestParsing(t *testing.T) {
// it 'allows # in quoted value' do
// 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#bang\" # comment", "FOO", "bar#baz#bang")
parseAndCompare(t, `FOO="bar#baz#bang" # comment`, "FOO", "bar#baz#bang")
// 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')
parseAndCompare(t, "FOO=\"ba#r\"", "FOO", "ba#r")
parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r")
parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r")
// it 'throws an error if line format is incorrect' do
@ -216,7 +269,24 @@ func TestLinesToIgnore(t *testing.T) {
}
// 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")
}
}
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)
}
}

View File

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