Compare commits

...

124 Commits

Author SHA1 Message Date
4d0ca41daa
fix: coop-cloud -> toolshed 2025-01-03 18:18:50 +01:00
01bff82843 change module location 2023-11-30 11:05:09 +01:00
e396573785 add basic implementation of modifiers 2023-11-30 10:58:50 +01:00
dependabot[bot]
e3b6eee84d
Bump actions/setup-go from 3 to 4 (#207)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 12:40:39 +10:00
Chris Jones
193c9aba29
Add whitespace tests. (#210) 2023-04-20 10:52:14 +10:00
John Barton
3fc4292b58
Fix bug where internal unquoted whitespace truncates values (#205)
* Add tests to cover the regression reported in #204

* Add a comment on regex for clarity

* Remove some old code that wasn't doing anything

* Push _all_ parse code into the parser and get tests calling live code

* Add some newline specific tests

* Add some YAML tests for the newline/space split bug

* Fix incorrect terminating of lines on whitespace

* Fix most of the parser regressions

* Bring back FOO.BAR names

* remove some commented out code
2023-02-06 08:47:38 +11:00
Rakibul Yeasin
b311b2657d
Fix: ioutil.ReadAll() is deprecated, so removed it's dependency (#202) 2023-02-04 11:10:05 +11:00
2tef
4321598b05
add overload flag (#200)
* add -o flag

increases compatibility with the ruby command

* update README
2023-02-04 11:00:21 +11:00
2tef
32a3b9b960
fix whitespace with gofmt (#203) 2023-02-04 10:58:06 +11:00
John Barton
06bf2d6190
Update CI to test go 1.20 (#201) 2023-02-02 12:05:32 +11:00
John Barton
cc9e9b7de7
Multiline string support (#156)
* refactor dotenv parser in order to support multi-line variable values declaration

Signed-off-by: x1unix <denis0051@gmail.com>

* Add multi-line var values test case and update comment test

Signed-off-by: x1unix <denis0051@gmail.com>

* Expand fixture tests to include multiline strings

* Update go versions to test against

* Switch to GOINSECURE for power8 CI task

* When tests fail, show source version of string (inc special chars)

* Update parser.go

Co-authored-by: Austin Sasko <austintyler0239@yahoo.com>

* Fix up bad merge

* Add a full fixture for comments for extra piece of mind

* Fix up some lint/staticcheck recommendations

* Test against go 1.19 too

Signed-off-by: x1unix <denis0051@gmail.com>
Co-authored-by: x1unix <denis0051@gmail.com>
Co-authored-by: Austin Sasko <austintyler0239@yahoo.com>
2023-01-27 13:14:16 +11:00
2tef
0f21d20acb
fix tiny details (#199)
* remove empty line

* remove unnecessary assignments

following commit 2ed25fcb281a9102d39c8cdf7b1bd87101ea6cb9.
2023-01-27 13:01:43 +11:00
John Barton
5c76d3e02c
Add punctuation to please godoc (#197)
Fixes issue reported over at https://github.com/golang/go/issues/55888
2022-10-30 12:38:35 +11:00
Doarakko
85a2237126
sort go import in readme (#193) 2022-09-12 16:55:41 +10:00
John Barton
add39c6f94 Remove power8 again as it wasn't fixed 2022-06-12 15:47:52 +12:00
John Barton
1b6c7eb7cc README updates around contributing 2022-06-12 15:47:37 +12:00
Elliot Murphy
b898a8b0d1
Add darwin arm64 build (#174)
* Add darwin arm64 build

Signed-off-by: Elliot Murphy <statik@users.noreply.github.com>

* update url for go version

Signed-off-by: Elliot Murphy <statik@users.noreply.github.com>

Co-authored-by: John Barton <jb@johnbarton.co>
2022-06-12 15:43:35 +12:00
dependabot[bot]
60df7dd66c
Bump actions/setup-go from 2 to 3 (#185)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-12 15:40:02 +12:00
dependabot[bot]
aa20cd96bb
Bump actions/checkout from 2 to 3 (#184)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-12 15:16:09 +12:00
John Barton
a904479007
Create codeql-analysis.yml (#186) 2022-06-12 15:09:09 +12:00
John Barton
a582f0c9d3
Remove renovate, add dependabot (#183) 2022-06-12 15:04:14 +12:00
Bahtiyar Biksayev
26b87a7e2c
Update README for go install post go 1.17 (#170)
* tune README

* tune
2022-06-12 14:06:04 +12:00
John Barton
76d246ae25 Add a few new go versions 2022-06-12 13:57:20 +12:00
John Barton
139777a9ff remove power8 tests as they stopped working 2022-06-12 13:56:53 +12:00
Oleksandr Redko
e74c6cadd5
Fix typos in comments and extend README (#177) 2022-04-25 14:35:55 +10:00
John Barton
c40e9c6392
Fix CI for power8 arch (#157) 2021-09-24 21:38:50 +10:00
Alexander Klein
ddf83eb33b
Add missing newline to created env files (#133) 2021-03-04 20:35:31 +11:00
renovate[bot]
f562099a43
Add renovate.json (#124)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-11-11 15:36:58 +11:00
John Barton
c49ef66cd3 Fix release name 2020-11-11 15:35:55 +11:00
John Barton
c142d16052 Try a different github action 2020-11-11 15:30:39 +11:00
John Barton
4e2d182c9a
Add action to publish binaries on each release (#125) 2020-11-11 15:15:18 +11:00
John Barton
65218afbaa
Add a go.mod file (#123) 2020-11-11 14:54:56 +11:00
John Barton
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
John Barton
23296b91aa
Merge pull request #121 from joho/github-actions
Setup GitHub actions for CI
2020-11-11 11:33:11 +11:00
John Barton
e57c08db27 Add CI badge to readme 2020-11-11 11:31:34 +11:00
John Barton
573e9186b2 Matrix test across mac/win/linux 2020-11-11 11:24:40 +11:00
John Barton
5451d82b77 Add Barebones github actions test 2020-11-11 11:19:52 +11:00
John Barton
0da8ce72f0
Merge pull request #115 from adombeck/master
Fix typo
2020-11-08 12:01:29 +11:00
John Barton
3e4069b9b2
Merge pull request #109 from mniak/pr
Write ints without quotes
2020-11-08 11:58:27 +11:00
John Barton
a4d9cf1d6d
Merge pull request #120 from santosh653/master
PR_AddingPowerSupport_golang-github-joho-godotenv
2020-10-16 06:51:22 +11:00
santosh653
7fbe752d59
Update .travis.yml
adding power support.
2020-10-15 02:09:51 -04:00
santosh653
46ee0dcae8
Update .travis.yml
Adding arch: amd64
2020-10-15 00:58:50 -04:00
adombeck
bc7d5cd181
Fix typo 2020-09-04 23:35:59 +02:00
Andre Soares
6e653f9adf add newline back 2020-06-26 15:39:50 -03:00
Andre Soares
fccdfd265d Fix package name 2020-06-26 15:39:06 -03:00
Andre Soares
f4e7418908 Remove go.mod 2020-06-26 15:36:35 -03:00
Andre Soares
63ea8bf09b Change package name back to joho/godotenv 2020-06-26 15:35:47 -03:00
Andre Soares
29b5be9cdc Rename and implement int-able without quotes 2020-06-26 15:22:04 -03:00
John Barton
d6ee6871f2
Merge pull request #95 from orxobo/Write-adjustment
Fixed Write bugs
2020-03-02 07:46:15 +11:00
orxobo
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
John Barton
b09de681dc
Merge pull request #90 from djherbis/master
#89 move regexp.MustCompile to globals
2019-10-21 07:25:22 +11:00
Dustin H
992ab0ec47
#89 move regexp.MustCompile to globals 2019-10-18 10:35:15 -04:00
John Barton
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
John Barton
61baafa627
Merge branch 'master' into ignore-leading-whitespace 2019-02-04 15:28:23 +11:00
John Barton
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
Dave Henderson
263a1dda9d
Support key names beginning with 'export'
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2019-02-03 23:11:51 -05:00
Dave Henderson
79711eebaf
Ignoring leading whitespace
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2019-02-03 22:39:38 -05:00
John Barton
69ed1d913a
Merge pull request #63 from mdanzinger/patch-1
Update cmd.go
2018-11-21 06:47:48 +11:00
John Barton
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
Mendy Danzinger
5917dd2291
Update cmd.go
Tiny little grammatical error.
2018-11-20 13:25:32 -05:00
John Barton
c0b86d615e
Merge pull request #60 from coolaj86/precedence
explicitly state env precedence and convention
2018-09-27 15:32:03 +10:00
AJ ONeal
3896766f7d explicitly state env precedence and convention 2018-09-24 21:35:43 -06:00
John Barton
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
Lucas Tétreault
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
John Barton
1709ab122c
Merge pull request #54 from egorse/master
The value expand fallback to actual ENV values
2018-04-05 15:36:34 +10:00
egorse
8ad714e304 The value expand fallback to actual ENV values 2018-03-31 23:18:36 +03:00
John Barton
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
John Barton
06e67b5ef3
Update my homepage link 2017-12-23 10:35:39 +11:00
Takumasa Sakao
2707e9ff66 Fix test, $ should not be escaped 2017-11-20 23:41:46 +09:00
Takumasa Sakao
50c29652a0 Expand variables on parseValue 2017-11-20 23:20:38 +09:00
Takumasa Sakao
33977c2d8d Add test for substitutions 2017-11-20 23:20:19 +09:00
Takumasa Sakao
9be76b3741 Pass envMap to parseLine & parseValue 2017-11-20 22:21:39 +09:00
John Barton
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
John Barton (joho)
05be8ccbf7 Try and replicate reported bug #45 2017-11-10 10:38:18 +11:00
John Barton
0f92a24bb0
Merge pull request #44 from dvrkps/patch-1
travis: update go version
2017-11-02 13:11:18 +11:00
Davor Kapsa
b7bbb3624e
travis: update go version 2017-11-01 07:18:32 +01:00
John Barton
a79fa1e548 Merge pull request #41 from alexquick/document-write-dotenv
Document Marshal, Unmarshal, and Write
2017-09-18 16:32:59 +10:00
John Barton
144189c1ed Merge pull request #42 from alexquick/feature-sorted-output
Sort output of Marshal/Write
2017-09-18 16:32:10 +10:00
Alex Quick
3dd2dbe832 sort output of Write/Marshal 2017-09-16 18:02:27 -04:00
Alex Quick
9f04f40640 Be more careful with TestRoundtrip 2017-09-16 17:55:04 -04:00
Alex Quick
e6264cf869 document Marshal, Unmarshal, and Write 2017-09-16 17:12:55 -04:00
John Barton
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
Alex Quick
b1bb9d9fc3 rename WriteString/ReadString to Marshal/Unmarshal 2017-09-14 00:24:22 -04:00
Alex Quick
5d289f4405 escape some other bash-y special chars ($!) 2017-09-13 23:13:08 -04:00
Alex Quick
88e7c8bd35 support for writing envs out in dotenv format 2017-09-13 23:13:08 -04:00
John Barton
c9360df4d1 Merge pull request #34 from alexquick/fix-parsing-issues
Fix some small parsing bugs
2017-08-22 14:21:26 +10:00
John Barton
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
John Barton
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
Paul Annesley
390de3704e README.md mentions Parse(io.Reader) 2017-08-06 17:34:10 +10:00
Paul Annesley
ebf1036af6 Parse(io.Reader) => map[string]string 2017-08-06 17:34:10 +10:00
Alex Quick
a905e99577 fix panic with " as the value 2017-07-16 18:43:49 -04:00
Alex Quick
6f30f0c011 support for equals in yaml-style lines 2017-07-16 17:25:28 -04:00
Alex Quick
84bf91f40e rudimentry support for nested quotes 2017-07-16 17:24:36 -04:00
Alex Quick
b9324c6f3c handle escaping more comprehensively 2017-07-16 17:15:29 -04:00
Christian Musa
12b7e03247 Add Go Report Card badge and fix spelling error 2017-07-14 21:33:04 -03:00
John Barton
3ddb2792f3 README housekeeping 2017-07-05 14:31:29 +10:00
John Barton
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
John Barton (joho)
034acc2190 Change check of existing env to respect empty (but set) vars. 2017-03-28 11:54:56 +11:00
John Barton (joho)
cd1272609d Add failing test for override of empty var 2017-03-28 11:39:40 +11:00
John Barton
eaf676fc03 Merge pull request #27 from goenning/empty_var
allow usage of empty var on .env
2017-03-23 07:07:31 +11:00
Oenning
a42a65518c allow empty_var 2017-03-22 13:05:44 +00:00
John Barton
b01826f956 Merge pull request #25 from matiasanaya/master
Fix quoted values check
2017-03-21 20:56:48 +11:00
Matias Anaya
6a1233b2f6 Fix quoted values check 2017-03-21 19:04:19 +11:00
John Barton
d10b3fbe00 Merge pull request #24 from joho/setup_travis
Move CI
2017-02-22 08:49:41 +11:00
John Barton (joho)
0a959c8d8f Add a badge for the windows build too. 2017-02-22 08:43:40 +11:00
John Barton (joho)
bcaccd4f68 Apparently this file is meant to be hidden? 2017-02-22 08:29:43 +11:00
John Barton (joho)
22e45bfff4 Switch build badge over to travis 2017-02-22 08:27:57 +11:00
John Barton (joho)
2fc79dff51 Replace wercker.yml with travis.yml 2017-02-22 08:23:25 +11:00
John Barton
726cc8b906 Merge pull request #22 from mmilata/dont-swallow-errors
Improve error handling
2016-12-17 10:05:37 +11:00
Martin Milata
861984c215 Don't hide line parsing errors 2016-12-12 14:43:30 +01:00
Martin Milata
0ff0c0fc7a Propagate errors encountered when reading file 2016-12-12 14:41:36 +01:00
John Barton
4ed13390c0 Merge pull request #13 from jmervine/master
Add Overload methods.
2015-09-07 11:02:28 +10:00
Josh Mervine
008304c688 adding Overload method 2015-09-05 08:59:08 -07:00
John Barton
443e926da0 Merge pull request #11 from buddhamagnet/master
Remove unecessary assignment in autoloader
2015-06-10 07:30:23 +10:00
buddhamagnet
2ed25fcb28 remove unecessary assignment in autoloader 2015-06-09 18:24:15 +01:00
John Barton (joho)
f6581828bb outdent else because golint said so. 2015-03-23 12:17:14 +11:00
John Barton (joho)
d29c003c20 Still trying to please golint with package comments. 2015-03-23 12:15:55 +11:00
John Barton (joho)
19b5c2bf30 Some golint feedback from http://goreportcard.com/report/joho/godotenv 2015-03-23 12:15:01 +11:00
John Barton (joho)
e1c92610d7 run gofmt -w -s ./.. 2015-03-23 12:06:31 +11:00
John Barton
ead2e75027 Merge pull request #8 from calavera/add_values_to_envmap
Fix issue reading file.
2015-01-02 15:46:44 +11:00
David Calavera
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
John Barton (joho)
a01a834e16 Update docs for the bin command. 2014-10-12 10:38:08 +11:00
John Barton (joho)
d2ce5befea Move cmd.go so it installs as "godotenv" 2014-10-12 10:27:22 +11:00
John Barton
a86c254d7d Merge pull request #6 from joho/add_bin_command
Add a bin command
2014-10-12 09:58:57 +11:00
18 changed files with 1259 additions and 201 deletions

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: /
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

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

@ -0,0 +1,20 @@
name: CI
on: [push]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
go: [ '1.20', '1.19', '1.18', '1.17', '1.16' ]
os: [ ubuntu-latest, macOS-latest, windows-latest ]
name: ${{ matrix.os }} Go ${{ matrix.go }} Tests
steps:
- uses: actions/checkout@v3
- name: Setup go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
- run: go test

72
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '31 4 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

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

@ -0,0 +1,31 @@
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@v3
- name: Generate build files
uses: thatisuday/go-cross-build@v1.0.2
with:
platforms: 'linux/amd64, linux/ppc64le, darwin/amd64, darwin/arm64, 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

116
README.md
View File

@ -1,6 +1,6 @@
# GoDotEnv [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78 "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) # 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](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file).
From the original Library: From the original Library:
@ -8,12 +8,30 @@ 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
go >= 1.17
```shell
go install github.com/joho/godotenv/cmd/godotenv@latest
```
go < 1.17
```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:
@ -29,9 +47,10 @@ Then in your Go app you can do something like
package main package main
import ( import (
"github.com/joho/godotenv"
"log" "log"
"os" "os"
"github.com/joho/godotenv"
) )
func main() { func main() {
@ -56,8 +75,8 @@ import _ "github.com/joho/godotenv/autoload"
While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
```go ```go
_ = godotenv.Load("somerandomfile") godotenv.Load("somerandomfile")
_ = godotenv.Load("filenumberone.env", "filenumbertwo.env") godotenv.Load("filenumberone.env", "filenumbertwo.env")
``` ```
If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
@ -85,13 +104,86 @@ 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)
```
### 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
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`
By default, it won't override existing environment variables; you can do that with the `-o` flag.
### 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 welcome, but with some caveats.
*code changes without tests will not be accepted* This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API.
Contributions would be gladly accepted that:
* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv)
* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries)
* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments
*code changes without tests and references to peer dotenv implementations will not be accepted*
1. Fork it 1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`) 2. Create your feature branch (`git checkout -b my-new-feature`)
@ -99,10 +191,12 @@ 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
## CI ## Releases
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) 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`
## 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

@ -8,8 +8,8 @@ package autoload
And bob's your mother's brother And bob's your mother's brother
*/ */
import "github.com/joho/godotenv" import "git.coopcloud.tech/toolshed/godotenv"
func init() { func init() {
_ = godotenv.Load() godotenv.Load()
} }

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
"github.com/joho/godotenv" "git.coopcloud.tech/toolshed/godotenv"
) )
func main() { func main() {
@ -15,13 +15,15 @@ func main() {
flag.BoolVar(&showHelp, "h", false, "show help") flag.BoolVar(&showHelp, "h", false, "show help")
var rawEnvFilenames string var rawEnvFilenames string
flag.StringVar(&rawEnvFilenames, "f", "", "comma separated paths to .env files") flag.StringVar(&rawEnvFilenames, "f", "", "comma separated paths to .env files")
var overload bool
flag.BoolVar(&overload, "o", false, "override existing .env variables")
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 [-o] [-f ENV_FILE_PATHS] COMMAND_ARGS
ENV_FILE_PATHS: comma separated paths to .env files ENV_FILE_PATHS: comma separated paths to .env files
COMMAND_ARGS: command and args you want to run COMMAND_ARGS: command and args you want to run
@ -45,9 +47,9 @@ 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, overload)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

4
fixtures/comments.env Normal file
View File

@ -0,0 +1,4 @@
# Full line comment
foo=bar # baz
bar=foo#baz
baz="foo"#bar

View File

@ -1,2 +1 @@
export OPTION_A='postgres://localhost:5432/database?sslmode=disable' export OPTION_A='postgres://localhost:5432/database?sslmode=disable'

2
fixtures/invalid1.env Normal file
View File

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

View File

@ -3,3 +3,6 @@ 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=
OPTION_H=1 2

View File

@ -6,3 +6,14 @@ OPTION_E="1"
OPTION_F="2" OPTION_F="2"
OPTION_G="" OPTION_G=""
OPTION_H="\n" OPTION_H="\n"
OPTION_I = "echo 'asd'"
OPTION_J='line 1
line 2'
OPTION_K='line one
this is \'quoted\'
one more line'
OPTION_L="line 1
line 2"
OPTION_M="line one
this is \"quoted\"
one more line"

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 git.coopcloud.tech/toolshed/godotenv
go 1.12

View File

@ -1,44 +1,58 @@
/* // 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" "bytes"
"errors" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"sort"
"strconv"
"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 // Parse reads an env file from io.Reader, returning a map of keys and values.
func Parse(r io.Reader) (map[string]string, map[string]map[string]string, error) {
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
return nil, nil, err
}
You can otherwise tell it which files to load (there can be more than one) like return UnmarshalBytes(buf.Bytes())
}
godotenv.Load("fileone", "filetwo") // Load will read your env file(s) and load them into ENV for this process.
//
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 // 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) { 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,16 +60,38 @@ 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).
*/ //
func Read(filenames ...string) (envMap map[string]string, err error) { // 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 forcefully set all vars.
func Overload(filenames ...string) (err error) {
filenames = filenamesOrDefault(filenames) filenames = filenamesOrDefault(filenames)
envMap = make(map[string]string)
for _, filename := range filenames { for _, filename := range filenames {
individualEnvMap, individualErr := readFile(filename) 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, modMap map[string]map[string]string, err error) {
filenames = filenamesOrDefault(filenames)
envMap = make(map[string]string)
modMap = make(map[string]map[string]string)
for _, filename := range filenames {
individualEnvMap, individualModMap, individualErr := readFile(filename)
if individualErr != nil { if individualErr != nil {
err = individualErr err = individualErr
@ -65,22 +101,44 @@ func Read(filenames ...string) (envMap map[string]string, err error) {
for key, value := range individualEnvMap { for key, value := range individualEnvMap {
envMap[key] = value envMap[key] = value
} }
for key, value := range individualModMap {
modMap[key] = value
}
} }
return return
} }
/* // Unmarshal reads an env file from a string, returning a map of keys and values.
Loads env vars from the specified filenames (empty map falls back to default) func Unmarshal(str string) (envMap map[string]string, modifierMap map[string]map[string]string, err error) {
then executes the cmd specified. return UnmarshalBytes([]byte(str))
}
Simply hooks up os.Stdin/err/out to the command and calls Run() // UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values.
func UnmarshalBytes(src []byte) (map[string]string, map[string]map[string]string, error) {
vars := make(map[string]string)
modifiers := make(map[string]map[string]string)
err := parseBytes(src, vars, modifiers)
If you want more fine grained control over your command it's recommended return vars, modifiers, err
that you use `Load()` or `Read()` and the `os/exec` package yourself. }
*/
func Exec(filenames []string, cmd string, cmdArgs []string) error { // Exec loads env vars from the specified filenames (empty map falls back to default)
Load(filenames...) // 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()`, `Overload()` or `Read()` and the `os/exec` package yourself.
func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error {
op := Load
if overload {
op = Overload
}
if err := op(filenames...); err != nil {
return err
}
command := exec.Command(cmd, cmdArgs...) command := exec.Command(cmd, cmdArgs...)
command.Stdin = os.Stdin command.Stdin = os.Stdin
@ -89,123 +147,88 @@ 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
}
return file.Sync()
}
// 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"}
} else { }
return filenames return filenames
} }
func loadFile(filename string, overload bool) error {
envMap, _, err := readFile(filename)
if err != nil {
return err
} }
func loadFile(filename string) (err error) { currentEnv := map[string]bool{}
envMap, err := readFile(filename) rawEnv := os.Environ()
if err != nil { for _, rawEnvLine := range rawEnv {
return 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, modMap map[string]map[string]string, err error) {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return return
} }
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 { func doubleQuoteEscape(line string) string {
if !isIgnoredLine(fullLine) { for _, c := range doubleQuoteSpecialChars {
key, value, err := parseLine(fullLine) toReplace := "\\" + string(c)
if c == '\n' {
if err == nil && os.Getenv(key) == "" { toReplace = `\n`
envMap[key] = value
} }
if c == '\r' {
toReplace = `\r`
} }
line = strings.Replace(line, string(c), toReplace, -1)
} }
return return line
}
func parseLine(line string) (key string, value string, err error) {
if len(line) == 0 {
err = errors.New("zero length string")
return
}
// ditch the comments (but keep quoted hashes)
if strings.Contains(line, "#") {
segmentsBetweenHashes := strings.Split(line, "#")
quotesAreOpen := false
segmentsToKeep := make([]string, 0)
for _, segment := range segmentsBetweenHashes {
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
if quotesAreOpen {
quotesAreOpen = false
segmentsToKeep = append(segmentsToKeep, segment)
} else {
quotesAreOpen = true
}
}
if len(segmentsToKeep) == 0 || quotesAreOpen {
segmentsToKeep = append(segmentsToKeep, segment)
}
}
line = strings.Join(segmentsToKeep, "#")
}
// now split key from value
splitString := strings.SplitN(line, "=", 2)
if len(splitString) != 2 {
// try yaml mode!
splitString = strings.SplitN(line, ":", 2)
}
if len(splitString) != 2 {
err = errors.New("Can't separate key from value")
return
}
// Parse the key
key = splitString[0]
if strings.HasPrefix(key, "export") {
key = strings.TrimPrefix(key, "export")
}
key = strings.Trim(key, " ")
// 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 {
// 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
}
func isIgnoredLine(line string) bool {
trimmedLine := strings.Trim(line, " \n\t")
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
} }

View File

@ -1,22 +1,36 @@
package godotenv package godotenv
import ( import (
"bytes"
"fmt"
"os" "os"
"reflect"
"strings"
"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) result, _, err := Unmarshal(rawEnvLine)
if key != expectedKey || value != expectedValue { if err != nil {
t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value) t.Errorf("Expected %q to parse as %q: %q, errored %q", rawEnvLine, expectedKey, expectedValue, err)
return
}
if result[expectedKey] != expectedValue {
t.Errorf("Expected '%v' to parse as '%v' => '%v', got %q instead", rawEnvLine, expectedKey, expectedValue, result)
} }
} }
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)
} }
@ -25,7 +39,7 @@ func loadEnvAndCompareValues(t *testing.T, envFileName string, expectedValues ma
envValue := os.Getenv(k) envValue := os.Getenv(k)
v := expectedValues[k] v := expectedValues[k]
if envValue != v { if envValue != v {
t.Errorf("Mismatch for key '%v': expected '%v' got '%v'", k, v, envValue) t.Errorf("Mismatch for key '%v': expected '%#v' got '%#v'", k, v, envValue)
} }
} }
} }
@ -38,6 +52,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 +67,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,9 +82,12 @@ 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": "",
"OPTION_H": "1 2",
} }
envMap, err := Read(envFileName) envMap, _, err := Read(envFileName)
if err != nil { if err != nil {
t.Error("Error reading file") t.Error("Error reading file")
} }
@ -71,6 +103,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 TestOverloadDoesOverride(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{
@ -79,19 +158,20 @@ func TestLoadPlainEnv(t *testing.T) {
"OPTION_C": "3", "OPTION_C": "3",
"OPTION_D": "4", "OPTION_D": "4",
"OPTION_E": "5", "OPTION_E": "5",
"OPTION_H": "1 2",
} }
loadEnvAndCompareValues(t, envFileName, expectedValues) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
} }
func TestLoadExportedEnv(t *testing.T) { 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, envFileName, expectedValues) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
} }
func TestLoadEqualsEnv(t *testing.T) { func TestLoadEqualsEnv(t *testing.T) {
@ -100,7 +180,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) {
@ -109,14 +189,123 @@ 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'",
"OPTION_J": "line 1\nline 2",
"OPTION_K": "line one\nthis is \\'quoted\\'\none more line",
"OPTION_L": "line 1\nline 2",
"OPTION_M": "line one\nthis is \"quoted\"\none more line",
} }
loadEnvAndCompareValues(t, envFileName, expectedValues) 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 TestVariableStringValueSeparator(t *testing.T) {
input := "TEST_URLS=\"stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443\""
want := map[string]string{
"TEST_URLS": "stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443",
}
got, _, err := Parse(strings.NewReader(input))
if err != nil {
t.Error(err)
}
if len(got) != len(want) {
t.Fatalf(
"unexpected value:\nwant:\n\t%#v\n\ngot:\n\t%#v", want, got)
}
for k, wantVal := range want {
gotVal, ok := got[k]
if !ok {
t.Fatalf("key %q doesn't present in result", k)
}
if wantVal != gotVal {
t.Fatalf(
"mismatch in %q value:\nwant:\n\t%s\n\ngot:\n\t%s", k,
wantVal, gotVal)
}
}
} }
func TestActualEnvVarsAreLeftAlone(t *testing.T) { func TestActualEnvVarsAreLeftAlone(t *testing.T) {
@ -138,30 +327,46 @@ 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 variables 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')
parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar") parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar")
// it 'parses varibales with several "=" in the value' do // it 'parses variables with several "=" in the value' do
// expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=') // expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=')
parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=") parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=")
@ -169,54 +374,336 @@ func TestParsing(t *testing.T) {
// expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar ' // expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar '
parseAndCompare(t, "FOO=bar ", "FOO", "bar") parseAndCompare(t, "FOO=bar ", "FOO", "bar")
// unquoted internal whitespace is preserved
parseAndCompare(t, `KEY=value value`, "KEY", "value value")
// it 'ignores inline comments' do // it 'ignores inline comments' do
// expect(env("foo=bar # this is foo")).to eql('foo' => 'bar') // expect(env("foo=bar # this is foo")).to eql('foo' => 'bar')
parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar") parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar")
// 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")
// unquoted whitespace around keys 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 := Unmarshal(badlyFormattedLine)
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)
} }
} }
func TestLinesToIgnore(t *testing.T) { func TestLinesToIgnore(t *testing.T) {
// it 'ignores empty lines' do cases := map[string]struct {
// expect(env("\n \t \nfoo=bar\n \nfizz=buzz")).to eql('foo' => 'bar', 'fizz' => 'buzz') input string
if !isIgnoredLine("\n") { want string
t.Error("Line with nothing but line break wasn't ignored") }{
"Line with nothing but line break": {
input: "\n",
},
"Line with nothing but windows-style line break": {
input: "\r\n",
},
"Line full of whitespace": {
input: "\t\t ",
},
"Comment": {
input: "# Comment",
},
"Indented comment": {
input: "\t # comment",
},
"non-ignored value": {
input: `export OPTION_B='\n'`,
want: `export OPTION_B='\n'`,
},
} }
if !isIgnoredLine("\t\t ") { for n, c := range cases {
t.Error("Line full of whitespace wasn't ignored") t.Run(n, func(t *testing.T) {
got := string(getStatementStart([]byte(c.input)))
if got != c.want {
t.Errorf("Expected:\t %q\nGot:\t %q", c.want, got)
}
})
}
} }
// it 'ignores comment lines' do func TestErrorReadDirectory(t *testing.T) {
// expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql('foo' => 'bar') envFileName := "fixtures/"
if !isIgnoredLine("# comment") { envMap, _, err := Read(envFileName)
t.Error("Comment wasn't ignored")
if err == nil {
t.Errorf("Expected error, got %v", envMap)
}
} }
if !isIgnoredLine("\t#comment") { func TestErrorParsing(t *testing.T) {
t.Error("Indented comment wasn't ignored") envFileName := "fixtures/invalid1.env"
envMap, _, err := Read(envFileName)
if err == nil {
t.Errorf("Expected error, got %v", envMap)
}
} }
// make sure we're not getting false positives func TestComments(t *testing.T) {
if isIgnoredLine("export OPTION_B='\\n'") { envFileName := "fixtures/comments.env"
t.Error("ignoring a perfectly valid line to parse") expectedValues := map[string]string{
"foo": "bar",
"bar": "foo#baz",
"baz": "foo",
}
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
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)
}
}
}
func TestTrailingNewlines(t *testing.T) {
cases := map[string]struct {
input string
key string
value string
}{
"Simple value without trailing newline": {
input: "KEY=value",
key: "KEY",
value: "value",
},
"Value with internal whitespace without trailing newline": {
input: "KEY=value value",
key: "KEY",
value: "value value",
},
"Value with internal whitespace with trailing newline": {
input: "KEY=value value\n",
key: "KEY",
value: "value value",
},
"YAML style - value with internal whitespace without trailing newline": {
input: "KEY: value value",
key: "KEY",
value: "value value",
},
"YAML style - value with internal whitespace with trailing newline": {
input: "KEY: value value\n",
key: "KEY",
value: "value value",
},
}
for n, c := range cases {
t.Run(n, func(t *testing.T) {
result, _, err := Unmarshal(c.input)
if err != nil {
t.Errorf("Input: %q Unexpected error:\t%q", c.input, err)
}
if result[c.key] != c.value {
t.Errorf("Input %q Expected:\t %q/%q\nGot:\t %q", c.input, c.key, c.value, result)
}
})
}
}
func TestWhitespace(t *testing.T) {
cases := map[string]struct {
input string
key string
value string
}{
"Leading whitespace": {
input: " A=a\n",
key: "A",
value: "a",
},
"Leading tab": {
input: "\tA=a\n",
key: "A",
value: "a",
},
"Leading mixed whitespace": {
input: " \t \t\n\t \t A=a\n",
key: "A",
value: "a",
},
"Leading whitespace before export": {
input: " \t\t export A=a\n",
key: "A",
value: "a",
},
"Trailing whitespace": {
input: "A=a \t \t\n",
key: "A",
value: "a",
},
"Trailing whitespace with export": {
input: "export A=a\t \t \n",
key: "A",
value: "a",
},
"No EOL": {
input: "A=a",
key: "A",
value: "a",
},
"Trailing whitespace with no EOL": {
input: "A=a ",
key: "A",
value: "a",
},
}
for n, c := range cases {
t.Run(n, func(t *testing.T) {
result, _, err := Unmarshal(c.input)
if err != nil {
t.Errorf("Input: %q Unexpected error:\t%q", c.input, err)
}
if result[c.key] != c.value {
t.Errorf("Input %q Expected:\t %q/%q\nGot:\t %q", c.input, c.key, c.value, result)
}
})
}
}
func TestModfiers(t *testing.T) {
cases := map[string]struct {
input string
key string
value string
modifiers map[string]string
}{
"No Modifier": {
input: "A=a",
key: "A",
value: "a",
},
"With comment": {
input: "A=a # my comment",
key: "A",
value: "a",
},
"With single modifier": {
input: "A=a # foo=bar",
key: "A",
value: "a",
modifiers: map[string]string{
"foo": "bar",
},
},
"With multiple modifiers": {
input: "A=a # foo=bar length=10",
key: "A",
value: "a",
modifiers: map[string]string{
"foo": "bar",
"length": "10",
},
},
"With quoted var": {
input: "A='a' # foo=bar",
key: "A",
value: "a",
modifiers: map[string]string{
"foo": "bar",
},
},
"With quoted var 2": {
input: "A='a' # foo=bar\nB=b",
key: "A",
value: "a",
modifiers: map[string]string{
"foo": "bar",
},
},
}
for n, c := range cases {
t.Run(n, func(t *testing.T) {
values, modifiers, err := Unmarshal(c.input)
if err != nil {
t.Errorf("Input: %q Unexpected error:\t%q", c.input, err)
}
if values[c.key] != c.value {
t.Errorf("Input %q Expected:\t %q/%q\nGot:\t %q", c.input, c.key, c.value, values)
}
if modifiers[c.key] == nil && c.modifiers != nil {
t.Errorf("Input %q Expected modifiers\n Got: none", c.input)
} else {
for k, v := range c.modifiers {
if modifiers[c.key][k] != v {
t.Errorf("Input %q Expected modifier %s=%s\n Got: %s=%s", c.input, k, v, k, modifiers[c.key][k])
}
}
}
})
} }
} }

