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}