1// Copyright (c) 2017, Daniel MartΓ <mvdan@mvdan.cc>
2// See LICENSE for licensing information
3
4package interp
5
6import (
7 "bytes"
8 "context"
9 "errors"
10 "fmt"
11 "io"
12 "io/fs"
13 "math"
14 "math/rand"
15 "os"
16 "path/filepath"
17 "regexp"
18 "runtime"
19 "strconv"
20 "strings"
21 "sync"
22 "time"
23
24 "mvdan.cc/sh/v3/expand"
25 "mvdan.cc/sh/v3/pattern"
26 "mvdan.cc/sh/v3/syntax"
27)
28
29const (
30 // shellReplyPS3Var, or PS3, is a special variable in Bash used by the select command,
31 // while the shell is awaiting for input. the default value is [shellDefaultPS3]
32 shellReplyPS3Var = "PS3"
33 // shellDefaultPS3, or #?, is PS3's default value
34 shellDefaultPS3 = "#? "
35 // shellReplyVar, or REPLY, is a special variable in Bash that is used to store the result of
36 // the select command or of the read command, when no variable name is specified
37 shellReplyVar = "REPLY"
38
39 fifoNamePrefix = "sh-interp-"
40)
41
42func (r *Runner) fillExpandConfig(ctx context.Context) {
43 r.ectx = ctx
44 r.ecfg = &expand.Config{
45 Env: expandEnv{r},
46 CmdSubst: func(w io.Writer, cs *syntax.CmdSubst) error {
47 switch len(cs.Stmts) {
48 case 0: // nothing to do
49 return nil
50 case 1: // $(<file)
51 word := catShortcutArg(cs.Stmts[0])
52 if word == nil {
53 break
54 }
55 path := r.literal(word)
56 f, err := r.open(ctx, path, os.O_RDONLY, 0, true)
57 if err != nil {
58 return err
59 }
60 _, err = io.Copy(w, f)
61 f.Close()
62 return err
63 }
64 r2 := r.Subshell()
65 r2.stdout = w
66 r2.stmts(ctx, cs.Stmts)
67 r.lastExpandExit = r2.exit
68 return r2.err
69 },
70 ProcSubst: func(ps *syntax.ProcSubst) (string, error) {
71 if runtime.GOOS == "windows" {
72 return "", fmt.Errorf("TODO: support process substitution on Windows")
73 }
74 if len(ps.Stmts) == 0 { // nothing to do
75 return os.DevNull, nil
76 }
77
78 if r.rand == nil {
79 r.rand = rand.New(rand.NewSource(time.Now().UnixNano()))
80 }
81
82 // We can't atomically create a random unused temporary FIFO.
83 // Similar to [os.CreateTemp],
84 // keep trying new random paths until one does not exist.
85 // We use a uint64 because a uint32 easily runs into retries.
86 var path string
87 try := 0
88 for {
89 path = filepath.Join(r.tempDir, fifoNamePrefix+strconv.FormatUint(r.rand.Uint64(), 16))
90 err := mkfifo(path, 0o666)
91 if err == nil {
92 break
93 }
94 if !os.IsExist(err) {
95 return "", fmt.Errorf("cannot create fifo: %v", err)
96 }
97 if try++; try > 100 {
98 return "", fmt.Errorf("giving up at creating fifo: %v", err)
99 }
100 }
101
102 r2 := r.Subshell()
103 stdout := r.origStdout
104 // TODO: note that `man bash` mentions that `wait` only waits for the last
105 // process substitution; the logic here would mean we wait for all of them.
106 r.bgShells.Add(1)
107 go func() {
108 defer r.bgShells.Done()
109 switch ps.Op {
110 case syntax.CmdIn:
111 f, err := os.OpenFile(path, os.O_WRONLY, 0)
112 if err != nil {
113 r.errf("cannot open fifo for stdout: %v\n", err)
114 return
115 }
116 r2.stdout = f
117 defer func() {
118 if err := f.Close(); err != nil {
119 r.errf("closing stdout fifo: %v\n", err)
120 }
121 os.Remove(path)
122 }()
123 default: // syntax.CmdOut
124 f, err := os.OpenFile(path, os.O_RDONLY, 0)
125 if err != nil {
126 r.errf("cannot open fifo for stdin: %v\n", err)
127 return
128 }
129 r2.stdin = f
130 r2.stdout = stdout
131
132 defer func() {
133 f.Close()
134 os.Remove(path)
135 }()
136 }
137 r2.stmts(ctx, ps.Stmts)
138 }()
139 return path, nil
140 },
141 }
142 r.updateExpandOpts()
143}
144
145// catShortcutArg checks if a statement is of the form "$(<file)". The redirect
146// word is returned if there's a match, and nil otherwise.
147func catShortcutArg(stmt *syntax.Stmt) *syntax.Word {
148 if stmt.Cmd != nil || stmt.Negated || stmt.Background || stmt.Coprocess {
149 return nil
150 }
151 if len(stmt.Redirs) != 1 {
152 return nil
153 }
154 redir := stmt.Redirs[0]
155 if redir.Op != syntax.RdrIn {
156 return nil
157 }
158 return redir.Word
159}
160
161func (r *Runner) updateExpandOpts() {
162 if r.opts[optNoGlob] {
163 r.ecfg.ReadDir2 = nil
164 } else {
165 r.ecfg.ReadDir2 = func(s string) ([]fs.DirEntry, error) {
166 return r.readDirHandler(r.handlerCtx(context.Background()), s)
167 }
168 }
169 r.ecfg.GlobStar = r.opts[optGlobStar]
170 r.ecfg.NoCaseGlob = r.opts[optNoCaseGlob]
171 r.ecfg.NullGlob = r.opts[optNullGlob]
172 r.ecfg.NoUnset = r.opts[optNoUnset]
173}
174
175func (r *Runner) expandErr(err error) {
176 if err != nil {
177 errMsg := err.Error()
178 fmt.Fprintln(r.stderr, errMsg)
179 switch {
180 case errors.As(err, &expand.UnsetParameterError{}):
181 case errMsg == "invalid indirect expansion":
182 // TODO: These errors are treated as fatal by bash.
183 // Make the error type reflect that.
184 case strings.HasSuffix(errMsg, "not supported"):
185 // TODO: This "has suffix" is a temporary measure until the expand
186 // package supports all syntax nodes like extended globbing.
187 default:
188 return // other cases do not exit
189 }
190 r.exitShell(context.TODO(), 1)
191 }
192}
193
194func (r *Runner) arithm(expr syntax.ArithmExpr) int {
195 n, err := expand.Arithm(r.ecfg, expr)
196 r.expandErr(err)
197 return n
198}
199
200func (r *Runner) fields(words ...*syntax.Word) []string {
201 strs, err := expand.Fields(r.ecfg, words...)
202 r.expandErr(err)
203 return strs
204}
205
206func (r *Runner) literal(word *syntax.Word) string {
207 str, err := expand.Literal(r.ecfg, word)
208 r.expandErr(err)
209 return str
210}
211
212func (r *Runner) document(word *syntax.Word) string {
213 str, err := expand.Document(r.ecfg, word)
214 r.expandErr(err)
215 return str
216}
217
218func (r *Runner) pattern(word *syntax.Word) string {
219 str, err := expand.Pattern(r.ecfg, word)
220 r.expandErr(err)
221 return str
222}
223
224// expandEnviron exposes [Runner]'s variables to the expand package.
225type expandEnv struct {
226 r *Runner
227}
228
229var _ expand.WriteEnviron = expandEnv{}
230
231func (e expandEnv) Get(name string) expand.Variable {
232 return e.r.lookupVar(name)
233}
234
235func (e expandEnv) Set(name string, vr expand.Variable) error {
236 e.r.setVar(name, vr)
237 return nil // TODO: return any errors
238}
239
240func (e expandEnv) Each(fn func(name string, vr expand.Variable) bool) {
241 e.r.writeEnv.Each(fn)
242}
243
244func (r *Runner) handlerCtx(ctx context.Context) context.Context {
245 hc := HandlerContext{
246 Env: &overlayEnviron{parent: r.writeEnv},
247 Dir: r.Dir,
248 Stdout: r.stdout,
249 Stderr: r.stderr,
250 }
251 if r.stdin != nil { // do not leave hc.Stdin as a typed nil
252 hc.Stdin = r.stdin
253 }
254 return context.WithValue(ctx, handlerCtxKey{}, hc)
255}
256
257func (r *Runner) setErr(err error) {
258 if r.err == nil {
259 r.err = err
260 }
261}
262
263func (r *Runner) out(s string) {
264 io.WriteString(r.stdout, s)
265}
266
267func (r *Runner) outf(format string, a ...any) {
268 fmt.Fprintf(r.stdout, format, a...)
269}
270
271func (r *Runner) errf(format string, a ...any) {
272 fmt.Fprintf(r.stderr, format, a...)
273}
274
275func (r *Runner) stop(ctx context.Context) bool {
276 if r.err != nil || r.Exited() {
277 return true
278 }
279 if err := ctx.Err(); err != nil {
280 r.err = err
281 return true
282 }
283 if r.opts[optNoExec] {
284 return true
285 }
286 return false
287}
288
289func (r *Runner) stmt(ctx context.Context, st *syntax.Stmt) {
290 if r.stop(ctx) {
291 return
292 }
293 r.exit = 0
294 if st.Background {
295 r2 := r.Subshell()
296 st2 := *st
297 st2.Background = false
298 r.bgShells.Add(1)
299 go func() {
300 r2.Run(ctx, &st2)
301 r.bgShells.Done()
302 }()
303 } else {
304 r.stmtSync(ctx, st)
305 }
306 r.lastExit = r.exit
307}
308
309func (r *Runner) stmtSync(ctx context.Context, st *syntax.Stmt) {
310 oldIn, oldOut, oldErr := r.stdin, r.stdout, r.stderr
311 for _, rd := range st.Redirs {
312 cls, err := r.redir(ctx, rd)
313 if err != nil {
314 r.exit = 1
315 break
316 }
317 if cls != nil {
318 defer cls.Close()
319 }
320 }
321 if r.exit == 0 && st.Cmd != nil {
322 r.cmd(ctx, st.Cmd)
323 }
324 if st.Negated {
325 r.exit = oneIf(r.exit == 0)
326 } else if _, ok := st.Cmd.(*syntax.CallExpr); !ok {
327 } else if r.exit != 0 && !r.noErrExit && r.opts[optErrExit] {
328 // If the "errexit" option is set and a simple command failed,
329 // exit the shell. Exceptions:
330 //
331 // conditions (if <cond>, while <cond>, etc)
332 // part of && or || lists
333 // preceded by !
334 r.exitShell(ctx, r.exit)
335 } else if r.exit != 0 && !r.noErrExit {
336 r.trapCallback(ctx, r.callbackErr, "error")
337 }
338 if !r.keepRedirs {
339 r.stdin, r.stdout, r.stderr = oldIn, oldOut, oldErr
340 }
341}
342
343func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
344 if r.stop(ctx) {
345 return
346 }
347
348 tracingEnabled := r.opts[optXTrace]
349 trace := r.tracer()
350
351 switch cm := cm.(type) {
352 case *syntax.Block:
353 r.stmts(ctx, cm.Stmts)
354 case *syntax.Subshell:
355 r2 := r.Subshell()
356 r2.stmts(ctx, cm.Stmts)
357 r.exit = r2.exit
358 r.setErr(r2.err)
359 case *syntax.CallExpr:
360 // Use a new slice, to not modify the slice in the alias map.
361 var args []*syntax.Word
362 left := cm.Args
363 for len(left) > 0 && r.opts[optExpandAliases] {
364 als, ok := r.alias[left[0].Lit()]
365 if !ok {
366 break
367 }
368 args = append(args, als.args...)
369 left = left[1:]
370 if !als.blank {
371 break
372 }
373 }
374 args = append(args, left...)
375 r.lastExpandExit = 0
376 fields := r.fields(args...)
377 if len(fields) == 0 {
378 for _, as := range cm.Assigns {
379 prev := r.lookupVar(as.Name.Value)
380 vr := r.assignVal(prev, as, "")
381 r.setVarWithIndex(prev, as.Name.Value, as.Index, vr)
382
383 if !tracingEnabled {
384 continue
385 }
386
387 // Strangely enough, it seems like Bash prints original
388 // source for arrays, but the expanded value otherwise.
389 // TODO: add test cases for x[i]=y and x+=y.
390 if as.Array != nil {
391 trace.expr(as)
392 } else if as.Value != nil {
393 val, err := syntax.Quote(vr.String(), syntax.LangBash)
394 if err != nil { // should never happen
395 panic(err)
396 }
397 trace.stringf("%s=%s", as.Name.Value, val)
398 }
399 trace.newLineFlush()
400 }
401 // If interpreting the last expansion like $(foo) failed,
402 // and the expansion and assignments otherwise succeeded,
403 // we need to surface that last exit code.
404 if r.exit == 0 {
405 r.exit = r.lastExpandExit
406 }
407 break
408 }
409
410 type restoreVar struct {
411 name string
412 vr expand.Variable
413 }
414 var restores []restoreVar
415
416 for _, as := range cm.Assigns {
417 name := as.Name.Value
418 prev := r.lookupVar(name)
419
420 vr := r.assignVal(prev, as, "")
421 // Inline command vars are always exported.
422 vr.Exported = true
423
424 restores = append(restores, restoreVar{name, prev})
425
426 r.setVar(name, vr)
427 }
428
429 trace.call(fields[0], fields[1:]...)
430 trace.newLineFlush()
431
432 r.call(ctx, cm.Args[0].Pos(), fields)
433 for _, restore := range restores {
434 r.setVar(restore.name, restore.vr)
435 }
436 case *syntax.BinaryCmd:
437 switch cm.Op {
438 case syntax.AndStmt, syntax.OrStmt:
439 oldNoErrExit := r.noErrExit
440 r.noErrExit = true
441 r.stmt(ctx, cm.X)
442 r.noErrExit = oldNoErrExit
443 if (r.exit == 0) == (cm.Op == syntax.AndStmt) {
444 r.stmt(ctx, cm.Y)
445 }
446 case syntax.Pipe, syntax.PipeAll:
447 pr, pw, err := os.Pipe()
448 if err != nil {
449 r.setErr(err)
450 return
451 }
452 r2 := r.Subshell()
453 r2.stdout = pw
454 if cm.Op == syntax.PipeAll {
455 r2.stderr = pw
456 } else {
457 r2.stderr = r.stderr
458 }
459 r.stdin = pr
460 var wg sync.WaitGroup
461 wg.Add(1)
462 go func() {
463 r2.stmt(ctx, cm.X)
464 pw.Close()
465 wg.Done()
466 }()
467 r.stmt(ctx, cm.Y)
468 pr.Close()
469 wg.Wait()
470 if r.opts[optPipeFail] && r2.exit != 0 && r.exit == 0 {
471 r.exit = r2.exit
472 r.shellExited = r2.shellExited
473 }
474 r.setErr(r2.err)
475 }
476 case *syntax.IfClause:
477 oldNoErrExit := r.noErrExit
478 r.noErrExit = true
479 r.stmts(ctx, cm.Cond)
480 r.noErrExit = oldNoErrExit
481
482 if r.exit == 0 {
483 r.stmts(ctx, cm.Then)
484 break
485 }
486 r.exit = 0
487 if cm.Else != nil {
488 r.cmd(ctx, cm.Else)
489 }
490 case *syntax.WhileClause:
491 for !r.stop(ctx) {
492 oldNoErrExit := r.noErrExit
493 r.noErrExit = true
494 r.stmts(ctx, cm.Cond)
495 r.noErrExit = oldNoErrExit
496
497 stop := (r.exit == 0) == cm.Until
498 r.exit = 0
499 if stop || r.loopStmtsBroken(ctx, cm.Do) {
500 break
501 }
502 }
503 case *syntax.ForClause:
504 switch y := cm.Loop.(type) {
505 case *syntax.WordIter:
506 name := y.Name.Value
507 items := r.Params // for i; do ...
508
509 inToken := y.InPos.IsValid()
510 if inToken {
511 items = r.fields(y.Items...) // for i in ...; do ...
512 }
513
514 if cm.Select {
515 ps3 := shellDefaultPS3
516 if e := r.envGet(shellReplyPS3Var); e != "" {
517 ps3 = e
518 }
519
520 prompt := func() []byte {
521 // display menu
522 for i, word := range items {
523 r.errf("%d) %v\n", i+1, word)
524 }
525 r.errf("%s", ps3)
526
527 line, err := r.readLine(ctx, true)
528 if err != nil {
529 r.exit = 1
530 return nil
531 }
532 return line
533 }
534
535 retry:
536 choice := prompt()
537 if len(choice) == 0 {
538 goto retry // no reply; try again
539 }
540
541 reply := string(choice)
542 r.setVarString(shellReplyVar, reply)
543
544 c, _ := strconv.Atoi(reply)
545 if c > 0 && c <= len(items) {
546 r.setVarString(name, items[c-1])
547 }
548
549 // execute commands until break or return is encountered
550 if r.loopStmtsBroken(ctx, cm.Do) {
551 break
552 }
553 }
554
555 for _, field := range items {
556 r.setVarString(name, field)
557 trace.stringf("for %s in", y.Name.Value)
558 if inToken {
559 for _, item := range y.Items {
560 trace.string(" ")
561 trace.expr(item)
562 }
563 } else {
564 trace.string(` "$@"`)
565 }
566 trace.newLineFlush()
567 if r.loopStmtsBroken(ctx, cm.Do) {
568 break
569 }
570 }
571 case *syntax.CStyleLoop:
572 if y.Init != nil {
573 r.arithm(y.Init)
574 }
575 for y.Cond == nil || r.arithm(y.Cond) != 0 {
576 if r.exit != 0 || r.loopStmtsBroken(ctx, cm.Do) {
577 break
578 }
579 if y.Post != nil {
580 r.arithm(y.Post)
581 }
582 }
583 }
584 case *syntax.FuncDecl:
585 r.setFunc(cm.Name.Value, cm.Body)
586 case *syntax.ArithmCmd:
587 r.exit = oneIf(r.arithm(cm.X) == 0)
588 case *syntax.LetClause:
589 var val int
590 for _, expr := range cm.Exprs {
591 val = r.arithm(expr)
592
593 if !tracingEnabled {
594 continue
595 }
596
597 switch expr := expr.(type) {
598 case *syntax.Word:
599 qs, err := syntax.Quote(r.literal(expr), syntax.LangBash)
600 if err != nil {
601 return
602 }
603 trace.stringf("let %v", qs)
604 case *syntax.BinaryArithm, *syntax.UnaryArithm:
605 trace.expr(cm)
606 case *syntax.ParenArithm:
607 // TODO
608 }
609 }
610
611 trace.newLineFlush()
612 r.exit = oneIf(val == 0)
613 case *syntax.CaseClause:
614 trace.string("case ")
615 trace.expr(cm.Word)
616 trace.string(" in")
617 trace.newLineFlush()
618 str := r.literal(cm.Word)
619 for _, ci := range cm.Items {
620 for _, word := range ci.Patterns {
621 pattern := r.pattern(word)
622 if match(pattern, str) {
623 r.stmts(ctx, ci.Stmts)
624 return
625 }
626 }
627 }
628 case *syntax.TestClause:
629 if r.bashTest(ctx, cm.X, false) == "" && r.exit == 0 {
630 // to preserve exit status code 2 for regex errors, etc
631 r.exit = 1
632 }
633 case *syntax.DeclClause:
634 local, global := false, false
635 var modes []string
636 valType := ""
637 switch cm.Variant.Value {
638 case "declare":
639 // When used in a function, "declare" acts as "local"
640 // unless the "-g" option is used.
641 local = r.inFunc
642 case "local":
643 if !r.inFunc {
644 r.errf("local: can only be used in a function\n")
645 r.exit = 1
646 return
647 }
648 local = true
649 case "export":
650 modes = append(modes, "-x")
651 case "readonly":
652 modes = append(modes, "-r")
653 case "nameref":
654 valType = "-n"
655 }
656 for _, as := range cm.Args {
657 for _, as := range r.flattenAssign(as) {
658 name := as.Name.Value
659 if strings.HasPrefix(name, "-") {
660 switch name {
661 case "-x", "-r":
662 modes = append(modes, name)
663 case "-a", "-A", "-n":
664 valType = name
665 case "-g":
666 global = true
667 default:
668 r.errf("declare: invalid option %q\n", name)
669 r.exit = 2
670 return
671 }
672 continue
673 }
674 if !syntax.ValidName(name) {
675 r.errf("declare: invalid name %q\n", name)
676 r.exit = 1
677 return
678 }
679 vr := r.lookupVar(as.Name.Value)
680 if as.Naked {
681 if valType == "-A" {
682 vr.Kind = expand.Associative
683 } else {
684 vr.Kind = expand.KeepValue
685 }
686 } else {
687 vr = r.assignVal(vr, as, valType)
688 }
689 if global {
690 vr.Local = false
691 } else if local {
692 vr.Local = true
693 }
694 for _, mode := range modes {
695 switch mode {
696 case "-x":
697 vr.Exported = true
698 case "-r":
699 vr.ReadOnly = true
700 }
701 }
702 r.setVar(name, vr)
703 }
704 }
705 case *syntax.TimeClause:
706 start := time.Now()
707 if cm.Stmt != nil {
708 r.stmt(ctx, cm.Stmt)
709 }
710 format := "%s\t%s\n"
711 if cm.PosixFormat {
712 format = "%s %s\n"
713 } else {
714 r.outf("\n")
715 }
716 real := time.Since(start)
717 r.outf(format, "real", elapsedString(real, cm.PosixFormat))
718 // TODO: can we do these?
719 r.outf(format, "user", elapsedString(0, cm.PosixFormat))
720 r.outf(format, "sys", elapsedString(0, cm.PosixFormat))
721 default:
722 panic(fmt.Sprintf("unhandled command node: %T", cm))
723 }
724}
725
726func (r *Runner) trapCallback(ctx context.Context, callback, name string) {
727 if callback == "" {
728 return // nothing to do
729 }
730 if r.handlingTrap {
731 return // don't recurse, as that could lead to cycles
732 }
733 r.handlingTrap = true
734
735 p := syntax.NewParser()
736 // TODO: do this parsing when "trap" is called?
737 file, err := p.Parse(strings.NewReader(callback), name+" trap")
738 if err != nil {
739 r.errf(name+"trap: %v\n", err)
740 // ignore errors in the callback
741 return
742 }
743 r.stmts(ctx, file.Stmts)
744
745 r.handlingTrap = false
746}
747
748// exitShell exits the current shell session with the given status code.
749func (r *Runner) exitShell(ctx context.Context, status int) {
750 if status != 0 {
751 r.trapCallback(ctx, r.callbackErr, "error")
752 }
753 r.trapCallback(ctx, r.callbackExit, "exit")
754
755 r.shellExited = true
756 // Restore the original exit status. We ignore the callbacks.
757 r.exit = status
758}
759
760func (r *Runner) flattenAssign(as *syntax.Assign) []*syntax.Assign {
761 // Convert "declare $x" into "declare value".
762 // Don't use syntax.Parser here, as we only want the basic
763 // splitting by '='.
764 if as.Name != nil {
765 return []*syntax.Assign{as} // nothing to do
766 }
767 var asgns []*syntax.Assign
768 for _, field := range r.fields(as.Value) {
769 as := &syntax.Assign{}
770 parts := strings.SplitN(field, "=", 2)
771 as.Name = &syntax.Lit{Value: parts[0]}
772 if len(parts) == 1 {
773 as.Naked = true
774 } else {
775 as.Value = &syntax.Word{Parts: []syntax.WordPart{
776 &syntax.Lit{Value: parts[1]},
777 }}
778 }
779 asgns = append(asgns, as)
780 }
781 return asgns
782}
783
784func match(pat, name string) bool {
785 expr, err := pattern.Regexp(pat, pattern.EntireString)
786 if err != nil {
787 return false
788 }
789 rx := regexp.MustCompile(expr)
790 return rx.MatchString(name)
791}
792
793func elapsedString(d time.Duration, posix bool) string {
794 if posix {
795 return fmt.Sprintf("%.2f", d.Seconds())
796 }
797 min := int(d.Minutes())
798 sec := math.Mod(d.Seconds(), 60.0)
799 return fmt.Sprintf("%dm%.3fs", min, sec)
800}
801
802func (r *Runner) stmts(ctx context.Context, stmts []*syntax.Stmt) {
803 for _, stmt := range stmts {
804 r.stmt(ctx, stmt)
805 }
806}
807
808func (r *Runner) hdocReader(rd *syntax.Redirect) (*os.File, error) {
809 pr, pw, err := os.Pipe()
810 if err != nil {
811 return nil, err
812 }
813 // We write to the pipe in a new goroutine,
814 // as pipe writes may block once the buffer gets full.
815 // We still construct and buffer the entire heredoc first,
816 // as doing it concurrently would lead to different semantics and be racy.
817 if rd.Op != syntax.DashHdoc {
818 hdoc := r.document(rd.Hdoc)
819 go func() {
820 pw.WriteString(hdoc)
821 pw.Close()
822 }()
823 return pr, nil
824 }
825 var buf bytes.Buffer
826 var cur []syntax.WordPart
827 flushLine := func() {
828 if buf.Len() > 0 {
829 buf.WriteByte('\n')
830 }
831 buf.WriteString(r.document(&syntax.Word{Parts: cur}))
832 cur = cur[:0]
833 }
834 for _, wp := range rd.Hdoc.Parts {
835 lit, ok := wp.(*syntax.Lit)
836 if !ok {
837 cur = append(cur, wp)
838 continue
839 }
840 for i, part := range strings.Split(lit.Value, "\n") {
841 if i > 0 {
842 flushLine()
843 cur = cur[:0]
844 }
845 part = strings.TrimLeft(part, "\t")
846 cur = append(cur, &syntax.Lit{Value: part})
847 }
848 }
849 flushLine()
850 go func() {
851 pw.Write(buf.Bytes())
852 pw.Close()
853 }()
854 return pr, nil
855}
856
857func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, error) {
858 if rd.Hdoc != nil {
859 pr, err := r.hdocReader(rd)
860 if err != nil {
861 return nil, err
862 }
863 r.stdin = pr
864 return pr, nil
865 }
866
867 orig := &r.stdout
868 if rd.N != nil {
869 switch rd.N.Value {
870 case "0":
871 // Note that the input redirects below always use stdin (0)
872 // because we don't support anything else right now.
873 case "1":
874 // The default for the output redirects below.
875 case "2":
876 orig = &r.stderr
877 default:
878 panic(fmt.Sprintf("unsupported redirect fd: %v", rd.N.Value))
879 }
880 }
881 arg := r.literal(rd.Word)
882 switch rd.Op {
883 case syntax.WordHdoc:
884 pr, pw, err := os.Pipe()
885 if err != nil {
886 return nil, err
887 }
888 r.stdin = pr
889 // We write to the pipe in a new goroutine,
890 // as pipe writes may block once the buffer gets full.
891 go func() {
892 pw.WriteString(arg)
893 pw.WriteString("\n")
894 pw.Close()
895 }()
896 return pr, nil
897 case syntax.DplOut:
898 switch arg {
899 case "1":
900 *orig = r.stdout
901 case "2":
902 *orig = r.stderr
903 case "-":
904 *orig = io.Discard // closing the output writer
905 default:
906 panic(fmt.Sprintf("unhandled %v arg: %q", rd.Op, arg))
907 }
908 return nil, nil
909 case syntax.RdrIn, syntax.RdrOut, syntax.AppOut,
910 syntax.RdrAll, syntax.AppAll:
911 // done further below
912 case syntax.DplIn:
913 switch arg {
914 case "-":
915 r.stdin = nil // closing the input file
916 default:
917 panic(fmt.Sprintf("unhandled %v arg: %q", rd.Op, arg))
918 }
919 return nil, nil
920 default:
921 panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op))
922 }
923 mode := os.O_RDONLY
924 switch rd.Op {
925 case syntax.AppOut, syntax.AppAll:
926 mode = os.O_WRONLY | os.O_CREATE | os.O_APPEND
927 case syntax.RdrOut, syntax.RdrAll:
928 mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
929 }
930 f, err := r.open(ctx, arg, mode, 0o644, true)
931 if err != nil {
932 return nil, err
933 }
934 switch rd.Op {
935 case syntax.RdrIn:
936 stdin, err := stdinFile(f)
937 if err != nil {
938 return nil, err
939 }
940 r.stdin = stdin
941 case syntax.RdrOut, syntax.AppOut:
942 *orig = f
943 case syntax.RdrAll, syntax.AppAll:
944 r.stdout = f
945 r.stderr = f
946 default:
947 panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op))
948 }
949 return f, nil
950}
951
952func (r *Runner) loopStmtsBroken(ctx context.Context, stmts []*syntax.Stmt) bool {
953 oldInLoop := r.inLoop
954 r.inLoop = true
955 defer func() { r.inLoop = oldInLoop }()
956 for _, stmt := range stmts {
957 r.stmt(ctx, stmt)
958 if r.contnEnclosing > 0 {
959 r.contnEnclosing--
960 return r.contnEnclosing > 0
961 }
962 if r.breakEnclosing > 0 {
963 r.breakEnclosing--
964 return true
965 }
966 }
967 return false
968}
969
970type returnStatus uint8
971
972func (s returnStatus) Error() string { return fmt.Sprintf("return status %d", s) }
973
974func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) {
975 if r.stop(ctx) {
976 return
977 }
978 if r.callHandler != nil {
979 var err error
980 args, err = r.callHandler(r.handlerCtx(ctx), args)
981 if err != nil {
982 // handler's custom fatal error
983 r.setErr(err)
984 return
985 }
986 }
987 name := args[0]
988 if body := r.Funcs[name]; body != nil {
989 // stack them to support nested func calls
990 oldParams := r.Params
991 r.Params = args[1:]
992 oldInFunc := r.inFunc
993 r.inFunc = true
994
995 // Functions run in a nested scope.
996 // Note that [Runner.exec] below does something similar.
997 origEnv := r.writeEnv
998 r.writeEnv = &overlayEnviron{parent: r.writeEnv, funcScope: true}
999
1000 r.stmt(ctx, body)
1001
1002 r.writeEnv = origEnv
1003
1004 r.Params = oldParams
1005 r.inFunc = oldInFunc
1006 if code, ok := r.err.(returnStatus); ok {
1007 r.err = nil
1008 r.exit = int(code)
1009 }
1010 return
1011 }
1012 if isBuiltin(name) {
1013 r.exit = r.builtinCode(ctx, pos, name, args[1:])
1014 return
1015 }
1016 r.exec(ctx, args)
1017}
1018
1019func (r *Runner) exec(ctx context.Context, args []string) {
1020 err := r.execHandler(r.handlerCtx(ctx), args)
1021 if status, ok := IsExitStatus(err); ok {
1022 r.exit = int(status)
1023 return
1024 }
1025 if err != nil {
1026 // handler's custom fatal error
1027 r.setErr(err)
1028 return
1029 }
1030 r.exit = 0
1031}
1032
1033func (r *Runner) open(ctx context.Context, path string, flags int, mode os.FileMode, print bool) (io.ReadWriteCloser, error) {
1034 // If we are opening a FIFO temporary file created by the interpreter itself,
1035 // don't pass this along to the open handler as it will not work at all
1036 // unless [os.OpenFile] is used directly with it.
1037 // Matching by directory and basename prefix isn't perfect, but works.
1038 //
1039 // If we want FIFOs to use a handler in the future, they probably
1040 // need their own separate handler API matching Unix-like semantics.
1041 dir, name := filepath.Split(path)
1042 dir = strings.TrimSuffix(dir, "/")
1043 if dir == r.tempDir && strings.HasPrefix(name, fifoNamePrefix) {
1044 return os.OpenFile(path, flags, mode)
1045 }
1046
1047 f, err := r.openHandler(r.handlerCtx(ctx), path, flags, mode)
1048 // TODO: support wrapped PathError returned from openHandler.
1049 switch err.(type) {
1050 case nil:
1051 return f, nil
1052 case *os.PathError:
1053 if print {
1054 r.errf("%v\n", err)
1055 }
1056 default: // handler's custom fatal error
1057 r.setErr(err)
1058 }
1059 return nil, err
1060}
1061
1062func (r *Runner) stat(ctx context.Context, name string) (fs.FileInfo, error) {
1063 path := absPath(r.Dir, name)
1064 return r.statHandler(ctx, path, true)
1065}
1066
1067func (r *Runner) lstat(ctx context.Context, name string) (fs.FileInfo, error) {
1068 path := absPath(r.Dir, name)
1069 return r.statHandler(ctx, path, false)
1070}