Compare commits

...

78 Commits

Author SHA1 Message Date
ddf83eb33b Add missing newline to created env files (#133) 2021-03-04 20:35:31 +11:00
f562099a43 Add renovate.json (#124)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-11-11 15:36:58 +11:00
c49ef66cd3 Fix release name 2020-11-11 15:35:55 +11:00
c142d16052 Try a different github action 2020-11-11 15:30:39 +11:00
4e2d182c9a Add action to publish binaries on each release (#125) 2020-11-11 15:15:18 +11:00
65218afbaa Add a go.mod file (#123) 2020-11-11 14:54:56 +11:00
aa035a1808 Non amd tests (#122)
* Add non-amd64 tests

* Try shifting env around

* Update input to match action

* Include git

* Comment out IBM Z for now and quieten apt-get
2020-11-11 12:54:49 +11:00
23296b91aa Merge pull request #121 from joho/github-actions
Setup GitHub actions for CI
2020-11-11 11:33:11 +11:00
e57c08db27 Add CI badge to readme 2020-11-11 11:31:34 +11:00
573e9186b2 Matrix test across mac/win/linux 2020-11-11 11:24:40 +11:00
5451d82b77 Add Barebones github actions test 2020-11-11 11:19:52 +11:00
0da8ce72f0 Merge pull request #115 from adombeck/master
Fix typo
2020-11-08 12:01:29 +11:00
3e4069b9b2 Merge pull request #109 from mniak/pr
Write ints without quotes
2020-11-08 11:58:27 +11:00
a4d9cf1d6d Merge pull request #120 from santosh653/master
PR_AddingPowerSupport_golang-github-joho-godotenv
2020-10-16 06:51:22 +11:00
7fbe752d59 Update .travis.yml
adding power support.
2020-10-15 02:09:51 -04:00
46ee0dcae8 Update .travis.yml
Adding arch: amd64
2020-10-15 00:58:50 -04:00
bc7d5cd181 Fix typo 2020-09-04 23:35:59 +02:00
6e653f9adf add newline back 2020-06-26 15:39:50 -03:00
fccdfd265d Fix package name 2020-06-26 15:39:06 -03:00
f4e7418908 Remove go.mod 2020-06-26 15:36:35 -03:00
63ea8bf09b Change package name back to joho/godotenv 2020-06-26 15:35:47 -03:00
29b5be9cdc Rename and implement int-able without quotes 2020-06-26 15:22:04 -03:00
d6ee6871f2 Merge pull request #95 from orxobo/Write-adjustment
Fixed Write bugs
2020-03-02 07:46:15 +11:00
dbcf4b53b8 Fixed Write bugs
This should address a fix #93 and #94
This has not been addressed in any tests.
2020-02-11 17:05:34 +10:00
b09de681dc Merge pull request #90 from djherbis/master
#89 move regexp.MustCompile to globals
2019-10-21 07:25:22 +11:00
992ab0ec47 #89 move regexp.MustCompile to globals 2019-10-18 10:35:15 -04:00
5c0e6c6ab1 Merge pull request #69 from hairyhenderson/ignore-leading-whitespace
Fixing a couple whitespace bugs: ignoring leading whitespace, and supporting more kinds of empty lines
2019-02-04 15:41:09 +11:00
61baafa627 Merge branch 'master' into ignore-leading-whitespace 2019-02-04 15:28:23 +11:00
823f94bb9a Merge pull request #70 from hairyhenderson/support-keys-beginning-with-export-66
Support key names beginning with 'export'
2019-02-04 15:26:24 +11:00
263a1dda9d Support key names beginning with 'export'
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2019-02-03 23:11:51 -05:00
79711eebaf Ignoring leading whitespace
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2019-02-03 22:39:38 -05:00
69ed1d913a Merge pull request #63 from mdanzinger/patch-1
Update cmd.go
2018-11-21 06:47:48 +11:00
2841430efc An even tinier grammar fix
I read `.env` aloud as dotenv so the A makes sense.
2018-11-21 06:41:23 +11:00
5917dd2291 Update cmd.go
Tiny little grammatical error.
2018-11-20 13:25:32 -05:00
c0b86d615e Merge pull request #60 from coolaj86/precedence
explicitly state env precedence and convention
2018-09-27 15:32:03 +10:00
3896766f7d explicitly state env precedence and convention 2018-09-24 21:35:43 -06:00
23d116af35 Merge pull request #58 from lucastetreault/master
feat(Expand Variables): Custom variable expansion instead of Go's os.Expand
2018-09-11 18:04:31 +10:00
2d8b3aab88 feat(Expand Variables): Custom variable expansion instead of Go's os.Expand
Copy over the tests from https://github.com/bkeepers/dotenv/blob/master/spec/dotenv/parser_spec.rb
related to expanding variables and implement the required changes. I also realized as part of this
that this implementation was not handling values in single quotes properly (e.g.: not the same was
as the ruby package mentionned) so that has been fixed as well along with the related tests.

Fixes: #52
2018-09-11 00:55:10 -06:00
1709ab122c Merge pull request #54 from egorse/master
The value expand fallback to actual ENV values
2018-04-05 15:36:34 +10:00
8ad714e304 The value expand fallback to actual ENV values 2018-03-31 23:18:36 +03:00
6bb0851667 Merge pull request #47 from sachaos/feature-expand-variables-on-value
Support variable substitution in dotenv files
2018-01-15 13:49:21 +11:00
06e67b5ef3 Update my homepage link 2017-12-23 10:35:39 +11:00
2707e9ff66 Fix test, $ should not be escaped 2017-11-20 23:41:46 +09:00
50c29652a0 Expand variables on parseValue 2017-11-20 23:20:38 +09:00
33977c2d8d Add test for substitutions 2017-11-20 23:20:19 +09:00
9be76b3741 Pass envMap to parseLine & parseValue 2017-11-20 22:21:39 +09:00
6d367c18ed Merge pull request #46 from joho/fix_quoting_parser_bug
WIP Parsing bug with nested quotes
2017-11-10 12:03:15 +11:00
05be8ccbf7 Try and replicate reported bug #45 2017-11-10 10:38:18 +11:00
0f92a24bb0 Merge pull request #44 from dvrkps/patch-1
travis: update go version
2017-11-02 13:11:18 +11:00
b7bbb3624e travis: update go version 2017-11-01 07:18:32 +01:00
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
12 changed files with 549 additions and 72 deletions

62
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: CI
on: [push]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
go: [ '1.15', '1.14' ]
os: [ ubuntu-latest, macOS-latest, windows-latest ]
name: ${{ matrix.os }} Go ${{ matrix.go }} Tests
steps:
- uses: actions/checkout@v2
- name: Setup go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- run: go test
test-non-amd64:
strategy:
fail-fast: false
matrix:
arch:
# For some reasons this is segfaulting on go env
# - name: IBM Z and LinuxONE
# architecture: "s390x"
- name: POWER8
architecture: "ppc64le"
runs-on: ubuntu-latest
name: Test on ${{ matrix.arch.name }}
steps:
- uses: actions/checkout@v2
- uses: uraimo/run-on-arch-action@master
with:
arch: ${{ matrix.arch.architecture }}
distro: ubuntu20.04
env: | # YAML pipe
GOARCH: ${{ matrix.arch.architecture }}
CGO_ENABLED: 0
run: |
apt-get update
apt-get install -q -y curl wget git
latestGo=$(curl "https://golang.org/VERSION?m=text")
wget "https://dl.google.com/go/${latestGo}.linux-${GOARCH}.tar.gz"
rm -f $(which go)
rm -rf /usr/local/go
tar -C /usr/local -xzf "${latestGo}.linux-${GOARCH}.tar.gz"
export PATH=/usr/local/go/bin:$PATH
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
go get -insecure -v -t -d ./...
go test ./...
cd ./cmd/godotenv
go build -trimpath -ldflags="-w -s" -v

32
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,32 @@
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Upload Release Assets
jobs:
build:
name: Upload Release Assets
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Generate build files
uses: thatisuday/go-cross-build@v1
with:
platforms: 'linux/amd64, linux/ppc64le, darwin/amd64, windows/amd64'
package: 'cmd/godotenv'
name: 'godotenv'
compress: 'true'
dest: 'dist'
- name: Publish Binaries
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: Release ${{ github.ref }}
tag: ${{ github.ref }}
file: dist/*
file_glob: true
overwrite: true

View File

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

View File

@ -1,4 +1,4 @@
# 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) # GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![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)
@ -96,6 +96,45 @@ myEnv, err := godotenv.Read()
s3Bucket := myEnv["S3_BUCKET"] s3Bucket := myEnv["S3_BUCKET"]
``` ```
... 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)
```
### Precedence & Conventions
Existing envs take precedence of envs that are loaded later.
The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use)
for managing multiple environments (i.e. development, test, production)
is to create an env named `{YOURAPP}_ENV` and load envs in this order:
```go
env := os.Getenv("FOO_ENV")
if "" == env {
env = "development"
}
godotenv.Load(".env." + env + ".local")
if "test" != env {
godotenv.Load(".env.local")
}
godotenv.Load(".env." + env)
godotenv.Load() // The Original .env
```
If you need to, you can also use `godotenv.Overload()` to defy this convention
and overwrite existing envs instead of only supplanting them. Use with caution.
### Command Mode ### Command Mode
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
@ -106,6 +145,22 @@ 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` 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
Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases. Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases.
@ -118,10 +173,16 @@ 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?
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](http://whoisjohnbarton.com) based off the tests/fixtures in the original library. The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library.

View File

@ -19,7 +19,7 @@ func main() {
flag.Parse() flag.Parse()
usage := ` usage := `
Run a process with a env setup from a .env file Run a process with an env setup from a .env file
godotenv [-f ENV_FILE_PATHS] COMMAND_ARGS godotenv [-f ENV_FILE_PATHS] COMMAND_ARGS

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

@ -6,3 +6,4 @@ OPTION_E="1"
OPTION_F="2" OPTION_F="2"
OPTION_G="" OPTION_G=""
OPTION_H="\n" OPTION_H="\n"
OPTION_I = "echo 'asd'"

View File

@ -0,0 +1,5 @@
OPTION_A=1
OPTION_B=${OPTION_A}
OPTION_C=$OPTION_B
OPTION_D=${OPTION_A}${OPTION_B}
OPTION_E=${OPTION_NOT_DEFINED}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/joho/godotenv
go 1.12

View File

@ -10,17 +10,24 @@
// //
// godotenv.Load() // godotenv.Load()
// //
// and all the env vars declared in .env will be avaiable through os.Getenv("SOME_ENV_VAR") // and all the env vars declared in .env will be available 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"
"strconv"
"strings" "strings"
) )
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
// Load will read your env file(s) and load them into ENV for this process. // 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) // Call this function as close as possible to the start of your program (ideally in main)
@ -89,6 +96,39 @@ 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.
func Parse(r io.Reader) (envMap map[string]string, err error) {
envMap = make(map[string]string)
var lines []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err = scanner.Err(); err != nil {
return
}
for _, fullLine := range lines {
if !isIgnoredLine(fullLine) {
var key, value string
key, value, err = parseLine(fullLine, envMap)
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) // Exec loads env vars from the specified filenames (empty map falls back to default)
// then executes the cmd specified. // then executes the cmd specified.
// //
@ -106,6 +146,40 @@ 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, err := Marshal(envMap)
if err != nil {
return err
}
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(content + "\n")
if err != nil {
return err
}
file.Sync()
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 {
if d, err := strconv.Atoi(v); err == nil {
lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
} else {
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"}
@ -119,8 +193,15 @@ func loadFile(filename string, overload bool) error {
return err 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 {
if os.Getenv(key) == "" || overload { if !currentEnv[key] || overload {
os.Setenv(key, value) os.Setenv(key, value)
} }
} }
@ -135,33 +216,12 @@ 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())
} }
if err = scanner.Err(); err != nil { var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`)
return
}
for _, fullLine := range lines { func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
if !isIgnoredLine(fullLine) {
var key, value string
key, value, err = parseLine(fullLine)
if err != nil {
return
}
envMap[key] = value
}
}
return
}
func parseLine(line string) (key string, value string, err error) {
if len(line) == 0 { if len(line) == 0 {
err = errors.New("zero length string") err = errors.New("zero length string")
return return
@ -190,11 +250,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)
} }
@ -208,28 +268,96 @@ func parseLine(line string) (key string, value string, err error) {
if strings.HasPrefix(key, "export") { if strings.HasPrefix(key, "export") {
key = strings.TrimPrefix(key, "export") key = strings.TrimPrefix(key, "export")
} }
key = strings.Trim(key, " ") key = strings.TrimSpace(key)
key = exportRegex.ReplaceAllString(splitString[0], "$1")
// Parse the value // Parse the value
value = splitString[1] value = parseValue(splitString[1], envMap)
// trim
value = strings.Trim(value, " ")
// check if we've got quoted values
if strings.Count(value, "\"") == 2 || strings.Count(value, "'") == 2 {
// pull the quotes off the edges
value = strings.Trim(value, "\"'")
// expand quotes
value = strings.Replace(value, "\\\"", "\"", -1)
// expand newlines
value = strings.Replace(value, "\\n", "\n", -1)
}
return return
} }
var (
singleQuotesRegex = regexp.MustCompile(`\A'(.*)'\z`)
doubleQuotesRegex = regexp.MustCompile(`\A"(.*)"\z`)
escapeRegex = regexp.MustCompile(`\\.`)
unescapeCharsRegex = regexp.MustCompile(`\\([^$])`)
)
func parseValue(value string, envMap map[string]string) string {
// trim
value = strings.Trim(value, " ")
// check if we've got quoted values or possible escapes
if len(value) > 1 {
singleQuotes := singleQuotesRegex.FindStringSubmatch(value)
doubleQuotes := doubleQuotesRegex.FindStringSubmatch(value)
if singleQuotes != nil || doubleQuotes != nil {
// pull the quotes off the edges
value = value[1 : len(value)-1]
}
if doubleQuotes != nil {
// expand newlines
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
c := strings.TrimPrefix(match, `\`)
switch c {
case "n":
return "\n"
case "r":
return "\r"
default:
return match
}
})
// unescape characters
value = unescapeCharsRegex.ReplaceAllString(value, "$1")
}
if singleQuotes == nil {
value = expandVariables(value, envMap)
}
}
return value
}
var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
func expandVariables(v string, m map[string]string) string {
return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {
submatch := expandVarRegex.FindStringSubmatch(s)
if submatch == nil {
return s
}
if submatch[1] == "\\" || submatch[2] == "(" {
return submatch[0][1:]
} else if submatch[4] != "" {
return m[submatch[4]]
}
return s
})
}
func isIgnoredLine(line string) bool { func isIgnoredLine(line string) bool {
trimmedLine := strings.Trim(line, " \n\t") trimmedLine := strings.TrimSpace(line)
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,14 +1,18 @@
package godotenv package godotenv
import ( import (
"bytes"
"fmt"
"os" "os"
"reflect"
"strings"
"testing" "testing"
) )
var noopPresets = make(map[string]string) 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, noopPresets)
if key != expectedKey || value != expectedValue { if key != expectedKey || value != expectedValue {
t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value) t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value)
} }
@ -74,6 +78,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)
@ -92,16 +98,35 @@ 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) { func TestLoadDoesNotOverride(t *testing.T) {
envFileName := "fixtures/plain.env" envFileName := "fixtures/plain.env"
// ensure NO overload // ensure NO overload
presets := map[string]string{ presets := map[string]string{
"OPTION_A": "do_not_override", "OPTION_A": "do_not_override",
"OPTION_B": "",
} }
expectedValues := map[string]string{ expectedValues := map[string]string{
"OPTION_A": "do_not_override", "OPTION_A": "do_not_override",
"OPTION_B": "",
} }
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)
} }
@ -137,7 +162,7 @@ func TestLoadExportedEnv(t *testing.T) {
envFileName := "fixtures/exported.env" envFileName := "fixtures/exported.env"
expectedValues := map[string]string{ expectedValues := map[string]string{
"OPTION_A": "2", "OPTION_A": "2",
"OPTION_B": "\n", "OPTION_B": "\\n",
} }
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
@ -158,16 +183,94 @@ func TestLoadQuotedEnv(t *testing.T) {
"OPTION_A": "1", "OPTION_A": "1",
"OPTION_B": "2", "OPTION_B": "2",
"OPTION_C": "", "OPTION_C": "",
"OPTION_D": "\n", "OPTION_D": "\\n",
"OPTION_E": "1", "OPTION_E": "1",
"OPTION_F": "2", "OPTION_F": "2",
"OPTION_G": "", "OPTION_G": "",
"OPTION_H": "\n", "OPTION_H": "\n",
"OPTION_I": "echo 'asd'",
} }
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
} }
func TestSubstitutions(t *testing.T) {
envFileName := "fixtures/substitutions.env"
expectedValues := map[string]string{
"OPTION_A": "1",
"OPTION_B": "1",
"OPTION_C": "1",
"OPTION_D": "11",
"OPTION_E": "",
}
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
func TestExpanding(t *testing.T) {
tests := []struct {
name string
input string
expected map[string]string
}{
{
"expands variables found in values",
"FOO=test\nBAR=$FOO",
map[string]string{"FOO": "test", "BAR": "test"},
},
{
"parses variables wrapped in brackets",
"FOO=test\nBAR=${FOO}bar",
map[string]string{"FOO": "test", "BAR": "testbar"},
},
{
"expands undefined variables to an empty string",
"BAR=$FOO",
map[string]string{"BAR": ""},
},
{
"expands variables in double quoted strings",
"FOO=test\nBAR=\"quote $FOO\"",
map[string]string{"FOO": "test", "BAR": "quote test"},
},
{
"does not expand variables in single quoted strings",
"BAR='quote $FOO'",
map[string]string{"BAR": "quote $FOO"},
},
{
"does not expand escaped variables",
`FOO="foo\$BAR"`,
map[string]string{"FOO": "foo$BAR"},
},
{
"does not expand escaped variables",
`FOO="foo\${BAR}"`,
map[string]string{"FOO": "foo${BAR}"},
},
{
"does not expand escaped variables",
"FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"",
map[string]string{"FOO": "test", "BAR": "foo${FOO} test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env, err := Parse(strings.NewReader(tt.input))
if err != nil {
t.Errorf("Error: %s", err.Error())
}
for k, v := range tt.expected {
if strings.Compare(env[k], v) != 0 {
t.Errorf("Expected: %s, Actual: %s", v, env[k])
}
}
})
}
}
func TestActualEnvVarsAreLeftAlone(t *testing.T) { func TestActualEnvVarsAreLeftAlone(t *testing.T) {
os.Clearenv() os.Clearenv()
os.Setenv("OPTION_A", "actualenv") os.Setenv("OPTION_A", "actualenv")
@ -187,24 +290,40 @@ 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")
parseAndCompare(t, "export exportFoo=2", "exportFoo", "2")
parseAndCompare(t, "exportFOO=2", "exportFOO", "2")
parseAndCompare(t, "export_FOO =2", "export_FOO", "2")
parseAndCompare(t, "export.FOO= 2", "export.FOO", "2")
parseAndCompare(t, "export\tOPTION_A=2", "OPTION_A", "2")
parseAndCompare(t, " export OPTION_A=2", "OPTION_A", "2")
parseAndCompare(t, "\texport OPTION_A=2", "OPTION_A", "2")
// 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')
@ -224,20 +343,34 @@ 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")
// leading whitespace should be ignored
parseAndCompare(t, " KEY =value", "KEY", "value")
parseAndCompare(t, " KEY=value", "KEY", "value")
parseAndCompare(t, "\tKEY=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"
_, _, err := parseLine(badlyFormattedLine) _, _, err := parseLine(badlyFormattedLine, noopPresets)
if err == nil { if err == nil {
t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine) t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine)
} }
@ -250,6 +383,10 @@ func TestLinesToIgnore(t *testing.T) {
t.Error("Line with nothing but line break wasn't ignored") t.Error("Line with nothing but line break wasn't ignored")
} }
if !isIgnoredLine("\r\n") {
t.Error("Line with nothing but windows-style line break wasn't ignored")
}
if !isIgnoredLine("\t\t ") { if !isIgnoredLine("\t\t ") {
t.Error("Line full of whitespace wasn't ignored") t.Error("Line full of whitespace wasn't ignored")
} }
@ -265,7 +402,7 @@ 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")
} }
} }
@ -286,3 +423,52 @@ func TestErrorParsing(t *testing.T) {
t.Errorf("Expected error, got %v", envMap) 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="\n\r\\r!"`, `foo="\n\r\\r\!"`)
// lines should be sorted
writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"")
// integers should not be quoted
writeAndCompare(`key="10"`, `key=10`)
}
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)
}
}
}

5
renovate.json Normal file
View File

@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}