1// Copyright (c) 2017, Daniel MartΓ <mvdan@mvdan.cc>
2// See LICENSE for licensing information
3
4package expand
5
6import (
7 "cmp"
8 "errors"
9 "fmt"
10 "io"
11 "io/fs"
12 "iter"
13 "maps"
14 "os"
15 "os/user"
16 "path/filepath"
17 "regexp"
18 "runtime"
19 "slices"
20 "strconv"
21 "strings"
22
23 "mvdan.cc/sh/v3/pattern"
24 "mvdan.cc/sh/v3/syntax"
25)
26
27// A Config specifies details about how shell expansion should be performed. The
28// zero value is a valid configuration.
29type Config struct {
30 // Env is used to get and set environment variables when performing
31 // shell expansions. Some special parameters are also expanded via this
32 // interface, such as:
33 //
34 // * "#", "@", "*", "0"-"9" for the shell's parameters
35 // * "?", "$", "PPID" for the shell's status and process
36 // * "HOME foo" to retrieve user foo's home directory (if unset,
37 // os/user.Lookup will be used)
38 //
39 // If nil, there are no environment variables set. Use
40 // ListEnviron(os.Environ()...) to use the system's environment
41 // variables.
42 Env Environ
43
44 // CmdSubst expands a command substitution node, writing its standard
45 // output to the provided [io.Writer].
46 //
47 // If nil, encountering a command substitution will result in an
48 // UnexpectedCommandError.
49 CmdSubst func(io.Writer, *syntax.CmdSubst) error
50
51 // ProcSubst expands a process substitution node.
52 //
53 // Note that this feature is a work in progress, and the signature of
54 // this field might change until #451 is completely fixed.
55 ProcSubst func(*syntax.ProcSubst) (string, error)
56
57 // TODO(v4): replace ReadDir with ReadDir2.
58
59 // ReadDir is the older form of [ReadDir2], before io/fs.
60 //
61 // Deprecated: use ReadDir2 instead.
62 ReadDir func(string) ([]fs.FileInfo, error)
63
64 // ReadDir2 is used for file path globbing.
65 // If nil, and [ReadDir] is nil as well, globbing is disabled.
66 // Use [os.ReadDir] to use the filesystem directly.
67 ReadDir2 func(string) ([]fs.DirEntry, error)
68
69 // GlobStar corresponds to the shell option that allows globbing with
70 // "**".
71 GlobStar bool
72
73 // NoCaseGlob corresponds to the shell option that causes case-insensitive
74 // pattern matching in pathname expansion.
75 NoCaseGlob bool
76
77 // NullGlob corresponds to the shell option that allows globbing
78 // patterns which match nothing to result in zero fields.
79 NullGlob bool
80
81 // NoUnset corresponds to the shell option that treats unset variables
82 // as errors.
83 NoUnset bool
84
85 bufferAlloc strings.Builder
86 fieldAlloc [4]fieldPart
87 fieldsAlloc [4][]fieldPart
88
89 ifs string
90 // A pointer to a parameter expansion node, if we're inside one.
91 // Necessary for ${LINENO}.
92 curParam *syntax.ParamExp
93}
94
95// UnexpectedCommandError is returned if a command substitution is encountered
96// when [Config.CmdSubst] is nil.
97type UnexpectedCommandError struct {
98 Node *syntax.CmdSubst
99}
100
101func (u UnexpectedCommandError) Error() string {
102 return fmt.Sprintf("unexpected command substitution at %s", u.Node.Pos())
103}
104
105var zeroConfig = &Config{}
106
107// TODO: note that prepareConfig is modifying the user's config in place,
108// which doesn't feel right - we should make a copy.
109
110func prepareConfig(cfg *Config) *Config {
111 cfg = cmp.Or(cfg, zeroConfig)
112 cfg.Env = cmp.Or(cfg.Env, FuncEnviron(func(string) string { return "" }))
113
114 cfg.ifs = " \t\n"
115 if vr := cfg.Env.Get("IFS"); vr.IsSet() {
116 cfg.ifs = vr.String()
117 }
118
119 if cfg.ReadDir != nil && cfg.ReadDir2 == nil {
120 cfg.ReadDir2 = func(path string) ([]fs.DirEntry, error) {
121 infos, err := cfg.ReadDir(path)
122 if err != nil {
123 return nil, err
124 }
125 entries := make([]fs.DirEntry, len(infos))
126 for i, info := range infos {
127 entries[i] = fs.FileInfoToDirEntry(info)
128 }
129 return entries, nil
130 }
131 }
132 return cfg
133}
134
135func (cfg *Config) ifsRune(r rune) bool {
136 for _, r2 := range cfg.ifs {
137 if r == r2 {
138 return true
139 }
140 }
141 return false
142}
143
144func (cfg *Config) ifsJoin(strs []string) string {
145 sep := ""
146 if cfg.ifs != "" {
147 sep = cfg.ifs[:1]
148 }
149 return strings.Join(strs, sep)
150}
151
152func (cfg *Config) strBuilder() *strings.Builder {
153 b := &cfg.bufferAlloc
154 b.Reset()
155 return b
156}
157
158func (cfg *Config) envGet(name string) string {
159 return cfg.Env.Get(name).String()
160}
161
162func (cfg *Config) envSet(name, value string) error {
163 wenv, ok := cfg.Env.(WriteEnviron)
164 if !ok {
165 return fmt.Errorf("environment is read-only")
166 }
167 return wenv.Set(name, Variable{Set: true, Kind: String, Str: value})
168}
169
170// Literal expands a single shell word. It is similar to [Fields], but the result
171// is a single string. This is the behavior when a word is used as the value in
172// a shell variable assignment, for example.
173//
174// The config specifies shell expansion options; nil behaves the same as an
175// empty config.
176func Literal(cfg *Config, word *syntax.Word) (string, error) {
177 if word == nil {
178 return "", nil
179 }
180 cfg = prepareConfig(cfg)
181 field, err := cfg.wordField(word.Parts, quoteNone)
182 if err != nil {
183 return "", err
184 }
185 return cfg.fieldJoin(field), nil
186}
187
188// Document expands a single shell word as if it were a here-document body.
189// It is similar to [Literal], but without brace expansion, tilde expansion, and
190// globbing.
191//
192// The config specifies shell expansion options; nil behaves the same as an
193// empty config.
194func Document(cfg *Config, word *syntax.Word) (string, error) {
195 if word == nil {
196 return "", nil
197 }
198 cfg = prepareConfig(cfg)
199 field, err := cfg.wordField(word.Parts, quoteSingle)
200 if err != nil {
201 return "", err
202 }
203 return cfg.fieldJoin(field), nil
204}
205
206const patMode = pattern.Filenames | pattern.Braces
207
208// Pattern expands a single shell word as a pattern, using [pattern.QuoteMeta]
209// on any non-quoted parts of the input word. The result can be used on
210// [pattern.Regexp] directly.
211//
212// The config specifies shell expansion options; nil behaves the same as an
213// empty config.
214func Pattern(cfg *Config, word *syntax.Word) (string, error) {
215 if word == nil {
216 return "", nil
217 }
218 cfg = prepareConfig(cfg)
219 field, err := cfg.wordField(word.Parts, quoteNone)
220 if err != nil {
221 return "", err
222 }
223 sb := cfg.strBuilder()
224 for _, part := range field {
225 if part.quote > quoteNone {
226 sb.WriteString(pattern.QuoteMeta(part.val, patMode))
227 } else {
228 sb.WriteString(part.val)
229 }
230 }
231 return sb.String(), nil
232}
233
234// Format expands a format string with a number of arguments, following the
235// shell's format specifications. These include printf(1), among others.
236//
237// The resulting string is returned, along with the number of arguments used.
238//
239// The config specifies shell expansion options; nil behaves the same as an
240// empty config.
241func Format(cfg *Config, format string, args []string) (string, int, error) {
242 cfg = prepareConfig(cfg)
243 sb := cfg.strBuilder()
244
245 consumed, err := formatInto(sb, format, args)
246 if err != nil {
247 return "", 0, err
248 }
249
250 return sb.String(), consumed, err
251}
252
253func formatInto(sb *strings.Builder, format string, args []string) (int, error) {
254 var fmts []byte
255 initialArgs := len(args)
256
257formatLoop:
258 for i := 0; i < len(format); i++ {
259 // readDigits reads from 0 to max digits, either octal or
260 // hexadecimal.
261 readDigits := func(max int, hex bool) string {
262 j := 0
263 for ; j < max; j++ {
264 c := format[i+j]
265 if (c >= '0' && c <= '9') ||
266 (hex && c >= 'a' && c <= 'f') ||
267 (hex && c >= 'A' && c <= 'F') {
268 // valid octal or hex char
269 } else {
270 break
271 }
272 }
273 digits := format[i : i+j]
274 i += j - 1 // -1 since the outer loop does i++
275 return digits
276 }
277 c := format[i]
278 switch {
279 case c == '\\': // escaped
280 i++
281 switch c = format[i]; c {
282 case 'a': // bell
283 sb.WriteByte('\a')
284 case 'b': // backspace
285 sb.WriteByte('\b')
286 case 'e', 'E': // escape
287 sb.WriteByte('\x1b')
288 case 'f': // form feed
289 sb.WriteByte('\f')
290 case 'n': // new line
291 sb.WriteByte('\n')
292 case 'r': // carriage return
293 sb.WriteByte('\r')
294 case 't': // horizontal tab
295 sb.WriteByte('\t')
296 case 'v': // vertical tab
297 sb.WriteByte('\v')
298 case '\\', '\'', '"', '?': // just the character
299 sb.WriteByte(c)
300 case '0', '1', '2', '3', '4', '5', '6', '7':
301 digits := readDigits(3, false)
302 // if digits don't fit in 8 bits, 0xff via strconv
303 n, _ := strconv.ParseUint(digits, 8, 8)
304 sb.WriteByte(byte(n))
305 case 'x', 'u', 'U':
306 i++
307 max := 2
308 switch c {
309 case 'u':
310 max = 4
311 case 'U':
312 max = 8
313 }
314 digits := readDigits(max, true)
315 if len(digits) > 0 {
316 // can't error
317 n, _ := strconv.ParseUint(digits, 16, 32)
318 if n == 0 {
319 // If we're about to print \x00,
320 // stop the entire loop, like bash.
321 break formatLoop
322 }
323 if c == 'x' {
324 // always as a single byte
325 sb.WriteByte(byte(n))
326 } else {
327 sb.WriteRune(rune(n))
328 }
329 break
330 }
331 fallthrough
332 default: // no escape sequence
333 sb.WriteByte('\\')
334 sb.WriteByte(c)
335 }
336 case len(fmts) > 0:
337 switch c {
338 case '%':
339 sb.WriteByte('%')
340 fmts = nil
341 case 'c':
342 var b byte
343 if len(args) > 0 {
344 arg := ""
345 arg, args = args[0], args[1:]
346 if len(arg) > 0 {
347 b = arg[0]
348 }
349 }
350 sb.WriteByte(b)
351 fmts = nil
352 case '+', '-', ' ':
353 if len(fmts) > 1 {
354 return 0, fmt.Errorf("invalid format char: %c", c)
355 }
356 fmts = append(fmts, c)
357 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
358 fmts = append(fmts, c)
359 case 's', 'b', 'd', 'i', 'u', 'o', 'x':
360 arg := ""
361 if len(args) > 0 {
362 arg, args = args[0], args[1:]
363 }
364 var farg any
365 if c == 'b' {
366 // Passing in nil for args ensures that % format
367 // strings aren't processed; only escape sequences
368 // will be handled.
369 _, err := formatInto(sb, arg, nil)
370 if err != nil {
371 return 0, err
372 }
373 } else if c != 's' {
374 n, _ := strconv.ParseInt(arg, 0, 0)
375 if c == 'i' || c == 'd' {
376 farg = int(n)
377 } else {
378 farg = uint(n)
379 }
380 if c == 'i' || c == 'u' {
381 c = 'd'
382 }
383 } else {
384 farg = arg
385 }
386 if farg != nil {
387 fmts = append(fmts, c)
388 fmt.Fprintf(sb, string(fmts), farg)
389 }
390 fmts = nil
391 default:
392 return 0, fmt.Errorf("invalid format char: %c", c)
393 }
394 case args != nil && c == '%':
395 // if args == nil, we are not doing format
396 // arguments
397 fmts = []byte{c}
398 default:
399 sb.WriteByte(c)
400 }
401 }
402 if len(fmts) > 0 {
403 return 0, fmt.Errorf("missing format char")
404 }
405 return initialArgs - len(args), nil
406}
407
408func (cfg *Config) fieldJoin(parts []fieldPart) string {
409 switch len(parts) {
410 case 0:
411 return ""
412 case 1: // short-cut without a string copy
413 return parts[0].val
414 }
415 sb := cfg.strBuilder()
416 for _, part := range parts {
417 sb.WriteString(part.val)
418 }
419 return sb.String()
420}
421
422func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob bool) {
423 sb := cfg.strBuilder()
424 for _, part := range parts {
425 if part.quote > quoteNone {
426 sb.WriteString(pattern.QuoteMeta(part.val, patMode))
427 continue
428 }
429 sb.WriteString(part.val)
430 if pattern.HasMeta(part.val, patMode) {
431 glob = true
432 }
433 }
434 if glob { // only copy the string if it will be used
435 escaped = sb.String()
436 }
437 return escaped, glob
438}
439
440// Fields is a pre-iterators API which now wraps [FieldsSeq].
441func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
442 var fields []string
443 for s, err := range FieldsSeq(cfg, words...) {
444 if err != nil {
445 return nil, err
446 }
447 fields = append(fields, s)
448 }
449 return fields, nil
450}
451
452// Fields expands a number of words as if they were arguments in a shell
453// command. This includes brace expansion, tilde expansion, parameter expansion,
454// command substitution, arithmetic expansion, and quote removal.
455func FieldsSeq(cfg *Config, words ...*syntax.Word) iter.Seq2[string, error] {
456 cfg = prepareConfig(cfg)
457 dir := cfg.envGet("PWD")
458 return func(yield func(string, error) bool) {
459 for _, word := range words {
460 word := *word // make a copy, since SplitBraces replaces the Parts slice
461 afterBraces := []*syntax.Word{&word}
462 if syntax.SplitBraces(&word) {
463 afterBraces = Braces(&word)
464 }
465 for _, word2 := range afterBraces {
466 wfields, err := cfg.wordFields(word2.Parts)
467 if err != nil {
468 yield("", err)
469 return
470 }
471 for _, field := range wfields {
472 path, doGlob := cfg.escapedGlobField(field)
473 if doGlob && cfg.ReadDir2 != nil {
474 // Note that globbing requires keeping a slice state, so it doesn't
475 // really benefit from using an iterator.
476 matches, err := cfg.glob(dir, path)
477 if err != nil {
478 // We avoid [errors.As] as it allocates,
479 // and we know that [Config.glob] returns [pattern.Regexp] errors without wrapping.
480 if _, ok := err.(*pattern.SyntaxError); !ok {
481 yield("", err)
482 return
483 }
484 } else if len(matches) > 0 || cfg.NullGlob {
485 for _, m := range matches {
486 yield(m, nil)
487 }
488 continue
489 }
490 }
491 yield(cfg.fieldJoin(field), nil)
492 }
493 }
494 }
495 }
496}
497
498type fieldPart struct {
499 val string
500 quote quoteLevel
501}
502
503type quoteLevel uint
504
505const (
506 quoteNone quoteLevel = iota
507 quoteDouble
508 quoteSingle
509)
510
511func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) {
512 var field []fieldPart
513 for i, wp := range wps {
514 switch wp := wp.(type) {
515 case *syntax.Lit:
516 s := wp.Value
517 if i == 0 && ql == quoteNone {
518 if prefix, rest := cfg.expandUser(s); prefix != "" {
519 // TODO: return two separate fieldParts,
520 // like in wordFields?
521 s = prefix + rest
522 }
523 }
524 if ql == quoteDouble && strings.Contains(s, "\\") {
525 sb := cfg.strBuilder()
526 for i := 0; i < len(s); i++ {
527 b := s[i]
528 if b == '\\' && i+1 < len(s) {
529 switch s[i+1] {
530 case '"', '\\', '$', '`': // special chars
531 i++
532 b = s[i] // write the special char, skipping the backslash
533 }
534 }
535 sb.WriteByte(b)
536 }
537 s = sb.String()
538 }
539 if i := strings.IndexByte(s, '\x00'); i >= 0 {
540 s = s[:i]
541 }
542 field = append(field, fieldPart{val: s})
543 case *syntax.SglQuoted:
544 fp := fieldPart{quote: quoteSingle, val: wp.Value}
545 if wp.Dollar {
546 fp.val, _, _ = Format(cfg, fp.val, nil)
547 }
548 field = append(field, fp)
549 case *syntax.DblQuoted:
550 wfield, err := cfg.wordField(wp.Parts, quoteDouble)
551 if err != nil {
552 return nil, err
553 }
554 for _, part := range wfield {
555 part.quote = quoteDouble
556 field = append(field, part)
557 }
558 case *syntax.ParamExp:
559 val, err := cfg.paramExp(wp)
560 if err != nil {
561 return nil, err
562 }
563 field = append(field, fieldPart{val: val})
564 case *syntax.CmdSubst:
565 val, err := cfg.cmdSubst(wp)
566 if err != nil {
567 return nil, err
568 }
569 field = append(field, fieldPart{val: val})
570 case *syntax.ArithmExp:
571 n, err := Arithm(cfg, wp.X)
572 if err != nil {
573 return nil, err
574 }
575 field = append(field, fieldPart{val: strconv.Itoa(n)})
576 case *syntax.ProcSubst:
577 path, err := cfg.ProcSubst(wp)
578 if err != nil {
579 return nil, err
580 }
581 field = append(field, fieldPart{val: path})
582 default:
583 panic(fmt.Sprintf("unhandled word part: %T", wp))
584 }
585 }
586 return field, nil
587}
588
589func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) {
590 if cfg.CmdSubst == nil {
591 return "", UnexpectedCommandError{Node: cs}
592 }
593 sb := cfg.strBuilder()
594 if err := cfg.CmdSubst(sb, cs); err != nil {
595 return "", err
596 }
597 out := sb.String()
598 if strings.IndexByte(out, '\x00') >= 0 {
599 out = strings.ReplaceAll(out, "\x00", "")
600 }
601 return strings.TrimRight(out, "\n"), nil
602}
603
604func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
605 fields := cfg.fieldsAlloc[:0]
606 curField := cfg.fieldAlloc[:0]
607 allowEmpty := false
608 flush := func() {
609 if len(curField) == 0 {
610 return
611 }
612 fields = append(fields, curField)
613 curField = nil
614 }
615 splitAdd := func(val string) {
616 fieldStart := -1
617 for i, r := range val {
618 if cfg.ifsRune(r) {
619 if fieldStart >= 0 { // ending a field
620 curField = append(curField, fieldPart{val: val[fieldStart:i]})
621 fieldStart = -1
622 }
623 flush()
624 } else {
625 if fieldStart < 0 { // starting a new field
626 fieldStart = i
627 }
628 }
629 }
630 if fieldStart >= 0 { // ending a field without IFS
631 curField = append(curField, fieldPart{val: val[fieldStart:]})
632 }
633 }
634 for i, wp := range wps {
635 switch wp := wp.(type) {
636 case *syntax.Lit:
637 s := wp.Value
638 if i == 0 {
639 prefix, rest := cfg.expandUser(s)
640 curField = append(curField, fieldPart{
641 quote: quoteSingle,
642 val: prefix,
643 })
644 s = rest
645 }
646 if strings.Contains(s, "\\") {
647 sb := cfg.strBuilder()
648 for i := 0; i < len(s); i++ {
649 b := s[i]
650 if b == '\\' {
651 if i++; i >= len(s) {
652 break
653 }
654 b = s[i]
655 }
656 sb.WriteByte(b)
657 }
658 s = sb.String()
659 }
660 curField = append(curField, fieldPart{val: s})
661 case *syntax.SglQuoted:
662 allowEmpty = true
663 fp := fieldPart{quote: quoteSingle, val: wp.Value}
664 if wp.Dollar {
665 fp.val, _, _ = Format(cfg, fp.val, nil)
666 }
667 curField = append(curField, fp)
668 case *syntax.DblQuoted:
669 if len(wp.Parts) == 1 {
670 pe, _ := wp.Parts[0].(*syntax.ParamExp)
671 if elems := cfg.quotedElemFields(pe); elems != nil {
672 for i, elem := range elems {
673 if i > 0 {
674 flush()
675 }
676 curField = append(curField, fieldPart{
677 quote: quoteDouble,
678 val: elem,
679 })
680 }
681 continue
682 }
683 }
684 allowEmpty = true
685 wfield, err := cfg.wordField(wp.Parts, quoteDouble)
686 if err != nil {
687 return nil, err
688 }
689 for _, part := range wfield {
690 part.quote = quoteDouble
691 curField = append(curField, part)
692 }
693 case *syntax.ParamExp:
694 val, err := cfg.paramExp(wp)
695 if err != nil {
696 return nil, err
697 }
698 splitAdd(val)
699 case *syntax.CmdSubst:
700 val, err := cfg.cmdSubst(wp)
701 if err != nil {
702 return nil, err
703 }
704 splitAdd(val)
705 case *syntax.ArithmExp:
706 n, err := Arithm(cfg, wp.X)
707 if err != nil {
708 return nil, err
709 }
710 curField = append(curField, fieldPart{val: strconv.Itoa(n)})
711 case *syntax.ProcSubst:
712 path, err := cfg.ProcSubst(wp)
713 if err != nil {
714 return nil, err
715 }
716 splitAdd(path)
717 case *syntax.ExtGlob:
718 return nil, fmt.Errorf("extended globbing is not supported")
719 default:
720 panic(fmt.Sprintf("unhandled word part: %T", wp))
721 }
722 }
723 flush()
724 if allowEmpty && len(fields) == 0 {
725 fields = append(fields, curField)
726 }
727 return fields, nil
728}
729
730// quotedElemFields returns the list of elements resulting from a quoted
731// parameter expansion that should be treated especially, like "${foo[@]}".
732func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string {
733 if pe == nil || pe.Length || pe.Width {
734 return nil
735 }
736 name := pe.Param.Value
737 if pe.Excl {
738 switch pe.Names {
739 case syntax.NamesPrefixWords: // "${!prefix@}"
740 return cfg.namesByPrefix(pe.Param.Value)
741 case syntax.NamesPrefix: // "${!prefix*}"
742 return nil
743 }
744 switch nodeLit(pe.Index) {
745 case "@": // "${!name[@]}"
746 switch vr := cfg.Env.Get(name); vr.Kind {
747 case Indexed:
748 // TODO: if an indexed array only has elements 0 and 10,
749 // we should not return all indices in between those.
750 keys := make([]string, 0, len(vr.List))
751 for key := range vr.List {
752 keys = append(keys, strconv.Itoa(key))
753 }
754 return keys
755 case Associative:
756 return slices.Collect(maps.Keys(vr.Map))
757 }
758 }
759 return nil
760 }
761 switch name {
762 case "*": // "${*}"
763 return []string{cfg.ifsJoin(cfg.Env.Get(name).List)}
764 case "@": // "${@}"
765 return cfg.Env.Get(name).List
766 }
767 switch nodeLit(pe.Index) {
768 case "@": // "${name[@]}"
769 switch vr := cfg.Env.Get(name); vr.Kind {
770 case Indexed:
771 return vr.List
772 case Associative:
773 return slices.Collect(maps.Values(vr.Map))
774 }
775 case "*": // "${name[*]}"
776 if vr := cfg.Env.Get(name); vr.Kind == Indexed {
777 return []string{cfg.ifsJoin(vr.List)}
778 }
779 }
780 return nil
781}
782
783func (cfg *Config) expandUser(field string) (prefix, rest string) {
784 if len(field) == 0 || field[0] != '~' {
785 return "", field
786 }
787 name := field[1:]
788 if i := strings.Index(name, "/"); i >= 0 {
789 rest = name[i:]
790 name = name[:i]
791 }
792 if name == "" {
793 // Current user; try via "HOME", otherwise fall back to the
794 // system's appropriate home dir env var. Don't use os/user, as
795 // that's overkill. We can't use [os.UserHomeDir], because we want
796 // to use cfg.Env, and we always want to check "HOME" first.
797
798 if vr := cfg.Env.Get("HOME"); vr.IsSet() {
799 return vr.String(), rest
800 }
801
802 if runtime.GOOS == "windows" {
803 if vr := cfg.Env.Get("USERPROFILE"); vr.IsSet() {
804 return vr.String(), rest
805 }
806 }
807 return "", field
808 }
809
810 // Not the current user; try via "HOME <name>", otherwise fall back to
811 // os/user. There isn't a way to lookup user home dirs without cgo.
812
813 if vr := cfg.Env.Get("HOME " + name); vr.IsSet() {
814 return vr.String(), rest
815 }
816
817 u, err := user.Lookup(name)
818 if err != nil {
819 return "", field
820 }
821 return u.HomeDir, rest
822}
823
824func findAllIndex(pat, name string, n int) [][]int {
825 expr, err := pattern.Regexp(pat, 0)
826 if err != nil {
827 return nil
828 }
829 rx := regexp.MustCompile(expr)
830 return rx.FindAllStringIndex(name, n)
831}
832
833var rxGlobStar = regexp.MustCompile(".*")
834
835// pathJoin2 is a simpler version of [filepath.Join] without cleaning the result,
836// since that's needed for globbing.
837func pathJoin2(elem1, elem2 string) string {
838 if elem1 == "" {
839 return elem2
840 }
841 if strings.HasSuffix(elem1, string(filepath.Separator)) {
842 return elem1 + elem2
843 }
844 return elem1 + string(filepath.Separator) + elem2
845}
846
847// pathSplit splits a file path into its elements, retaining empty ones. Before
848// splitting, slashes are replaced with [filepath.Separator], so that splitting
849// Unix paths on Windows works as well.
850func pathSplit(path string) []string {
851 path = filepath.FromSlash(path)
852 return strings.Split(path, string(filepath.Separator))
853}
854
855func (cfg *Config) glob(base, pat string) ([]string, error) {
856 parts := pathSplit(pat)
857 matches := []string{""}
858 if filepath.IsAbs(pat) {
859 if parts[0] == "" {
860 // unix-like
861 matches[0] = string(filepath.Separator)
862 } else {
863 // windows (for some reason it won't work without the
864 // trailing separator)
865 matches[0] = parts[0] + string(filepath.Separator)
866 }
867 parts = parts[1:]
868 }
869 // TODO: as an optimization, we could do chunks of the path all at once,
870 // like doing a single stat for "/foo/bar" in "/foo/bar/*".
871
872 // TODO: Another optimization would be to reduce the number of ReadDir2 calls.
873 // For example, /foo/* can end up doing one duplicate call:
874 //
875 // ReadDir2("/foo") to ensure that "/foo/" exists and only matches a directory
876 // ReadDir2("/foo") glob "*"
877
878 for i, part := range parts {
879 // Keep around for debugging.
880 // log.Printf("matches %q part %d %q", matches, i, part)
881
882 wantDir := i < len(parts)-1
883 switch {
884 case part == "", part == ".", part == "..":
885 for i, dir := range matches {
886 matches[i] = pathJoin2(dir, part)
887 }
888 continue
889 case !pattern.HasMeta(part, patMode):
890 var newMatches []string
891 for _, dir := range matches {
892 match := dir
893 if !filepath.IsAbs(match) {
894 match = filepath.Join(base, match)
895 }
896 match = pathJoin2(match, part)
897 // We can't use [Config.ReadDir2] on the parent and match the directory
898 // entry by name, because short paths on Windows break that.
899 // Our only option is to [Config.ReadDir2] on the directory entry itself,
900 // which can be wasteful if we only want to see if it exists,
901 // but at least it's correct in all scenarios.
902 if _, err := cfg.ReadDir2(match); err != nil {
903 if isWindowsErrPathNotFound(err) {
904 // Unfortunately, [os.File.Readdir] on a regular file on
905 // Windows returns an error that satisfies [fs.ErrNotExist].
906 // Luckily, it returns a special "path not found" rather
907 // than the normal "file not found" for missing files,
908 // so we can use that knowledge to work around the bug.
909 // See https://github.com/golang/go/issues/46734.
910 // TODO: remove when the Go issue above is resolved.
911 } else if errors.Is(err, fs.ErrNotExist) {
912 continue // simply doesn't exist
913 }
914 if wantDir {
915 continue // exists but not a directory
916 }
917 }
918 newMatches = append(newMatches, pathJoin2(dir, part))
919 }
920 matches = newMatches
921 continue
922 case part == "**" && cfg.GlobStar:
923 // Find all recursive matches for "**".
924 // Note that we need the results to be in depth-first order,
925 // and to avoid recursion, we use a slice as a stack.
926 // Since we pop from the back, we populate the stack backwards.
927 stack := make([]string, 0, len(matches))
928 for _, match := range slices.Backward(matches) {
929 // "a/**" should match "a/ a/b a/b/cfg ...";
930 // note how the zero-match case has a trailing separator.
931 stack = append(stack, pathJoin2(match, ""))
932 }
933 matches = matches[:0]
934 var newMatches []string // to reuse its capacity
935 for len(stack) > 0 {
936 dir := stack[len(stack)-1]
937 stack = stack[:len(stack)-1]
938
939 // Don't include the original "" match as it's not a valid path.
940 if dir != "" {
941 matches = append(matches, dir)
942 }
943
944 // If dir is not a directory, we keep the stack as-is and continue.
945 newMatches = newMatches[:0]
946 newMatches, _ = cfg.globDir(base, dir, rxGlobStar, false, wantDir, newMatches)
947 for _, match := range slices.Backward(newMatches) {
948 stack = append(stack, match)
949 }
950 }
951 continue
952 }
953 mode := pattern.Filenames | pattern.EntireString
954 if cfg.NoCaseGlob {
955 mode |= pattern.NoGlobCase
956 }
957 expr, err := pattern.Regexp(part, mode)
958 if err != nil {
959 return nil, err
960 }
961 rx := regexp.MustCompile(expr)
962 matchHidden := part[0] == byte('.')
963 var newMatches []string
964 for _, dir := range matches {
965 newMatches, err = cfg.globDir(base, dir, rx, matchHidden, wantDir, newMatches)
966 if err != nil {
967 return nil, err
968 }
969 }
970 matches = newMatches
971 }
972 return matches, nil
973}
974
975func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, matchHidden bool, wantDir bool, matches []string) ([]string, error) {
976 fullDir := dir
977 if !filepath.IsAbs(dir) {
978 fullDir = filepath.Join(base, dir)
979 }
980 infos, err := cfg.ReadDir2(fullDir)
981 if err != nil {
982 // We still want to return matches, for the sake of reusing slices.
983 return matches, err
984 }
985 for _, info := range infos {
986 name := info.Name()
987 if !wantDir {
988 // No filtering.
989 } else if mode := info.Type(); mode&os.ModeSymlink != 0 {
990 // We need to know if the symlink points to a directory.
991 // This requires an extra syscall, as [Config.ReadDir] on the parent directory
992 // does not follow symlinks for each of the directory entries.
993 // ReadDir is somewhat wasteful here, as we only want its error result,
994 // but we could try to reuse its result as per the TODO in [Config.glob].
995 if _, err := cfg.ReadDir2(filepath.Join(fullDir, info.Name())); err != nil {
996 continue
997 }
998 } else if !mode.IsDir() {
999 // Not a symlink nor a directory.
1000 continue
1001 }
1002 if !matchHidden && name[0] == '.' {
1003 continue
1004 }
1005 if rx.MatchString(name) {
1006 matches = append(matches, pathJoin2(dir, name))
1007 }
1008 }
1009 return matches, nil
1010}
1011
1012// ReadFields splits and returns n fields from s, like the "read" shell builtin.
1013// If raw is set, backslash escape sequences are not interpreted.
1014//
1015// The config specifies shell expansion options; nil behaves the same as an
1016// empty config.
1017func ReadFields(cfg *Config, s string, n int, raw bool) []string {
1018 cfg = prepareConfig(cfg)
1019 type pos struct {
1020 start, end int
1021 }
1022 var fpos []pos
1023
1024 runes := make([]rune, 0, len(s))
1025 infield := false
1026 esc := false
1027 for _, r := range s {
1028 if infield {
1029 if cfg.ifsRune(r) && (raw || !esc) {
1030 fpos[len(fpos)-1].end = len(runes)
1031 infield = false
1032 }
1033 } else {
1034 if !cfg.ifsRune(r) && (raw || !esc) {
1035 fpos = append(fpos, pos{start: len(runes), end: -1})
1036 infield = true
1037 }
1038 }
1039 if r == '\\' {
1040 if raw || esc {
1041 runes = append(runes, r)
1042 }
1043 esc = !esc
1044 continue
1045 }
1046 runes = append(runes, r)
1047 esc = false
1048 }
1049 if len(fpos) == 0 {
1050 return nil
1051 }
1052 if infield {
1053 fpos[len(fpos)-1].end = len(runes)
1054 }
1055
1056 switch {
1057 case n == 1:
1058 // include heading/trailing IFSs
1059 fpos[0].start, fpos[0].end = 0, len(runes)
1060 fpos = fpos[:1]
1061 case n != -1 && n < len(fpos):
1062 // combine to max n fields
1063 fpos[n-1].end = fpos[len(fpos)-1].end
1064 fpos = fpos[:n]
1065 }
1066
1067 fields := make([]string, len(fpos))
1068 for i, p := range fpos {
1069 fields[i] = string(runes[p.start:p.end])
1070 }
1071 return fields
1072}