vars.go

  1// Copyright (c) 2017, Daniel MartΓ­ <mvdan@mvdan.cc>
  2// See LICENSE for licensing information
  3
  4package interp
  5
  6import (
  7	"fmt"
  8	"maps"
  9	"os"
 10	"runtime"
 11	"slices"
 12	"strconv"
 13	"strings"
 14
 15	"mvdan.cc/sh/v3/expand"
 16	"mvdan.cc/sh/v3/syntax"
 17)
 18
 19type overlayEnviron struct {
 20	parent expand.Environ
 21	values map[string]expand.Variable
 22
 23	// We need to know if the current scope is a function's scope, because
 24	// functions can modify global variables.
 25	funcScope bool
 26}
 27
 28func (o *overlayEnviron) Get(name string) expand.Variable {
 29	if vr, ok := o.values[name]; ok {
 30		return vr
 31	}
 32	return o.parent.Get(name)
 33}
 34
 35func (o *overlayEnviron) Set(name string, vr expand.Variable) error {
 36	// Manipulation of a global var inside a function.
 37	if o.funcScope && !vr.Local && !o.values[name].Local {
 38		// In a function, the parent environment is ours, so it's always read-write.
 39		return o.parent.(expand.WriteEnviron).Set(name, vr)
 40	}
 41
 42	if o.values == nil {
 43		o.values = make(map[string]expand.Variable)
 44	}
 45	prev := o.Get(name)
 46	if vr.Kind == expand.KeepValue {
 47		vr.Kind = prev.Kind
 48		vr.Str = prev.Str
 49		vr.List = prev.List
 50		vr.Map = prev.Map
 51	} else if prev.ReadOnly {
 52		return fmt.Errorf("readonly variable")
 53	}
 54	if !vr.IsSet() { // unsetting
 55		if prev.Local {
 56			vr.Local = true
 57			o.values[name] = vr
 58			return nil
 59		}
 60		delete(o.values, name)
 61	}
 62	// modifying the entire variable
 63	vr.Local = prev.Local || vr.Local
 64	o.values[name] = vr
 65	return nil
 66}
 67
 68func (o *overlayEnviron) Each(f func(name string, vr expand.Variable) bool) {
 69	o.parent.Each(f)
 70	for name, vr := range o.values {
 71		if !f(name, vr) {
 72			return
 73		}
 74	}
 75}
 76
 77func execEnv(env expand.Environ) []string {
 78	list := make([]string, 0, 64)
 79	for name, vr := range env.Each {
 80		if !vr.IsSet() {
 81			// If a variable is set globally but unset in the
 82			// runner, we need to ensure it's not part of the final
 83			// list. Seems like zeroing the element is enough.
 84			// This is a linear search, but this scenario should be
 85			// rare, and the number of variables shouldn't be large.
 86			for i, kv := range list {
 87				if strings.HasPrefix(kv, name+"=") {
 88					list[i] = ""
 89				}
 90			}
 91		}
 92		if vr.Exported && vr.Kind == expand.String {
 93			list = append(list, name+"="+vr.String())
 94		}
 95	}
 96	return list
 97}
 98
 99func (r *Runner) lookupVar(name string) expand.Variable {
100	if name == "" {
101		panic("variable name must not be empty")
102	}
103	var vr expand.Variable
104	switch name {
105	case "#":
106		vr.Kind, vr.Str = expand.String, strconv.Itoa(len(r.Params))
107	case "@", "*":
108		vr.Kind = expand.Indexed
109		if r.Params == nil {
110			// r.Params may be nil but positional parameters always exist
111			vr.List = []string{}
112		} else {
113			vr.List = r.Params
114		}
115	case "?":
116		vr.Kind, vr.Str = expand.String, strconv.Itoa(r.lastExit)
117	case "$":
118		vr.Kind, vr.Str = expand.String, strconv.Itoa(os.Getpid())
119	case "PPID":
120		vr.Kind, vr.Str = expand.String, strconv.Itoa(os.Getppid())
121	case "DIRSTACK":
122		vr.Kind, vr.List = expand.Indexed, r.dirStack
123	case "0":
124		vr.Kind = expand.String
125		if r.filename != "" {
126			vr.Str = r.filename
127		} else {
128			vr.Str = "gosh"
129		}
130	case "1", "2", "3", "4", "5", "6", "7", "8", "9":
131		vr.Kind = expand.String
132		i := int(name[0] - '1')
133		if i < len(r.Params) {
134			vr.Str = r.Params[i]
135		} else {
136			vr.Str = ""
137		}
138	}
139	if vr.Kind != expand.Unknown {
140		vr.Set = true
141		return vr
142	}
143	if vr := r.writeEnv.Get(name); vr.Declared() {
144		return vr
145	}
146	if runtime.GOOS == "windows" {
147		upper := strings.ToUpper(name)
148		if vr := r.writeEnv.Get(upper); vr.Declared() {
149			return vr
150		}
151	}
152	return expand.Variable{}
153}
154
155func (r *Runner) envGet(name string) string {
156	return r.lookupVar(name).String()
157}
158
159func (r *Runner) delVar(name string) {
160	if err := r.writeEnv.Set(name, expand.Variable{}); err != nil {
161		r.errf("%s: %v\n", name, err)
162		r.exit = 1
163		return
164	}
165}
166
167func (r *Runner) setVarString(name, value string) {
168	r.setVar(name, expand.Variable{Set: true, Kind: expand.String, Str: value})
169}
170
171func (r *Runner) setVar(name string, vr expand.Variable) {
172	if r.opts[optAllExport] {
173		vr.Exported = true
174	}
175	if err := r.writeEnv.Set(name, vr); err != nil {
176		r.errf("%s: %v\n", name, err)
177		r.exit = 1
178		return
179	}
180}
181
182func (r *Runner) setVarWithIndex(prev expand.Variable, name string, index syntax.ArithmExpr, vr expand.Variable) {
183	prev.Set = true
184	if name2, var2 := prev.Resolve(r.writeEnv); name2 != "" {
185		name = name2
186		prev = var2
187	}
188
189	if vr.Kind == expand.String && index == nil {
190		// When assigning a string to an array, fall back to the
191		// zero value for the index.
192		switch prev.Kind {
193		case expand.Indexed:
194			index = &syntax.Word{Parts: []syntax.WordPart{
195				&syntax.Lit{Value: "0"},
196			}}
197		case expand.Associative:
198			index = &syntax.Word{Parts: []syntax.WordPart{
199				&syntax.DblQuoted{},
200			}}
201		}
202	}
203	if index == nil {
204		r.setVar(name, vr)
205		return
206	}
207
208	// from the syntax package, we know that value must be a string if index
209	// is non-nil; nested arrays are forbidden.
210	valStr := vr.Str
211
212	var list []string
213	switch prev.Kind {
214	case expand.String:
215		list = append(list, prev.Str)
216	case expand.Indexed:
217		// TODO: only clone when inside a subshell and getting a var from outside for the first time
218		list = slices.Clone(prev.List)
219	case expand.Associative:
220		// if the existing variable is already an AssocArray, try our
221		// best to convert the key to a string
222		w, ok := index.(*syntax.Word)
223		if !ok {
224			return
225		}
226		k := r.literal(w)
227
228		// TODO: only clone when inside a subshell and getting a var from outside for the first time
229		prev.Map = maps.Clone(prev.Map)
230		if prev.Map == nil {
231			prev.Map = make(map[string]string)
232		}
233		prev.Map[k] = valStr
234		r.setVar(name, prev)
235		return
236	}
237	k := r.arithm(index)
238	for len(list) < k+1 {
239		list = append(list, "")
240	}
241	list[k] = valStr
242	prev.Kind = expand.Indexed
243	prev.List = list
244	r.setVar(name, prev)
245}
246
247func (r *Runner) setFunc(name string, body *syntax.Stmt) {
248	if r.Funcs == nil {
249		r.Funcs = make(map[string]*syntax.Stmt, 4)
250	}
251	r.Funcs[name] = body
252}
253
254func stringIndex(index syntax.ArithmExpr) bool {
255	w, ok := index.(*syntax.Word)
256	if !ok || len(w.Parts) != 1 {
257		return false
258	}
259	switch w.Parts[0].(type) {
260	case *syntax.DblQuoted, *syntax.SglQuoted:
261		return true
262	}
263	return false
264}
265
266// TODO: make assignVal and [setVar] consistent with the [expand.WriteEnviron] interface
267
268func (r *Runner) assignVal(prev expand.Variable, as *syntax.Assign, valType string) expand.Variable {
269	prev.Set = true
270	if as.Value != nil {
271		s := r.literal(as.Value)
272		if !as.Append {
273			prev.Kind = expand.String
274			if valType == "-n" {
275				prev.Kind = expand.NameRef
276			}
277			prev.Str = s
278			return prev
279		}
280		switch prev.Kind {
281		case expand.String, expand.Unknown:
282			prev.Kind = expand.String
283			prev.Str += s
284		case expand.Indexed:
285			if len(prev.List) == 0 {
286				prev.List = append(prev.List, "")
287			}
288			prev.List[0] += s
289		case expand.Associative:
290			// TODO
291		}
292		return prev
293	}
294	if as.Array == nil {
295		// don't return the zero value, as that's an unset variable
296		prev.Kind = expand.String
297		if valType == "-n" {
298			prev.Kind = expand.NameRef
299		}
300		prev.Str = ""
301		return prev
302	}
303	// Array assignment.
304	elems := as.Array.Elems
305	if valType == "" {
306		valType = "-a" // indexed
307		if len(elems) > 0 && stringIndex(elems[0].Index) {
308			valType = "-A" // associative
309		}
310	}
311	if valType == "-A" {
312		amap := make(map[string]string, len(elems))
313		for _, elem := range elems {
314			k := r.literal(elem.Index.(*syntax.Word))
315			amap[k] = r.literal(elem.Value)
316		}
317		if !as.Append {
318			prev.Kind = expand.Associative
319			prev.Map = amap
320			return prev
321		}
322		// TODO
323		return prev
324	}
325	// Evaluate values for each array element.
326	elemValues := make([]struct {
327		index  int
328		values []string
329	}, len(elems))
330	var index, maxIndex int
331	for i, elem := range elems {
332		if elem.Index != nil {
333			// Index resets our index with a literal value.
334			index = r.arithm(elem.Index)
335			elemValues[i].values = []string{r.literal(elem.Value)}
336		} else {
337			// Implicit index, advancing for every word.
338			elemValues[i].values = r.fields(elem.Value)
339		}
340		elemValues[i].index = index
341		index += len(elemValues[i].values)
342		maxIndex = max(maxIndex, index)
343	}
344	// Flatten down the values.
345	strs := make([]string, maxIndex)
346	for _, ev := range elemValues {
347		for i, str := range ev.values {
348			strs[ev.index+i] = str
349		}
350	}
351	if !as.Append {
352		prev.Kind = expand.Indexed
353		prev.List = strs
354		return prev
355	}
356	switch prev.Kind {
357	case expand.Unknown:
358		prev.Kind = expand.Indexed
359		prev.List = strs
360	case expand.String:
361		prev.Kind = expand.Indexed
362		prev.List = append([]string{prev.Str}, strs...)
363	case expand.Indexed:
364		prev.List = append(prev.List, strs...)
365	case expand.Associative:
366		// TODO
367	default:
368		panic(fmt.Sprintf("unhandled conversion of kind %d", prev.Kind))
369	}
370	return prev
371}