Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vendor go-dotenv #955

Merged
merged 2 commits into from Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion default.nix
Expand Up @@ -12,7 +12,7 @@ buildGoModule rec {
version = lib.fileContents ./version.txt;
subPackages = [ "." ];

vendorSha256 = "sha256-gFGGnnR1UNT4MYC411X8NwIqVJZqhnmUlVR+XAnrKY8=";
vendorSha256 = "sha256-u/LukIOYRudFYOrrlZTMtDAlM3+WjoSBiueR7aySSVU=";

src = builtins.fetchGit ./.;

Expand Down
1 change: 0 additions & 1 deletion go.mod
Expand Up @@ -4,7 +4,6 @@ go 1.16

require (
github.com/BurntSushi/toml v1.1.0
github.com/direnv/go-dotenv v0.0.0-20220613081022-872ea3db4cb5
github.com/mattn/go-isatty v0.0.14
golang.org/x/mod v0.5.1
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
@@ -1,7 +1,5 @@
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/direnv/go-dotenv v0.0.0-20220613081022-872ea3db4cb5 h1:lMrU+5WHwaPfmvp+CIxUi2ujVJSu/mYh1Rcu23n7HaY=
github.com/direnv/go-dotenv v0.0.0-20220613081022-872ea3db4cb5/go.mod h1:K5R9ofHu1mQRwNWMZgDnr0s1Ca1nkvz9r09NN/U2UyM=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/cmd_dotenv.go
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"io/ioutil"

"github.com/direnv/go-dotenv"
"github.com/direnv/direnv/v2/pkg/dotenv"
)

// CmdDotEnv is `direnv dotenv [SHELL [PATH_TO_DOTENV]]`
Expand Down
29 changes: 29 additions & 0 deletions pkg/dotenv/README.md
@@ -0,0 +1,29 @@
# go-dotenv

Go parsing library for the dotenv format.

There is no formal definition of the dotenv format but it has been introduced
by https://github.com/bkeepers/dotenv which is thus canonical. This library is a port of that.

This library was developed specifically for [direnv](https://direnv.net).

## Features

* `k=v` format
* bash `export k=v` format
* yaml `k: v` format
* variable expansion, including default values as in `${FOO:-default}`
* comments

## Missing

* probably needs API breakage

## Alternatives

Some other good alternatives with various variations.

* https://github.com/joho/godotenv
* https://github.com/lazureykis/dotenv
* https://github.com/subosito/gotenv

150 changes: 150 additions & 0 deletions pkg/dotenv/parse.go
@@ -0,0 +1,150 @@
// Package dotenv implements the parsing of the .env format.
//
// There is no formal definition of the format but it has been introduced by
// https://github.com/bkeepers/dotenv which is thus canonical.
package dotenv

import (
"fmt"
"os"
"regexp"
"strings"
)

// LINE is the regexp matching a single line
const LINE = `
\A
\s*
(?:|#.*| # comment line
(?:export\s+)? # optional export
([\w\.]+) # key
(?:\s*=\s*|:\s+?) # separator
( # optional value begin
'(?:\'|[^'])*' # single quoted value
| # or
"(?:\"|[^"])*" # double quoted value
| # or
[^\s#\n]+ # unquoted value
)? # value end
\s*
(?:\#.*)? # optional comment
)
\z
`

var linesRe = regexp.MustCompile("[\\r\\n]+")
var lineRe = regexp.MustCompile(
regexp.MustCompile("\\s+").ReplaceAllLiteralString(
regexp.MustCompile("\\s+# .*").ReplaceAllLiteralString(LINE, ""), ""))

// Parse reads a string in the .env format and returns a map of the extracted key=values.
//
// Ported from https://github.com/bkeepers/dotenv/blob/84f33f48107c492c3a99bd41c1059e7b4c1bb67a/lib/dotenv/parser.rb
func Parse(data string) (map[string]string, error) {
var dotenv = make(map[string]string)

for _, line := range linesRe.Split(data, -1) {
if !lineRe.MatchString(line) {
return nil, fmt.Errorf("invalid line: %s", line)
}

match := lineRe.FindStringSubmatch(line)
// commented or empty line
if len(match) == 0 {
continue
}
if len(match[1]) == 0 {
continue
}

key := match[1]
value := match[2]

parseValue(key, value, dotenv)
}

return dotenv, nil
}

// MustParse works the same as Parse but panics on error
func MustParse(data string) map[string]string {
env, err := Parse(data)
if err != nil {
panic(err)
}
return env
}

func parseValue(key string, value string, dotenv map[string]string) {
if len(value) <= 1 {
dotenv[key] = value
return
}

singleQuoted := false

if value[0:1] == "'" && value[len(value)-1:] == "'" {
// single-quoted string, do not expand
singleQuoted = true
value = value[1 : len(value)-1]
} else if value[0:1] == `"` && value[len(value)-1:] == `"` {
value = value[1 : len(value)-1]
value = expandNewLines(value)
value = unescapeCharacters(value)
}

if !singleQuoted {
value = expandEnv(value, dotenv)
}

dotenv[key] = value
}

var escRe = regexp.MustCompile("\\\\([^$])")

func unescapeCharacters(value string) string {
return escRe.ReplaceAllString(value, "$1")
}

func expandNewLines(value string) string {
value = strings.Replace(value, "\\n", "\n", -1)
value = strings.Replace(value, "\\r", "\r", -1)
return value
}

func expandEnv(value string, dotenv map[string]string) string {
expander := func(value string) string {
envKey, defaultValue, hasDefault := splitKeyAndDefault(value, ":-")
expanded, found := lookupDotenv(envKey, dotenv)

if found {
return expanded
}
return getFromEnvOrDefault(envKey, defaultValue, hasDefault)
}

return os.Expand(value, expander)
}

func splitKeyAndDefault(value string, sep string) (string, string, bool) {
var i = strings.Index(value, sep)

if i == -1 {
return value, "", false
}
return value[0:i], value[i+len(sep):], true
}

func lookupDotenv(value string, dotenv map[string]string) (string, bool) {
retval, ok := dotenv[value]
return retval, ok
}

func getFromEnvOrDefault(envKey string, defaultValue string, hasDefault bool) string {
var envValue = os.Getenv(envKey)

if len(envValue) == 0 && hasDefault {
return defaultValue
}
return envValue
}