printer.go

   1// Copyright (c) 2016, Daniel MartΓ­ <mvdan@mvdan.cc>
   2// See LICENSE for licensing information
   3
   4package syntax
   5
   6import (
   7	"bufio"
   8	"bytes"
   9	"fmt"
  10	"io"
  11	"strings"
  12	"text/tabwriter"
  13	"unicode"
  14
  15	"mvdan.cc/sh/v3/fileutil"
  16)
  17
  18// PrinterOption is a function which can be passed to NewPrinter
  19// to alter its behavior. To apply option to existing Printer
  20// call it directly, for example KeepPadding(true)(printer).
  21type PrinterOption func(*Printer)
  22
  23// Indent sets the number of spaces used for indentation. If set to 0,
  24// tabs will be used instead.
  25func Indent(spaces uint) PrinterOption {
  26	return func(p *Printer) { p.indentSpaces = spaces }
  27}
  28
  29// BinaryNextLine will make binary operators appear on the next line
  30// when a binary command, such as a pipe, spans multiple lines. A
  31// backslash will be used.
  32func BinaryNextLine(enabled bool) PrinterOption {
  33	return func(p *Printer) { p.binNextLine = enabled }
  34}
  35
  36// SwitchCaseIndent will make switch cases be indented. As such, switch
  37// case bodies will be two levels deeper than the switch itself.
  38func SwitchCaseIndent(enabled bool) PrinterOption {
  39	return func(p *Printer) { p.swtCaseIndent = enabled }
  40}
  41
  42// TODO(v4): consider turning this into a "space all operators" option, to also
  43// allow foo=( bar baz ), (( x + y )), and so on.
  44
  45// SpaceRedirects will put a space after most redirection operators. The
  46// exceptions are '>&', '<&', '>(', and '<('.
  47func SpaceRedirects(enabled bool) PrinterOption {
  48	return func(p *Printer) { p.spaceRedirects = enabled }
  49}
  50
  51// KeepPadding will keep most nodes and tokens in the same column that
  52// they were in the original source. This allows the user to decide how
  53// to align and pad their code with spaces.
  54//
  55// Note that this feature is best-effort and will only keep the
  56// alignment stable, so it may need some human help the first time it is
  57// run.
  58//
  59// Deprecated: this formatting option is flawed and buggy, and often does
  60// not result in what the user wants when the code gets complex enough.
  61// The next major version, v4, will remove this feature entirely.
  62// See: https://github.com/mvdan/sh/issues/658
  63func KeepPadding(enabled bool) PrinterOption {
  64	return func(p *Printer) {
  65		if enabled && !p.keepPadding {
  66			// Enable the flag, and set up the writer wrapper.
  67			p.keepPadding = true
  68			p.cols.Writer = p.bufWriter.(*bufio.Writer)
  69			p.bufWriter = &p.cols
  70
  71		} else if !enabled && p.keepPadding {
  72			// Ensure we reset the state to that of NewPrinter.
  73			p.keepPadding = false
  74			p.bufWriter = p.cols.Writer
  75			p.cols = colCounter{}
  76		}
  77	}
  78}
  79
  80// Minify will print programs in a way to save the most bytes possible.
  81// For example, indentation and comments are skipped, and extra
  82// whitespace is avoided when possible.
  83func Minify(enabled bool) PrinterOption {
  84	return func(p *Printer) { p.minify = enabled }
  85}
  86
  87// SingleLine will attempt to print programs in one line. For example, lists of
  88// commands or nested blocks do not use newlines in this mode. Note that some
  89// newlines must still appear, such as those following comments or around
  90// here-documents.
  91//
  92// Print's trailing newline when given a [*File] is not affected by this option.
  93func SingleLine(enabled bool) PrinterOption {
  94	return func(p *Printer) { p.singleLine = enabled }
  95}
  96
  97// FunctionNextLine will place a function's opening braces on the next line.
  98func FunctionNextLine(enabled bool) PrinterOption {
  99	return func(p *Printer) { p.funcNextLine = enabled }
 100}
 101
 102// NewPrinter allocates a new Printer and applies any number of options.
 103func NewPrinter(opts ...PrinterOption) *Printer {
 104	p := &Printer{
 105		bufWriter: bufio.NewWriter(nil),
 106		tabWriter: new(tabwriter.Writer),
 107	}
 108	for _, opt := range opts {
 109		opt(p)
 110	}
 111	return p
 112}
 113
 114// Print "pretty-prints" the given syntax tree node to the given writer. Writes
 115// to w are buffered.
 116//
 117// The node types supported at the moment are [*File], [*Stmt], [*Word], [*Assign], any
 118// [Command] node, and any WordPart node. A trailing newline will only be printed
 119// when a [*File] is used.
 120func (p *Printer) Print(w io.Writer, node Node) error {
 121	p.reset()
 122
 123	if p.minify && p.singleLine {
 124		return fmt.Errorf("Minify and SingleLine together are not supported yet; please file an issue describing your use case: https://github.com/mvdan/sh/issues")
 125	}
 126
 127	// TODO: consider adding a raw mode to skip the tab writer, much like in
 128	// go/printer.
 129	twmode := tabwriter.DiscardEmptyColumns | tabwriter.StripEscape
 130	tabwidth := 8
 131	if p.indentSpaces == 0 {
 132		// indenting with tabs
 133		twmode |= tabwriter.TabIndent
 134	} else {
 135		// indenting with spaces
 136		tabwidth = int(p.indentSpaces)
 137	}
 138	p.tabWriter.Init(w, 0, tabwidth, 1, ' ', twmode)
 139	w = p.tabWriter
 140
 141	p.bufWriter.Reset(w)
 142	switch node := node.(type) {
 143	case *File:
 144		p.stmtList(node.Stmts, node.Last)
 145		p.newline(Pos{})
 146	case *Stmt:
 147		p.stmtList([]*Stmt{node}, nil)
 148	case Command:
 149		p.command(node, nil)
 150	case *Word:
 151		p.line = node.Pos().Line()
 152		p.word(node)
 153	case WordPart:
 154		p.line = node.Pos().Line()
 155		p.wordPart(node, nil)
 156	case *Assign:
 157		p.line = node.Pos().Line()
 158		p.assigns([]*Assign{node})
 159	default:
 160		return fmt.Errorf("unsupported node type: %T", node)
 161	}
 162	p.flushHeredocs()
 163	p.flushComments()
 164
 165	// flush the writers
 166	if err := p.bufWriter.Flush(); err != nil {
 167		return err
 168	}
 169	if tw, _ := w.(*tabwriter.Writer); tw != nil {
 170		if err := tw.Flush(); err != nil {
 171			return err
 172		}
 173	}
 174	return nil
 175}
 176
 177type bufWriter interface {
 178	Write([]byte) (int, error)
 179	WriteString(string) (int, error)
 180	WriteByte(byte) error
 181	Reset(io.Writer)
 182	Flush() error
 183}
 184
 185type colCounter struct {
 186	*bufio.Writer
 187	column    int
 188	lineStart bool
 189}
 190
 191func (c *colCounter) addByte(b byte) {
 192	switch b {
 193	case '\n':
 194		c.column = 0
 195		c.lineStart = true
 196	case '\t', ' ', tabwriter.Escape:
 197	default:
 198		c.lineStart = false
 199	}
 200	c.column++
 201}
 202
 203func (c *colCounter) WriteByte(b byte) error {
 204	c.addByte(b)
 205	return c.Writer.WriteByte(b)
 206}
 207
 208func (c *colCounter) WriteString(s string) (int, error) {
 209	for _, b := range []byte(s) {
 210		c.addByte(b)
 211	}
 212	return c.Writer.WriteString(s)
 213}
 214
 215func (c *colCounter) Reset(w io.Writer) {
 216	c.column = 1
 217	c.lineStart = true
 218	c.Writer.Reset(w)
 219}
 220
 221// Printer holds the internal state of the printing mechanism of a
 222// program.
 223type Printer struct {
 224	bufWriter // TODO: embedding this makes the methods part of the API, which we did not intend
 225	tabWriter *tabwriter.Writer
 226	cols      colCounter
 227
 228	indentSpaces   uint
 229	binNextLine    bool
 230	swtCaseIndent  bool
 231	spaceRedirects bool
 232	keepPadding    bool
 233	minify         bool
 234	singleLine     bool
 235	funcNextLine   bool
 236
 237	wantSpace wantSpaceState // whether space is required or has been written
 238
 239	wantNewline bool // newline is wanted for pretty-printing; ignored by singleLine; ignored by singleLine
 240	mustNewline bool // newline is required to keep shell syntax valid
 241	wroteSemi   bool // wrote ';' for the current statement
 242
 243	// pendingComments are any comments in the current line or statement
 244	// that we have yet to print. This is useful because that way, we can
 245	// ensure that all comments are written immediately before a newline.
 246	// Otherwise, in some edge cases we might wrongly place words after a
 247	// comment in the same line, breaking programs.
 248	pendingComments []Comment
 249
 250	// firstLine means we are still writing the first line
 251	firstLine bool
 252	// line is the current line number
 253	line uint
 254
 255	// lastLevel is the last level of indentation that was used.
 256	lastLevel uint
 257	// level is the current level of indentation.
 258	level uint
 259	// levelIncs records which indentation level increments actually
 260	// took place, to revert them once their section ends.
 261	levelIncs []bool
 262
 263	nestedBinary bool
 264
 265	// pendingHdocs is the list of pending heredocs to write.
 266	pendingHdocs []*Redirect
 267
 268	// used when printing <<- heredocs with tab indentation
 269	tabsPrinter *Printer
 270}
 271
 272func (p *Printer) reset() {
 273	p.wantSpace = spaceWritten
 274	p.wantNewline, p.mustNewline = false, false
 275	p.pendingComments = p.pendingComments[:0]
 276
 277	// minification uses its own newline logic
 278	p.firstLine = !p.minify
 279	p.line = 0
 280
 281	p.lastLevel, p.level = 0, 0
 282	p.levelIncs = p.levelIncs[:0]
 283	p.nestedBinary = false
 284	p.pendingHdocs = p.pendingHdocs[:0]
 285}
 286
 287func (p *Printer) spaces(n uint) {
 288	for i := uint(0); i < n; i++ {
 289		p.WriteByte(' ')
 290	}
 291}
 292
 293func (p *Printer) space() {
 294	p.WriteByte(' ')
 295	p.wantSpace = spaceWritten
 296}
 297
 298func (p *Printer) spacePad(pos Pos) {
 299	if p.cols.lineStart && p.indentSpaces == 0 {
 300		// Never add padding at the start of a line unless we are indenting
 301		// with spaces, since this may result in mixing of spaces and tabs.
 302		return
 303	}
 304	if p.wantSpace == spaceRequired {
 305		p.WriteByte(' ')
 306		p.wantSpace = spaceWritten
 307	}
 308	for p.cols.column > 0 && p.cols.column < int(pos.Col()) {
 309		p.WriteByte(' ')
 310	}
 311}
 312
 313// wantsNewline reports whether we want to print at least one newline before
 314// printing a node at a given position. A zero position can be given to simply
 315// tell if we want a newline following what's just been printed.
 316func (p *Printer) wantsNewline(pos Pos, escapingNewline bool) bool {
 317	if p.mustNewline {
 318		// We must have a newline here.
 319		return true
 320	}
 321	if p.singleLine && len(p.pendingComments) == 0 {
 322		// The newline is optional, and singleLine skips it.
 323		// Don't skip if there are any pending comments,
 324		// as that might move them further down to the wrong place.
 325		return false
 326	}
 327	if escapingNewline && p.minify {
 328		return false
 329	}
 330	// The newline is optional, and we want it via either wantNewline or via
 331	// the position's line.
 332	return p.wantNewline || pos.Line() > p.line
 333}
 334
 335func (p *Printer) bslashNewl() {
 336	if p.wantSpace == spaceRequired {
 337		p.space()
 338	}
 339	p.WriteString("\\\n")
 340	p.line++
 341	p.indent()
 342}
 343
 344func (p *Printer) spacedString(s string, pos Pos) {
 345	p.spacePad(pos)
 346	p.WriteString(s)
 347	p.wantSpace = spaceRequired
 348}
 349
 350func (p *Printer) spacedToken(s string, pos Pos) {
 351	if p.minify {
 352		p.WriteString(s)
 353		p.wantSpace = spaceNotRequired
 354		return
 355	}
 356	p.spacePad(pos)
 357	p.WriteString(s)
 358	p.wantSpace = spaceRequired
 359}
 360
 361func (p *Printer) semiOrNewl(s string, pos Pos) {
 362	if p.wantsNewline(Pos{}, false) {
 363		p.newline(pos)
 364		p.indent()
 365	} else {
 366		if !p.wroteSemi {
 367			p.WriteByte(';')
 368		}
 369		if !p.minify {
 370			p.space()
 371		}
 372		p.advanceLine(pos.Line())
 373	}
 374	p.WriteString(s)
 375	p.wantSpace = spaceRequired
 376}
 377
 378func (p *Printer) writeLit(s string) {
 379	// If p.tabWriter is nil, this is the nested printer being used to print
 380	// <<- heredoc bodies, so the parent printer will add the escape bytes
 381	// later.
 382	if p.tabWriter != nil && strings.Contains(s, "\t") {
 383		p.WriteByte(tabwriter.Escape)
 384		defer p.WriteByte(tabwriter.Escape)
 385	}
 386	p.WriteString(s)
 387}
 388
 389func (p *Printer) incLevel() {
 390	inc := false
 391	if p.level <= p.lastLevel || len(p.levelIncs) == 0 {
 392		p.level++
 393		inc = true
 394	} else if last := &p.levelIncs[len(p.levelIncs)-1]; *last {
 395		*last = false
 396		inc = true
 397	}
 398	p.levelIncs = append(p.levelIncs, inc)
 399}
 400
 401func (p *Printer) decLevel() {
 402	if p.levelIncs[len(p.levelIncs)-1] {
 403		p.level--
 404	}
 405	p.levelIncs = p.levelIncs[:len(p.levelIncs)-1]
 406}
 407
 408func (p *Printer) indent() {
 409	if p.minify {
 410		return
 411	}
 412	p.lastLevel = p.level
 413	switch {
 414	case p.level == 0:
 415	case p.indentSpaces == 0:
 416		p.WriteByte(tabwriter.Escape)
 417		for i := uint(0); i < p.level; i++ {
 418			p.WriteByte('\t')
 419		}
 420		p.WriteByte(tabwriter.Escape)
 421	default:
 422		p.spaces(p.indentSpaces * p.level)
 423	}
 424}
 425
 426// TODO(mvdan): add an indent call at the end of newline?
 427
 428// newline prints one newline and advances p.line to pos.Line().
 429func (p *Printer) newline(pos Pos) {
 430	p.flushHeredocs()
 431	p.flushComments()
 432	p.WriteByte('\n')
 433	p.wantSpace = spaceWritten
 434	p.wantNewline, p.mustNewline = false, false
 435	p.advanceLine(pos.Line())
 436}
 437
 438func (p *Printer) advanceLine(line uint) {
 439	if p.line < line {
 440		p.line = line
 441	}
 442}
 443
 444func (p *Printer) flushHeredocs() {
 445	if len(p.pendingHdocs) == 0 {
 446		return
 447	}
 448	hdocs := p.pendingHdocs
 449	p.pendingHdocs = p.pendingHdocs[:0]
 450	coms := p.pendingComments
 451	p.pendingComments = nil
 452	if len(coms) > 0 {
 453		c := coms[0]
 454		if c.Pos().Line() == p.line {
 455			p.pendingComments = append(p.pendingComments, c)
 456			p.flushComments()
 457			coms = coms[1:]
 458		}
 459	}
 460
 461	// Reuse the last indentation level, as
 462	// indentation levels are usually changed before
 463	// newlines are printed along with their
 464	// subsequent indentation characters.
 465	newLevel := p.level
 466	p.level = p.lastLevel
 467
 468	for _, r := range hdocs {
 469		p.line++
 470		p.WriteByte('\n')
 471		p.wantSpace = spaceWritten
 472		p.wantNewline, p.wantNewline = false, false
 473		if r.Op == DashHdoc && p.indentSpaces == 0 && !p.minify {
 474			if r.Hdoc != nil {
 475				extra := extraIndenter{
 476					bufWriter:   p.bufWriter,
 477					baseIndent:  int(p.level + 1),
 478					firstIndent: -1,
 479				}
 480				p.tabsPrinter = &Printer{
 481					bufWriter: &extra,
 482
 483					// The options need to persist.
 484					indentSpaces:   p.indentSpaces,
 485					binNextLine:    p.binNextLine,
 486					swtCaseIndent:  p.swtCaseIndent,
 487					spaceRedirects: p.spaceRedirects,
 488					keepPadding:    p.keepPadding,
 489					minify:         p.minify,
 490					funcNextLine:   p.funcNextLine,
 491
 492					line: r.Hdoc.Pos().Line(),
 493				}
 494				p.tabsPrinter.wordParts(r.Hdoc.Parts, true)
 495			}
 496			p.indent()
 497		} else if r.Hdoc != nil {
 498			p.wordParts(r.Hdoc.Parts, true)
 499		}
 500		p.unquotedWord(r.Word)
 501		if r.Hdoc != nil {
 502			// Overwrite p.line, since printing r.Word again can set
 503			// p.line to the beginning of the heredoc again.
 504			p.advanceLine(r.Hdoc.End().Line())
 505		}
 506		p.wantSpace = spaceNotRequired
 507	}
 508	p.level = newLevel
 509	p.pendingComments = coms
 510	p.mustNewline = true
 511}
 512
 513// newline prints between zero and two newlines.
 514// If any newlines are printed, it advances p.line to pos.Line().
 515func (p *Printer) newlines(pos Pos) {
 516	if p.firstLine && len(p.pendingComments) == 0 {
 517		p.firstLine = false
 518		return // no empty lines at the top
 519	}
 520	if !p.wantsNewline(pos, false) {
 521		return
 522	}
 523	p.flushHeredocs()
 524	p.flushComments()
 525	p.WriteByte('\n')
 526	p.wantSpace = spaceWritten
 527	p.wantNewline, p.mustNewline = false, false
 528
 529	l := pos.Line()
 530	if l > p.line+1 && !p.minify {
 531		p.WriteByte('\n') // preserve single empty lines
 532	}
 533	p.advanceLine(l)
 534	p.indent()
 535}
 536
 537func (p *Printer) rightParen(pos Pos) {
 538	if len(p.pendingHdocs) > 0 || !p.minify {
 539		p.newlines(pos)
 540	}
 541	p.WriteByte(')')
 542	p.wantSpace = spaceRequired
 543}
 544
 545func (p *Printer) semiRsrv(s string, pos Pos) {
 546	if p.wantsNewline(pos, false) {
 547		p.newlines(pos)
 548	} else {
 549		if !p.wroteSemi {
 550			p.WriteByte(';')
 551		}
 552		if !p.minify {
 553			p.spacePad(pos)
 554		}
 555	}
 556	p.WriteString(s)
 557	p.wantSpace = spaceRequired
 558}
 559
 560func (p *Printer) flushComments() {
 561	for i, c := range p.pendingComments {
 562		if i == 0 {
 563			// Flush any pending heredocs first. Otherwise, the
 564			// comments would become part of a heredoc body.
 565			p.flushHeredocs()
 566		}
 567		p.firstLine = false
 568		// We can't call any of the newline methods, as they call this
 569		// function and we'd recurse forever.
 570		cline := c.Hash.Line()
 571		switch {
 572		case p.mustNewline, i > 0, cline > p.line && p.line > 0:
 573			p.WriteByte('\n')
 574			if cline > p.line+1 {
 575				p.WriteByte('\n')
 576			}
 577			p.indent()
 578			p.wantSpace = spaceWritten
 579			p.spacePad(c.Pos())
 580		case p.wantSpace == spaceRequired:
 581			if p.keepPadding {
 582				p.spacePad(c.Pos())
 583			} else {
 584				p.WriteByte('\t')
 585			}
 586		case p.wantSpace != spaceWritten:
 587			p.space()
 588		}
 589		// don't go back one line, which may happen in some edge cases
 590		p.advanceLine(cline)
 591		p.WriteByte('#')
 592		p.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace))
 593		p.wantNewline = true
 594		p.mustNewline = true
 595	}
 596	p.pendingComments = nil
 597}
 598
 599func (p *Printer) comments(comments ...Comment) {
 600	if p.minify {
 601		for _, c := range comments {
 602			if fileutil.Shebang([]byte("#"+c.Text)) != "" && c.Hash.Col() == 1 && c.Hash.Line() == 1 {
 603				p.WriteString(strings.TrimRightFunc("#"+c.Text, unicode.IsSpace))
 604				p.WriteString("\n")
 605				p.line++
 606			}
 607		}
 608		return
 609	}
 610	p.pendingComments = append(p.pendingComments, comments...)
 611}
 612
 613func (p *Printer) wordParts(wps []WordPart, quoted bool) {
 614	// We disallow unquoted escaped newlines between word parts below.
 615	// However, we want to allow a leading escaped newline for cases such as:
 616	//
 617	//   foo <<< \
 618	//     "bar baz"
 619	if !quoted && !p.singleLine && wps[0].Pos().Line() > p.line {
 620		p.bslashNewl()
 621	}
 622	for i, wp := range wps {
 623		var next WordPart
 624		if i+1 < len(wps) {
 625			next = wps[i+1]
 626		}
 627		// Keep escaped newlines separating word parts when quoted.
 628		// Note that those escaped newlines don't cause indentaiton.
 629		// When not quoted, we strip them out consistently,
 630		// because attempting to keep them would prevent indentation.
 631		// Can't use p.wantsNewline here, since this is only about
 632		// escaped newlines.
 633		for quoted && !p.singleLine && wp.Pos().Line() > p.line {
 634			p.WriteString("\\\n")
 635			p.line++
 636		}
 637		p.wordPart(wp, next)
 638		p.advanceLine(wp.End().Line())
 639	}
 640}
 641
 642func (p *Printer) wordPart(wp, next WordPart) {
 643	switch wp := wp.(type) {
 644	case *Lit:
 645		p.writeLit(wp.Value)
 646	case *SglQuoted:
 647		if wp.Dollar {
 648			p.WriteByte('$')
 649		}
 650		p.WriteByte('\'')
 651		p.writeLit(wp.Value)
 652		p.WriteByte('\'')
 653		p.advanceLine(wp.End().Line())
 654	case *DblQuoted:
 655		p.dblQuoted(wp)
 656	case *CmdSubst:
 657		p.advanceLine(wp.Pos().Line())
 658		switch {
 659		case wp.TempFile:
 660			p.WriteString("${")
 661			p.wantSpace = spaceRequired
 662			p.nestedStmts(wp.Stmts, wp.Last, wp.Right)
 663			p.wantSpace = spaceNotRequired
 664			p.semiRsrv("}", wp.Right)
 665		case wp.ReplyVar:
 666			p.WriteString("${|")
 667			p.nestedStmts(wp.Stmts, wp.Last, wp.Right)
 668			p.wantSpace = spaceNotRequired
 669			p.semiRsrv("}", wp.Right)
 670		// Special case: `# inline comment`
 671		case wp.Backquotes && len(wp.Stmts) == 0 &&
 672			len(wp.Last) == 1 && wp.Right.Line() == p.line:
 673			p.WriteString("`#")
 674			p.WriteString(wp.Last[0].Text)
 675			p.WriteString("`")
 676		default:
 677			p.WriteString("$(")
 678			if len(wp.Stmts) > 0 && startsWithLparen(wp.Stmts[0]) {
 679				p.wantSpace = spaceRequired
 680			} else {
 681				p.wantSpace = spaceNotRequired
 682			}
 683			p.nestedStmts(wp.Stmts, wp.Last, wp.Right)
 684			p.rightParen(wp.Right)
 685		}
 686	case *ParamExp:
 687		litCont := ";"
 688		if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" {
 689			litCont = nextLit.Value[:1]
 690		}
 691		name := wp.Param.Value
 692		switch {
 693		case !p.minify:
 694		case wp.Excl, wp.Length, wp.Width:
 695		case wp.Index != nil, wp.Slice != nil:
 696		case wp.Repl != nil, wp.Exp != nil:
 697		case len(name) > 1 && !ValidName(name): // ${10}
 698		case ValidName(name + litCont): // ${var}cont
 699		default:
 700			x2 := *wp
 701			x2.Short = true
 702			p.paramExp(&x2)
 703			return
 704		}
 705		p.paramExp(wp)
 706	case *ArithmExp:
 707		p.WriteString("$((")
 708		if wp.Unsigned {
 709			p.WriteString("# ")
 710		}
 711		p.arithmExpr(wp.X, false, false)
 712		p.WriteString("))")
 713	case *ExtGlob:
 714		p.WriteString(wp.Op.String())
 715		p.writeLit(wp.Pattern.Value)
 716		p.WriteByte(')')
 717	case *ProcSubst:
 718		// avoid conflict with << and others
 719		if p.wantSpace == spaceRequired {
 720			p.space()
 721		}
 722		p.WriteString(wp.Op.String())
 723		p.nestedStmts(wp.Stmts, wp.Last, wp.Rparen)
 724		p.rightParen(wp.Rparen)
 725	}
 726}
 727
 728func (p *Printer) dblQuoted(dq *DblQuoted) {
 729	if dq.Dollar {
 730		p.WriteByte('$')
 731	}
 732	p.WriteByte('"')
 733	if len(dq.Parts) > 0 {
 734		p.wordParts(dq.Parts, true)
 735	}
 736	// Add any trailing escaped newlines.
 737	for p.line < dq.Right.Line() {
 738		p.WriteString("\\\n")
 739		p.line++
 740	}
 741	p.WriteByte('"')
 742}
 743
 744func (p *Printer) wroteIndex(index ArithmExpr) bool {
 745	if index == nil {
 746		return false
 747	}
 748	p.WriteByte('[')
 749	p.arithmExpr(index, false, false)
 750	p.WriteByte(']')
 751	return true
 752}
 753
 754func (p *Printer) paramExp(pe *ParamExp) {
 755	if pe.nakedIndex() { // arr[x]
 756		p.writeLit(pe.Param.Value)
 757		p.wroteIndex(pe.Index)
 758		return
 759	}
 760	if pe.Short { // $var
 761		p.WriteByte('$')
 762		p.writeLit(pe.Param.Value)
 763		return
 764	}
 765	// ${var...}
 766	p.WriteString("${")
 767	switch {
 768	case pe.Length:
 769		p.WriteByte('#')
 770	case pe.Width:
 771		p.WriteByte('%')
 772	case pe.Excl:
 773		p.WriteByte('!')
 774	}
 775	p.writeLit(pe.Param.Value)
 776	p.wroteIndex(pe.Index)
 777	switch {
 778	case pe.Slice != nil:
 779		p.WriteByte(':')
 780		p.arithmExpr(pe.Slice.Offset, true, true)
 781		if pe.Slice.Length != nil {
 782			p.WriteByte(':')
 783			p.arithmExpr(pe.Slice.Length, true, false)
 784		}
 785	case pe.Repl != nil:
 786		if pe.Repl.All {
 787			p.WriteByte('/')
 788		}
 789		p.WriteByte('/')
 790		if pe.Repl.Orig != nil {
 791			p.word(pe.Repl.Orig)
 792		}
 793		p.WriteByte('/')
 794		if pe.Repl.With != nil {
 795			p.word(pe.Repl.With)
 796		}
 797	case pe.Names != 0:
 798		p.writeLit(pe.Names.String())
 799	case pe.Exp != nil:
 800		p.WriteString(pe.Exp.Op.String())
 801		if pe.Exp.Word != nil {
 802			p.word(pe.Exp.Word)
 803		}
 804	}
 805	p.WriteByte('}')
 806}
 807
 808func (p *Printer) loop(loop Loop) {
 809	switch loop := loop.(type) {
 810	case *WordIter:
 811		p.writeLit(loop.Name.Value)
 812		if loop.InPos.IsValid() {
 813			p.spacedString(" in", Pos{})
 814			p.wordJoin(loop.Items)
 815		}
 816	case *CStyleLoop:
 817		p.WriteString("((")
 818		if loop.Init == nil {
 819			p.space()
 820		}
 821		p.arithmExpr(loop.Init, false, false)
 822		p.WriteString("; ")
 823		p.arithmExpr(loop.Cond, false, false)
 824		p.WriteString("; ")
 825		p.arithmExpr(loop.Post, false, false)
 826		p.WriteString("))")
 827	}
 828}
 829
 830func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) {
 831	if p.minify {
 832		compact = true
 833	}
 834	switch expr := expr.(type) {
 835	case *Word:
 836		p.word(expr)
 837	case *BinaryArithm:
 838		if compact {
 839			p.arithmExpr(expr.X, compact, spacePlusMinus)
 840			p.WriteString(expr.Op.String())
 841			p.arithmExpr(expr.Y, compact, false)
 842		} else {
 843			p.arithmExpr(expr.X, compact, spacePlusMinus)
 844			if expr.Op != Comma {
 845				p.space()
 846			}
 847			p.WriteString(expr.Op.String())
 848			p.space()
 849			p.arithmExpr(expr.Y, compact, false)
 850		}
 851	case *UnaryArithm:
 852		if expr.Post {
 853			p.arithmExpr(expr.X, compact, spacePlusMinus)
 854			p.WriteString(expr.Op.String())
 855		} else {
 856			if spacePlusMinus {
 857				switch expr.Op {
 858				case Plus, Minus:
 859					p.space()
 860				}
 861			}
 862			p.WriteString(expr.Op.String())
 863			p.arithmExpr(expr.X, compact, false)
 864		}
 865	case *ParenArithm:
 866		p.WriteByte('(')
 867		p.arithmExpr(expr.X, false, false)
 868		p.WriteByte(')')
 869	}
 870}
 871
 872func (p *Printer) testExpr(expr TestExpr) {
 873	// Multi-line test expressions don't need to escape newlines.
 874	if expr.Pos().Line() > p.line {
 875		p.newlines(expr.Pos())
 876		p.spacePad(expr.Pos())
 877	} else if p.wantSpace == spaceRequired {
 878		p.space()
 879	}
 880	p.testExprSameLine(expr)
 881}
 882
 883func (p *Printer) testExprSameLine(expr TestExpr) {
 884	p.advanceLine(expr.Pos().Line())
 885	switch expr := expr.(type) {
 886	case *Word:
 887		p.word(expr)
 888	case *BinaryTest:
 889		p.testExprSameLine(expr.X)
 890		p.space()
 891		p.WriteString(expr.Op.String())
 892		switch expr.Op {
 893		case AndTest, OrTest:
 894			p.wantSpace = spaceRequired
 895			p.testExpr(expr.Y)
 896		default:
 897			p.space()
 898			p.testExprSameLine(expr.Y)
 899		}
 900	case *UnaryTest:
 901		p.WriteString(expr.Op.String())
 902		p.space()
 903		p.testExprSameLine(expr.X)
 904	case *ParenTest:
 905		p.WriteByte('(')
 906		if startsWithLparen(expr.X) {
 907			p.wantSpace = spaceRequired
 908		} else {
 909			p.wantSpace = spaceNotRequired
 910		}
 911		p.testExpr(expr.X)
 912		p.WriteByte(')')
 913	}
 914}
 915
 916func (p *Printer) word(w *Word) {
 917	p.wordParts(w.Parts, false)
 918	p.wantSpace = spaceRequired
 919}
 920
 921func (p *Printer) unquotedWord(w *Word) {
 922	for _, wp := range w.Parts {
 923		switch wp := wp.(type) {
 924		case *SglQuoted:
 925			p.writeLit(wp.Value)
 926		case *DblQuoted:
 927			p.wordParts(wp.Parts, true)
 928		case *Lit:
 929			for i := 0; i < len(wp.Value); i++ {
 930				if b := wp.Value[i]; b == '\\' {
 931					if i++; i < len(wp.Value) {
 932						p.WriteByte(wp.Value[i])
 933					}
 934				} else {
 935					p.WriteByte(b)
 936				}
 937			}
 938		}
 939	}
 940}
 941
 942func (p *Printer) wordJoin(ws []*Word) {
 943	anyNewline := false
 944	for _, w := range ws {
 945		if pos := w.Pos(); pos.Line() > p.line && !p.singleLine {
 946			if !anyNewline {
 947				p.incLevel()
 948				anyNewline = true
 949			}
 950			p.bslashNewl()
 951		}
 952		p.spacePad(w.Pos())
 953		p.word(w)
 954	}
 955	if anyNewline {
 956		p.decLevel()
 957	}
 958}
 959
 960func (p *Printer) casePatternJoin(pats []*Word) {
 961	anyNewline := false
 962	for i, w := range pats {
 963		if i > 0 {
 964			p.spacedToken("|", Pos{})
 965		}
 966		if p.wantsNewline(w.Pos(), true) {
 967			if !anyNewline {
 968				p.incLevel()
 969				anyNewline = true
 970			}
 971			p.bslashNewl()
 972		} else {
 973			p.spacePad(w.Pos())
 974		}
 975		p.word(w)
 976	}
 977	if anyNewline {
 978		p.decLevel()
 979	}
 980}
 981
 982func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) {
 983	p.incLevel()
 984	for _, el := range elems {
 985		var left []Comment
 986		for _, c := range el.Comments {
 987			if c.Pos().After(el.Pos()) {
 988				left = append(left, c)
 989				break
 990			}
 991			p.comments(c)
 992		}
 993		// Multi-line array expressions don't need to escape newlines.
 994		if el.Pos().Line() > p.line {
 995			p.newlines(el.Pos())
 996			p.spacePad(el.Pos())
 997		} else if p.wantSpace == spaceRequired {
 998			p.space()
 999		}
1000		if p.wroteIndex(el.Index) {
1001			p.WriteByte('=')
1002		}
1003		if el.Value != nil {
1004			p.word(el.Value)
1005		}
1006		p.comments(left...)
1007	}
1008	if len(last) > 0 {
1009		p.comments(last...)
1010		p.flushComments()
1011	}
1012	p.decLevel()
1013}
1014
1015func (p *Printer) stmt(s *Stmt) {
1016	p.wroteSemi = false
1017	if s.Negated {
1018		p.spacedString("!", s.Pos())
1019	}
1020	var startRedirs int
1021	if s.Cmd != nil {
1022		startRedirs = p.command(s.Cmd, s.Redirs)
1023	}
1024	p.incLevel()
1025	for _, r := range s.Redirs[startRedirs:] {
1026		if p.wantsNewline(r.OpPos, true) {
1027			p.bslashNewl()
1028		}
1029		if p.wantSpace == spaceRequired {
1030			p.spacePad(r.Pos())
1031		}
1032		if r.N != nil {
1033			p.writeLit(r.N.Value)
1034		}
1035		p.WriteString(r.Op.String())
1036		if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) {
1037			p.space()
1038		} else {
1039			p.wantSpace = spaceRequired
1040		}
1041		p.word(r.Word)
1042		if r.Op == Hdoc || r.Op == DashHdoc {
1043			p.pendingHdocs = append(p.pendingHdocs, r)
1044		}
1045	}
1046	sep := s.Semicolon.IsValid() && s.Semicolon.Line() > p.line && !p.singleLine
1047	if sep || s.Background || s.Coprocess {
1048		if sep {
1049			p.bslashNewl()
1050		} else if !p.minify {
1051			p.space()
1052		}
1053		if s.Background {
1054			p.WriteString("&")
1055		} else if s.Coprocess {
1056			p.WriteString("|&")
1057		} else {
1058			p.WriteString(";")
1059		}
1060		p.wroteSemi = true
1061		p.wantSpace = spaceRequired
1062	}
1063	p.decLevel()
1064}
1065
1066func (p *Printer) printRedirsUntil(redirs []*Redirect, startRedirs int, pos Pos) int {
1067	for _, r := range redirs[startRedirs:] {
1068		if r.Pos().After(pos) || r.Op == Hdoc || r.Op == DashHdoc {
1069			break
1070		}
1071		if p.wantSpace == spaceRequired {
1072			p.spacePad(r.Pos())
1073		}
1074		if r.N != nil {
1075			p.writeLit(r.N.Value)
1076		}
1077		p.WriteString(r.Op.String())
1078		if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) {
1079			p.space()
1080		} else {
1081			p.wantSpace = spaceRequired
1082		}
1083		p.word(r.Word)
1084		startRedirs++
1085	}
1086	return startRedirs
1087}
1088
1089func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
1090	p.advanceLine(cmd.Pos().Line())
1091	p.spacePad(cmd.Pos())
1092	switch cmd := cmd.(type) {
1093	case *CallExpr:
1094		p.assigns(cmd.Assigns)
1095		if len(cmd.Args) > 0 {
1096			startRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[0].Pos())
1097		}
1098		if len(cmd.Args) <= 1 {
1099			p.wordJoin(cmd.Args)
1100			return startRedirs
1101		}
1102		p.wordJoin(cmd.Args[:1])
1103		startRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[1].Pos())
1104		p.wordJoin(cmd.Args[1:])
1105	case *Block:
1106		p.WriteByte('{')
1107		p.wantSpace = spaceRequired
1108		// Forbid "foo()\n{ bar; }"
1109		p.wantNewline = p.wantNewline || p.funcNextLine
1110		p.nestedStmts(cmd.Stmts, cmd.Last, cmd.Rbrace)
1111		p.semiRsrv("}", cmd.Rbrace)
1112	case *IfClause:
1113		p.ifClause(cmd, false)
1114	case *Subshell:
1115		p.WriteByte('(')
1116		stmts := cmd.Stmts
1117		if len(stmts) > 0 && startsWithLparen(stmts[0]) {
1118			p.wantSpace = spaceRequired
1119			// Add a space between nested parentheses if we're printing them in a single line,
1120			// to avoid the ambiguity between `((` and `( (`.
1121			if (cmd.Lparen.Line() != stmts[0].Pos().Line() || len(stmts) > 1) && !p.singleLine {
1122				p.wantSpace = spaceNotRequired
1123
1124				if p.minify {
1125					p.mustNewline = true
1126				}
1127			}
1128		} else {
1129			p.wantSpace = spaceNotRequired
1130		}
1131
1132		p.spacePad(stmtsPos(cmd.Stmts, cmd.Last))
1133		p.nestedStmts(cmd.Stmts, cmd.Last, cmd.Rparen)
1134		p.wantSpace = spaceNotRequired
1135		p.spacePad(cmd.Rparen)
1136		p.rightParen(cmd.Rparen)
1137	case *WhileClause:
1138		if cmd.Until {
1139			p.spacedString("until", cmd.Pos())
1140		} else {
1141			p.spacedString("while", cmd.Pos())
1142		}
1143		p.nestedStmts(cmd.Cond, cmd.CondLast, Pos{})
1144		p.semiOrNewl("do", cmd.DoPos)
1145		p.nestedStmts(cmd.Do, cmd.DoLast, cmd.DonePos)
1146		p.semiRsrv("done", cmd.DonePos)
1147	case *ForClause:
1148		if cmd.Select {
1149			p.WriteString("select ")
1150		} else {
1151			p.WriteString("for ")
1152		}
1153		p.loop(cmd.Loop)
1154		p.semiOrNewl("do", cmd.DoPos)
1155		p.nestedStmts(cmd.Do, cmd.DoLast, cmd.DonePos)
1156		p.semiRsrv("done", cmd.DonePos)
1157	case *BinaryCmd:
1158		p.stmt(cmd.X)
1159		if p.minify || p.singleLine || cmd.Y.Pos().Line() <= p.line {
1160			// leave p.nestedBinary untouched
1161			p.spacedToken(cmd.Op.String(), cmd.OpPos)
1162			p.advanceLine(cmd.Y.Pos().Line())
1163			p.stmt(cmd.Y)
1164			break
1165		}
1166		indent := !p.nestedBinary
1167		if indent {
1168			p.incLevel()
1169		}
1170		if p.binNextLine {
1171			if len(p.pendingHdocs) == 0 {
1172				p.bslashNewl()
1173			}
1174			p.spacedToken(cmd.Op.String(), cmd.OpPos)
1175			if len(cmd.Y.Comments) > 0 {
1176				p.wantSpace = spaceNotRequired
1177				p.newline(cmd.Y.Pos())
1178				p.indent()
1179				p.comments(cmd.Y.Comments...)
1180				p.newline(Pos{})
1181				p.indent()
1182			}
1183		} else {
1184			p.spacedToken(cmd.Op.String(), cmd.OpPos)
1185			p.advanceLine(cmd.OpPos.Line())
1186			p.comments(cmd.Y.Comments...)
1187			p.newline(Pos{})
1188			p.indent()
1189		}
1190		p.advanceLine(cmd.Y.Pos().Line())
1191		_, p.nestedBinary = cmd.Y.Cmd.(*BinaryCmd)
1192		p.stmt(cmd.Y)
1193		if indent {
1194			p.decLevel()
1195		}
1196		p.nestedBinary = false
1197	case *FuncDecl:
1198		if cmd.RsrvWord {
1199			p.WriteString("function ")
1200		}
1201		p.writeLit(cmd.Name.Value)
1202		if !cmd.RsrvWord || cmd.Parens {
1203			p.WriteString("()")
1204		}
1205		if p.funcNextLine {
1206			p.newline(Pos{})
1207			p.indent()
1208		} else if !cmd.Parens || !p.minify {
1209			p.space()
1210		}
1211		p.advanceLine(cmd.Body.Pos().Line())
1212		p.comments(cmd.Body.Comments...)
1213		p.stmt(cmd.Body)
1214	case *CaseClause:
1215		p.WriteString("case ")
1216		p.word(cmd.Word)
1217		p.WriteString(" in")
1218		p.advanceLine(cmd.In.Line())
1219		p.wantSpace = spaceRequired
1220		if p.swtCaseIndent {
1221			p.incLevel()
1222		}
1223		if len(cmd.Items) == 0 {
1224			// Apparently "case x in; esac" is invalid shell.
1225			p.mustNewline = true
1226		}
1227		for i, ci := range cmd.Items {
1228			var last []Comment
1229			for i, c := range ci.Comments {
1230				if c.Pos().After(ci.Pos()) {
1231					last = ci.Comments[i:]
1232					break
1233				}
1234				p.comments(c)
1235			}
1236			p.newlines(ci.Pos())
1237			p.spacePad(ci.Pos())
1238			p.casePatternJoin(ci.Patterns)
1239			p.WriteByte(')')
1240			if !p.minify {
1241				p.wantSpace = spaceRequired
1242			} else {
1243				p.wantSpace = spaceNotRequired
1244			}
1245
1246			bodyPos := stmtsPos(ci.Stmts, ci.Last)
1247			bodyEnd := stmtsEnd(ci.Stmts, ci.Last)
1248			sep := len(ci.Stmts) > 1 || bodyPos.Line() > p.line ||
1249				(bodyEnd.IsValid() && ci.OpPos.Line() > bodyEnd.Line())
1250			p.nestedStmts(ci.Stmts, ci.Last, ci.OpPos)
1251			p.level++
1252			if !p.minify || i != len(cmd.Items)-1 {
1253				if sep {
1254					p.newlines(ci.OpPos)
1255					p.wantNewline = true
1256				}
1257				p.spacedToken(ci.Op.String(), ci.OpPos)
1258				p.advanceLine(ci.OpPos.Line())
1259				// avoid ; directly after tokens like ;;
1260				p.wroteSemi = true
1261			}
1262			p.comments(last...)
1263			p.flushComments()
1264			p.level--
1265		}
1266		p.comments(cmd.Last...)
1267		if p.swtCaseIndent {
1268			p.flushComments()
1269			p.decLevel()
1270		}
1271		p.semiRsrv("esac", cmd.Esac)
1272	case *ArithmCmd:
1273		p.WriteString("((")
1274		if cmd.Unsigned {
1275			p.WriteString("# ")
1276		}
1277		p.arithmExpr(cmd.X, false, false)
1278		p.WriteString("))")
1279	case *TestClause:
1280		p.WriteString("[[ ")
1281		p.incLevel()
1282		p.testExpr(cmd.X)
1283		p.decLevel()
1284		p.spacedString("]]", cmd.Right)
1285	case *DeclClause:
1286		p.spacedString(cmd.Variant.Value, cmd.Pos())
1287		p.assigns(cmd.Args)
1288	case *TimeClause:
1289		p.spacedString("time", cmd.Pos())
1290		if cmd.PosixFormat {
1291			p.spacedString("-p", cmd.Pos())
1292		}
1293		if cmd.Stmt != nil {
1294			p.stmt(cmd.Stmt)
1295		}
1296	case *CoprocClause:
1297		p.spacedString("coproc", cmd.Pos())
1298		if cmd.Name != nil {
1299			p.space()
1300			p.word(cmd.Name)
1301		}
1302		p.space()
1303		p.stmt(cmd.Stmt)
1304	case *LetClause:
1305		p.spacedString("let", cmd.Pos())
1306		for _, n := range cmd.Exprs {
1307			p.space()
1308			p.arithmExpr(n, true, false)
1309		}
1310	case *TestDecl:
1311		p.spacedString("@test", cmd.Pos())
1312		p.space()
1313		p.word(cmd.Description)
1314		p.space()
1315		p.stmt(cmd.Body)
1316	default:
1317		panic(fmt.Sprintf("syntax.Printer: unexpected node type %T", cmd))
1318	}
1319	return startRedirs
1320}
1321
1322func (p *Printer) ifClause(ic *IfClause, elif bool) {
1323	if !elif {
1324		p.spacedString("if", ic.Pos())
1325	}
1326	p.nestedStmts(ic.Cond, ic.CondLast, Pos{})
1327	p.semiOrNewl("then", ic.ThenPos)
1328	thenEnd := ic.FiPos
1329	el := ic.Else
1330	if el != nil {
1331		thenEnd = el.Position
1332	}
1333	p.nestedStmts(ic.Then, ic.ThenLast, thenEnd)
1334
1335	if el != nil && el.ThenPos.IsValid() {
1336		p.comments(ic.Last...)
1337		p.semiRsrv("elif", el.Position)
1338		p.ifClause(el, true)
1339		return
1340	}
1341	if el == nil {
1342		p.comments(ic.Last...)
1343	} else {
1344		var left []Comment
1345		for _, c := range ic.Last {
1346			if c.Pos().After(el.Position) {
1347				left = append(left, c)
1348				break
1349			}
1350			p.comments(c)
1351		}
1352		p.semiRsrv("else", el.Position)
1353		p.comments(left...)
1354		p.nestedStmts(el.Then, el.ThenLast, ic.FiPos)
1355		p.comments(el.Last...)
1356	}
1357	p.semiRsrv("fi", ic.FiPos)
1358}
1359
1360func (p *Printer) stmtList(stmts []*Stmt, last []Comment) {
1361	sep := p.wantNewline || (len(stmts) > 0 && stmts[0].Pos().Line() > p.line)
1362	for i, s := range stmts {
1363		if i > 0 && p.singleLine && p.wantNewline && !p.wroteSemi {
1364			// In singleLine mode, ensure we use semicolons between
1365			// statements.
1366			p.WriteByte(';')
1367			p.wantSpace = spaceRequired
1368		}
1369		pos := s.Pos()
1370		var midComs, endComs []Comment
1371		for _, c := range s.Comments {
1372			// Comments after the end of this command. Note that
1373			// this includes "<<EOF # comment".
1374			if s.Cmd != nil && c.End().After(s.Cmd.End()) {
1375				endComs = append(endComs, c)
1376				break
1377			}
1378			// Comments between the beginning of the statement and
1379			// the end of the command.
1380			if c.Pos().After(pos) {
1381				midComs = append(midComs, c)
1382				continue
1383			}
1384			// The rest of the comments are before the entire
1385			// statement.
1386			p.comments(c)
1387		}
1388		if p.mustNewline || !p.minify || p.wantSpace == spaceRequired {
1389			p.newlines(pos)
1390		}
1391		p.advanceLine(pos.Line())
1392		p.comments(midComs...)
1393		p.stmt(s)
1394		p.comments(endComs...)
1395		p.wantNewline = true
1396	}
1397	if len(stmts) == 1 && !sep {
1398		p.wantNewline = false
1399	}
1400	p.comments(last...)
1401}
1402
1403func (p *Printer) nestedStmts(stmts []*Stmt, last []Comment, closing Pos) {
1404	p.incLevel()
1405	switch {
1406	case len(stmts) > 1:
1407		// Force a newline if we find:
1408		//     { stmt; stmt; }
1409		p.wantNewline = true
1410	case closing.Line() > p.line && len(stmts) > 0 &&
1411		stmtsEnd(stmts, last).Line() < closing.Line():
1412		// Force a newline if we find:
1413		//     { stmt
1414		//     }
1415		p.wantNewline = true
1416	case len(p.pendingComments) > 0 && len(stmts) > 0:
1417		// Force a newline if we find:
1418		//     for i in a b # stmt
1419		//     do foo; done
1420		p.wantNewline = true
1421	}
1422	p.stmtList(stmts, last)
1423	if closing.IsValid() {
1424		p.flushComments()
1425	}
1426	p.decLevel()
1427}
1428
1429func (p *Printer) assigns(assigns []*Assign) {
1430	p.incLevel()
1431	for _, a := range assigns {
1432		if p.wantsNewline(a.Pos(), true) {
1433			p.bslashNewl()
1434		} else {
1435			p.spacePad(a.Pos())
1436		}
1437		if a.Name != nil {
1438			p.writeLit(a.Name.Value)
1439			p.wroteIndex(a.Index)
1440			if a.Append {
1441				p.WriteByte('+')
1442			}
1443			if !a.Naked {
1444				p.WriteByte('=')
1445			}
1446		}
1447		if a.Value != nil {
1448			// Ensure we don't use an escaped newline after '=',
1449			// because that can result in indentation, thus
1450			// splitting "foo=bar" into "foo= bar".
1451			p.advanceLine(a.Value.Pos().Line())
1452			p.word(a.Value)
1453		} else if a.Array != nil {
1454			p.wantSpace = spaceNotRequired
1455			p.WriteByte('(')
1456			p.elemJoin(a.Array.Elems, a.Array.Last)
1457			p.rightParen(a.Array.Rparen)
1458		}
1459		p.wantSpace = spaceRequired
1460	}
1461	p.decLevel()
1462}
1463
1464type wantSpaceState uint8
1465
1466const (
1467	spaceNotRequired wantSpaceState = iota
1468	spaceRequired                   // we should generally print a space or a newline next
1469	spaceWritten                    // we have just written a space or newline
1470)
1471
1472// extraIndenter ensures that all lines in a '<<-' heredoc body have at least
1473// baseIndent leading tabs. Those that had more tab indentation than the first
1474// heredoc line will keep that relative indentation.
1475type extraIndenter struct {
1476	bufWriter
1477	baseIndent int
1478
1479	firstIndent int
1480	firstChange int
1481	curLine     []byte
1482}
1483
1484func (e *extraIndenter) WriteByte(b byte) error {
1485	e.curLine = append(e.curLine, b)
1486	if b != '\n' {
1487		return nil
1488	}
1489	trimmed := bytes.TrimLeft(e.curLine, "\t")
1490	if len(trimmed) == 1 {
1491		// no tabs if this is an empty line, i.e. "\n"
1492		e.bufWriter.Write(trimmed)
1493		e.curLine = e.curLine[:0]
1494		return nil
1495	}
1496
1497	lineIndent := len(e.curLine) - len(trimmed)
1498	if e.firstIndent < 0 {
1499		// This is the first heredoc line we add extra indentation to.
1500		// Keep track of how much we indented.
1501		e.firstIndent = lineIndent
1502		e.firstChange = e.baseIndent - lineIndent
1503		lineIndent = e.baseIndent
1504
1505	} else if lineIndent < e.firstIndent {
1506		// This line did not have enough indentation; simply indent it
1507		// like the first line.
1508		lineIndent = e.firstIndent
1509	} else {
1510		// This line had plenty of indentation. Add the extra
1511		// indentation that the first line had, for consistency.
1512		lineIndent += e.firstChange
1513	}
1514	e.bufWriter.WriteByte(tabwriter.Escape)
1515	for range lineIndent {
1516		e.bufWriter.WriteByte('\t')
1517	}
1518	e.bufWriter.WriteByte(tabwriter.Escape)
1519	e.bufWriter.Write(trimmed)
1520	e.curLine = e.curLine[:0]
1521	return nil
1522}
1523
1524func (e *extraIndenter) WriteString(s string) (int, error) {
1525	for i := range len(s) {
1526		e.WriteByte(s[i])
1527	}
1528	return len(s), nil
1529}
1530
1531func startsWithLparen(node Node) bool {
1532	switch node := node.(type) {
1533	case *Stmt:
1534		return startsWithLparen(node.Cmd)
1535	case *BinaryCmd:
1536		return startsWithLparen(node.X)
1537	case *Subshell:
1538		return true // keep ( (
1539	case *ArithmCmd:
1540		return true // keep ( ((
1541	}
1542	return false
1543}