Compare commits

...
46 Commits
Author SHA1 Message Date
John Barton 97a2850142 Merge pull request #265 from joho/ci/pin-windows-2025-vs2026
Point CI to new windows tag
2026-05-25 10:52:27 +10:00
John Barton e77328b146 Update the windows version per github warning.
"NOTICE: windows-latest requests are being redirected to windows-2025-vs2026 by June 15, 2028"
2026-05-25 10:37:27 +10:00
John Barton efa32f116f Merge pull request #264 from joho/fix/allow-hyphens-in-keys
Definitively commit to allowing hyphens (again)
2026-05-25 10:33:10 +10:00
John Barton 139cdfde14 Update readme to capture ruby/node key compatibililty 2026-05-25 10:30:10 +10:00
John Barton 87021fd05a test: drop stale parser-error case for hyphens in keys
PR #245 (commit 82194f2) widened the variable-name charset to include
'-', restoring v1.4.0 behaviour. The "Invalid char" case in
TestParserErrors predates that change and has been failing ever since;
CI only started surfacing it after the recent go.mod bump.

The deliberate "hyphens allowed" contract is already covered by
TestLoadHyphenEnv (positive) and TestKeyNameCharsetRejectsDisallowed
(negative lock-in on '+', '@', '/'), so removing this case is the
correct fix rather than reverting #245.
2026-05-25 10:11:01 +10:00
John Barton 32bfb2c280 Merge pull request #188 from joho/dependabot/github_actions/thatisuday/go-cross-build-1.1.0
Bump thatisuday/go-cross-build from 1.0.2 to 1.1.0
2026-05-20 14:21:31 +10:00
John Barton 17f409325c Bump go mod minimum version 2026-05-20 14:20:46 +10:00
Rohan Verma c92e518f6a tests: add unit tests for parser errors
- Declares errors and wraps them for error checking
by clients
- Adds two unit tests for
  - terminated quote error
  - unexpected char
2026-05-20 14:20:46 +10:00
John Barton 59ad2adcd3 Merge pull request #240 from ignatij/patch-1
docs: add usage for tool dependency in go1.24
2026-05-20 14:19:24 +10:00
John Barton 8680ef74f4 Merge pull request #263 from joho/ci-and-versioning-updates-may-26
CI and Versioning updates
2026-05-20 14:11:22 +10:00
John Barton f5c7006914 Ignore the clanker config while we're at it 2026-05-20 14:10:07 +10:00
John Barton 43a1154afe Update a bunch of actions to latest versions 2026-05-20 14:09:02 +10:00
John Barton 2b82d75077 Update the test matrix & officially supported versions 2026-05-20 14:07:37 +10:00
John Barton eea6c4a3a0 Merge pull request #216 from joho/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2026-05-19 21:56:17 +10:00
John Barton 59c0ac271a Merge pull request #252 from lotusirous/main
simplify Replace(..,-1) by ReplaceAll for strings and bytes
2026-05-19 21:55:59 +10:00
John Barton 045f59bfe5 Merge pull request #254 from guettli/patch-1
Update README to reflect Go version requirement
2026-05-19 21:55:22 +10:00
John Barton 7980d1a6ee Merge pull request #243 from kvokka/patch-1
Fix Precedence & Conventions in README.md
2026-05-19 21:54:49 +10:00
John Barton ffde38feff Merge pull request #198 from ghaninia/bugfix/ParseErrorOnReadme
The `env` variable is already defined but has been declared again
2026-05-19 21:54:18 +10:00
John Barton d1658129f9 Merge branch 'pr-235-merge-prep' 2026-05-19 21:50:28 +10:00
John Barton fecda3c8d4 test: cover +sign and leading-zero preservation in Marshal
Locks in the two data-preservation fixes from #235: values with a
leading + (e.g. phone numbers) and values with leading zeros (e.g.
zero-padded codes) must be quoted on write so they roundtrip
without the strconv.Atoi-style coercion that previously stripped
them.
2026-05-19 21:50:02 +10:00
John Barton 03bb54a6e3 Merge branch 'pr-245-merge-prep' 2026-05-19 16:30:42 +10:00
John Barton 65e5809324 test: lock in variable-name charset after hyphen widening
Adds a negative-test case asserting that '+', '@', and '/' still
return errors after #245 widens the variable-name charset to include
'-'. If a future change widens the charset further, the relevant
case here must be removed on purpose.
2026-05-19 16:29:27 +10:00
John Barton 529bc60899 Merge branch 'fix/escaped-backslash-quote'
fix: don't treat closing quote as escaped after an escaped backslash

