Files
podman/pkg/env/env.go
Black-Hole1 170a78631b feat(env): support multiline in env-file
Close: https://github.com/containers/podman/issues/18724

Signed-off-by: Black-Hole1 <bh@bugs.cc>
2023-07-31 09:59:45 +08:00

199 lines
5.0 KiB
Go

package env
import (
"fmt"
"io"
"os"
"strings"
"github.com/containers/storage/pkg/regexp"
)
var (
// Form: https://github.com/motdotla/dotenv/blob/aa03dcad1002027390dac1e8d96ac236274de354/lib/main.js#L9C76-L9C76
// (?:export\s+)?([\w.-]+) match key
// ([\w.%-]+)(\s*[=|*]\s*?|:\s+?) match separator
// Remaining match value
// e.g. KEY=VALUE => KEY, =, VALUE
//
// KEY= => KEY, =, ""
// KEY* => KEY, *, ""
// KEY*=1 => KEY, *, =1
lineRegexp = regexp.Delayed(
`(?m)(?:^|^)\s*(?:export\s+)?([\w.%-]+)(\s*[=|*]\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*` +
"`(?:\\`|[^`])*`" + `|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)`,
)
onlyKeyRegexp = regexp.Delayed(`^[\w.-]+$`)
)
const whiteSpaces = " \t"
// DefaultEnvVariables returns a default environment, with $PATH and $TERM set.
func DefaultEnvVariables() map[string]string {
return map[string]string{
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM": "xterm",
"container": "podman",
}
}
// Slice transforms the specified map of environment variables into a
// slice. If a value is non-empty, the key and value are joined with '='.
func Slice(m map[string]string) []string {
env := make([]string, 0, len(m))
for k, v := range m {
var s string
if len(v) > 0 {
s = fmt.Sprintf("%s=%s", k, v)
} else {
s = k
}
env = append(env, s)
}
return env
}
// Map transforms the specified slice of environment variables into a
// map.
func Map(slice []string) map[string]string {
envmap := make(map[string]string, len(slice))
for _, val := range slice {
data := strings.SplitN(val, "=", 2)
if len(data) > 1 {
envmap[data[0]] = data[1]
} else {
envmap[data[0]] = ""
}
}
return envmap
}
// Join joins the two environment maps with override overriding base.
func Join(base map[string]string, override map[string]string) map[string]string {
if len(base) == 0 {
return override
}
for k, v := range override {
base[k] = v
}
return base
}
// ParseFile parses the specified path for environment variables and returns them
// as a map.
func ParseFile(path string) (_ map[string]string, err error) {
env := make(map[string]string)
defer func() {
if err != nil {
err = fmt.Errorf("parsing file %q: %w", path, err)
}
}()
fh, err := os.Open(path)
if err != nil {
return nil, err
}
defer fh.Close()
content, err := io.ReadAll(fh)
if err != nil {
return nil, err
}
// replace all \r\n and \r with \n
text := strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(string(content))
if err := parseEnv(env, text, false); err != nil {
return nil, err
}
return env, nil
}
// parseEnv parse the given content into env format
// @param enforceMatch bool "it throws an error if there is no match"
//
// @example: parseEnv(env, "#comment", true) => error("invalid variable: #comment")
// @example: parseEnv(env, "#comment", false) => nil
// @example: parseEnv(env, "", false) => nil
// @example: parseEnv(env, "KEY=FOO", true) => nil
// @example: parseEnv(env, "KEY", true) => nil
func parseEnv(env map[string]string, content string, enforceMatch bool) error {
m := envMatch(content)
if len(m) == 0 && enforceMatch {
return fmt.Errorf("invalid variable: %q", content)
}
for _, match := range m {
key := match[1]
separator := strings.Trim(match[2], whiteSpaces)
value := match[3]
if strings.Contains(value, "\n") {
if strings.HasPrefix(value, "`") {
return fmt.Errorf("only support multi-line environment variables surrounded by "+
"double quotation marks or single quotation marks. invalid variable: %q", match[0])
}
// In the case of multi-line values, we need to remove the surrounding " '
value = strings.Trim(value, "\"'")
}
// KEY*=1 => KEY, *, =1 => KEY*, =, 1
if separator == "*" && strings.HasPrefix(value, "=") {
key += "*"
separator = "="
value = strings.TrimPrefix(value, "=")
}
switch separator {
case "=":
// KEY=
if value == "" {
if val, ok := os.LookupEnv(key); ok {
env[key] = val
}
} else {
env[key] = value
}
case "*":
for _, e := range os.Environ() {
part := strings.SplitN(e, "=", 2)
if len(part) < 2 {
continue
}
if strings.HasPrefix(part[0], key) {
env[part[0]] = part[1]
}
}
}
}
return nil
}
func envMatch(content string) [][]string {
m := lineRegexp.FindAllStringSubmatch(content, -1)
// KEY => KEY, =, ""
// Due to the above regex pattern, it will skip cases where only KEY is present (e.g., foo).
// However, in our requirement, this situation is equivalent to foo=(i.e., "foo" == "foo=").
// Therefore, we need to perform additional processing.
// The reason for needing to support this scenario is that we need to consider: `podman run -e CI -e USERNAME`.
{
noMatched := lineRegexp.ReplaceAllString(content, "")
nl := strings.Split(noMatched, "\n")
for _, key := range nl {
key := strings.Trim(key, whiteSpaces)
if key == "" {
continue
}
if onlyKeyRegexp.MatchString(key) {
m = append(m, []string{key, key, "=", ""})
}
}
}
return m
}