From 30b6d1f47a35d00fca7cff0daa2ff59a98c5a85e Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 13 Apr 2022 11:35:28 -0600 Subject: cmd: Enhance .env (dotenv) file parsing Basic support for quoted values, newlines in quoted values, and comments. Does not support variable or command expansion. --- cmd/main.go | 58 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 16 deletions(-) (limited to 'cmd/main.go') diff --git a/cmd/main.go b/cmd/main.go index f111ba4..498a8ae 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -368,42 +368,68 @@ func loadEnvFromFile(envFile string) error { return nil } +// parseEnvFile parses an env file from KEY=VALUE format. +// It's pretty naive. Limited value quotation is supported, +// but variable and command expansions are not supported. func parseEnvFile(envInput io.Reader) (map[string]string, error) { envMap := make(map[string]string) scanner := bufio.NewScanner(envInput) - var line string - lineNumber := 0 + var lineNumber int for scanner.Scan() { - line = strings.TrimSpace(scanner.Text()) + line := strings.TrimSpace(scanner.Text()) lineNumber++ - // skip lines starting with comment - if strings.HasPrefix(line, "#") { - continue - } - - // skip empty line - if len(line) == 0 { + // skip empty lines and lines starting with comment + if line == "" || strings.HasPrefix(line, "#") { continue } + // split line into key and value fields := strings.SplitN(line, "=", 2) if len(fields) != 2 { return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber) } + key, val := fields[0], fields[1] - if strings.Contains(fields[0], " ") { - return nil, fmt.Errorf("bad key on line %d: contains whitespace", lineNumber) - } - - key := fields[0] - val := fields[1] + // sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here + key = strings.TrimPrefix(key, "export ") + // validate key and value if key == "" { return nil, fmt.Errorf("missing or empty key on line %d", lineNumber) } + if strings.Contains(key, " ") { + return nil, fmt.Errorf("invalid key on line %d: contains whitespace: %s", lineNumber, key) + } + if strings.HasPrefix(val, " ") || strings.HasPrefix(val, "\t") { + return nil, fmt.Errorf("invalid value on line %d: whitespace before value: '%s'", lineNumber, val) + } + + // remove any trailing comment after value + if commentStart := strings.Index(val, "#"); commentStart > 0 { + before := val[commentStart-1] + if before == '\t' || before == ' ' { + val = strings.TrimRight(val[:commentStart], " \t") + } + } + + // quoted value: support newlines + if strings.HasPrefix(val, `"`) { + for !(strings.HasSuffix(line, `"`) && !strings.HasSuffix(line, `\"`)) { + val = strings.ReplaceAll(val, `\"`, `"`) + if !scanner.Scan() { + break + } + lineNumber++ + line = strings.ReplaceAll(scanner.Text(), `\"`, `"`) + val += "\n" + line + } + val = strings.TrimPrefix(val, `"`) + val = strings.TrimSuffix(val, `"`) + } + envMap[key] = val } -- cgit v1.2.3