The quoted-value terminator loop in extractVarValue treated any
backslash before a quote as an escape, so values ending in an even
number of backslashes (e.g. FOO="bar\\") parsed as "unterminated
quoted value". Replace the single-byte check with a walk-back that
counts the preceding backslash run — escape only when odd.

Closes #225. Does not address #226 (TrimRightFunc stripping a
legitimate escaped quote at the tail of a value) — that's a
separate bug in a different code path.
2026-05-19 15:54:54 +10:00
John Barton f57f0eebf6 Add an extra test coverage for #261 2026-05-19 14:17:09 +10:00
Sai Asish Y b724238db6 fix: don't treat closing quote as escaped after an escaped backslash 2026-05-12 21:37:38 -07:00
Yanhu007 3ec7e17a8d refactor: replace recursion with loop in getStatementStart
Convert the recursive getStatementStart function to an iterative
loop. This prevents potential stack overflow on files with a very
large number of consecutive comment lines.

Ref #232
2026-04-14 14:39:45 +08:00
Thomas Güttler b3d8c45c0f Update README to reflect Go version requirement
Removed instructions for Go versions less than 1.17.
2026-01-07 14:58:49 +01:00
Kha Nguyen 44124b6598 simplify Replace(..,-1) by ReplaceAll for strings and bytes 2025-11-09 10:44:05 +07:00
Parker Brown a2be92d182 Fix typo in hasQuotePrefix return variable (#251)
Corrected the spelling of the return variable from 'isQuored' to 'isQuoted' in the hasQuotePrefix function.
2025-10-22 10:26:18 +11:00
James Wert 82194f2733 Version 1.4.0 allowed hyphen "-" to be in environment variable names, after 1.4.0 this resulted in a parsing error which breaks backwards compatibility. Added back in hyphen support in variable names with associated test cases. 2025-03-31 09:34:30 -04:00
Mikhail Beliakov e1f87cdb6c Fix Precedence & Conventions in README.md 2025-03-11 14:11:04 +00:00
ignatij fab96ba47b docs: add usage for tool dependency in go1.24 2025-02-19 15:25:32 +01:00
柚子uccs 3a7a190201 fix: if a line contains multiple # characters, there will be issues w… (#238)
* fix: if a line contains multiple # characters, there will be issues when traversing from back to front

* fix: typo
2024-12-16 15:14:19 +11:00
Andrey NovikovandStanislau Arsoba a7f6c4c583 Re-add global env variable substitution (#227)
Co-authored-by: Stanislau Arsoba <sarsoba@klika-tech.com>
2024-11-01 09:24:06 +11:00
t3pm14r3 f7b3a6b517 add really big numbers isInt tests 2024-09-03 08:48:01 +03:00
t3pm14r3 bfb9a566c9 add the comment to isInt, run go fmt 2024-09-03 00:23:52 +03:00
t3pm14r3 209910751f replace unicode.IsDigit for number checking 2024-09-02 13:56:44 +03:00
t3pm14r3 2f570db853 fix '-' character trimming and expand the test 2024-09-02 10:54:36 +03:00
t3pm14r3 de05e2d993 got rid of regex, added a check for negative numbers, added a test 2024-09-02 10:42:46 +03:00
t3pm14r3 8e06a029aa fix an int parsing bug in godotenv.Marshal 2024-08-30 22:47:24 +03:00
ALX99 32e64fa834 chore: fix typo (#231) 2024-05-20 16:43:01 +10:00
Kha Nguyen 7765d9d198 Fix panic because of wrong function (#223) 2024-01-13 13:49:45 +11:00
Vintage 383d64cb7e Update cmd.go (#221)
Renuewed Update
2024-01-06 18:10:56 +11:00
dependabot[bot] 805932dc80 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [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/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-05 09:53:28 +00:00
Amin Ghaninia 310d3f598c The env variable is already defined but has been declared again 2022-12-26 01:21:52 +03:30
dependabot[bot] 892a220300 Bump thatisuday/go-cross-build from 1.0.2 to 1.1.0
Bumps [thatisuday/go-cross-build](https://github.com/thatisuday/go-cross-build) from 1.0.2 to 1.1.0.
- [Release notes](https://github.com/thatisuday/go-cross-build/releases)
- [Commits](https://github.com/thatisuday/go-cross-build/compare/v1.0.2...v1.1.0)

---
updated-dependencies:
- dependency-name: thatisuday/go-cross-build
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-13 09:58:14 +00:00
13 changed files with 206 additions and 63 deletions
+4 -4
View File
@@ -8,13 +8,13 @@ jobs:
strategy:
fail-fast: false
matrix:
go: [ '1.20', '1.19', '1.18', '1.17', '1.16' ]
os: [ ubuntu-latest, macOS-latest, windows-latest ]
go: [ '1.26', '1.25', '1.24', '1.23', '1.22' ]
os: [ ubuntu-latest, macOS-latest, windows-2025-vs2026 ]
name: ${{ matrix.os }} Go ${{ matrix.go }} Tests
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- name: Setup go
uses: actions/setup-go@v4
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- run: go test
+4 -4
View File
@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# 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
uses: github/codeql-action/autobuild@v4
# ️ 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
@@ -69,4 +69,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v4
+2 -2
View File
@@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Generate build files
uses: thatisuday/go-cross-build@v1.0.2
uses: thatisuday/go-cross-build@v1.1.0
with:
platforms: 'linux/amd64, linux/ppc64le, darwin/amd64, darwin/arm64, windows/amd64'
package: 'cmd/godotenv'
+2
View File
@@ -1 +1,3 @@
.DS_Store
CLAUDE.md
.claude
+14 -9
View File
@@ -20,16 +20,17 @@ As a library
go get github.com/joho/godotenv
```
or if you want to use it as a bin command
as a tool dependency:
go >= 1.17
go >= 1.24
```shell
go install github.com/joho/godotenv/cmd/godotenv@latest
go get -tool github.com/joho/godotenv/cmd/godotenv
```
go < 1.17
or if you want to use it as a bin command
```shell
go get github.com/joho/godotenv/cmd/godotenv
go install github.com/joho/godotenv/cmd/godotenv@latest
```
## Usage
@@ -99,7 +100,7 @@ as a final aside, if you don't want godotenv munging your env you can just get a
```go
var myEnv map[string]string
myEnv, err := godotenv.Read()
myEnv, err = godotenv.Read()
s3Bucket := myEnv["S3_BUCKET"]
```
@@ -118,6 +119,10 @@ content := getRemoteFileContent()
myEnv, err := godotenv.Unmarshal(content)
```
### Variable name compatibility
Ruby's dotenv only allows `[A-Za-z0-9_.]` in key names, while Node's dotenv also permits `-`. godotenv has matched the Node charset since v1.4.0, so keys like `MY-VAR` parse cleanly here and in Node but will error under Ruby's dotenv.
### Precedence & Conventions
Existing envs take precedence of envs that are loaded later.
@@ -132,12 +137,12 @@ if "" == env {
env = "development"
}
godotenv.Load(".env." + env + ".local")
godotenv.Load() // The Original .env
godotenv.Load(".env." + env)
if "test" != env {
godotenv.Load(".env.local")
}
godotenv.Load(".env." + env)
godotenv.Load() // The Original .env
godotenv.Load(".env." + env + ".local")
```
If you need to, you can also use `godotenv.Overload()` to defy this convention
-1
View File
@@ -4,7 +4,6 @@ import (
"flag"
"fmt"
"log"
"strings"
"github.com/joho/godotenv"
+3
View File
@@ -1,4 +1,7 @@
# Full line comment
qux=thud # fred # other
thud=fred#qux # other
fred=qux#baz # other # more
foo=bar # baz
bar=foo#baz
baz="foo"#bar
+3
View File
@@ -0,0 +1,3 @@
OPTION_A=abc
OPTION-B=def
+1
View File
@@ -3,3 +3,4 @@ OPTION_B=${OPTION_A}
OPTION_C=$OPTION_B
OPTION_D=${OPTION_A}${OPTION_B}
OPTION_E=${OPTION_NOT_DEFINED}
OPTION_F=${GLOBAL_OPTION}
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/joho/godotenv
go 1.12
go 1.13
+23 -4
View File
@@ -20,7 +20,6 @@ import (
"os"
"os/exec"
"sort"
"strconv"
"strings"
)
@@ -159,13 +158,33 @@ func Write(envMap map[string]string, filename string) error {
return file.Sync()
}
// isInt checks if the string may be serialized as a number value, leading
// "-" symbol is allowed for negative numbers, leading "+" sign is not. The
// length of the value is not limited.
func isInt(s string) bool {
s = strings.TrimPrefix(s, "-")
if len(s) == 0 {
return false
}
for _, r := range s {
if '0' <= r && r <= '9' {
continue
}
return false
}
return true
}
// 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))
if isInt(v) {
lines = append(lines, fmt.Sprintf(`%s=%s`, k, v))
} else {
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
}
@@ -222,7 +241,7 @@ func doubleQuoteEscape(line string) string {
if c == '\r' {
toReplace = `\r`
}
line = strings.Replace(line, string(c), toReplace, -1)
line = strings.ReplaceAll(line, string(c), toReplace)
}
return line
}
+103 -12
View File
@@ -2,6 +2,7 @@ package godotenv
import (
"bytes"
"errors"
"fmt"
"os"
"reflect"
@@ -184,6 +185,31 @@ func TestLoadEqualsEnv(t *testing.T) {
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
func TestLoadHyphenEnv(t *testing.T) {
envFileName := "fixtures/hyphen.env"
expectedValues := map[string]string{
"OPTION_A": "abc",
"OPTION-B": "def",
}
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
func TestKeyNameCharsetRejectsDisallowed(t *testing.T) {
// Locks in the variable-name charset to [A-Za-z0-9_.-]. If you widen
// this further, delete the relevant case here on purpose.
disallowed := []string{
"FOO+BAR=baz",
"FOO@BAR=baz",
"FOO/BAR=baz",
}
for _, input := range disallowed {
if _, err := Unmarshal(input); err == nil {
t.Errorf("expected error parsing %q, got nil", input)
}
}
}
func TestLoadQuotedEnv(t *testing.T) {
envFileName := "fixtures/quoted.env"
expectedValues := map[string]string{
@@ -207,15 +233,21 @@ func TestLoadQuotedEnv(t *testing.T) {
func TestSubstitutions(t *testing.T) {
envFileName := "fixtures/substitutions.env"
presets := map[string]string{
"GLOBAL_OPTION": "global",
}
expectedValues := map[string]string{
"OPTION_A": "1",
"OPTION_B": "1",
"OPTION_C": "1",
"OPTION_D": "11",
"OPTION_E": "",
"OPTION_F": "global",
}
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)
}
func TestExpanding(t *testing.T) {
@@ -336,6 +368,9 @@ func TestParsing(t *testing.T) {
// parses escaped double quotes
parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`)
// a trailing escaped backslash does not escape the closing quote
parseAndCompare(t, `FOO="bar\\"`, "FOO", `bar\`)
// parses single quotes inside double quotes
parseAndCompare(t, `FOO="'d'"`, "FOO", `'d'`)
@@ -435,6 +470,10 @@ func TestLinesToIgnore(t *testing.T) {
"Indented comment": {
input: "\t # comment",
},
"Multiple consecutive comments": {
input: "# one\n# two\n# three\nFOO=bar",
want: "FOO=bar",
},
"non-ignored value": {
input: `export OPTION_B='\n'`,
want: `export OPTION_B='\n'`,
@@ -471,14 +510,41 @@ func TestErrorParsing(t *testing.T) {
func TestComments(t *testing.T) {
envFileName := "fixtures/comments.env"
expectedValues := map[string]string{
"foo": "bar",
"bar": "foo#baz",
"baz": "foo",
"qux": "thud",
"thud": "fred#qux",
"fred": "qux#baz",
"foo": "bar",
"bar": "foo#baz",
"baz": "foo",
}
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}
func TestIsInt(t *testing.T) {
checkAndCompare := func(s string, expected bool) {
if isInt(s) != expected {
t.Fail()
}
}
// invalid values
checkAndCompare("", false)
checkAndCompare("+123", false)
checkAndCompare("+12a3", false)
checkAndCompare("12a3", false)
checkAndCompare("abc", false)
checkAndCompare("12 3", false)
checkAndCompare("-", false)
checkAndCompare(" ", false)
// valid values
checkAndCompare("-123", true)
checkAndCompare("123", true)
checkAndCompare("-922337203685477580868712", true)
checkAndCompare("922337203685477580837281", true)
}
func TestWrite(t *testing.T) {
writeAndCompare := func(env string, expected string) {
envMap, _ := Unmarshal(env)
@@ -502,6 +568,10 @@ func TestWrite(t *testing.T) {
writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"")
// integers should not be quoted
writeAndCompare(`key="10"`, `key=10`)
// leading + is not numeric — must be quoted to preserve the sign
writeAndCompare(`key=+123`, `key="+123"`)
// leading zeros must be preserved (not collapsed to a smaller int)
writeAndCompare(`key=007`, `key=007`)
}
@@ -582,42 +652,42 @@ func TestWhitespace(t *testing.T) {
}{
"Leading whitespace": {
input: " A=a\n",
key: "A",
key: "A",
value: "a",
},
"Leading tab": {
input: "\tA=a\n",
key: "A",
key: "A",
value: "a",
},
"Leading mixed whitespace": {
input: " \t \t\n\t \t A=a\n",
key: "A",
key: "A",
value: "a",
},
"Leading whitespace before export": {
input: " \t\t export A=a\n",
key: "A",
key: "A",
value: "a",
},
"Trailing whitespace": {
input: "A=a \t \t\n",
key: "A",
key: "A",
value: "a",
},
"Trailing whitespace with export": {
input: "export A=a\t \t \n",
key: "A",
key: "A",
value: "a",
},
"No EOL": {
input: "A=a",
key: "A",
key: "A",
value: "a",
},
"Trailing whitespace with no EOL": {
input: "A=a ",
key: "A",
key: "A",
value: "a",
},
}
@@ -634,3 +704,24 @@ func TestWhitespace(t *testing.T) {
})
}
}
func TestParserErrors(t *testing.T) {
cases := map[string]struct {
input string
err error
}{
"UnterminatedQuote": {
input: "foo=\"bar",
err: ErrUnterminatedQuote,
},
}
for n, c := range cases {
t.Run(n, func(t *testing.T) {
v, err := Unmarshal(c.input)
if !errors.Is(err, c.err) {
t.Errorf("Input: %q Expected:\t %q\nGot:\t %q Val: %v", c.input, c.err, err, v)
}
})
}
}
+46 -26
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"os"
"regexp"
"strings"
"unicode"
@@ -17,8 +18,14 @@ const (
exportPrefix = "export"
)
var (
ErrZeroLengthString = errors.New("zero length string")
ErrUnexpectedChar = errors.New("unexpected character")
ErrUnterminatedQuote = errors.New("unterminated quoted value")
)
func parseBytes(src []byte, out map[string]string) error {
src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1)
src = bytes.ReplaceAll(src, []byte("\r\n"), []byte("\n"))
cutset := src
for {
cutset = getStatementStart(cutset)
@@ -48,23 +55,25 @@ func parseBytes(src []byte, out map[string]string) error {
//
// It skips any comment line or non-whitespace character.
func getStatementStart(src []byte) []byte {
pos := indexOfNonSpaceChar(src)
if pos == -1 {
return nil
}
for {
pos := indexOfNonSpaceChar(src)
if pos == -1 {
return nil
}
src = src[pos:]
if src[0] != charComment {
return src
}
src = src[pos:]
if src[0] != charComment {
return src
}
// skip comment section
pos = bytes.IndexFunc(src, isCharFunc('\n'))
if pos == -1 {
return nil
}
// skip comment section
pos = bytes.IndexFunc(src, isCharFunc('\n'))
if pos == -1 {
return nil
}
return getStatementStart(src[pos:])
src = src[pos:]
}
}
// locateKeyName locates and parses key name and returns rest of slice
@@ -95,19 +104,20 @@ loop:
break loop
case '_':
default:
// variable name should match [A-Za-z0-9_.]
if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' {
// variable name should match [A-Za-z0-9_.-]
if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' || rchar == '-' {
continue
}
return "", nil, fmt.Errorf(
`unexpected character %q in variable name near %q`,
`%w %q in variable name near %q`,
ErrUnexpectedChar,
string(char), string(src))
}
}
if len(src) == 0 {
return "", nil, errors.New("zero length string")
return "", nil, ErrZeroLengthString
}
// trim whitespace
@@ -142,9 +152,9 @@ func extractVarValue(src []byte, vars map[string]string) (value string, rest []b
}
// 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 {
// a comment, ie: foo=bar # baz # other
for i := 0; i < endOfVar; i++ {
if line[i] == charComment && i < endOfVar {
if isSpace(line[i-1]) {
endOfVar = i
break
@@ -163,8 +173,12 @@ func extractVarValue(src []byte, vars map[string]string) (value string, rest []b
continue
}
// skip escaped quote symbol (\" or \', depends on quote)
if prevChar := src[i-1]; prevChar == '\\' {
// skip escaped quote symbol; a quote is escaped only when preceded by an odd number of backslashes
backslashes := 0
for j := i - 1; j >= 0 && src[j] == '\\'; j-- {
backslashes++
}
if backslashes%2 == 1 {
continue
}
@@ -186,7 +200,7 @@ func extractVarValue(src []byte, vars map[string]string) (value string, rest []b
valEndIndex = len(src)
}
return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex])
return "", nil, fmt.Errorf("%w %s", ErrUnterminatedQuote, src[:valEndIndex])
}
func expandEscapes(str string) string {
@@ -211,7 +225,7 @@ func indexOfNonSpaceChar(src []byte) int {
}
// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) {
func hasQuotePrefix(src []byte) (prefix byte, isQuoted bool) {
if len(src) == 0 {
return 0, false
}
@@ -264,6 +278,12 @@ func expandVariables(v string, m map[string]string) string {
if submatch[1] == "\\" || submatch[2] == "(" {
return submatch[0][1:]
} else if submatch[4] != "" {
if val, ok := m[submatch[4]]; ok {
return val
}
if val, ok := os.LookupEnv(submatch[4]); ok {
return val
}
return m[submatch[4]]
}
return s