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}