1package interp
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "strings"
8
9 "mvdan.cc/sh/v3/syntax"
10)
11
12// tracer prints expressions like a shell would do if its
13// options '-o' is set to either 'xtrace' or its shorthand, '-x'.
14type tracer struct {
15 buf bytes.Buffer
16 printer *syntax.Printer
17 output io.Writer
18 needsPlus bool
19}
20
21func (r *Runner) tracer() *tracer {
22 if !r.opts[optXTrace] {
23 return nil
24 }
25
26 return &tracer{
27 printer: syntax.NewPrinter(),
28 output: r.stderr,
29 needsPlus: true,
30 }
31}
32
33// string writes s to tracer.buf if tracer is non-nil,
34// prepending "+" if tracer.needsPlus is true.
35func (t *tracer) string(s string) {
36 if t == nil {
37 return
38 }
39
40 if t.needsPlus {
41 t.buf.WriteString("+ ")
42 }
43 t.needsPlus = false
44 t.buf.WriteString(s)
45}
46
47func (t *tracer) stringf(f string, a ...any) {
48 if t == nil {
49 return
50 }
51
52 t.string(fmt.Sprintf(f, a...))
53}
54
55// expr prints x to tracer.buf if tracer is non-nil,
56// prepending "+" if tracer.isFirstPrint is true.
57func (t *tracer) expr(x syntax.Node) {
58 if t == nil {
59 return
60 }
61
62 if t.needsPlus {
63 t.buf.WriteString("+ ")
64 }
65 t.needsPlus = false
66 if err := t.printer.Print(&t.buf, x); err != nil {
67 panic(err)
68 }
69}
70
71// flush writes the contents of tracer.buf to the tracer.stdout.
72func (t *tracer) flush() {
73 if t == nil {
74 return
75 }
76
77 t.output.Write(t.buf.Bytes())
78 t.buf.Reset()
79}
80
81// newLineFlush is like flush, but with extra new line before tracer.buf gets flushed.
82func (t *tracer) newLineFlush() {
83 if t == nil {
84 return
85 }
86
87 t.buf.WriteString("\n")
88 t.flush()
89 // reset state
90 t.needsPlus = true
91}
92
93// call prints a command and its arguments with varying formats depending on the cmd type,
94// for example, built-in command's arguments are printed enclosed in single quotes,
95// otherwise, call defaults to printing with double quotes.
96func (t *tracer) call(cmd string, args ...string) {
97 if t == nil {
98 return
99 }
100
101 s := strings.Join(args, " ")
102 if strings.TrimSpace(s) == "" {
103 // fields may be empty for function () {} declarations
104 t.string(cmd)
105 } else if isBuiltin(cmd) {
106 if cmd == "set" {
107 // TODO: only first occurrence of set is not printed, succeeding calls are printed
108 return
109 }
110
111 qs, err := syntax.Quote(s, syntax.LangBash)
112 if err != nil { // should never happen
113 panic(err)
114 }
115 t.stringf("%s %s", cmd, qs)
116 } else {
117 t.stringf("%s %s", cmd, s)
118 }
119}