pkg/runner/environment.go (107 lines of code) (raw):

package runner import ( "fmt" "maps" "os" "strings" "sync" "golang.org/x/exp/slices" ) // Environment represents environment variables. // Environment variables are used in places such as the step export_file, step definitions with ENV, and OS environment. // Environment does not merge maps, and so does not lose information. Merging of environments occurs when values are retrieved. // An environment can be added as "lexical scope", these values have higher precedence when looking up a variable. // Mutations to the environment take precedence over initialized variables, most recent mutations have the highest precedence type Environment struct { vars map[string]string // Variables of this lexical scoped environment parent *Environment // Variables of the parent lexical scoped environment mutationsMu sync.Mutex mutations []*Environment // Mutations to the environment } // NewEnvironmentFromOS returns the environment variables found in the OS runtime. // Variables can be filtered by name, passing no names will return all variables. func NewEnvironmentFromOS(rejectIf ...func(string) bool) (*Environment, error) { vars := map[string]string{} for _, nameValue := range os.Environ() { name, value, ok := strings.Cut(nameValue, "=") if !ok { return nil, fmt.Errorf("failed to parse environment variable: %s", nameValue) } if acceptEnvName(name, rejectIf) { vars[name] = value } } return NewEnvironment(vars), nil } func NewEnvironmentFromOSWithKnownVars() (*Environment, error) { knownVars := []string{ "HTTPS_PROXY", "HTTP_PROXY", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME", "NO_PROXY", "PATH", "SHELL", "TERM", "TMPDIR", "TZ", "USER", "all_proxy", "http_proxy", "https_proxy", "no_proxy", } return NewEnvironmentFromOS(func(envName string) bool { return !slices.Contains(knownVars, envName) }) } func NewEmptyEnvironment() *Environment { return NewEnvironment(map[string]string{}) } func NewEnvironment(vars map[string]string) *Environment { return &Environment{vars: vars, parent: nil, mutations: nil} } // NewKVEnvironment creates an environment from a list of key values func NewKVEnvironment(keyValues ...string) *Environment { if len(keyValues)%2 == 1 { panic("NewEnvironmentFromValues: odd argument count") } vars := make(map[string]string) for i := 0; i < len(keyValues); i += 2 { vars[keyValues[i]] = keyValues[i+1] } return NewEnvironment(vars) } func (e *Environment) AddLexicalScope(vars map[string]string) *Environment { if len(vars) == 0 { return e } return &Environment{vars: vars, parent: e, mutations: nil} } func (e *Environment) Len() int { return len(e.Values()) } func (e *Environment) Values() map[string]string { e.mutationsMu.Lock() defer e.mutationsMu.Unlock() values := map[string]string{} if e.parent != nil { maps.Copy(values, e.parent.Values()) } maps.Copy(values, e.vars) for _, mutation := range e.mutations { maps.Copy(values, mutation.Values()) } return values } func (e *Environment) ValueOf(key string) string { return e.Values()[key] } func (e *Environment) Mutate(env *Environment) { if env == nil || env.Len() == 0 { return } e.mutationsMu.Lock() defer e.mutationsMu.Unlock() e.mutations = append(e.mutations, env) } func acceptEnvName(name string, rejectIf []func(string) bool) bool { for _, rejectIfFn := range rejectIf { if rejectIfFn(name) { return false } } return true }