trace.go

  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}