github.com/cilki/sh@v2.6.4+incompatible/syntax/printer.go (about) 1 // Copyright (c) 2016, Daniel Martà <mvdan@mvdan.cc> 2 // See LICENSE for licensing information 3 4 package syntax 5 6 import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "io" 11 "strings" 12 "unicode" 13 ) 14 15 // Indent sets the number of spaces used for indentation. If set to 0, 16 // tabs will be used instead. 17 func Indent(spaces uint) func(*Printer) { 18 return func(p *Printer) { p.indentSpaces = spaces } 19 } 20 21 // BinaryNextLine will make binary operators appear on the next line 22 // when a binary command, such as a pipe, spans multiple lines. A 23 // backslash will be used. 24 func BinaryNextLine(p *Printer) { p.binNextLine = true } 25 26 // SwitchCaseIndent will make switch cases be indented. As such, switch 27 // case bodies will be two levels deeper than the switch itself. 28 func SwitchCaseIndent(p *Printer) { p.swtCaseIndent = true } 29 30 // SpaceRedirects will put a space after most redirection operators. The 31 // exceptions are '>&', '<&', '>(', and '<('. 32 func SpaceRedirects(p *Printer) { p.spaceRedirects = true } 33 34 // KeepPadding will keep most nodes and tokens in the same column that 35 // they were in the original source. This allows the user to decide how 36 // to align and pad their code with spaces. 37 // 38 // Note that this feature is best-effort and will only keep the 39 // alignment stable, so it may need some human help the first time it is 40 // run. 41 func KeepPadding(p *Printer) { 42 p.keepPadding = true 43 p.cols.Writer = p.bufWriter.(*bufio.Writer) 44 p.bufWriter = &p.cols 45 } 46 47 // Minify will print programs in a way to save the most bytes possible. 48 // For example, indentation and comments are skipped, and extra 49 // whitespace is avoided when possible. 50 func Minify(p *Printer) { p.minify = true } 51 52 // NewPrinter allocates a new Printer and applies any number of options. 53 func NewPrinter(options ...func(*Printer)) *Printer { 54 p := &Printer{ 55 bufWriter: bufio.NewWriter(nil), 56 lenPrinter: new(Printer), 57 tabsPrinter: new(Printer), 58 } 59 for _, opt := range options { 60 opt(p) 61 } 62 return p 63 } 64 65 // Print "pretty-prints" the given syntax tree node to the given writer. Writes 66 // to w are buffered. 67 // 68 // The node types supported at the moment are *File, *Stmt, *Word, any Command 69 // node, and any WordPart node. A trailing newline will only be printed when a 70 // *File is used. 71 func (p *Printer) Print(w io.Writer, node Node) error { 72 p.reset() 73 p.bufWriter.Reset(w) 74 switch x := node.(type) { 75 case *File: 76 p.stmtList(x.StmtList) 77 p.newline(x.End()) 78 case *Stmt: 79 p.stmtList(StmtList{Stmts: []*Stmt{x}}) 80 case Command: 81 p.line = x.Pos().Line() 82 p.command(x, nil) 83 case *Word: 84 p.word(x) 85 case WordPart: 86 p.wordPart(x, nil) 87 default: 88 return fmt.Errorf("unsupported node type: %T", x) 89 } 90 p.flushHeredocs() 91 p.flushComments() 92 return p.bufWriter.Flush() 93 } 94 95 type bufWriter interface { 96 Write([]byte) (int, error) 97 WriteString(string) (int, error) 98 WriteByte(byte) error 99 Reset(io.Writer) 100 Flush() error 101 } 102 103 type colCounter struct { 104 *bufio.Writer 105 column int 106 lineStart bool 107 } 108 109 func (c *colCounter) WriteByte(b byte) error { 110 switch b { 111 case '\n': 112 c.column = 0 113 c.lineStart = true 114 case '\t', ' ': 115 default: 116 c.lineStart = false 117 } 118 c.column++ 119 return c.Writer.WriteByte(b) 120 } 121 122 func (c *colCounter) WriteString(s string) (int, error) { 123 c.lineStart = false 124 for _, r := range s { 125 if r == '\n' { 126 c.column = 0 127 } 128 c.column++ 129 } 130 return c.Writer.WriteString(s) 131 } 132 133 func (c *colCounter) Reset(w io.Writer) { 134 c.column = 1 135 c.lineStart = true 136 c.Writer.Reset(w) 137 } 138 139 // Printer holds the internal state of the printing mechanism of a 140 // program. 141 type Printer struct { 142 bufWriter 143 cols colCounter 144 145 indentSpaces uint 146 binNextLine bool 147 swtCaseIndent bool 148 spaceRedirects bool 149 keepPadding bool 150 minify bool 151 152 wantSpace bool 153 wantNewline bool 154 wroteSemi bool 155 156 commentPadding uint 157 158 // pendingComments are any comments in the current line or statement 159 // that we have yet to print. This is useful because that way, we can 160 // ensure that all comments are written immediately before a newline. 161 // Otherwise, in some edge cases we might wrongly place words after a 162 // comment in the same line, breaking programs. 163 pendingComments []Comment 164 165 // firstLine means we are still writing the first line 166 firstLine bool 167 // line is the current line number 168 line uint 169 170 // lastLevel is the last level of indentation that was used. 171 lastLevel uint 172 // level is the current level of indentation. 173 level uint 174 // levelIncs records which indentation level increments actually 175 // took place, to revert them once their section ends. 176 levelIncs []bool 177 178 nestedBinary bool 179 180 // pendingHdocs is the list of pending heredocs to write. 181 pendingHdocs []*Redirect 182 183 // used in stmtCols to align comments 184 lenPrinter *Printer 185 lenCounter byteCounter 186 187 // used when printing <<- heredocs with tab indentation 188 tabsPrinter *Printer 189 } 190 191 func (p *Printer) reset() { 192 p.wantSpace, p.wantNewline = false, false 193 p.commentPadding = 0 194 p.pendingComments = p.pendingComments[:0] 195 196 // minification uses its own newline logic 197 p.firstLine = !p.minify 198 p.line = 0 199 200 p.lastLevel, p.level = 0, 0 201 p.levelIncs = p.levelIncs[:0] 202 p.nestedBinary = false 203 p.pendingHdocs = p.pendingHdocs[:0] 204 } 205 206 func (p *Printer) spaces(n uint) { 207 for i := uint(0); i < n; i++ { 208 p.WriteByte(' ') 209 } 210 } 211 212 func (p *Printer) space() { 213 p.WriteByte(' ') 214 p.wantSpace = false 215 } 216 217 func (p *Printer) spacePad(pos Pos) { 218 if p.wantSpace { 219 p.WriteByte(' ') 220 p.wantSpace = false 221 } 222 if p.cols.lineStart { 223 // Never add padding at the start of a line, since this may 224 // result in broken indentation or mixing of spaces and tabs. 225 return 226 } 227 for !p.cols.lineStart && p.cols.column > 0 && p.cols.column < int(pos.col) { 228 p.WriteByte(' ') 229 } 230 } 231 232 func (p *Printer) bslashNewl() { 233 if p.wantSpace { 234 p.space() 235 } 236 p.WriteString("\\\n") 237 p.line++ 238 p.indent() 239 } 240 241 func (p *Printer) spacedString(s string, pos Pos) { 242 p.spacePad(pos) 243 p.WriteString(s) 244 p.wantSpace = true 245 } 246 247 func (p *Printer) spacedToken(s string, pos Pos) { 248 if p.minify { 249 p.WriteString(s) 250 p.wantSpace = false 251 return 252 } 253 p.spacePad(pos) 254 p.WriteString(s) 255 p.wantSpace = true 256 } 257 258 func (p *Printer) semiOrNewl(s string, pos Pos) { 259 if p.wantNewline { 260 p.newline(pos) 261 p.indent() 262 } else { 263 if !p.wroteSemi { 264 p.WriteByte(';') 265 } 266 if !p.minify { 267 p.space() 268 } 269 p.line = pos.Line() 270 } 271 p.WriteString(s) 272 p.wantSpace = true 273 } 274 275 func (p *Printer) incLevel() { 276 inc := false 277 if p.level <= p.lastLevel || len(p.levelIncs) == 0 { 278 p.level++ 279 inc = true 280 } else if last := &p.levelIncs[len(p.levelIncs)-1]; *last { 281 *last = false 282 inc = true 283 } 284 p.levelIncs = append(p.levelIncs, inc) 285 } 286 287 func (p *Printer) decLevel() { 288 if p.levelIncs[len(p.levelIncs)-1] { 289 p.level-- 290 } 291 p.levelIncs = p.levelIncs[:len(p.levelIncs)-1] 292 } 293 294 func (p *Printer) indent() { 295 if p.minify { 296 return 297 } 298 p.lastLevel = p.level 299 switch { 300 case p.level == 0: 301 case p.indentSpaces == 0: 302 for i := uint(0); i < p.level; i++ { 303 p.WriteByte('\t') 304 } 305 default: 306 p.spaces(p.indentSpaces * p.level) 307 } 308 } 309 310 func (p *Printer) newline(pos Pos) { 311 p.flushHeredocs() 312 p.flushComments() 313 p.WriteByte('\n') 314 p.wantNewline, p.wantSpace = false, false 315 if p.line < pos.Line() { 316 p.line++ 317 } 318 } 319 320 func (p *Printer) flushHeredocs() { 321 if len(p.pendingHdocs) == 0 { 322 return 323 } 324 hdocs := p.pendingHdocs 325 p.pendingHdocs = p.pendingHdocs[:0] 326 coms := p.pendingComments 327 p.pendingComments = nil 328 if len(coms) > 0 { 329 c := coms[0] 330 if c.Pos().Line() == p.line { 331 p.pendingComments = append(p.pendingComments, c) 332 p.flushComments() 333 coms = coms[1:] 334 } 335 } 336 337 // Reuse the last indentation level, as 338 // indentation levels are usually changed before 339 // newlines are printed along with their 340 // subsequent indentation characters. 341 newLevel := p.level 342 p.level = p.lastLevel 343 344 for _, r := range hdocs { 345 p.line++ 346 p.WriteByte('\n') 347 p.wantNewline, p.wantSpace = false, false 348 if r.Op == DashHdoc && p.indentSpaces == 0 && 349 !p.minify && p.tabsPrinter != nil { 350 if r.Hdoc != nil { 351 extra := extraIndenter{ 352 bufWriter: p.bufWriter, 353 baseIndent: int(p.level + 1), 354 firstIndent: -1, 355 } 356 *p.tabsPrinter = Printer{ 357 bufWriter: &extra, 358 } 359 p.tabsPrinter.line = r.Hdoc.Pos().Line() 360 p.tabsPrinter.word(r.Hdoc) 361 p.indent() 362 p.line = r.Hdoc.End().Line() 363 } else { 364 p.indent() 365 } 366 } else if r.Hdoc != nil { 367 p.word(r.Hdoc) 368 p.line = r.Hdoc.End().Line() 369 } 370 p.unquotedWord(r.Word) 371 p.wantSpace = false 372 } 373 p.level = newLevel 374 p.pendingComments = coms 375 } 376 377 func (p *Printer) newlines(pos Pos) { 378 if p.firstLine && len(p.pendingComments) == 0 { 379 p.firstLine = false 380 return // no empty lines at the top 381 } 382 if !p.wantNewline && pos.Line() <= p.line { 383 return 384 } 385 p.newline(pos) 386 if pos.Line() > p.line { 387 if !p.minify { 388 // preserve single empty lines 389 p.WriteByte('\n') 390 } 391 p.line++ 392 } 393 p.indent() 394 } 395 396 func (p *Printer) rightParen(pos Pos) { 397 if !p.minify { 398 p.newlines(pos) 399 } 400 p.WriteByte(')') 401 p.wantSpace = true 402 } 403 404 func (p *Printer) semiRsrv(s string, pos Pos) { 405 if p.wantNewline || pos.Line() > p.line { 406 p.newlines(pos) 407 } else { 408 if !p.wroteSemi { 409 p.WriteByte(';') 410 } 411 if !p.minify { 412 p.spacePad(pos) 413 } 414 } 415 p.WriteString(s) 416 p.wantSpace = true 417 } 418 419 func (p *Printer) flushComments() { 420 for i, c := range p.pendingComments { 421 p.firstLine = false 422 // We can't call any of the newline methods, as they call this 423 // function and we'd recurse forever. 424 cline := c.Hash.Line() 425 switch { 426 case i > 0, cline > p.line && p.line > 0: 427 p.WriteByte('\n') 428 if cline > p.line+1 { 429 p.WriteByte('\n') 430 } 431 p.indent() 432 case p.wantSpace: 433 if p.keepPadding { 434 p.spacePad(c.Pos()) 435 } else { 436 p.spaces(p.commentPadding + 1) 437 } 438 } 439 // don't go back one line, which may happen in some edge cases 440 if p.line < cline { 441 p.line = cline 442 } 443 p.WriteByte('#') 444 p.WriteString(strings.TrimRightFunc(c.Text, unicode.IsSpace)) 445 p.wantNewline = true 446 } 447 p.pendingComments = nil 448 } 449 450 func (p *Printer) comments(comments ...Comment) { 451 if p.minify { 452 return 453 } 454 p.pendingComments = append(p.pendingComments, comments...) 455 } 456 457 func (p *Printer) wordParts(wps []WordPart) { 458 for i, n := range wps { 459 var next WordPart 460 if i+1 < len(wps) { 461 next = wps[i+1] 462 } 463 p.wordPart(n, next) 464 } 465 } 466 467 func (p *Printer) wordPart(wp, next WordPart) { 468 switch x := wp.(type) { 469 case *Lit: 470 p.WriteString(x.Value) 471 case *SglQuoted: 472 if x.Dollar { 473 p.WriteByte('$') 474 } 475 p.WriteByte('\'') 476 p.WriteString(x.Value) 477 p.WriteByte('\'') 478 p.line = x.End().Line() 479 case *DblQuoted: 480 p.dblQuoted(x) 481 case *CmdSubst: 482 p.line = x.Pos().Line() 483 switch { 484 case x.TempFile: 485 p.WriteString("${") 486 p.wantSpace = true 487 p.nestedStmts(x.StmtList, x.Right) 488 p.wantSpace = false 489 p.semiRsrv("}", x.Right) 490 case x.ReplyVar: 491 p.WriteString("${|") 492 p.nestedStmts(x.StmtList, x.Right) 493 p.wantSpace = false 494 p.semiRsrv("}", x.Right) 495 default: 496 p.WriteString("$(") 497 p.wantSpace = len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) 498 p.nestedStmts(x.StmtList, x.Right) 499 p.rightParen(x.Right) 500 } 501 case *ParamExp: 502 litCont := ";" 503 if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" { 504 litCont = nextLit.Value[:1] 505 } 506 name := x.Param.Value 507 switch { 508 case !p.minify: 509 case x.Excl, x.Length, x.Width: 510 case x.Index != nil, x.Slice != nil: 511 case x.Repl != nil, x.Exp != nil: 512 case len(name) > 1 && !ValidName(name): // ${10} 513 case ValidName(name + litCont): // ${var}cont 514 default: 515 x2 := *x 516 x2.Short = true 517 p.paramExp(&x2) 518 return 519 } 520 p.paramExp(x) 521 case *ArithmExp: 522 p.WriteString("$((") 523 if x.Unsigned { 524 p.WriteString("# ") 525 } 526 p.arithmExpr(x.X, false, false) 527 p.WriteString("))") 528 case *ExtGlob: 529 p.WriteString(x.Op.String()) 530 p.WriteString(x.Pattern.Value) 531 p.WriteByte(')') 532 case *ProcSubst: 533 // avoid conflict with << and others 534 if p.wantSpace { 535 p.space() 536 } 537 p.WriteString(x.Op.String()) 538 p.nestedStmts(x.StmtList, x.Rparen) 539 p.rightParen(x.Rparen) 540 } 541 } 542 543 func (p *Printer) dblQuoted(dq *DblQuoted) { 544 if dq.Dollar { 545 p.WriteByte('$') 546 } 547 p.WriteByte('"') 548 if len(dq.Parts) > 0 { 549 p.wordParts(dq.Parts) 550 p.line = dq.Parts[len(dq.Parts)-1].End().Line() 551 } 552 p.WriteByte('"') 553 } 554 555 func (p *Printer) wroteIndex(index ArithmExpr) bool { 556 if index == nil { 557 return false 558 } 559 p.WriteByte('[') 560 p.arithmExpr(index, false, false) 561 p.WriteByte(']') 562 return true 563 } 564 565 func (p *Printer) paramExp(pe *ParamExp) { 566 if pe.nakedIndex() { // arr[x] 567 p.WriteString(pe.Param.Value) 568 p.wroteIndex(pe.Index) 569 return 570 } 571 if pe.Short { // $var 572 p.WriteByte('$') 573 p.WriteString(pe.Param.Value) 574 return 575 } 576 // ${var...} 577 p.WriteString("${") 578 switch { 579 case pe.Length: 580 p.WriteByte('#') 581 case pe.Width: 582 p.WriteByte('%') 583 case pe.Excl: 584 p.WriteByte('!') 585 } 586 p.WriteString(pe.Param.Value) 587 p.wroteIndex(pe.Index) 588 switch { 589 case pe.Slice != nil: 590 p.WriteByte(':') 591 p.arithmExpr(pe.Slice.Offset, true, true) 592 if pe.Slice.Length != nil { 593 p.WriteByte(':') 594 p.arithmExpr(pe.Slice.Length, true, false) 595 } 596 case pe.Repl != nil: 597 if pe.Repl.All { 598 p.WriteByte('/') 599 } 600 p.WriteByte('/') 601 if pe.Repl.Orig != nil { 602 p.word(pe.Repl.Orig) 603 } 604 p.WriteByte('/') 605 if pe.Repl.With != nil { 606 p.word(pe.Repl.With) 607 } 608 case pe.Names != 0: 609 p.WriteString(pe.Names.String()) 610 case pe.Exp != nil: 611 p.WriteString(pe.Exp.Op.String()) 612 if pe.Exp.Word != nil { 613 p.word(pe.Exp.Word) 614 } 615 } 616 p.WriteByte('}') 617 } 618 619 func (p *Printer) loop(loop Loop) { 620 switch x := loop.(type) { 621 case *WordIter: 622 p.WriteString(x.Name.Value) 623 if x.InPos.IsValid() { 624 p.spacedString(" in", Pos{}) 625 p.wordJoin(x.Items) 626 } 627 case *CStyleLoop: 628 p.WriteString("((") 629 if x.Init == nil { 630 p.space() 631 } 632 p.arithmExpr(x.Init, false, false) 633 p.WriteString("; ") 634 p.arithmExpr(x.Cond, false, false) 635 p.WriteString("; ") 636 p.arithmExpr(x.Post, false, false) 637 p.WriteString("))") 638 } 639 } 640 641 func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) { 642 if p.minify { 643 compact = true 644 } 645 switch x := expr.(type) { 646 case *Word: 647 p.word(x) 648 case *BinaryArithm: 649 if compact { 650 p.arithmExpr(x.X, compact, spacePlusMinus) 651 p.WriteString(x.Op.String()) 652 p.arithmExpr(x.Y, compact, false) 653 } else { 654 p.arithmExpr(x.X, compact, spacePlusMinus) 655 if x.Op != Comma { 656 p.space() 657 } 658 p.WriteString(x.Op.String()) 659 p.space() 660 p.arithmExpr(x.Y, compact, false) 661 } 662 case *UnaryArithm: 663 if x.Post { 664 p.arithmExpr(x.X, compact, spacePlusMinus) 665 p.WriteString(x.Op.String()) 666 } else { 667 if spacePlusMinus { 668 switch x.Op { 669 case Plus, Minus: 670 p.space() 671 } 672 } 673 p.WriteString(x.Op.String()) 674 p.arithmExpr(x.X, compact, false) 675 } 676 case *ParenArithm: 677 p.WriteByte('(') 678 p.arithmExpr(x.X, false, false) 679 p.WriteByte(')') 680 } 681 } 682 683 func (p *Printer) testExpr(expr TestExpr) { 684 switch x := expr.(type) { 685 case *Word: 686 p.word(x) 687 case *BinaryTest: 688 p.testExpr(x.X) 689 p.space() 690 p.WriteString(x.Op.String()) 691 p.space() 692 p.testExpr(x.Y) 693 case *UnaryTest: 694 p.WriteString(x.Op.String()) 695 p.space() 696 p.testExpr(x.X) 697 case *ParenTest: 698 p.WriteByte('(') 699 p.testExpr(x.X) 700 p.WriteByte(')') 701 } 702 } 703 704 func (p *Printer) word(w *Word) { 705 p.wordParts(w.Parts) 706 p.wantSpace = true 707 } 708 709 func (p *Printer) unquotedWord(w *Word) { 710 for _, wp := range w.Parts { 711 switch x := wp.(type) { 712 case *SglQuoted: 713 p.WriteString(x.Value) 714 case *DblQuoted: 715 p.wordParts(x.Parts) 716 case *Lit: 717 for i := 0; i < len(x.Value); i++ { 718 if b := x.Value[i]; b == '\\' { 719 if i++; i < len(x.Value) { 720 p.WriteByte(x.Value[i]) 721 } 722 } else { 723 p.WriteByte(b) 724 } 725 } 726 } 727 } 728 } 729 730 func (p *Printer) wordJoin(ws []*Word) { 731 anyNewline := false 732 for _, w := range ws { 733 if pos := w.Pos(); pos.Line() > p.line { 734 if !anyNewline { 735 p.incLevel() 736 anyNewline = true 737 } 738 p.bslashNewl() 739 } else { 740 p.spacePad(w.Pos()) 741 } 742 p.word(w) 743 } 744 if anyNewline { 745 p.decLevel() 746 } 747 } 748 749 func (p *Printer) casePatternJoin(pats []*Word) { 750 anyNewline := false 751 for i, w := range pats { 752 if i > 0 { 753 p.spacedToken("|", Pos{}) 754 } 755 if pos := w.Pos(); pos.Line() > p.line { 756 if !anyNewline { 757 p.incLevel() 758 anyNewline = true 759 } 760 p.bslashNewl() 761 } else { 762 p.spacePad(w.Pos()) 763 } 764 p.word(w) 765 } 766 if anyNewline { 767 p.decLevel() 768 } 769 } 770 771 func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) { 772 p.incLevel() 773 for _, el := range elems { 774 var left []Comment 775 for _, c := range el.Comments { 776 if c.Pos().After(el.Pos()) { 777 left = append(left, c) 778 break 779 } 780 p.comments(c) 781 } 782 if el.Pos().Line() > p.line { 783 p.newline(el.Pos()) 784 p.indent() 785 } else if p.wantSpace { 786 p.space() 787 } 788 if p.wroteIndex(el.Index) { 789 p.WriteByte('=') 790 } 791 if el.Value != nil { 792 p.word(el.Value) 793 } 794 p.comments(left...) 795 } 796 if len(last) > 0 { 797 p.comments(last...) 798 p.flushComments() 799 } 800 p.decLevel() 801 } 802 803 func (p *Printer) stmt(s *Stmt) { 804 p.wroteSemi = false 805 if s.Negated { 806 p.spacedString("!", s.Pos()) 807 } 808 var startRedirs int 809 if s.Cmd != nil { 810 startRedirs = p.command(s.Cmd, s.Redirs) 811 } 812 p.incLevel() 813 for _, r := range s.Redirs[startRedirs:] { 814 if r.OpPos.Line() > p.line { 815 p.bslashNewl() 816 } 817 if p.wantSpace { 818 p.spacePad(r.Pos()) 819 } 820 if r.N != nil { 821 p.WriteString(r.N.Value) 822 } 823 p.WriteString(r.Op.String()) 824 if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { 825 p.space() 826 } else { 827 p.wantSpace = true 828 } 829 p.word(r.Word) 830 if r.Op == Hdoc || r.Op == DashHdoc { 831 p.pendingHdocs = append(p.pendingHdocs, r) 832 } 833 } 834 switch { 835 case s.Semicolon.IsValid() && s.Semicolon.Line() > p.line: 836 p.bslashNewl() 837 p.WriteByte(';') 838 p.wroteSemi = true 839 case s.Background: 840 if !p.minify { 841 p.space() 842 } 843 p.WriteString("&") 844 case s.Coprocess: 845 if !p.minify { 846 p.space() 847 } 848 p.WriteString("|&") 849 } 850 p.decLevel() 851 } 852 853 func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { 854 p.spacePad(cmd.Pos()) 855 switch x := cmd.(type) { 856 case *CallExpr: 857 p.assigns(x.Assigns) 858 if len(x.Args) <= 1 { 859 p.wordJoin(x.Args) 860 return 0 861 } 862 p.wordJoin(x.Args[:1]) 863 for _, r := range redirs { 864 if r.Pos().After(x.Args[1].Pos()) || r.Op == Hdoc || r.Op == DashHdoc { 865 break 866 } 867 if p.wantSpace { 868 p.spacePad(r.Pos()) 869 } 870 if r.N != nil { 871 p.WriteString(r.N.Value) 872 } 873 p.WriteString(r.Op.String()) 874 if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { 875 p.space() 876 } else { 877 p.wantSpace = true 878 } 879 p.word(r.Word) 880 startRedirs++ 881 } 882 p.wordJoin(x.Args[1:]) 883 case *Block: 884 p.WriteByte('{') 885 p.wantSpace = true 886 p.nestedStmts(x.StmtList, x.Rbrace) 887 p.semiRsrv("}", x.Rbrace) 888 case *IfClause: 889 p.ifClause(x, false) 890 case *Subshell: 891 p.WriteByte('(') 892 p.wantSpace = len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) 893 p.spacePad(x.StmtList.pos()) 894 p.nestedStmts(x.StmtList, x.Rparen) 895 p.wantSpace = false 896 p.spacePad(x.Rparen) 897 p.rightParen(x.Rparen) 898 case *WhileClause: 899 if x.Until { 900 p.spacedString("until", x.Pos()) 901 } else { 902 p.spacedString("while", x.Pos()) 903 } 904 p.nestedStmts(x.Cond, Pos{}) 905 p.semiOrNewl("do", x.DoPos) 906 p.nestedStmts(x.Do, x.DonePos) 907 p.semiRsrv("done", x.DonePos) 908 case *ForClause: 909 if x.Select { 910 p.WriteString("select ") 911 } else { 912 p.WriteString("for ") 913 } 914 p.loop(x.Loop) 915 p.semiOrNewl("do", x.DoPos) 916 p.nestedStmts(x.Do, x.DonePos) 917 p.semiRsrv("done", x.DonePos) 918 case *BinaryCmd: 919 p.stmt(x.X) 920 if p.minify || x.Y.Pos().Line() <= p.line { 921 // leave p.nestedBinary untouched 922 p.spacedToken(x.Op.String(), x.OpPos) 923 p.line = x.Y.Pos().Line() 924 p.stmt(x.Y) 925 break 926 } 927 indent := !p.nestedBinary 928 if indent { 929 p.incLevel() 930 } 931 if p.binNextLine { 932 if len(p.pendingHdocs) == 0 { 933 p.bslashNewl() 934 } 935 p.spacedToken(x.Op.String(), x.OpPos) 936 if len(x.Y.Comments) > 0 { 937 p.wantSpace = false 938 p.newline(Pos{}) 939 p.indent() 940 p.comments(x.Y.Comments...) 941 p.newline(Pos{}) 942 p.indent() 943 } 944 } else { 945 p.spacedToken(x.Op.String(), x.OpPos) 946 p.line = x.OpPos.Line() 947 p.comments(x.Y.Comments...) 948 p.newline(Pos{}) 949 p.indent() 950 } 951 p.line = x.Y.Pos().Line() 952 _, p.nestedBinary = x.Y.Cmd.(*BinaryCmd) 953 p.stmt(x.Y) 954 if indent { 955 p.decLevel() 956 } 957 p.nestedBinary = false 958 case *FuncDecl: 959 if x.RsrvWord { 960 p.WriteString("function ") 961 } 962 p.WriteString(x.Name.Value) 963 p.WriteString("()") 964 if !p.minify { 965 p.space() 966 } 967 p.line = x.Body.Pos().Line() 968 p.comments(x.Body.Comments...) 969 p.stmt(x.Body) 970 case *CaseClause: 971 p.WriteString("case ") 972 p.word(x.Word) 973 p.WriteString(" in") 974 if p.swtCaseIndent { 975 p.incLevel() 976 } 977 for i, ci := range x.Items { 978 var last []Comment 979 for i, c := range ci.Comments { 980 if c.Pos().After(ci.Pos()) { 981 last = ci.Comments[i:] 982 break 983 } 984 p.comments(c) 985 } 986 p.newlines(ci.Pos()) 987 p.casePatternJoin(ci.Patterns) 988 p.WriteByte(')') 989 p.wantSpace = !p.minify 990 sep := len(ci.Stmts) > 1 || ci.StmtList.pos().Line() > p.line || 991 (!ci.StmtList.empty() && ci.OpPos.Line() > ci.StmtList.end().Line()) 992 p.nestedStmts(ci.StmtList, ci.OpPos) 993 p.level++ 994 if !p.minify || i != len(x.Items)-1 { 995 if sep { 996 p.newlines(ci.OpPos) 997 p.wantNewline = true 998 } 999 p.spacedToken(ci.Op.String(), ci.OpPos) 1000 // avoid ; directly after tokens like ;; 1001 p.wroteSemi = true 1002 } 1003 p.comments(last...) 1004 p.flushComments() 1005 p.level-- 1006 } 1007 p.comments(x.Last...) 1008 if p.swtCaseIndent { 1009 p.flushComments() 1010 p.decLevel() 1011 } 1012 p.semiRsrv("esac", x.Esac) 1013 case *ArithmCmd: 1014 p.WriteString("((") 1015 if x.Unsigned { 1016 p.WriteString("# ") 1017 } 1018 p.arithmExpr(x.X, false, false) 1019 p.WriteString("))") 1020 case *TestClause: 1021 p.WriteString("[[ ") 1022 p.testExpr(x.X) 1023 p.spacedString("]]", x.Right) 1024 case *DeclClause: 1025 p.spacedString(x.Variant.Value, x.Pos()) 1026 for _, w := range x.Opts { 1027 p.space() 1028 p.word(w) 1029 } 1030 p.assigns(x.Assigns) 1031 case *TimeClause: 1032 p.spacedString("time", x.Pos()) 1033 if x.PosixFormat { 1034 p.spacedString("-p", x.Pos()) 1035 } 1036 if x.Stmt != nil { 1037 p.stmt(x.Stmt) 1038 } 1039 case *CoprocClause: 1040 p.spacedString("coproc", x.Pos()) 1041 if x.Name != nil { 1042 p.space() 1043 p.WriteString(x.Name.Value) 1044 } 1045 p.space() 1046 p.stmt(x.Stmt) 1047 case *LetClause: 1048 p.spacedString("let", x.Pos()) 1049 for _, n := range x.Exprs { 1050 p.space() 1051 p.arithmExpr(n, true, false) 1052 } 1053 } 1054 return startRedirs 1055 } 1056 1057 func (p *Printer) ifClause(ic *IfClause, elif bool) { 1058 if !elif { 1059 p.spacedString("if", ic.Pos()) 1060 } 1061 p.nestedStmts(ic.Cond, Pos{}) 1062 p.semiOrNewl("then", ic.ThenPos) 1063 p.nestedStmts(ic.Then, ic.bodyEndPos()) 1064 1065 var left []Comment 1066 for _, c := range ic.ElseComments { 1067 if c.Pos().After(ic.ElsePos) { 1068 left = append(left, c) 1069 break 1070 } 1071 p.comments(c) 1072 } 1073 if ic.FollowedByElif() { 1074 s := ic.Else.Stmts[0] 1075 p.comments(s.Comments...) 1076 p.semiRsrv("elif", ic.ElsePos) 1077 p.ifClause(s.Cmd.(*IfClause), true) 1078 return 1079 } 1080 if !ic.Else.empty() { 1081 p.semiRsrv("else", ic.ElsePos) 1082 p.comments(left...) 1083 p.nestedStmts(ic.Else, ic.FiPos) 1084 } else if ic.ElsePos.IsValid() { 1085 p.line = ic.ElsePos.Line() 1086 } 1087 p.comments(ic.FiComments...) 1088 p.semiRsrv("fi", ic.FiPos) 1089 } 1090 1091 func startsWithLparen(s *Stmt) bool { 1092 switch x := s.Cmd.(type) { 1093 case *Subshell: 1094 return true 1095 case *BinaryCmd: 1096 return startsWithLparen(x.X) 1097 } 1098 return false 1099 } 1100 1101 func (p *Printer) hasInline(s *Stmt) bool { 1102 for _, c := range s.Comments { 1103 if c.Pos().Line() == s.End().Line() { 1104 return true 1105 } 1106 } 1107 return false 1108 } 1109 1110 func (p *Printer) stmtList(sl StmtList) { 1111 sep := p.wantNewline || 1112 (len(sl.Stmts) > 0 && sl.Stmts[0].Pos().Line() > p.line) 1113 inlineIndent := 0 1114 lastIndentedLine := uint(0) 1115 for i, s := range sl.Stmts { 1116 pos := s.Pos() 1117 var midComs, endComs []Comment 1118 for _, c := range s.Comments { 1119 if c.End().After(s.End()) { 1120 endComs = append(endComs, c) 1121 break 1122 } 1123 if c.Pos().After(s.Pos()) { 1124 midComs = append(midComs, c) 1125 continue 1126 } 1127 p.comments(c) 1128 } 1129 if !p.minify || p.wantSpace { 1130 p.newlines(pos) 1131 } 1132 p.line = pos.Line() 1133 if !p.hasInline(s) { 1134 inlineIndent = 0 1135 p.commentPadding = 0 1136 p.comments(midComs...) 1137 p.stmt(s) 1138 p.wantNewline = true 1139 continue 1140 } 1141 p.comments(midComs...) 1142 p.stmt(s) 1143 if s.Pos().Line() > lastIndentedLine+1 { 1144 inlineIndent = 0 1145 } 1146 if inlineIndent == 0 { 1147 for _, s2 := range sl.Stmts[i:] { 1148 if !p.hasInline(s2) { 1149 break 1150 } 1151 if l := p.stmtCols(s2); l > inlineIndent { 1152 inlineIndent = l 1153 } 1154 } 1155 } 1156 if inlineIndent > 0 { 1157 if l := p.stmtCols(s); l > 0 { 1158 p.commentPadding = uint(inlineIndent - l) 1159 } 1160 lastIndentedLine = p.line 1161 } 1162 p.comments(endComs...) 1163 p.wantNewline = true 1164 } 1165 if len(sl.Stmts) == 1 && !sep { 1166 p.wantNewline = false 1167 } 1168 p.comments(sl.Last...) 1169 } 1170 1171 type byteCounter int 1172 1173 func (c *byteCounter) WriteByte(b byte) error { 1174 switch { 1175 case *c < 0: 1176 case b == '\n': 1177 *c = -1 1178 default: 1179 *c++ 1180 } 1181 return nil 1182 } 1183 func (c *byteCounter) Write(p []byte) (int, error) { 1184 return c.WriteString(string(p)) 1185 } 1186 func (c *byteCounter) WriteString(s string) (int, error) { 1187 switch { 1188 case *c < 0: 1189 case strings.Contains(s, "\n"): 1190 *c = -1 1191 default: 1192 *c += byteCounter(len(s)) 1193 } 1194 return 0, nil 1195 } 1196 func (c *byteCounter) Reset(io.Writer) { *c = 0 } 1197 func (c *byteCounter) Flush() error { return nil } 1198 1199 // extraIndenter ensures that all lines in a '<<-' heredoc body have at least 1200 // baseIndent leading tabs. Those that had more tab indentation than the first 1201 // heredoc line will keep that relative indentation. 1202 type extraIndenter struct { 1203 bufWriter 1204 baseIndent int 1205 1206 firstIndent int 1207 firstChange int 1208 curLine []byte 1209 } 1210 1211 func (e *extraIndenter) WriteByte(b byte) error { 1212 e.curLine = append(e.curLine, b) 1213 if b != '\n' { 1214 return nil 1215 } 1216 trimmed := bytes.TrimLeft(e.curLine, "\t") 1217 lineIndent := len(e.curLine) - len(trimmed) 1218 if e.firstIndent < 0 { 1219 e.firstIndent = lineIndent 1220 e.firstChange = e.baseIndent - lineIndent 1221 lineIndent = e.baseIndent 1222 } else { 1223 if lineIndent < e.firstIndent { 1224 lineIndent = e.firstIndent 1225 } else { 1226 lineIndent += e.firstChange 1227 } 1228 } 1229 for i := 0; i < lineIndent; i++ { 1230 e.bufWriter.WriteByte('\t') 1231 } 1232 e.bufWriter.Write(trimmed) 1233 e.curLine = e.curLine[:0] 1234 return nil 1235 } 1236 1237 func (e *extraIndenter) WriteString(s string) (int, error) { 1238 for i := 0; i < len(s); i++ { 1239 e.WriteByte(s[i]) 1240 } 1241 return len(s), nil 1242 } 1243 1244 // stmtCols reports the length that s will take when formatted in a 1245 // single line. If it will span multiple lines, stmtCols will return -1. 1246 func (p *Printer) stmtCols(s *Stmt) int { 1247 if p.lenPrinter == nil { 1248 return -1 // stmtCols call within stmtCols, bail 1249 } 1250 *p.lenPrinter = Printer{ 1251 bufWriter: &p.lenCounter, 1252 line: s.Pos().Line(), 1253 } 1254 p.lenPrinter.bufWriter.Reset(nil) 1255 p.lenPrinter.stmt(s) 1256 return int(p.lenCounter) 1257 } 1258 1259 func (p *Printer) nestedStmts(sl StmtList, closing Pos) { 1260 p.incLevel() 1261 switch { 1262 case len(sl.Stmts) > 1: 1263 // Force a newline if we find: 1264 // { stmt; stmt; } 1265 p.wantNewline = true 1266 case closing.Line() > p.line && len(sl.Stmts) > 0 && 1267 sl.end().Line() < closing.Line(): 1268 // Force a newline if we find: 1269 // { stmt 1270 // } 1271 p.wantNewline = true 1272 case len(p.pendingComments) > 0 && len(sl.Stmts) > 0: 1273 // Force a newline if we find: 1274 // for i in a b # stmt 1275 // do foo; done 1276 p.wantNewline = true 1277 } 1278 p.stmtList(sl) 1279 if closing.IsValid() { 1280 p.flushComments() 1281 } 1282 p.decLevel() 1283 } 1284 1285 func (p *Printer) assigns(assigns []*Assign) { 1286 p.incLevel() 1287 for _, a := range assigns { 1288 if a.Pos().Line() > p.line { 1289 p.bslashNewl() 1290 } else { 1291 p.spacePad(a.Pos()) 1292 } 1293 if a.Name != nil { 1294 p.WriteString(a.Name.Value) 1295 p.wroteIndex(a.Index) 1296 if a.Append { 1297 p.WriteByte('+') 1298 } 1299 if !a.Naked { 1300 p.WriteByte('=') 1301 } 1302 } 1303 if a.Value != nil { 1304 p.word(a.Value) 1305 } else if a.Array != nil { 1306 p.wantSpace = false 1307 p.WriteByte('(') 1308 p.elemJoin(a.Array.Elems, a.Array.Last) 1309 p.rightParen(a.Array.Rparen) 1310 } 1311 p.wantSpace = true 1312 } 1313 p.decLevel() 1314 }