param.go

  1// Copyright (c) 2017, Daniel MartΓ­ <mvdan@mvdan.cc>
  2// See LICENSE for licensing information
  3
  4package expand
  5
  6import (
  7	"fmt"
  8	"maps"
  9	"regexp"
 10	"slices"
 11	"strconv"
 12	"strings"
 13	"unicode"
 14	"unicode/utf8"
 15
 16	"mvdan.cc/sh/v3/pattern"
 17	"mvdan.cc/sh/v3/syntax"
 18)
 19
 20func nodeLit(node syntax.Node) string {
 21	if word, ok := node.(*syntax.Word); ok {
 22		return word.Lit()
 23	}
 24	return ""
 25}
 26
 27type UnsetParameterError struct {
 28	Node    *syntax.ParamExp
 29	Message string
 30}
 31
 32func (u UnsetParameterError) Error() string {
 33	return fmt.Sprintf("%s: %s", u.Node.Param.Value, u.Message)
 34}
 35
 36func overridingUnset(pe *syntax.ParamExp) bool {
 37	if pe.Exp == nil {
 38		return false
 39	}
 40	switch pe.Exp.Op {
 41	case syntax.AlternateUnset, syntax.AlternateUnsetOrNull,
 42		syntax.DefaultUnset, syntax.DefaultUnsetOrNull,
 43		syntax.ErrorUnset, syntax.ErrorUnsetOrNull,
 44		syntax.AssignUnset, syntax.AssignUnsetOrNull:
 45		return true
 46	}
 47	return false
 48}
 49
 50func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) {
 51	oldParam := cfg.curParam
 52	cfg.curParam = pe
 53	defer func() { cfg.curParam = oldParam }()
 54
 55	name := pe.Param.Value
 56	index := pe.Index
 57	switch name {
 58	case "@", "*":
 59		index = &syntax.Word{Parts: []syntax.WordPart{
 60			&syntax.Lit{Value: name},
 61		}}
 62	}
 63	var vr Variable
 64	switch name {
 65	case "LINENO":
 66		// This is the only parameter expansion that the environment
 67		// interface cannot satisfy.
 68		line := uint64(cfg.curParam.Pos().Line())
 69		vr = Variable{Set: true, Kind: String, Str: strconv.FormatUint(line, 10)}
 70	default:
 71		vr = cfg.Env.Get(name)
 72	}
 73	orig := vr
 74	_, vr = vr.Resolve(cfg.Env)
 75	if cfg.NoUnset && !vr.IsSet() && !overridingUnset(pe) {
 76		return "", UnsetParameterError{
 77			Node:    pe,
 78			Message: "unbound variable",
 79		}
 80	}
 81
 82	var sliceOffset, sliceLen int
 83	if pe.Slice != nil {
 84		var err error
 85		if pe.Slice.Offset != nil {
 86			sliceOffset, err = Arithm(cfg, pe.Slice.Offset)
 87			if err != nil {
 88				return "", err
 89			}
 90		}
 91		if pe.Slice.Length != nil {
 92			sliceLen, err = Arithm(cfg, pe.Slice.Length)
 93			if err != nil {
 94				return "", err
 95			}
 96		}
 97	}
 98
 99	var (
100		str   string
101		elems []string
102
103		indexAllElements bool // true if var has been accessed with * or @ index
104		callVarInd       = true
105	)
106
107	switch nodeLit(index) {
108	case "@", "*":
109		switch vr.Kind {
110		case Unknown:
111			elems = nil
112			indexAllElements = true
113		case Indexed:
114			indexAllElements = true
115			callVarInd = false
116			elems = vr.List
117			slicePos := func(n int) int {
118				if n < 0 {
119					n = len(elems) + n
120					if n < 0 {
121						n = len(elems)
122					}
123				} else if n > len(elems) {
124					n = len(elems)
125				}
126				return n
127			}
128			if pe.Slice != nil && pe.Slice.Offset != nil {
129				elems = elems[slicePos(sliceOffset):]
130			}
131			if pe.Slice != nil && pe.Slice.Length != nil {
132				elems = elems[:slicePos(sliceLen)]
133			}
134			str = strings.Join(elems, " ")
135		}
136	}
137	if callVarInd {
138		var err error
139		str, err = cfg.varInd(vr, index)
140		if err != nil {
141			return "", err
142		}
143	}
144	if !indexAllElements {
145		elems = []string{str}
146	}
147
148	switch {
149	case pe.Length:
150		n := len(elems)
151		switch nodeLit(index) {
152		case "@", "*":
153		default:
154			n = utf8.RuneCountInString(str)
155		}
156		str = strconv.Itoa(n)
157	case pe.Excl:
158		var strs []string
159		switch {
160		case pe.Names != 0:
161			strs = cfg.namesByPrefix(pe.Param.Value)
162		case orig.Kind == NameRef:
163			strs = append(strs, orig.Str)
164		case pe.Index != nil && vr.Kind == Indexed:
165			for i, e := range vr.List {
166				if e != "" {
167					strs = append(strs, strconv.Itoa(i))
168				}
169			}
170		case pe.Index != nil && vr.Kind == Associative:
171			strs = slices.AppendSeq(strs, maps.Keys(vr.Map))
172		case !vr.IsSet():
173			return "", fmt.Errorf("invalid indirect expansion")
174		case str == "":
175			return "", nil
176		default:
177			vr = cfg.Env.Get(str)
178			strs = append(strs, vr.String())
179		}
180		slices.Sort(strs)
181		str = strings.Join(strs, " ")
182	case pe.Slice != nil:
183		if callVarInd {
184			slicePos := func(n int) int {
185				if n < 0 {
186					n = len(str) + n
187					if n < 0 {
188						n = len(str)
189					}
190				} else if n > len(str) {
191					n = len(str)
192				}
193				return n
194			}
195			if pe.Slice.Offset != nil {
196				str = str[slicePos(sliceOffset):]
197			}
198			if pe.Slice.Length != nil {
199				str = str[:slicePos(sliceLen)]
200			}
201		} // else, elems are already sliced
202	case pe.Repl != nil:
203		orig, err := Pattern(cfg, pe.Repl.Orig)
204		if err != nil {
205			return "", err
206		}
207		if orig == "" {
208			break // nothing to replace
209		}
210		with, err := Literal(cfg, pe.Repl.With)
211		if err != nil {
212			return "", err
213		}
214		n := 1
215		if pe.Repl.All {
216			n = -1
217		}
218		locs := findAllIndex(orig, str, n)
219		sb := cfg.strBuilder()
220		last := 0
221		for _, loc := range locs {
222			sb.WriteString(str[last:loc[0]])
223			sb.WriteString(with)
224			last = loc[1]
225		}
226		sb.WriteString(str[last:])
227		str = sb.String()
228	case pe.Exp != nil:
229		arg, err := Literal(cfg, pe.Exp.Word)
230		if err != nil {
231			return "", err
232		}
233		switch op := pe.Exp.Op; op {
234		case syntax.AlternateUnsetOrNull:
235			if str == "" {
236				break
237			}
238			fallthrough
239		case syntax.AlternateUnset:
240			if vr.IsSet() {
241				str = arg
242			}
243		case syntax.DefaultUnset:
244			if vr.IsSet() {
245				break
246			}
247			fallthrough
248		case syntax.DefaultUnsetOrNull:
249			if str == "" {
250				str = arg
251			}
252		case syntax.ErrorUnset:
253			if vr.IsSet() {
254				break
255			}
256			fallthrough
257		case syntax.ErrorUnsetOrNull:
258			if str == "" {
259				return "", UnsetParameterError{
260					Node:    pe,
261					Message: arg,
262				}
263			}
264		case syntax.AssignUnset:
265			if vr.IsSet() {
266				break
267			}
268			fallthrough
269		case syntax.AssignUnsetOrNull:
270			if str == "" {
271				if err := cfg.envSet(name, arg); err != nil {
272					return "", err
273				}
274				str = arg
275			}
276		case syntax.RemSmallPrefix, syntax.RemLargePrefix,
277			syntax.RemSmallSuffix, syntax.RemLargeSuffix:
278			suffix := op == syntax.RemSmallSuffix || op == syntax.RemLargeSuffix
279			small := op == syntax.RemSmallPrefix || op == syntax.RemSmallSuffix
280			for i, elem := range elems {
281				elems[i] = removePattern(elem, arg, suffix, small)
282			}
283			str = strings.Join(elems, " ")
284		case syntax.UpperFirst, syntax.UpperAll,
285			syntax.LowerFirst, syntax.LowerAll:
286
287			caseFunc := unicode.ToLower
288			if op == syntax.UpperFirst || op == syntax.UpperAll {
289				caseFunc = unicode.ToUpper
290			}
291			all := op == syntax.UpperAll || op == syntax.LowerAll
292
293			// empty string means '?'; nothing to do there
294			expr, err := pattern.Regexp(arg, 0)
295			if err != nil {
296				return str, nil
297			}
298			rx := regexp.MustCompile(expr)
299
300			for i, elem := range elems {
301				rs := []rune(elem)
302				for ri, r := range rs {
303					if rx.MatchString(string(r)) {
304						rs[ri] = caseFunc(r)
305						if !all {
306							break
307						}
308					}
309				}
310				elems[i] = string(rs)
311			}
312			str = strings.Join(elems, " ")
313		case syntax.OtherParamOps:
314			switch arg {
315			case "Q":
316				str, err = syntax.Quote(str, syntax.LangBash)
317				if err != nil {
318					// Is this even possible? If a user runs into this panic,
319					// it's most likely a bug we need to fix.
320					panic(err)
321				}
322			case "E":
323				tail := str
324				var rns []rune
325				for tail != "" {
326					var rn rune
327					rn, _, tail, _ = strconv.UnquoteChar(tail, 0)
328					rns = append(rns, rn)
329				}
330				str = string(rns)
331			case "P", "A", "a":
332				panic(fmt.Sprintf("unhandled @%s param expansion", arg))
333			default:
334				panic(fmt.Sprintf("unexpected @%s param expansion", arg))
335			}
336		}
337	}
338	return str, nil
339}
340
341func removePattern(str, pat string, fromEnd, shortest bool) string {
342	var mode pattern.Mode
343	if shortest {
344		mode |= pattern.Shortest
345	}
346	expr, err := pattern.Regexp(pat, mode)
347	if err != nil {
348		return str
349	}
350	switch {
351	case fromEnd && shortest:
352		// use .* to get the right-most shortest match
353		expr = ".*(" + expr + ")$"
354	case fromEnd:
355		// simple suffix
356		expr = "(" + expr + ")$"
357	default:
358		// simple prefix
359		expr = "^(" + expr + ")"
360	}
361	// no need to check error as Translate returns one
362	rx := regexp.MustCompile(expr)
363	if loc := rx.FindStringSubmatchIndex(str); loc != nil {
364		// remove the original pattern (the submatch)
365		str = str[:loc[2]] + str[loc[3]:]
366	}
367	return str
368}
369
370func (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, error) {
371	if idx == nil {
372		return vr.String(), nil
373	}
374	switch vr.Kind {
375	case String:
376		n, err := Arithm(cfg, idx)
377		if err != nil {
378			return "", err
379		}
380		if n == 0 {
381			return vr.Str, nil
382		}
383	case Indexed:
384		switch nodeLit(idx) {
385		case "*", "@":
386			return strings.Join(vr.List, " "), nil
387		}
388		i, err := Arithm(cfg, idx)
389		if err != nil {
390			return "", err
391		}
392		if i < 0 {
393			return "", fmt.Errorf("negative array index")
394		}
395		if i < len(vr.List) {
396			return vr.List[i], nil
397		}
398	case Associative:
399		switch lit := nodeLit(idx); lit {
400		case "@", "*":
401			strs := slices.Sorted(maps.Values(vr.Map))
402			if lit == "*" {
403				return cfg.ifsJoin(strs), nil
404			}
405			return strings.Join(strs, " "), nil
406		}
407		val, err := Literal(cfg, idx.(*syntax.Word))
408		if err != nil {
409			return "", err
410		}
411		return vr.Map[val], nil
412	}
413	return "", nil
414}
415
416func (cfg *Config) namesByPrefix(prefix string) []string {
417	var names []string
418	for name := range cfg.Env.Each {
419		if strings.HasPrefix(name, prefix) {
420			names = append(names, name)
421		}
422	}
423	return names
424}