Skip to content

Instantly share code, notes, and snippets.

@juliankoehn
Created October 30, 2020 13:56

Revisions

  1. juliankoehn created this gist Oct 30, 2020.
    205 changes: 205 additions & 0 deletions env.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,205 @@
    package env

    import (
    "bufio"
    "errors"
    "fmt"
    "io"
    "os"
    "regexp"
    "strings"
    )

    func init() {
    fmt.Println("trying to load environemtn from .env")
    loadFile(".env", false)
    }

    func loadFile(filename string, overload bool) error {
    envMap, err := readFile(filename)
    if err != nil {
    return err
    }

    currentEnv := map[string]bool{}
    rawEnv := os.Environ()
    for _, rawEnvLine := range rawEnv {
    key := strings.Split(rawEnvLine, "=")[0]
    currentEnv[key] = true
    }

    for key, value := range envMap {
    if !currentEnv[key] || overload {
    os.Setenv(key, value)
    }
    }

    return nil
    }

    func readFile(filename string) (envMap map[string]string, err error) {
    file, err := os.Open(filename)
    if err != nil {
    return
    }
    defer file.Close()

    return Parse(file)
    }

    // Parse reads an env file from io.Reader, returning a map of keys and values.
    func Parse(r io.Reader) (envMap map[string]string, err error) {
    envMap = make(map[string]string)

    var lines []string
    scanner := bufio.NewScanner(r)
    for scanner.Scan() {
    lines = append(lines, scanner.Text())
    }

    if err = scanner.Err(); err != nil {
    return
    }

    for _, fullLine := range lines {
    if !isIgnoredLine(fullLine) {
    var key, value string
    key, value, err = parseLine(fullLine, envMap)

    if err != nil {
    return
    }
    envMap[key] = value
    }
    }
    return
    }

    func isIgnoredLine(line string) bool {
    trimmedLine := strings.TrimSpace(line)
    return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
    }

    var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`)

    func parseLine(line string, envMap map[string]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
    var segmentsToKeep []string
    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, "#")
    }

    firstEquals := strings.Index(line, "=")
    firstColon := strings.Index(line, ":")
    splitString := strings.SplitN(line, "=", 2)
    if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
    //this is a yaml-style line
    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.TrimSpace(key)

    key = exportRegex.ReplaceAllString(splitString[0], "$1")

    // Parse the value
    value = parseValue(splitString[1], envMap)
    return
    }

    var (
    singleQuotesRegex = regexp.MustCompile(`\A'(.*)'\z`)
    doubleQuotesRegex = regexp.MustCompile(`\A"(.*)"\z`)
    escapeRegex = regexp.MustCompile(`\\.`)
    unescapeCharsRegex = regexp.MustCompile(`\\([^$])`)
    )

    func parseValue(value string, envMap map[string]string) string {

    // trim
    value = strings.Trim(value, " ")

    // check if we've got quoted values or possible escapes
    if len(value) > 1 {
    singleQuotes := singleQuotesRegex.FindStringSubmatch(value)

    doubleQuotes := doubleQuotesRegex.FindStringSubmatch(value)

    if singleQuotes != nil || doubleQuotes != nil {
    // pull the quotes off the edges
    value = value[1 : len(value)-1]
    }

    if doubleQuotes != nil {
    // expand newlines
    value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
    c := strings.TrimPrefix(match, `\`)
    switch c {
    case "n":
    return "\n"
    case "r":
    return "\r"
    default:
    return match
    }
    })
    // unescape characters
    value = unescapeCharsRegex.ReplaceAllString(value, "$1")
    }

    if singleQuotes == nil {
    value = expandVariables(value, envMap)
    }
    }

    return value
    }

    var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)

    func expandVariables(v string, m map[string]string) string {
    return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {
    submatch := expandVarRegex.FindStringSubmatch(s)

    if submatch == nil {
    return s
    }
    if submatch[1] == "\\" || submatch[2] == "(" {
    return submatch[0][1:]
    } else if submatch[4] != "" {
    return m[submatch[4]]
    }
    return s
    })
    }