environ.go

  1// Copyright (c) 2018, Daniel MartΓ­ <mvdan@mvdan.cc>
  2// See LICENSE for licensing information
  3
  4package expand
  5
  6import (
  7	"cmp"
  8	"runtime"
  9	"slices"
 10	"strings"
 11)
 12
 13// Environ is the base interface for a shell's environment, allowing it to fetch
 14// variables by name and to iterate over all the currently set variables.
 15type Environ interface {
 16	// Get retrieves a variable by its name. To check if the variable is
 17	// set, use Variable.IsSet.
 18	Get(name string) Variable
 19
 20	// TODO(v4): make Each below a func that returns an iterator.
 21
 22	// Each iterates over all the currently set variables, calling the
 23	// supplied function on each variable. Iteration is stopped if the
 24	// function returns false.
 25	//
 26	// The names used in the calls aren't required to be unique or sorted.
 27	// If a variable name appears twice, the latest occurrence takes
 28	// priority.
 29	//
 30	// Each is required to forward exported variables when executing
 31	// programs.
 32	Each(func(name string, vr Variable) bool)
 33}
 34
 35// TODO(v4): [WriteEnviron.Set] below is overloaded to the point that correctly
 36// implementing both sides of the interface is tricky. In particular, some operations
 37// such as `export foo` or `readonly foo` alter the attributes but not the value,
 38// and `foo=bar` or `foo=[3]=baz` alter the value but not the attributes.
 39
 40// WriteEnviron is an extension on Environ that supports modifying and deleting
 41// variables.
 42type WriteEnviron interface {
 43	Environ
 44	// Set sets a variable by name. If !vr.IsSet(), the variable is being
 45	// unset; otherwise, the variable is being replaced.
 46	//
 47	// The given variable can have the kind [KeepValue] to replace an existing
 48	// variable's attributes without changing its value at all.
 49	// This is helpful to implement `readonly foo=bar; export foo`,
 50	// as the second declaration needs to clearly signal that the value is not modified.
 51	//
 52	// An error may be returned if the operation is invalid, such as if the
 53	// name is empty or if we're trying to overwrite a read-only variable.
 54	Set(name string, vr Variable) error
 55}
 56
 57//go:generate stringer -type=ValueKind
 58
 59// ValueKind describes which kind of value the variable holds.
 60// While most unset variables will have an [Unknown] kind, an unset variable may
 61// have a kind associated too, such as via `declare -a foo` resulting in [Indexed].
 62type ValueKind uint8
 63
 64const (
 65	// Unknown is used for unset variables which do not have a kind yet.
 66	Unknown ValueKind = iota
 67	// String describes plain string variables, such as `foo=bar`.
 68	String
 69	// NameRef describes variables which reference another by name, such as `declare -n foo=foo2`.
 70	NameRef
 71	// Indexed describes indexed array variables, such as `foo=(bar baz)`.
 72	Indexed
 73	// Associative describes associative array variables, such as `foo=([bar]=x [baz]=y)`.
 74	Associative
 75
 76	// KeepValue is used by [WriteEnviron.Set] to signal that we are changing attributes
 77	// about a variable, such as exporting it, without changing its value at all.
 78	KeepValue
 79
 80	// Deprecated: use [Unknown], as tracking whether or not a variable is set
 81	// is now done via [Variable.Set].
 82	Unset = Unknown
 83)
 84
 85// Variable describes a shell variable, which can have a number of attributes
 86// and a value.
 87type Variable struct {
 88	Set bool
 89
 90	Local    bool
 91	Exported bool
 92	ReadOnly bool
 93
 94	// Kind defines which of the value fields below should be used.
 95	Kind ValueKind
 96
 97	Str  string            // Used when Kind is String or NameRef.
 98	List []string          // Used when Kind is Indexed.
 99	Map  map[string]string // Used when Kind is Associative.
100}
101
102// IsSet reports whether the variable has been set to a value.
103// The zero value of a Variable is unset.
104func (v Variable) IsSet() bool {
105	return v.Set
106}
107
108// Declared reports whether the variable has been declared.
109// Declared variables may not be set; `export foo` is exported but not set to a value,
110// and `declare -a foo` is an indexed array but not set to a value.
111func (v Variable) Declared() bool {
112	return v.Set || v.Local || v.Exported || v.ReadOnly || v.Kind != Unknown
113}
114
115// String returns the variable's value as a string. In general, this only makes
116// sense if the variable has a string value or no value at all.
117func (v Variable) String() string {
118	switch v.Kind {
119	case String:
120		return v.Str
121	case Indexed:
122		if len(v.List) > 0 {
123			return v.List[0]
124		}
125	case Associative:
126		// nothing to do
127	}
128	return ""
129}
130
131// maxNameRefDepth defines the maximum number of times to follow references when
132// resolving a variable. Otherwise, simple name reference loops could crash a
133// program quite easily.
134const maxNameRefDepth = 100
135
136// Resolve follows a number of nameref variables, returning the last reference
137// name that was followed and the variable that it points to.
138func (v Variable) Resolve(env Environ) (string, Variable) {
139	name := ""
140	for range maxNameRefDepth {
141		if v.Kind != NameRef {
142			return name, v
143		}
144		name = v.Str // keep name for the next iteration
145		v = env.Get(name)
146	}
147	return name, Variable{}
148}
149
150// FuncEnviron wraps a function mapping variable names to their string values,
151// and implements [Environ]. Empty strings returned by the function will be
152// treated as unset variables. All variables will be exported.
153//
154// Note that the returned Environ's Each method will be a no-op.
155func FuncEnviron(fn func(string) string) Environ {
156	return funcEnviron(fn)
157}
158
159type funcEnviron func(string) string
160
161func (f funcEnviron) Get(name string) Variable {
162	value := f(name)
163	if value == "" {
164		return Variable{}
165	}
166	return Variable{Set: true, Exported: true, Kind: String, Str: value}
167}
168
169func (f funcEnviron) Each(func(name string, vr Variable) bool) {}
170
171// ListEnviron returns an [Environ] with the supplied variables, in the form
172// "key=value". All variables will be exported. The last value in pairs is used
173// if multiple values are present.
174//
175// On Windows, where environment variable names are case-insensitive, the
176// resulting variable names will all be uppercase.
177func ListEnviron(pairs ...string) Environ {
178	return listEnvironWithUpper(runtime.GOOS == "windows", pairs...)
179}
180
181// listEnvironWithUpper implements [ListEnviron], but letting the tests specify
182// whether to uppercase all names or not.
183func listEnvironWithUpper(upper bool, pairs ...string) Environ {
184	list := slices.Clone(pairs)
185	if upper {
186		// Uppercase before sorting, so that we can remove duplicates
187		// without the need for linear search nor a map.
188		for i, s := range list {
189			if sep := strings.IndexByte(s, '='); sep > 0 {
190				list[i] = strings.ToUpper(s[:sep]) + s[sep:]
191			}
192		}
193	}
194
195	slices.SortStableFunc(list, func(a, b string) int {
196		isep := strings.IndexByte(a, '=')
197		jsep := strings.IndexByte(b, '=')
198		if isep < 0 {
199			isep = 0
200		} else {
201			isep += 1
202		}
203		if jsep < 0 {
204			jsep = 0
205		} else {
206			jsep += 1
207		}
208		return strings.Compare(a[:isep], b[:jsep])
209	})
210
211	last := ""
212	for i := 0; i < len(list); {
213		s := list[i]
214		sep := strings.IndexByte(s, '=')
215		if sep <= 0 {
216			// invalid element; remove it
217			list = slices.Delete(list, i, i+1)
218			continue
219		}
220		name := s[:sep]
221		if last == name {
222			// duplicate; the last one wins
223			list = slices.Delete(list, i-1, i)
224			continue
225		}
226		last = name
227		i++
228	}
229	return listEnviron(list)
230}
231
232// listEnviron is a sorted list of "name=value" strings.
233type listEnviron []string
234
235func (l listEnviron) Get(name string) Variable {
236	eqpos := len(name)
237	endpos := len(name) + 1
238	i, ok := slices.BinarySearchFunc(l, name, func(l, name string) int {
239		if len(l) < endpos {
240			// Too short; see if we are before or after the name.
241			return strings.Compare(l, name)
242		}
243		// Compare the name prefix, then the equal character.
244		c := strings.Compare(l[:eqpos], name)
245		eq := l[eqpos]
246		if c == 0 {
247			return cmp.Compare(eq, '=')
248		}
249		return c
250	})
251	if ok {
252		return Variable{Set: true, Exported: true, Kind: String, Str: l[i][endpos:]}
253	}
254	return Variable{}
255}
256
257func (l listEnviron) Each(fn func(name string, vr Variable) bool) {
258	for _, pair := range l {
259		i := strings.IndexByte(pair, '=')
260		if i < 0 {
261			// should never happen; see listEnvironWithUpper
262			panic("expand.listEnviron: did not expect malformed name-value pair: " + pair)
263		}
264		name, value := pair[:i], pair[i+1:]
265		if !fn(name, Variable{Set: true, Exported: true, Kind: String, Str: value}) {
266			return
267		}
268	}
269}