293
parser.go Normal file
View File

@ -0,0 +1,293 @@
package godotenv
import (
"bytes"
"errors"
"fmt"
"regexp"
"strings"
"unicode"
)
const (
charComment = '#'
prefixSingleQuote = '\''
prefixDoubleQuote = '"'
exportPrefix = "export"
)
func parseBytes(src []byte, vars map[string]string, modifiers map[string]map[string]string) error {
src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1)
cutset := src
for {
cutset = getStatementStart(cutset)
if cutset == nil {
// reached end of file
break
}
key, left, err := locateKeyName(cutset)
if err != nil {
return err
}
value, mods, left, err := extractVarValue(left, vars)
if err != nil {
return err
}
vars[key] = value
modifiers[key] = mods
cutset = left
}
return nil
}
// getStatementPosition returns position of statement begin.
//
// It skips any comment line or non-whitespace character.
func getStatementStart(src []byte) []byte {
pos := indexOfNonSpaceChar(src)
if pos == -1 {
return nil
}
src = src[pos:]
if src[0] != charComment {
return src
}
// skip comment section
pos = bytes.IndexFunc(src, isCharFunc('\n'))
if pos == -1 {
return nil
}
return getStatementStart(src[pos:])
}
// locateKeyName locates and parses key name and returns rest of slice
func locateKeyName(src []byte) (key string, cutset []byte, err error) {
// trim "export" and space at beginning
src = bytes.TrimLeftFunc(src, isSpace)
if bytes.HasPrefix(src, []byte(exportPrefix)) {
trimmed := bytes.TrimPrefix(src, []byte(exportPrefix))
if bytes.IndexFunc(trimmed, isSpace) == 0 {
src = bytes.TrimLeftFunc(trimmed, isSpace)
}
}
// locate key name end and validate it in single loop
offset := 0
loop:
for i, char := range src {
rchar := rune(char)
if isSpace(rchar) {
continue
}
switch char {
case '=', ':':
// library also supports yaml-style value declaration
key = string(src[0:i])
offset = i + 1
break loop
case '_':
default:
// variable name should match [A-Za-z0-9_.]
if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' {
continue
}
return "", nil, fmt.Errorf(
`unexpected character %q in variable name near %q`,
string(char), string(src))
}
}
if len(src) == 0 {
return "", nil, errors.New("zero length string")
}
// trim whitespace
key = strings.TrimRightFunc(key, unicode.IsSpace)
cutset = bytes.TrimLeftFunc(src[offset:], isSpace)
return key, cutset, nil
}
// extractVarValue extracts variable value and returns rest of slice
func extractVarValue(src []byte, vars map[string]string) (value string, modifiers map[string]string, rest []byte, err error) {
quote, hasPrefix := hasQuotePrefix(src)
// unquoted value - read until end of line
endOfLine := bytes.IndexFunc(src, isLineEnd)
// Hit EOF without a trailing newline
if endOfLine == -1 {
endOfLine = len(src)
if endOfLine == 0 {
return "", nil, nil, nil
}
}
if !hasPrefix {
// Convert line to rune away to do accurate countback of runes
line := []rune(string(src[0:endOfLine]))
// Assume end of line is end of var
endOfVar := len(line)
if endOfVar == 0 {
return "", nil, src[endOfLine:], nil
}
comment := ""
// Work backwards to check if the line ends in whitespace then
// a comment (ie asdasd # some comment)
for i := endOfVar - 1; i >= 0; i-- {
if line[i] == charComment && i > 0 {
comment = string(line[i+1:])
if isSpace(line[i-1]) {
endOfVar = i
break
}
}
}
trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace)
return expandVariables(trimmed, vars), extractModifiers(comment), src[endOfLine:], nil
}
// lookup quoted string terminator
for i := 1; i < len(src); i++ {
if char := src[i]; char != quote {
continue
}
// skip escaped quote symbol (\" or \', depends on quote)
if prevChar := src[i-1]; prevChar == '\\' {
continue
}
// trim quotes
trimFunc := isCharFunc(rune(quote))
value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))
if quote == prefixDoubleQuote {
// unescape newlines for double quote (this is compat feature)
// and expand environment variables
value = expandVariables(expandEscapes(value), vars)
}
var mods map[string]string
if endOfLine > i {
mods = extractModifiers(string(src[i+1 : endOfLine]))
}
return value, mods, src[i+1:], nil
}
// return formatted error if quoted string is not terminated
valEndIndex := bytes.IndexFunc(src, isCharFunc('\n'))
if valEndIndex == -1 {
valEndIndex = len(src)
}
return "", nil, nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex])
}
func extractModifiers(comment string) map[string]string {
if comment == "" {
return nil
}
comment = strings.TrimSpace(comment)
kvpairs := strings.Split(comment, " ")
mods := make(map[string]string)
for _, kv := range kvpairs {
kvsplit := strings.Split(kv, "=")
if len(kvsplit) != 2 {
continue
}
mods[kvsplit[0]] = kvsplit[1]
}
return mods
}
func expandEscapes(str string) string {
out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string {
c := strings.TrimPrefix(match, `\`)
switch c {
case "n":
return "\n"
case "r":
return "\r"
default:
return match
}
})
return unescapeCharsRegex.ReplaceAllString(out, "$1")
}
func indexOfNonSpaceChar(src []byte) int {
return bytes.IndexFunc(src, func(r rune) bool {
return !unicode.IsSpace(r)
})
}
// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) {
if len(src) == 0 {
return 0, false
}
switch prefix := src[0]; prefix {
case prefixDoubleQuote, prefixSingleQuote:
return prefix, true
default:
return 0, false
}
}
func isCharFunc(char rune) func(rune) bool {
return func(v rune) bool {
return v == char
}
}
// isSpace reports whether the rune is a space character but not line break character
//
// this differs from unicode.IsSpace, which also applies line break as space
func isSpace(r rune) bool {
switch r {
case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0:
return true
}
return false
}
func isLineEnd(r rune) bool {
if r == '\n' || r == '\r' {
return true
}
return false
}
var (
escapeRegex = regexp.MustCompile(`\\.`)
expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
unescapeCharsRegex = regexp.MustCompile(`\\([^$])`)
)
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
})
}

View File

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