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

Marshal #17

Merged
merged 4 commits into from May 30, 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
144 changes: 102 additions & 42 deletions gotenv.go
Expand Up @@ -6,7 +6,10 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)

Expand All @@ -24,41 +27,31 @@ const (
// Env holds key/value pair of valid environment variable
type Env map[string]string

/*
Load is a function to load a file or multiple files and then export the valid variables into environment variables if they do not exist.
When it's called with no argument, it will load `.env` file on the current path and set the environment variables.
Otherwise, it will loop over the filenames parameter and set the proper environment variables.
*/
// Load is a function to load a file or multiple files and then export the valid variables into environment variables if they do not exist.
// When it's called with no argument, it will load `.env` file on the current path and set the environment variables.
// Otherwise, it will loop over the filenames parameter and set the proper environment variables.
func Load(filenames ...string) error {
return loadenv(false, filenames...)
}

/*
OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.
*/
// OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.
func OverLoad(filenames ...string) error {
return loadenv(true, filenames...)
}

/*
Must is wrapper function that will panic when supplied function returns an error.
*/
// Must is wrapper function that will panic when supplied function returns an error.
func Must(fn func(filenames ...string) error, filenames ...string) {
if err := fn(filenames...); err != nil {
panic(err.Error())
}
}

/*
Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.
*/
// Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.
func Apply(r io.Reader) error {
return parset(r, false)
}

/*
OverApply is a function to load an io Reader then export and override the valid variables into environment variables.
*/
// OverApply is a function to load an io Reader then export and override the valid variables into environment variables.
func OverApply(r io.Reader) error {
return parset(r, true)
}
Expand All @@ -75,11 +68,10 @@ func loadenv(override bool, filenames ...string) error {
}

err = parset(f, override)
f.Close()
if err != nil {
return err
}

f.Close()
}

return nil
Expand Down Expand Up @@ -124,6 +116,63 @@ func StrictParse(r io.Reader) (Env, error) {
return strictParse(r, false)
}

// Read is a function to parse a file line by line and returns the valid Env key/value pair of valid variables.
// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
// This function is skipping any invalid lines and only processing the valid one.
func Read(filename string) (Env, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
return strictParse(f, false)
}

//Unmarshal reads a string line by line and returns the valid Env key/value pair of valid variables.
// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
// This function is returning an error if there are any invalid lines.
func Unmarshal(str string) (Env, error) {
return strictParse(strings.NewReader(str), false)
}

// Marshal outputs the given environment as a env file.
// Variables will be sorted by name.
func Marshal(env Env) (string, error) {
lines := make([]string, 0, len(env))
for k, v := range env {
if d, err := strconv.Atoi(v); err == nil {
lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
} else {
lines = append(lines, fmt.Sprintf(`%s=%q`, k, v))
}
}
sort.Strings(lines)
return strings.Join(lines, "\n"), nil
}

// Write serializes the given environment and writes it to a file
func Write(env Env, filename string) error {
content, err := Marshal(env)
if err != nil {
return err
}
// ensure the path exists
if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {
return err
}
// create or truncate the file
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()
}

func strictParse(r io.Reader, override bool) (Env, error) {
env := make(Env)
scanner := bufio.NewScanner(r)
Expand All @@ -143,28 +192,34 @@ func strictParse(r io.Reader, override bool) (Env, error) {
}

quote := ""
// look for the delimiter character
idx := strings.Index(line, "=")
if idx == -1 {
idx = strings.Index(line, ":")
}
// look for a quote character
if idx > 0 && idx < len(line)-1 {
val := strings.TrimSpace(line[idx+1:])
if val[0] == '"' || val[0] == '\'' {
quote = val[:1]
// look for the closing quote character within the same line
idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)
if idx >= 0 && val[idx] != '\\' {
quote = ""
}
}
}
// look for the closing quote character
for quote != "" && scanner.Scan() {
l := scanner.Text()
line += "\n" + l
idx := strings.LastIndex(l, quote)
if idx > 0 && l[idx-1] == '\\' {
// foud a matching quote character but it's escaped
continue
}
if idx >= 0 {
// foud a matching quote
quote = ""
}
}
Expand Down Expand Up @@ -195,21 +250,23 @@ func parseLine(s string, env Env, override bool) error {
return checkFormat(s, env)
}

key := rm[1]
val := rm[2]

// trim whitespace
val = strings.TrimSpace(val)
key := strings.TrimSpace(rm[1])
val := strings.TrimSpace(rm[2])

// determine if string has quote prefix
hdq := strings.HasPrefix(val, `"`)
var hsq, hdq bool

// determine if string has single quote prefix
hsq := strings.HasPrefix(val, `'`)
// check if the value is quoted
if l := len(val); l >= 2 {
l -= 1
// has double quotes
hdq = val[0] == '"' && val[l] == '"'
// has single quotes
hsq = val[0] == '\'' && val[l] == '\''

// remove quotes '' or ""
if l := len(val); (hsq || hdq) && l >= 2 {
val = val[1 : l-1]
// remove quotes '' or ""
if hsq || hdq {
val = val[1:l]
}
}

if hdq {
Expand All @@ -220,11 +277,10 @@ func parseLine(s string, env Env, override bool) error {
val = unescapeRgx.ReplaceAllString(val, "$1")
}

fv := func(s string) string {
return varReplacement(s, hsq, env, override)
}

if !hsq {
fv := func(s string) string {
return varReplacement(s, hsq, env, override)
}
val = varRgx.ReplaceAllStringFunc(val, fv)
val = parseVal(val, env, hdq, override)
}
Expand All @@ -250,8 +306,13 @@ func parseExport(st string, env Env) error {
var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)

func varReplacement(s string, hsq bool, env Env, override bool) string {
if strings.HasPrefix(s, "\\") {
return strings.TrimPrefix(s, "\\")
if s == "" {
return s
}

if s[0] == '\\' {
// the dollar sign is escaped
return s[1:]
}

if hsq {
Expand All @@ -270,18 +331,17 @@ func varReplacement(s string, hsq bool, env Env, override bool) string {
return replace
}

replace, ok := env[v]
if !ok {
replace = os.Getenv(v)
if replace, ok := env[v]; ok {
return replace
}

return replace
return os.Getenv(v)
}

func checkFormat(s string, env Env) error {
st := strings.TrimSpace(s)

if (st == "") || strings.HasPrefix(st, "#") {
if st == "" || st[0] == '#' {
return nil
}

Expand Down
21 changes: 21 additions & 0 deletions gotenv_test.go
Expand Up @@ -405,3 +405,24 @@ func TestOverApply(t *testing.T) {
assert.Equal(t, "universe", os.Getenv("HELLO"))
os.Clearenv()
}

func TestMarshal(t *testing.T) {
env := gotenv.Env{
"FOO": "BAR",
"ONE": "1",
"QUOTED": `some "quoted" text`,
"EMPTY": "",
}
expected := `EMPTY=""
FOO="BAR"
ONE=1
QUOTED="some \"quoted\" text"`

actual, err := gotenv.Marshal(env)
assert.Nil(t, err)
assert.Equal(t, expected, actual)

out, err := gotenv.Unmarshal(expected)
assert.Nil(t, err)
assert.Equal(t, env, out)
}