1// Copyright (c) 2017, Daniel MartΓ <mvdan@mvdan.cc>
2// See LICENSE for licensing information
3
4package interp
5
6import (
7 "bufio"
8 "bytes"
9 "cmp"
10 "context"
11 "errors"
12 "fmt"
13 "os"
14 "path/filepath"
15 "slices"
16 "strconv"
17 "strings"
18 "syscall"
19 "time"
20
21 "golang.org/x/term"
22
23 "mvdan.cc/sh/v3/expand"
24 "mvdan.cc/sh/v3/syntax"
25)
26
27func isBuiltin(name string) bool {
28 switch name {
29 case "true", ":", "false", "exit", "set", "shift", "unset",
30 "echo", "printf", "break", "continue", "pwd", "cd",
31 "wait", "builtin", "trap", "type", "source", ".", "command",
32 "dirs", "pushd", "popd", "umask", "alias", "unalias",
33 "fg", "bg", "getopts", "eval", "test", "[", "exec",
34 "return", "read", "mapfile", "readarray", "shopt":
35 return true
36 }
37 return false
38}
39
40// TODO: oneIf and atoi are duplicated in the expand package.
41
42func oneIf(b bool) int {
43 if b {
44 return 1
45 }
46 return 0
47}
48
49// atoi is like [strconv.Atoi], but it ignores errors and trims whitespace.
50func atoi(s string) int {
51 s = strings.TrimSpace(s)
52 n, _ := strconv.Atoi(s)
53 return n
54}
55
56func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, args []string) int {
57 switch name {
58 case "true", ":":
59 case "false":
60 return 1
61 case "exit":
62 exit := 0
63 switch len(args) {
64 case 0:
65 exit = r.lastExit
66 case 1:
67 n, err := strconv.Atoi(args[0])
68 if err != nil {
69 r.errf("invalid exit status code: %q\n", args[0])
70 return 2
71 }
72 exit = n
73 default:
74 r.errf("exit cannot take multiple arguments\n")
75 return 1
76 }
77 r.exitShell(ctx, exit)
78 return exit
79 case "set":
80 if err := Params(args...)(r); err != nil {
81 r.errf("set: %v\n", err)
82 return 2
83 }
84 r.updateExpandOpts()
85 case "shift":
86 n := 1
87 switch len(args) {
88 case 0:
89 case 1:
90 if n2, err := strconv.Atoi(args[0]); err == nil {
91 n = n2
92 break
93 }
94 fallthrough
95 default:
96 r.errf("usage: shift [n]\n")
97 return 2
98 }
99 if n >= len(r.Params) {
100 r.Params = nil
101 } else {
102 r.Params = r.Params[n:]
103 }
104 case "unset":
105 vars := true
106 funcs := true
107 unsetOpts:
108 for i, arg := range args {
109 switch arg {
110 case "-v":
111 funcs = false
112 case "-f":
113 vars = false
114 default:
115 args = args[i:]
116 break unsetOpts
117 }
118 }
119
120 for _, arg := range args {
121 if vars && r.lookupVar(arg).IsSet() {
122 r.delVar(arg)
123 } else if _, ok := r.Funcs[arg]; ok && funcs {
124 delete(r.Funcs, arg)
125 }
126 }
127 case "echo":
128 newline, doExpand := true, false
129 echoOpts:
130 for len(args) > 0 {
131 switch args[0] {
132 case "-n":
133 newline = false
134 case "-e":
135 doExpand = true
136 case "-E": // default
137 default:
138 break echoOpts
139 }
140 args = args[1:]
141 }
142 for i, arg := range args {
143 if i > 0 {
144 r.out(" ")
145 }
146 if doExpand {
147 arg, _, _ = expand.Format(r.ecfg, arg, nil)
148 }
149 r.out(arg)
150 }
151 if newline {
152 r.out("\n")
153 }
154 case "printf":
155 if len(args) == 0 {
156 r.errf("usage: printf format [arguments]\n")
157 return 2
158 }
159 format, args := args[0], args[1:]
160 for {
161 s, n, err := expand.Format(r.ecfg, format, args)
162 if err != nil {
163 r.errf("%v\n", err)
164 return 1
165 }
166 r.out(s)
167 args = args[n:]
168 if n == 0 || len(args) == 0 {
169 break
170 }
171 }
172 case "break", "continue":
173 if !r.inLoop {
174 r.errf("%s is only useful in a loop\n", name)
175 break
176 }
177 enclosing := &r.breakEnclosing
178 if name == "continue" {
179 enclosing = &r.contnEnclosing
180 }
181 switch len(args) {
182 case 0:
183 *enclosing = 1
184 case 1:
185 if n, err := strconv.Atoi(args[0]); err == nil {
186 *enclosing = n
187 break
188 }
189 fallthrough
190 default:
191 r.errf("usage: %s [n]\n", name)
192 return 2
193 }
194 case "pwd":
195 evalSymlinks := false
196 for len(args) > 0 {
197 switch args[0] {
198 case "-L":
199 evalSymlinks = false
200 case "-P":
201 evalSymlinks = true
202 default:
203 r.errf("invalid option: %q\n", args[0])
204 return 2
205 }
206 args = args[1:]
207 }
208 pwd := r.envGet("PWD")
209 if evalSymlinks {
210 var err error
211 pwd, err = filepath.EvalSymlinks(pwd)
212 if err != nil {
213 r.setErr(err)
214 return 1
215 }
216 }
217 r.outf("%s\n", pwd)
218 case "cd":
219 var path string
220 switch len(args) {
221 case 0:
222 path = r.envGet("HOME")
223 case 1:
224 path = args[0]
225
226 // replicate the commonly implemented behavior of `cd -`
227 // ref: https://www.man7.org/linux/man-pages/man1/cd.1p.html#OPERANDS
228 if path == "-" {
229 path = r.envGet("OLDPWD")
230 r.outf("%s\n", path)
231 }
232 default:
233 r.errf("usage: cd [dir]\n")
234 return 2
235 }
236 return r.changeDir(ctx, path)
237 case "wait":
238 if len(args) > 0 {
239 panic("wait with args not handled yet")
240 }
241 // Note that "wait" without arguments always returns exit status zero.
242 r.bgShells.Wait()
243 case "builtin":
244 if len(args) < 1 {
245 break
246 }
247 if !isBuiltin(args[0]) {
248 return 1
249 }
250 return r.builtinCode(ctx, pos, args[0], args[1:])
251 case "type":
252 anyNotFound := false
253 mode := ""
254 fp := flagParser{remaining: args}
255 for fp.more() {
256 switch flag := fp.flag(); flag {
257 case "-a", "-f", "-P", "--help":
258 r.errf("command: NOT IMPLEMENTED\n")
259 return 3
260 case "-p", "-t":
261 mode = flag
262 default:
263 r.errf("command: invalid option %q\n", flag)
264 return 2
265 }
266 }
267 args := fp.args()
268 for _, arg := range args {
269 if mode == "-p" {
270 if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {
271 r.outf("%s\n", path)
272 } else {
273 anyNotFound = true
274 }
275 continue
276 }
277 if syntax.IsKeyword(arg) {
278 if mode == "-t" {
279 r.out("keyword\n")
280 } else {
281 r.outf("%s is a shell keyword\n", arg)
282 }
283 continue
284 }
285 if als, ok := r.alias[arg]; ok && r.opts[optExpandAliases] {
286 var buf bytes.Buffer
287 if len(als.args) > 0 {
288 printer := syntax.NewPrinter()
289 printer.Print(&buf, &syntax.CallExpr{
290 Args: als.args,
291 })
292 }
293 if als.blank {
294 buf.WriteByte(' ')
295 }
296 if mode == "-t" {
297 r.out("alias\n")
298 } else {
299 r.outf("%s is aliased to `%s'\n", arg, &buf)
300 }
301 continue
302 }
303 if _, ok := r.Funcs[arg]; ok {
304 if mode == "-t" {
305 r.out("function\n")
306 } else {
307 r.outf("%s is a function\n", arg)
308 }
309 continue
310 }
311 if isBuiltin(arg) {
312 if mode == "-t" {
313 r.out("builtin\n")
314 } else {
315 r.outf("%s is a shell builtin\n", arg)
316 }
317 continue
318 }
319 if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {
320 if mode == "-t" {
321 r.out("file\n")
322 } else {
323 r.outf("%s is %s\n", arg, path)
324 }
325 continue
326 }
327 if mode != "-t" {
328 r.errf("type: %s: not found\n", arg)
329 }
330 anyNotFound = true
331 }
332 if anyNotFound {
333 return 1
334 }
335 case "eval":
336 src := strings.Join(args, " ")
337 p := syntax.NewParser()
338 file, err := p.Parse(strings.NewReader(src), "")
339 if err != nil {
340 r.errf("eval: %v\n", err)
341 return 1
342 }
343 r.stmts(ctx, file.Stmts)
344 return r.exit
345 case "source", ".":
346 if len(args) < 1 {
347 r.errf("%v: source: need filename\n", pos)
348 return 2
349 }
350 path, err := scriptFromPathDir(r.Dir, r.writeEnv, args[0])
351 if err != nil {
352 // If the script was not found in PATH or there was any error, pass
353 // the source path to the open handler so it has a chance to look
354 // at files it manages (eg: virtual filesystem), and also allow
355 // it to look for the sourced script in the current directory.
356 path = args[0]
357 }
358 f, err := r.open(ctx, path, os.O_RDONLY, 0, false)
359 if err != nil {
360 r.errf("source: %v\n", err)
361 return 1
362 }
363 defer f.Close()
364 p := syntax.NewParser()
365 file, err := p.Parse(f, path)
366 if err != nil {
367 r.errf("source: %v\n", err)
368 return 1
369 }
370
371 // Keep the current versions of some fields we might modify.
372 oldParams := r.Params
373 oldSourceSetParams := r.sourceSetParams
374 oldInSource := r.inSource
375
376 // If we run "source file args...", set said args as parameters.
377 // Otherwise, keep the current parameters.
378 sourceArgs := len(args[1:]) > 0
379 if sourceArgs {
380 r.Params = args[1:]
381 r.sourceSetParams = false
382 }
383 // We want to track if the sourced file explicitly sets the
384 // parameters.
385 r.sourceSetParams = false
386 r.inSource = true // know that we're inside a sourced script.
387 r.stmts(ctx, file.Stmts)
388
389 // If we modified the parameters and the sourced file didn't
390 // explicitly set them, we restore the old ones.
391 if sourceArgs && !r.sourceSetParams {
392 r.Params = oldParams
393 }
394 r.sourceSetParams = oldSourceSetParams
395 r.inSource = oldInSource
396
397 if code, ok := r.err.(returnStatus); ok {
398 r.err = nil
399 return int(code)
400 }
401 return r.exit
402 case "[":
403 if len(args) == 0 || args[len(args)-1] != "]" {
404 r.errf("%v: [: missing matching ]\n", pos)
405 return 2
406 }
407 args = args[:len(args)-1]
408 fallthrough
409 case "test":
410 parseErr := false
411 p := testParser{
412 rem: args,
413 err: func(err error) {
414 r.errf("%v: %v\n", pos, err)
415 parseErr = true
416 },
417 }
418 p.next()
419 expr := p.classicTest("[", false)
420 if parseErr {
421 return 2
422 }
423 return oneIf(r.bashTest(ctx, expr, true) == "")
424 case "exec":
425 // TODO: Consider unix.Exec, i.e. actually replacing
426 // the process. It's in theory what a shell should do,
427 // but in practice it would kill the entire Go process
428 // and it's not available on Windows.
429 if len(args) == 0 {
430 r.keepRedirs = true
431 break
432 }
433 r.exitShell(ctx, 1)
434 r.exec(ctx, args)
435 return r.exit
436 case "command":
437 show := false
438 fp := flagParser{remaining: args}
439 for fp.more() {
440 switch flag := fp.flag(); flag {
441 case "-v":
442 show = true
443 default:
444 r.errf("command: invalid option %q\n", flag)
445 return 2
446 }
447 }
448 args := fp.args()
449 if len(args) == 0 {
450 break
451 }
452 if !show {
453 if isBuiltin(args[0]) {
454 return r.builtinCode(ctx, pos, args[0], args[1:])
455 }
456 r.exec(ctx, args)
457 return r.exit
458 }
459 last := 0
460 for _, arg := range args {
461 last = 0
462 if r.Funcs[arg] != nil || isBuiltin(arg) {
463 r.outf("%s\n", arg)
464 } else if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {
465 r.outf("%s\n", path)
466 } else {
467 last = 1
468 }
469 }
470 return last
471 case "dirs":
472 for i, dir := range slices.Backward(r.dirStack) {
473 r.outf("%s", dir)
474 if i > 0 {
475 r.out(" ")
476 }
477 }
478 r.out("\n")
479 case "pushd":
480 change := true
481 if len(args) > 0 && args[0] == "-n" {
482 change = false
483 args = args[1:]
484 }
485 swap := func() string {
486 oldtop := r.dirStack[len(r.dirStack)-1]
487 top := r.dirStack[len(r.dirStack)-2]
488 r.dirStack[len(r.dirStack)-1] = top
489 r.dirStack[len(r.dirStack)-2] = oldtop
490 return top
491 }
492 switch len(args) {
493 case 0:
494 if !change {
495 break
496 }
497 if len(r.dirStack) < 2 {
498 r.errf("pushd: no other directory\n")
499 return 1
500 }
501 newtop := swap()
502 if code := r.changeDir(ctx, newtop); code != 0 {
503 return code
504 }
505 r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
506 case 1:
507 if change {
508 if code := r.changeDir(ctx, args[0]); code != 0 {
509 return code
510 }
511 r.dirStack = append(r.dirStack, r.Dir)
512 } else {
513 r.dirStack = append(r.dirStack, args[0])
514 swap()
515 }
516 r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
517 default:
518 r.errf("pushd: too many arguments\n")
519 return 2
520 }
521 case "popd":
522 change := true
523 if len(args) > 0 && args[0] == "-n" {
524 change = false
525 args = args[1:]
526 }
527 switch len(args) {
528 case 0:
529 if len(r.dirStack) < 2 {
530 r.errf("popd: directory stack empty\n")
531 return 1
532 }
533 oldtop := r.dirStack[len(r.dirStack)-1]
534 r.dirStack = r.dirStack[:len(r.dirStack)-1]
535 if change {
536 newtop := r.dirStack[len(r.dirStack)-1]
537 if code := r.changeDir(ctx, newtop); code != 0 {
538 return code
539 }
540 } else {
541 r.dirStack[len(r.dirStack)-1] = oldtop
542 }
543 r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
544 default:
545 r.errf("popd: invalid argument\n")
546 return 2
547 }
548 case "return":
549 if !r.inFunc && !r.inSource {
550 r.errf("return: can only be done from a func or sourced script\n")
551 return 1
552 }
553 code := 0
554 switch len(args) {
555 case 0:
556 case 1:
557 code = atoi(args[0])
558 default:
559 r.errf("return: too many arguments\n")
560 return 2
561 }
562 r.setErr(returnStatus(code))
563 case "read":
564 var prompt string
565 raw := false
566 silent := false
567 fp := flagParser{remaining: args}
568 for fp.more() {
569 switch flag := fp.flag(); flag {
570 case "-s":
571 silent = true
572 case "-r":
573 raw = true
574 case "-p":
575 prompt = fp.value()
576 if prompt == "" {
577 r.errf("read: -p: option requires an argument\n")
578 return 2
579 }
580 default:
581 r.errf("read: invalid option %q\n", flag)
582 return 2
583 }
584 }
585
586 args := fp.args()
587 for _, name := range args {
588 if !syntax.ValidName(name) {
589 r.errf("read: invalid identifier %q\n", name)
590 return 2
591 }
592 }
593
594 if prompt != "" {
595 r.out(prompt)
596 }
597
598 var line []byte
599 var err error
600 if silent {
601 line, err = term.ReadPassword(int(syscall.Stdin))
602 } else {
603 line, err = r.readLine(ctx, raw)
604 }
605 if len(args) == 0 {
606 args = append(args, shellReplyVar)
607 }
608
609 values := expand.ReadFields(r.ecfg, string(line), len(args), raw)
610 for i, name := range args {
611 val := ""
612 if i < len(values) {
613 val = values[i]
614 }
615 r.setVarString(name, val)
616 }
617
618 // We can get data back from readLine and an error at the same time, so
619 // check err after we process the data.
620 if err != nil {
621 return 1
622 }
623
624 return 0
625
626 case "getopts":
627 if len(args) < 2 {
628 r.errf("getopts: usage: getopts optstring name [arg ...]\n")
629 return 2
630 }
631 optind, _ := strconv.Atoi(r.envGet("OPTIND"))
632 if optind-1 != r.optState.argidx {
633 if optind < 1 {
634 optind = 1
635 }
636 r.optState = getopts{argidx: optind - 1}
637 }
638 optstr := args[0]
639 name := args[1]
640 if !syntax.ValidName(name) {
641 r.errf("getopts: invalid identifier: %q\n", name)
642 return 2
643 }
644 args = args[2:]
645 if len(args) == 0 {
646 args = r.Params
647 }
648 diagnostics := !strings.HasPrefix(optstr, ":")
649
650 opt, optarg, done := r.optState.next(optstr, args)
651
652 r.setVarString(name, string(opt))
653 r.delVar("OPTARG")
654 switch {
655 case opt == '?' && diagnostics && !done:
656 r.errf("getopts: illegal option -- %q\n", optarg)
657 case opt == ':' && diagnostics:
658 r.errf("getopts: option requires an argument -- %q\n", optarg)
659 default:
660 if optarg != "" {
661 r.setVarString("OPTARG", optarg)
662 }
663 }
664 if optind-1 != r.optState.argidx {
665 r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10))
666 }
667
668 return oneIf(done)
669
670 case "shopt":
671 mode := ""
672 posixOpts := false
673 fp := flagParser{remaining: args}
674 for fp.more() {
675 switch flag := fp.flag(); flag {
676 case "-s", "-u":
677 mode = flag
678 case "-o":
679 posixOpts = true
680 case "-p", "-q":
681 panic(fmt.Sprintf("unhandled shopt flag: %s", flag))
682 default:
683 r.errf("shopt: invalid option %q\n", flag)
684 return 2
685 }
686 }
687 args := fp.args()
688 bash := !posixOpts
689 if len(args) == 0 {
690 if bash {
691 for i, opt := range bashOptsTable {
692 r.printOptLine(opt.name, r.opts[len(shellOptsTable)+i], opt.supported)
693 }
694 break
695 }
696 for i, opt := range &shellOptsTable {
697 r.printOptLine(opt.name, r.opts[i], true)
698 }
699 break
700 }
701 for _, arg := range args {
702 i, opt := r.optByName(arg, bash)
703 if opt == nil {
704 r.errf("shopt: invalid option name %q\n", arg)
705 return 1
706 }
707
708 var (
709 bo *bashOpt
710 supported = true // default for shell options
711 )
712 if bash {
713 bo = &bashOptsTable[i-len(shellOptsTable)]
714 supported = bo.supported
715 }
716
717 switch mode {
718 case "-s", "-u":
719 if bash && !supported {
720 r.errf("shopt: invalid option name %q %q (%q not supported)\n", arg, r.optStatusText(bo.defaultState), r.optStatusText(!bo.defaultState))
721 return 1
722 }
723 *opt = mode == "-s"
724 default: // ""
725 r.printOptLine(arg, *opt, supported)
726 }
727 }
728 r.updateExpandOpts()
729
730 case "alias":
731 show := func(name string, als alias) {
732 var buf bytes.Buffer
733 if len(als.args) > 0 {
734 printer := syntax.NewPrinter()
735 printer.Print(&buf, &syntax.CallExpr{
736 Args: als.args,
737 })
738 }
739 if als.blank {
740 buf.WriteByte(' ')
741 }
742 r.outf("alias %s='%s'\n", name, &buf)
743 }
744
745 if len(args) == 0 {
746 for name, als := range r.alias {
747 show(name, als)
748 }
749 }
750 argsLoop:
751 for _, name := range args {
752 i := strings.IndexByte(name, '=')
753 if i < 1 { // don't save an empty name
754 als, ok := r.alias[name]
755 if !ok {
756 r.errf("alias: %q not found\n", name)
757 continue
758 }
759 show(name, als)
760 continue
761 }
762
763 // TODO: parse any CallExpr perhaps, or even any Stmt
764 parser := syntax.NewParser()
765 var words []*syntax.Word
766 src := name[i+1:]
767 for w, err := range parser.WordsSeq(strings.NewReader(src)) {
768 if err != nil {
769 r.errf("alias: could not parse %q: %v\n", src, err)
770 continue argsLoop
771 }
772 words = append(words, w)
773 }
774
775 name = name[:i]
776 if r.alias == nil {
777 r.alias = make(map[string]alias)
778 }
779 r.alias[name] = alias{
780 args: words,
781 blank: strings.TrimRight(src, " \t") != src,
782 }
783 }
784 case "unalias":
785 for _, name := range args {
786 delete(r.alias, name)
787 }
788
789 case "trap":
790 fp := flagParser{remaining: args}
791 callback := "-"
792 for fp.more() {
793 switch flag := fp.flag(); flag {
794 case "-l", "-p":
795 r.errf("trap: %q: NOT IMPLEMENTED flag\n", flag)
796 return 2
797 case "-":
798 // default signal
799 default:
800 r.errf("trap: %q: invalid option\n", flag)
801 r.errf("trap: usage: trap [-lp] [[arg] signal_spec ...]\n")
802 return 2
803 }
804 }
805 args := fp.args()
806 switch len(args) {
807 case 0:
808 // Print non-default signals
809 if r.callbackExit != "" {
810 r.outf("trap -- %q EXIT\n", r.callbackExit)
811 }
812 if r.callbackErr != "" {
813 r.outf("trap -- %q ERR\n", r.callbackErr)
814 }
815 case 1:
816 // assume it's a signal, the default will be restored
817 default:
818 callback = args[0]
819 args = args[1:]
820 }
821 // For now, treat both empty and - the same since ERR and EXIT have no
822 // default callback.
823 if callback == "-" {
824 callback = ""
825 }
826 for _, arg := range args {
827 switch arg {
828 case "ERR":
829 r.callbackErr = callback
830 case "EXIT":
831 r.callbackExit = callback
832 default:
833 r.errf("trap: %s: invalid signal specification\n", arg)
834 return 2
835 }
836 }
837
838 case "readarray", "mapfile":
839 dropDelim := false
840 delim := "\n"
841 fp := flagParser{remaining: args}
842 for fp.more() {
843 switch flag := fp.flag(); flag {
844 case "-t":
845 // Remove the delim from each line read
846 dropDelim = true
847 case "-d":
848 if len(fp.remaining) == 0 {
849 r.errf("%s: -d: option requires an argument\n", name)
850 return 2
851 }
852 delim = fp.value()
853 if delim == "" {
854 // Bash sets the delim to an ASCII NUL if provided with an empty
855 // string.
856 delim = "\x00"
857 }
858 default:
859 r.errf("%s: invalid option %q\n", name, flag)
860 return 2
861 }
862 }
863
864 args := fp.args()
865 var arrayName string
866 switch len(args) {
867 case 0:
868 arrayName = "MAPFILE"
869 case 1:
870 if !syntax.ValidName(args[0]) {
871 r.errf("%s: invalid identifier %q\n", name, args[0])
872 return 2
873 }
874 arrayName = args[0]
875 default:
876 r.errf("%s: Only one array name may be specified, %v\n", name, args)
877 return 2
878 }
879
880 var vr expand.Variable
881 vr.Kind = expand.Indexed
882 scanner := bufio.NewScanner(r.stdin)
883 scanner.Split(mapfileSplit(delim[0], dropDelim))
884 for scanner.Scan() {
885 vr.List = append(vr.List, scanner.Text())
886 }
887 if err := scanner.Err(); err != nil {
888 r.errf("%s: unable to read, %v\n", name, err)
889 return 2
890 }
891 r.setVar(arrayName, vr)
892
893 return 0
894
895 default:
896 // "umask", "fg", "bg",
897 r.errf("%s: unimplemented builtin\n", name)
898 return 2
899 }
900 return 0
901}
902
903// mapfileSplit returns a suitable Split function for a [bufio.Scanner];
904// the code is mostly stolen from [bufio.ScanLines].
905func mapfileSplit(delim byte, dropDelim bool) bufio.SplitFunc {
906 return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
907 if atEOF && len(data) == 0 {
908 return 0, nil, nil
909 }
910 if i := bytes.IndexByte(data, delim); i >= 0 {
911 // We have a full newline-terminated line.
912 if dropDelim {
913 return i + 1, data[0:i], nil
914 } else {
915 return i + 1, data[0 : i+1], nil
916 }
917 }
918 // If we're at EOF, we have a final, non-terminated line. Return it.
919 if atEOF {
920 return len(data), data, nil
921 }
922 // Request more data.
923 return 0, nil, nil
924 }
925}
926
927func (r *Runner) printOptLine(name string, enabled, supported bool) {
928 state := r.optStatusText(enabled)
929 if supported {
930 r.outf("%s\t%s\n", name, state)
931 return
932 }
933 r.outf("%s\t%s\t(%q not supported)\n", name, state, r.optStatusText(!enabled))
934}
935
936func (r *Runner) readLine(ctx context.Context, raw bool) ([]byte, error) {
937 if r.stdin == nil {
938 return nil, errors.New("interp: can't read, there's no stdin")
939 }
940
941 var line []byte
942 esc := false
943
944 stopc := make(chan struct{})
945 stop := context.AfterFunc(ctx, func() {
946 r.stdin.SetReadDeadline(time.Now())
947 close(stopc)
948 })
949 defer func() {
950 if !stop() {
951 // The AfterFunc was started.
952 // Wait for it to complete, and reset the file's deadline.
953 <-stopc
954 r.stdin.SetReadDeadline(time.Time{})
955 }
956 }()
957 for {
958 var buf [1]byte
959 n, err := r.stdin.Read(buf[:])
960 if n > 0 {
961 b := buf[0]
962 switch {
963 case !raw && b == '\\':
964 line = append(line, b)
965 esc = !esc
966 case !raw && b == '\n' && esc:
967 // line continuation
968 line = line[len(line)-1:]
969 esc = false
970 case b == '\n':
971 return line, nil
972 default:
973 line = append(line, b)
974 esc = false
975 }
976 }
977 if err != nil {
978 return line, err
979 }
980 }
981}
982
983func (r *Runner) changeDir(ctx context.Context, path string) int {
984 path = cmp.Or(path, ".")
985 path = r.absPath(path)
986 info, err := r.stat(ctx, path)
987 if err != nil || !info.IsDir() {
988 return 1
989 }
990 if r.access(ctx, path, access_X_OK) != nil {
991 return 1
992 }
993 r.Dir = path
994 r.setVarString("OLDPWD", r.envGet("PWD"))
995 r.setVarString("PWD", path)
996 return 0
997}
998
999func absPath(dir, path string) string {
1000 if path == "" {
1001 return ""
1002 }
1003 if !filepath.IsAbs(path) {
1004 path = filepath.Join(dir, path)
1005 }
1006 return filepath.Clean(path) // TODO: this clean is likely unnecessary
1007}
1008
1009func (r *Runner) absPath(path string) string {
1010 return absPath(r.Dir, path)
1011}
1012
1013// flagParser is used to parse builtin flags.
1014//
1015// It's similar to the getopts implementation, but with some key differences.
1016// First, the API is designed for Go loops, making it easier to use directly.
1017// Second, it doesn't require the awkward ":ab" syntax that getopts uses.
1018// Third, it supports "-a" flags as well as "+a".
1019type flagParser struct {
1020 current string
1021 remaining []string
1022}
1023
1024func (p *flagParser) more() bool {
1025 if p.current != "" {
1026 // We're still parsing part of "-ab".
1027 return true
1028 }
1029 if len(p.remaining) == 0 {
1030 // Nothing left.
1031 p.remaining = nil
1032 return false
1033 }
1034 arg := p.remaining[0]
1035 if arg == "--" {
1036 // We explicitly stop parsing flags.
1037 p.remaining = p.remaining[1:]
1038 return false
1039 }
1040 if len(arg) == 0 || (arg[0] != '-' && arg[0] != '+') {
1041 // The next argument is not a flag.
1042 return false
1043 }
1044 // More flags to come.
1045 return true
1046}
1047
1048func (p *flagParser) flag() string {
1049 arg := p.current
1050 if arg == "" {
1051 arg = p.remaining[0]
1052 p.remaining = p.remaining[1:]
1053 } else {
1054 p.current = ""
1055 }
1056 if len(arg) > 2 {
1057 // We have "-ab", so return "-a" and keep "-b".
1058 p.current = arg[:1] + arg[2:]
1059 arg = arg[:2]
1060 }
1061 return arg
1062}
1063
1064func (p *flagParser) value() string {
1065 if len(p.remaining) == 0 {
1066 return ""
1067 }
1068 arg := p.remaining[0]
1069 p.remaining = p.remaining[1:]
1070 return arg
1071}
1072
1073func (p *flagParser) args() []string { return p.remaining }
1074
1075type getopts struct {
1076 argidx int
1077 runeidx int
1078}
1079
1080func (g *getopts) next(optstr string, args []string) (opt rune, optarg string, done bool) {
1081 if len(args) == 0 || g.argidx >= len(args) {
1082 return '?', "", true
1083 }
1084 arg := []rune(args[g.argidx])
1085 if len(arg) < 2 || arg[0] != '-' || arg[1] == '-' {
1086 return '?', "", true
1087 }
1088
1089 opts := arg[1:]
1090 opt = opts[g.runeidx]
1091 if g.runeidx+1 < len(opts) {
1092 g.runeidx++
1093 } else {
1094 g.argidx++
1095 g.runeidx = 0
1096 }
1097
1098 i := strings.IndexRune(optstr, opt)
1099 if i < 0 {
1100 // invalid option
1101 return '?', string(opt), false
1102 }
1103
1104 if i+1 < len(optstr) && optstr[i+1] == ':' {
1105 if g.argidx >= len(args) {
1106 // missing argument
1107 return ':', string(opt), false
1108 }
1109 optarg = args[g.argidx]
1110 g.argidx++
1111 g.runeidx = 0
1112 }
1113
1114 return opt, optarg, false
1115}
1116
1117// optStatusText returns a shell option's status text display
1118func (r *Runner) optStatusText(status bool) string {
1119 if status {
1120 return "on"
1121 }
1122 return "off"
1123}