blob: 7f66509fc4bf706eaf312c7b1d0bb3c8f364594c [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
// Package environ is an environment variable manipulation library.
// It couples the system's environment, represented as a []string of KEY[=VALUE]
// strings, into a key/value map and back.
package environ
import (
// Env contains system environment variables. It preserves each environment
// variable verbatim (even if invalid).
// Internally env is represented by a map of environment key to its "KEY=VALUE"
// value. Note that the value here is the full "KEY=VALUE", not just the VALUE
// part. This allows us to reconstitute the original environment string slice
// without reallocating all of its composite strings.
type Env map[string]string
var ctxKey = "holds an Env"
// FromCtx returns a copy of the current Env in `ctx`.
// This is guaranteed to return a non-nil Env (i.e. it's always safe for
// assignment/manipulation)
// If no Env has been set with With, this returns `System()`.
func FromCtx(ctx context.Context) Env {
if cur, ok := ctx.Value(&ctxKey).(Env); ok {
if cur != nil {
return cur.Clone()
return Env{}
return System()
// SetInCtx installs a copy of the Env into the context, which can be retrieved
// with `FromCtx`.
func (e Env) SetInCtx(ctx context.Context) context.Context {
return context.WithValue(ctx, &ctxKey, e.Clone())
// System returns an Env instance instantiated with the current os.Environ
// values.
// The environment is automatically configured with the local system's case
// insensitivity.
func System() Env {
return New(os.Environ())
// New instantiates a new Env instance from the supplied set of environment
// KEY=VALUE strings using LoadSlice.
// The environment is automatically configured with the local system's case
// insensitivity.
func New(s []string) Env {
e := make(Env, len(s))
for _, entry := range s {
return e
// String returns debug representation of Env.
func (e Env) String() string { return fmt.Sprintf("%v", e.Sorted()) }
// Load adds environment variables defined in a key/value map to an existing
// environment.
func (e Env) Load(m map[string]string) {
for k, v := range m {
e.Set(k, v)
// Set sets the supplied environment key and value.
func (e Env) Set(k, v string) { e.SetEntry(Join(k, v)) }
// SetEntry sets the supplied environment to a "KEY[=VALUE]" entry.
func (e Env) SetEntry(entry string) {
if e == nil {
panic("cannot modify nil Env")
// "entry" must be a well-formed "key=value" entry. If it doesn't have an "=",
// we will forcibly add one to make it well-formed.
if strings.IndexRune(entry, '=') < 0 {
entry += "="
k, _ := Split(entry)
if len(k) > 0 {
e[normalizeKeyCase(k)] = entry
// Remove removes a value from the environment, returning true if the value was
// present. If the value was missing, Remove returns false.
// Remove is different from Set(k, "") in that Set persists the key with an
// empty value, while Remove removes it entirely.
func (e Env) Remove(k string) bool {
k = normalizeKeyCase(k)
if _, ok := e[k]; ok {
delete(e, k)
return true
return false
// RemoveMatch iterates over all keys and values in the environment, invoking
// the callback function, fn, for each key/value pair. If fn returns true, the
// key is removed from the environment.
func (e Env) RemoveMatch(fn func(k, v string) bool) {
e.iterate(func(realKey, k, v string) error {
if fn(k, v) {
delete(e, realKey)
return nil
// Update adds all key/value from other to the current environment. If there is
// a duplicate key, the value from other will overwrite the value from e.
// Values from other will be sorted and added in alphabetic order. This means
// that if e is case insensitive and there are multiple keys in other that
// converge on the same case insensitive key, the one that is alphabetically
// highest will be added.
func (e Env) Update(other Env) {
for _, entry := range other.Sorted() {
// Get returns the environment value for the supplied key.
// If the value is defined, ok will be true and v will be set to its value (note
// that this can be empty if the environment has an empty value). If the value
// is not defined, ok will be false.
func (e Env) Get(k string) (v string, ok bool) {
// NOTE: "v" is initially the combined "key=value" entry, and will need to be
// split.
if v, ok = e[normalizeKeyCase(k)]; ok {
_, v = splitEntryGivenKey(k, v)
// GetEmpty is the same as Get, except that instead of returning a separate
// boolean indicating the presence of a key, it will return an empty string if
// the key is missing.
func (e Env) GetEmpty(k string) string {
v, _ := e.Get(k)
return v
// Sorted returns the contents of the environment, sorted by key.
func (e Env) Sorted() []string {
var r []string
if len(e) > 0 {
r = make([]string, 0, len(e))
for _, v := range e {
r = append(r, v)
return r
// Map returns a map of the key/value values in the environment.
// This is a clone of the contents of e; manipulating this map will not change
// the values in e.
// If env is either nil or empty, returns nil.
func (e Env) Map() map[string]string {
if len(e) == 0 {
return nil
m := make(map[string]string, len(e))
for k, entry := range e {
ek, ev := splitEntryGivenKey(k, entry)
m[ek] = ev
return m
// Len returns the number of environment variables defined in e.
func (e Env) Len() int { return len(e) }
// Clone creates a new Env instance that is identical to, but independent from,
// e.
// If e is nil, returns nil. Otherwise returns a new (perhaps empty) Env.
func (e Env) Clone() Env {
if e == nil {
return nil
clone := make(Env, len(e))
for k, v := range e {
clone[k] = v
return clone
// Internal iteration function. Invokes cb for every entry in the environment.
// If the callback returns error, iteration stops and returns that error.
// It is safe to mutate the environment map during iteration.
// realKey is the real, normalized map key. envKey and envValue are the split
// map value.
func (e Env) iterate(cb func(realKey, envKey, envValue string) error) error {
for k, v := range e {
envKey, envValue := Split(v)
if err := cb(k, envKey, envValue); err != nil {
return err
return nil
// Iter iterates through all of the key/value pairs in Env and invokes the
// supplied callback, cb, for each element.
// If the callback returns error, iteration will stop immediately.
func (e Env) Iter(cb func(k, v string) error) error {
return e.iterate(func(_, k, v string) error { return cb(k, v) })
// Split splits the supplied environment variable value into a key/value pair.
// If v is of the form:
// - KEY, returns (KEY, "")
// - KEY=, returns (KEY, "")
// - KEY=VALUE, returns (KEY, VALUE)
func Split(v string) (key, value string) {
parts := strings.SplitN(v, "=", 2)
switch len(parts) {
case 2:
value = parts[1]
case 1:
key = parts[0]
// Join creates an environment variable definition for the supplied key/value.
func Join(k, v string) string {
if v == "" {
return k + "="
return k + "=" + v
// splitEntryGivenKey extracts the key and value components of the "env" map's
// combined "key=value" field, given the "key" component.
// This will work as long as the length of the supplied key equals the length of
// entry's key prefix, regardless of the key's contents which means that
// normalized keys can be used.
// It assumes that the environment variable is well-formed, one of:
// - KEY (returns "")
// - KEY= (returns "")
// - KEY=VALUE (returns "VALUE")
// If this is not the case, the result is undefined. However, this is an
// internal function and should never encounter a situation where the entry is
// not well-formed and prefixed by the supplied key.
func splitEntryGivenKey(key, entry string) (k, v string) {
prefixSize := len(key) + len("=")
switch {
case len(entry) < prefixSize:
return entry, ""
case len(entry) == prefixSize:
return entry[:len(key)], ""
return entry[:len(key)], entry[prefixSize:]
// normalizeKeyCase normalizes the map key by upper-casing it on Windows.
// Returns the key unchanged on other platforms. Mocked in tests.
var normalizeKeyCase func(string) string
func init() {
// Note: we don't use +build flags to be able to unit test both variants on
// a single platform just by mocking 'normalizeKeyCase' value.
if runtime.GOOS == "windows" {
normalizeKeyCase = strings.ToUpper
} else {
normalizeKeyCase = func(k string) string { return k }