// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	
	
	
	
	
	
	

	
)

// PrinterOption is a function which can be passed to NewPrinter
// to alter its behavior. To apply option to existing Printer
// call it directly, for example KeepPadding(true)(printer).
type PrinterOption func(*Printer)

// Indent sets the number of spaces used for indentation. If set to 0,
// tabs will be used instead.
func ( uint) PrinterOption {
	return func( *Printer) { .indentSpaces =  }
}

// BinaryNextLine will make binary operators appear on the next line
// when a binary command, such as a pipe, spans multiple lines. A
// backslash will be used.
func ( bool) PrinterOption {
	return func( *Printer) { .binNextLine =  }
}

// SwitchCaseIndent will make switch cases be indented. As such, switch
// case bodies will be two levels deeper than the switch itself.
func ( bool) PrinterOption {
	return func( *Printer) { .swtCaseIndent =  }
}

// TODO(v4): consider turning this into a "space all operators" option, to also
// allow foo=( bar baz ), (( x + y )), and so on.

// SpaceRedirects will put a space after most redirection operators. The
// exceptions are '>&', '<&', '>(', and '<('.
func ( bool) PrinterOption {
	return func( *Printer) { .spaceRedirects =  }
}

// KeepPadding will keep most nodes and tokens in the same column that
// they were in the original source. This allows the user to decide how
// to align and pad their code with spaces.
//
// Note that this feature is best-effort and will only keep the
// alignment stable, so it may need some human help the first time it is
// run.
func ( bool) PrinterOption {
	return func( *Printer) {
		if  && !.keepPadding {
			// Enable the flag, and set up the writer wrapper.
			.keepPadding = true
			.cols.Writer = .bufWriter.(*bufio.Writer)
			.bufWriter = &.cols

		} else if ! && .keepPadding {
			// Ensure we reset the state to that of NewPrinter.
			.keepPadding = false
			.bufWriter = .cols.Writer
			.cols = colCounter{}
		}
	}
}

// Minify will print programs in a way to save the most bytes possible.
// For example, indentation and comments are skipped, and extra
// whitespace is avoided when possible.
func ( bool) PrinterOption {
	return func( *Printer) { .minify =  }
}

// SingleLine will attempt to print programs in one line. For example, lists of
// commands or nested blocks do not use newlines in this mode. Note that some
// newlines must still appear, such as those following comments or around
// here-documents.
//
// Print's trailing newline when given a *File is not affected by this option.
func ( bool) PrinterOption {
	return func( *Printer) { .singleLine =  }
}

// FunctionNextLine will place a function's opening braces on the next line.
func ( bool) PrinterOption {
	return func( *Printer) { .funcNextLine =  }
}

// NewPrinter allocates a new Printer and applies any number of options.
func ( ...PrinterOption) *Printer {
	 := &Printer{
		bufWriter: bufio.NewWriter(nil),
		tabWriter: new(tabwriter.Writer),
	}
	for ,  := range  {
		()
	}
	return 
}

// Print "pretty-prints" the given syntax tree node to the given writer. Writes
// to w are buffered.
//
// The node types supported at the moment are *File, *Stmt, *Word, *Assign, any
// Command node, and any WordPart node. A trailing newline will only be printed
// when a *File is used.
func ( *Printer) ( io.Writer,  Node) error {
	.reset()

	if .minify && .singleLine {
		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")
	}

	// TODO: consider adding a raw mode to skip the tab writer, much like in
	// go/printer.
	 := tabwriter.DiscardEmptyColumns | tabwriter.StripEscape
	 := 8
	if .indentSpaces == 0 {
		// indenting with tabs
		 |= tabwriter.TabIndent
	} else {
		// indenting with spaces
		 = int(.indentSpaces)
	}
	.tabWriter.Init(, 0, , 1, ' ', )
	 = .tabWriter

	.bufWriter.Reset()
	switch x := .(type) {
	case *File:
		.stmtList(.Stmts, .Last)
		.newline(Pos{})
	case *Stmt:
		.stmtList([]*Stmt{}, nil)
	case Command:
		.command(, nil)
	case *Word:
		.line = .Pos().Line()
		.word()
	case WordPart:
		.line = .Pos().Line()
		.wordPart(, nil)
	case *Assign:
		.line = .Pos().Line()
		.assigns([]*Assign{})
	default:
		return fmt.Errorf("unsupported node type: %T", )
	}
	.flushHeredocs()
	.flushComments()

	// flush the writers
	if  := .bufWriter.Flush();  != nil {
		return 
	}
	if ,  := .(*tabwriter.Writer);  != nil {
		if  := .Flush();  != nil {
			return 
		}
	}
	return nil
}

type bufWriter interface {
	Write([]byte) (int, error)
	WriteString(string) (int, error)
	WriteByte(byte) error
	Reset(io.Writer)
	Flush() error
}

type colCounter struct {
	*bufio.Writer
	column    int
	lineStart bool
}

func ( *colCounter) ( byte) {
	switch  {
	case '\n':
		.column = 0
		.lineStart = true
	case '\t', ' ', tabwriter.Escape:
	default:
		.lineStart = false
	}
	.column++
}

func ( *colCounter) ( byte) error {
	.addByte()
	return .Writer.WriteByte()
}

func ( *colCounter) ( string) (int, error) {
	for ,  := range []byte() {
		.addByte()
	}
	return .Writer.WriteString()
}

func ( *colCounter) ( io.Writer) {
	.column = 1
	.lineStart = true
	.Writer.Reset()
}

// Printer holds the internal state of the printing mechanism of a
// program.
type Printer struct {
	bufWriter // TODO: embedding this makes the methods part of the API, which we did not intend
	tabWriter *tabwriter.Writer
	cols      colCounter

	indentSpaces   uint
	binNextLine    bool
	swtCaseIndent  bool
	spaceRedirects bool
	keepPadding    bool
	minify         bool
	singleLine     bool
	funcNextLine   bool

	wantSpace wantSpaceState // whether space is required or has been written

	wantNewline bool // newline is wanted for pretty-printing; ignored by singleLine; ignored by singleLine
	mustNewline bool // newline is required to keep shell syntax valid
	wroteSemi   bool // wrote ';' for the current statement

	// pendingComments are any comments in the current line or statement
	// that we have yet to print. This is useful because that way, we can
	// ensure that all comments are written immediately before a newline.
	// Otherwise, in some edge cases we might wrongly place words after a
	// comment in the same line, breaking programs.
	pendingComments []Comment

	// firstLine means we are still writing the first line
	firstLine bool
	// line is the current line number
	line uint

	// lastLevel is the last level of indentation that was used.
	lastLevel uint
	// level is the current level of indentation.
	level uint
	// levelIncs records which indentation level increments actually
	// took place, to revert them once their section ends.
	levelIncs []bool

	nestedBinary bool

	// pendingHdocs is the list of pending heredocs to write.
	pendingHdocs []*Redirect

	// used when printing <<- heredocs with tab indentation
	tabsPrinter *Printer
}

func ( *Printer) () {
	.wantSpace = spaceWritten
	.wantNewline, .mustNewline = false, false
	.pendingComments = .pendingComments[:0]

	// minification uses its own newline logic
	.firstLine = !.minify
	.line = 0

	.lastLevel, .level = 0, 0
	.levelIncs = .levelIncs[:0]
	.nestedBinary = false
	.pendingHdocs = .pendingHdocs[:0]
}

func ( *Printer) ( uint) {
	for  := uint(0);  < ; ++ {
		.WriteByte(' ')
	}
}

func ( *Printer) () {
	.WriteByte(' ')
	.wantSpace = spaceWritten
}

func ( *Printer) ( Pos) {
	if .cols.lineStart && .indentSpaces == 0 {
		// Never add padding at the start of a line unless we are indenting
		// with spaces, since this may result in mixing of spaces and tabs.
		return
	}
	if .wantSpace == spaceRequired {
		.WriteByte(' ')
		.wantSpace = spaceWritten
	}
	for .cols.column > 0 && .cols.column < int(.Col()) {
		.WriteByte(' ')
	}
}

// wantsNewline reports whether we want to print at least one newline before
// printing a node at a given position. A zero position can be given to simply
// tell if we want a newline following what's just been printed.
func ( *Printer) ( Pos,  bool) bool {
	if .mustNewline {
		// We must have a newline here.
		return true
	}
	if .singleLine && len(.pendingComments) == 0 {
		// The newline is optional, and singleLine skips it.
		// Don't skip if there are any pending comments,
		// as that might move them further down to the wrong place.
		return false
	}
	if  && .minify {
		return false
	}
	// The newline is optional, and we want it via either wantNewline or via
	// the position's line.
	return .wantNewline || .Line() > .line
}

func ( *Printer) () {
	if .wantSpace == spaceRequired {
		.space()
	}
	.WriteString("\\\n")
	.line++
	.indent()
}

func ( *Printer) ( string,  Pos) {
	.spacePad()
	.WriteString()
	.wantSpace = spaceRequired
}

func ( *Printer) ( string,  Pos) {
	if .minify {
		.WriteString()
		.wantSpace = spaceNotRequired
		return
	}
	.spacePad()
	.WriteString()
	.wantSpace = spaceRequired
}

func ( *Printer) ( string,  Pos) {
	if .wantsNewline(Pos{}, false) {
		.newline()
		.indent()
	} else {
		if !.wroteSemi {
			.WriteByte(';')
		}
		if !.minify {
			.space()
		}
		.advanceLine(.Line())
	}
	.WriteString()
	.wantSpace = spaceRequired
}

func ( *Printer) ( string) {
	// If p.tabWriter is nil, this is the nested printer being used to print
	// <<- heredoc bodies, so the parent printer will add the escape bytes
	// later.
	if .tabWriter != nil && strings.Contains(, "\t") {
		.WriteByte(tabwriter.Escape)
		defer .WriteByte(tabwriter.Escape)
	}
	.WriteString()
}

func ( *Printer) () {
	 := false
	if .level <= .lastLevel || len(.levelIncs) == 0 {
		.level++
		 = true
	} else if  := &.levelIncs[len(.levelIncs)-1]; * {
		* = false
		 = true
	}
	.levelIncs = append(.levelIncs, )
}

func ( *Printer) () {
	if .levelIncs[len(.levelIncs)-1] {
		.level--
	}
	.levelIncs = .levelIncs[:len(.levelIncs)-1]
}

func ( *Printer) () {
	if .minify {
		return
	}
	.lastLevel = .level
	switch {
	case .level == 0:
	case .indentSpaces == 0:
		.WriteByte(tabwriter.Escape)
		for  := uint(0);  < .level; ++ {
			.WriteByte('\t')
		}
		.WriteByte(tabwriter.Escape)
	default:
		.spaces(.indentSpaces * .level)
	}
}

// TODO(mvdan): add an indent call at the end of newline?

// newline prints one newline and advances p.line to pos.Line().
func ( *Printer) ( Pos) {
	.flushHeredocs()
	.flushComments()
	.WriteByte('\n')
	.wantSpace = spaceWritten
	.wantNewline, .mustNewline = false, false
	.advanceLine(.Line())
}

func ( *Printer) ( uint) {
	if .line <  {
		.line = 
	}
}

func ( *Printer) () {
	if len(.pendingHdocs) == 0 {
		return
	}
	 := .pendingHdocs
	.pendingHdocs = .pendingHdocs[:0]
	 := .pendingComments
	.pendingComments = nil
	if len() > 0 {
		 := [0]
		if .Pos().Line() == .line {
			.pendingComments = append(.pendingComments, )
			.flushComments()
			 = [1:]
		}
	}

	// Reuse the last indentation level, as
	// indentation levels are usually changed before
	// newlines are printed along with their
	// subsequent indentation characters.
	 := .level
	.level = .lastLevel

	for ,  := range  {
		.line++
		.WriteByte('\n')
		.wantSpace = spaceWritten
		.wantNewline, .wantNewline = false, false
		if .Op == DashHdoc && .indentSpaces == 0 && !.minify {
			if .Hdoc != nil {
				 := extraIndenter{
					bufWriter:   .bufWriter,
					baseIndent:  int(.level + 1),
					firstIndent: -1,
				}
				.tabsPrinter = &Printer{
					bufWriter: &,

					// The options need to persist.
					indentSpaces:   .indentSpaces,
					binNextLine:    .binNextLine,
					swtCaseIndent:  .swtCaseIndent,
					spaceRedirects: .spaceRedirects,
					keepPadding:    .keepPadding,
					minify:         .minify,
					funcNextLine:   .funcNextLine,

					line: .Hdoc.Pos().Line(),
				}
				.tabsPrinter.wordParts(.Hdoc.Parts, true)
			}
			.indent()
		} else if .Hdoc != nil {
			.wordParts(.Hdoc.Parts, true)
		}
		.unquotedWord(.Word)
		if .Hdoc != nil {
			// Overwrite p.line, since printing r.Word again can set
			// p.line to the beginning of the heredoc again.
			.advanceLine(.Hdoc.End().Line())
		}
		.wantSpace = spaceNotRequired
	}
	.level = 
	.pendingComments = 
	.mustNewline = true
}

// newline prints between zero and two newlines.
// If any newlines are printed, it advances p.line to pos.Line().
func ( *Printer) ( Pos) {
	if .firstLine && len(.pendingComments) == 0 {
		.firstLine = false
		return // no empty lines at the top
	}
	if !.wantsNewline(, false) {
		return
	}
	.flushHeredocs()
	.flushComments()
	.WriteByte('\n')
	.wantSpace = spaceWritten
	.wantNewline, .mustNewline = false, false

	 := .Line()
	if  > .line+1 && !.minify {
		.WriteByte('\n') // preserve single empty lines
	}
	.advanceLine()
	.indent()
}

func ( *Printer) ( Pos) {
	if len(.pendingHdocs) > 0 || !.minify {
		.newlines()
	}
	.WriteByte(')')
	.wantSpace = spaceRequired
}

func ( *Printer) ( string,  Pos) {
	if .wantsNewline(, false) {
		.newlines()
	} else {
		if !.wroteSemi {
			.WriteByte(';')
		}
		if !.minify {
			.spacePad()
		}
	}
	.WriteString()
	.wantSpace = spaceRequired
}

func ( *Printer) () {
	for ,  := range .pendingComments {
		if  == 0 {
			// Flush any pending heredocs first. Otherwise, the
			// comments would become part of a heredoc body.
			.flushHeredocs()
		}
		.firstLine = false
		// We can't call any of the newline methods, as they call this
		// function and we'd recurse forever.
		 := .Hash.Line()
		switch {
		case .mustNewline,  > 0,  > .line && .line > 0:
			.WriteByte('\n')
			if  > .line+1 {
				.WriteByte('\n')
			}
			.indent()
			.wantSpace = spaceWritten
			.spacePad(.Pos())
		case .wantSpace == spaceRequired:
			if .keepPadding {
				.spacePad(.Pos())
			} else {
				.WriteByte('\t')
			}
		case .wantSpace != spaceWritten:
			.space()
		}
		// don't go back one line, which may happen in some edge cases
		.advanceLine()
		.WriteByte('#')
		.writeLit(strings.TrimRightFunc(.Text, unicode.IsSpace))
		.wantNewline = true
		.mustNewline = true
	}
	.pendingComments = nil
}

func ( *Printer) ( ...Comment) {
	if .minify {
		for ,  := range  {
			if fileutil.Shebang([]byte("#"+.Text)) != "" && .Hash.Col() == 1 && .Hash.Line() == 1 {
				.WriteString(strings.TrimRightFunc("#"+.Text, unicode.IsSpace))
				.WriteString("\n")
				.line++
			}
		}
		return
	}
	.pendingComments = append(.pendingComments, ...)
}

func ( *Printer) ( []WordPart,  bool) {
	// We disallow unquoted escaped newlines between word parts below.
	// However, we want to allow a leading escaped newline for cases such as:
	//
	//   foo <<< \
	//     "bar baz"
	if ! && !.singleLine && [0].Pos().Line() > .line {
		.bslashNewl()
	}
	for ,  := range  {
		var  WordPart
		if +1 < len() {
			 = [+1]
		}
		// Keep escaped newlines separating word parts when quoted.
		// Note that those escaped newlines don't cause indentaiton.
		// When not quoted, we strip them out consistently,
		// because attempting to keep them would prevent indentation.
		// Can't use p.wantsNewline here, since this is only about
		// escaped newlines.
		for  && !.singleLine && .Pos().Line() > .line {
			.WriteString("\\\n")
			.line++
		}
		.wordPart(, )
		.advanceLine(.End().Line())
	}
}

func ( *Printer) (,  WordPart) {
	switch x := .(type) {
	case *Lit:
		.writeLit(.Value)
	case *SglQuoted:
		if .Dollar {
			.WriteByte('$')
		}
		.WriteByte('\'')
		.writeLit(.Value)
		.WriteByte('\'')
		.advanceLine(.End().Line())
	case *DblQuoted:
		.dblQuoted()
	case *CmdSubst:
		.advanceLine(.Pos().Line())
		switch {
		case .TempFile:
			.WriteString("${")
			.wantSpace = spaceRequired
			.nestedStmts(.Stmts, .Last, .Right)
			.wantSpace = spaceNotRequired
			.semiRsrv("}", .Right)
		case .ReplyVar:
			.WriteString("${|")
			.nestedStmts(.Stmts, .Last, .Right)
			.wantSpace = spaceNotRequired
			.semiRsrv("}", .Right)
		// Special case: `# inline comment`
		case .Backquotes && len(.Stmts) == 0 &&
			len(.Last) == 1 && .Right.Line() == .line:
			.WriteString("`#")
			.WriteString(.Last[0].Text)
			.WriteString("`")
		default:
			.WriteString("$(")
			if len(.Stmts) > 0 && startsWithLparen(.Stmts[0]) {
				.wantSpace = spaceRequired
			} else {
				.wantSpace = spaceNotRequired
			}
			.nestedStmts(.Stmts, .Last, .Right)
			.rightParen(.Right)
		}
	case *ParamExp:
		 := ";"
		if ,  := .(*Lit);  && .Value != "" {
			 = .Value[:1]
		}
		 := .Param.Value
		switch {
		case !.minify:
		case .Excl, .Length, .Width:
		case .Index != nil, .Slice != nil:
		case .Repl != nil, .Exp != nil:
		case len() > 1 && !ValidName(): // ${10}
		case ValidName( + ): // ${var}cont
		default:
			 := *
			.Short = true
			.paramExp(&)
			return
		}
		.paramExp()
	case *ArithmExp:
		.WriteString("$((")
		if .Unsigned {
			.WriteString("# ")
		}
		.arithmExpr(.X, false, false)
		.WriteString("))")
	case *ExtGlob:
		.WriteString(.Op.String())
		.writeLit(.Pattern.Value)
		.WriteByte(')')
	case *ProcSubst:
		// avoid conflict with << and others
		if .wantSpace == spaceRequired {
			.space()
		}
		.WriteString(.Op.String())
		.nestedStmts(.Stmts, .Last, .Rparen)
		.rightParen(.Rparen)
	}
}

func ( *Printer) ( *DblQuoted) {
	if .Dollar {
		.WriteByte('$')
	}
	.WriteByte('"')
	if len(.Parts) > 0 {
		.wordParts(.Parts, true)
	}
	// Add any trailing escaped newlines.
	for .line < .Right.Line() {
		.WriteString("\\\n")
		.line++
	}
	.WriteByte('"')
}

func ( *Printer) ( ArithmExpr) bool {
	if  == nil {
		return false
	}
	.WriteByte('[')
	.arithmExpr(, false, false)
	.WriteByte(']')
	return true
}

func ( *Printer) ( *ParamExp) {
	if .nakedIndex() { // arr[x]
		.writeLit(.Param.Value)
		.wroteIndex(.Index)
		return
	}
	if .Short { // $var
		.WriteByte('$')
		.writeLit(.Param.Value)
		return
	}
	// ${var...}
	.WriteString("${")
	switch {
	case .Length:
		.WriteByte('#')
	case .Width:
		.WriteByte('%')
	case .Excl:
		.WriteByte('!')
	}
	.writeLit(.Param.Value)
	.wroteIndex(.Index)
	switch {
	case .Slice != nil:
		.WriteByte(':')
		.arithmExpr(.Slice.Offset, true, true)
		if .Slice.Length != nil {
			.WriteByte(':')
			.arithmExpr(.Slice.Length, true, false)
		}
	case .Repl != nil:
		if .Repl.All {
			.WriteByte('/')
		}
		.WriteByte('/')
		if .Repl.Orig != nil {
			.word(.Repl.Orig)
		}
		.WriteByte('/')
		if .Repl.With != nil {
			.word(.Repl.With)
		}
	case .Names != 0:
		.writeLit(.Names.String())
	case .Exp != nil:
		.WriteString(.Exp.Op.String())
		if .Exp.Word != nil {
			.word(.Exp.Word)
		}
	}
	.WriteByte('}')
}

func ( *Printer) ( Loop) {
	switch x := .(type) {
	case *WordIter:
		.writeLit(.Name.Value)
		if .InPos.IsValid() {
			.spacedString(" in", Pos{})
			.wordJoin(.Items)
		}
	case *CStyleLoop:
		.WriteString("((")
		if .Init == nil {
			.space()
		}
		.arithmExpr(.Init, false, false)
		.WriteString("; ")
		.arithmExpr(.Cond, false, false)
		.WriteString("; ")
		.arithmExpr(.Post, false, false)
		.WriteString("))")
	}
}

func ( *Printer) ( ArithmExpr, ,  bool) {
	if .minify {
		 = true
	}
	switch x := .(type) {
	case *Word:
		.word()
	case *BinaryArithm:
		if  {
			.(.X, , )
			.WriteString(.Op.String())
			.(.Y, , false)
		} else {
			.(.X, , )
			if .Op != Comma {
				.space()
			}
			.WriteString(.Op.String())
			.space()
			.(.Y, , false)
		}
	case *UnaryArithm:
		if .Post {
			.(.X, , )
			.WriteString(.Op.String())
		} else {
			if  {
				switch .Op {
				case Plus, Minus:
					.space()
				}
			}
			.WriteString(.Op.String())
			.(.X, , false)
		}
	case *ParenArithm:
		.WriteByte('(')
		.(.X, false, false)
		.WriteByte(')')
	}
}

func ( *Printer) ( TestExpr) {
	// Multi-line test expressions don't need to escape newlines.
	if .Pos().Line() > .line {
		.newlines(.Pos())
		.spacePad(.Pos())
	} else if .wantSpace == spaceRequired {
		.space()
	}
	.testExprSameLine()
}

func ( *Printer) ( TestExpr) {
	.advanceLine(.Pos().Line())
	switch x := .(type) {
	case *Word:
		.word()
	case *BinaryTest:
		.(.X)
		.space()
		.WriteString(.Op.String())
		switch .Op {
		case AndTest, OrTest:
			.wantSpace = spaceRequired
			.testExpr(.Y)
		default:
			.space()
			.(.Y)
		}
	case *UnaryTest:
		.WriteString(.Op.String())
		.space()
		.(.X)
	case *ParenTest:
		.WriteByte('(')
		if startsWithLparen(.X) {
			.wantSpace = spaceRequired
		} else {
			.wantSpace = spaceNotRequired
		}
		.testExpr(.X)
		.WriteByte(')')
	}
}

func ( *Printer) ( *Word) {
	.wordParts(.Parts, false)
	.wantSpace = spaceRequired
}

func ( *Printer) ( *Word) {
	for ,  := range .Parts {
		switch x := .(type) {
		case *SglQuoted:
			.writeLit(.Value)
		case *DblQuoted:
			.wordParts(.Parts, true)
		case *Lit:
			for  := 0;  < len(.Value); ++ {
				if  := .Value[];  == '\\' {
					if ++;  < len(.Value) {
						.WriteByte(.Value[])
					}
				} else {
					.WriteByte()
				}
			}
		}
	}
}

func ( *Printer) ( []*Word) {
	 := false
	for ,  := range  {
		if  := .Pos(); .Line() > .line && !.singleLine {
			if ! {
				.incLevel()
				 = true
			}
			.bslashNewl()
		}
		.spacePad(.Pos())
		.word()
	}
	if  {
		.decLevel()
	}
}

func ( *Printer) ( []*Word) {
	 := false
	for ,  := range  {
		if  > 0 {
			.spacedToken("|", Pos{})
		}
		if .wantsNewline(.Pos(), true) {
			if ! {
				.incLevel()
				 = true
			}
			.bslashNewl()
		} else {
			.spacePad(.Pos())
		}
		.word()
	}
	if  {
		.decLevel()
	}
}

func ( *Printer) ( []*ArrayElem,  []Comment) {
	.incLevel()
	for ,  := range  {
		var  []Comment
		for ,  := range .Comments {
			if .Pos().After(.Pos()) {
				 = append(, )
				break
			}
			.comments()
		}
		// Multi-line array expressions don't need to escape newlines.
		if .Pos().Line() > .line {
			.newlines(.Pos())
			.spacePad(.Pos())
		} else if .wantSpace == spaceRequired {
			.space()
		}
		if .wroteIndex(.Index) {
			.WriteByte('=')
		}
		if .Value != nil {
			.word(.Value)
		}
		.comments(...)
	}
	if len() > 0 {
		.comments(...)
		.flushComments()
	}
	.decLevel()
}

func ( *Printer) ( *Stmt) {
	.wroteSemi = false
	if .Negated {
		.spacedString("!", .Pos())
	}
	var  int
	if .Cmd != nil {
		 = .command(.Cmd, .Redirs)
	}
	.incLevel()
	for ,  := range .Redirs[:] {
		if .wantsNewline(.OpPos, true) {
			.bslashNewl()
		}
		if .wantSpace == spaceRequired {
			.spacePad(.Pos())
		}
		if .N != nil {
			.writeLit(.N.Value)
		}
		.WriteString(.Op.String())
		if .spaceRedirects && (.Op != DplIn && .Op != DplOut) {
			.space()
		} else {
			.wantSpace = spaceRequired
		}
		.word(.Word)
		if .Op == Hdoc || .Op == DashHdoc {
			.pendingHdocs = append(.pendingHdocs, )
		}
	}
	 := .Semicolon.IsValid() && .Semicolon.Line() > .line && !.singleLine
	if  || .Background || .Coprocess {
		if  {
			.bslashNewl()
		} else if !.minify {
			.space()
		}
		if .Background {
			.WriteString("&")
		} else if .Coprocess {
			.WriteString("|&")
		} else {
			.WriteString(";")
		}
		.wroteSemi = true
		.wantSpace = spaceRequired
	}
	.decLevel()
}

func ( *Printer) ( Command,  []*Redirect) ( int) {
	.advanceLine(.Pos().Line())
	.spacePad(.Pos())
	switch x := .(type) {
	case *CallExpr:
		.assigns(.Assigns)
		if len(.Args) <= 1 {
			.wordJoin(.Args)
			return 0
		}
		.wordJoin(.Args[:1])
		for ,  := range  {
			if .Pos().After(.Args[1].Pos()) || .Op == Hdoc || .Op == DashHdoc {
				break
			}
			if .wantSpace == spaceRequired {
				.spacePad(.Pos())
			}
			if .N != nil {
				.writeLit(.N.Value)
			}
			.WriteString(.Op.String())
			if .spaceRedirects && (.Op != DplIn && .Op != DplOut) {
				.space()
			} else {
				.wantSpace = spaceRequired
			}
			.word(.Word)
			++
		}
		.wordJoin(.Args[1:])
	case *Block:
		.WriteByte('{')
		.wantSpace = spaceRequired
		// Forbid "foo()\n{ bar; }"
		.wantNewline = .wantNewline || .funcNextLine
		.nestedStmts(.Stmts, .Last, .Rbrace)
		.semiRsrv("}", .Rbrace)
	case *IfClause:
		.ifClause(, false)
	case *Subshell:
		.WriteByte('(')
		 := .Stmts
		if len() > 0 && startsWithLparen([0]) {
			.wantSpace = spaceRequired
			// Add a space between nested parentheses if we're printing them in a single line,
			// to avoid the ambiguity between `((` and `( (`.
			if (.Lparen.Line() != [0].Pos().Line() || len() > 1) && !.singleLine {
				.wantSpace = spaceNotRequired

				if .minify {
					.mustNewline = true
				}
			}
		} else {
			.wantSpace = spaceNotRequired
		}

		.spacePad(stmtsPos(.Stmts, .Last))
		.nestedStmts(.Stmts, .Last, .Rparen)
		.wantSpace = spaceNotRequired
		.spacePad(.Rparen)
		.rightParen(.Rparen)
	case *WhileClause:
		if .Until {
			.spacedString("until", .Pos())
		} else {
			.spacedString("while", .Pos())
		}
		.nestedStmts(.Cond, .CondLast, Pos{})
		.semiOrNewl("do", .DoPos)
		.nestedStmts(.Do, .DoLast, .DonePos)
		.semiRsrv("done", .DonePos)
	case *ForClause:
		if .Select {
			.WriteString("select ")
		} else {
			.WriteString("for ")
		}
		.loop(.Loop)
		.semiOrNewl("do", .DoPos)
		.nestedStmts(.Do, .DoLast, .DonePos)
		.semiRsrv("done", .DonePos)
	case *BinaryCmd:
		.stmt(.X)
		if .minify || .singleLine || .Y.Pos().Line() <= .line {
			// leave p.nestedBinary untouched
			.spacedToken(.Op.String(), .OpPos)
			.advanceLine(.Y.Pos().Line())
			.stmt(.Y)
			break
		}
		 := !.nestedBinary
		if  {
			.incLevel()
		}
		if .binNextLine {
			if len(.pendingHdocs) == 0 {
				.bslashNewl()
			}
			.spacedToken(.Op.String(), .OpPos)
			if len(.Y.Comments) > 0 {
				.wantSpace = spaceNotRequired
				.newline(.Y.Pos())
				.indent()
				.comments(.Y.Comments...)
				.newline(Pos{})
				.indent()
			}
		} else {
			.spacedToken(.Op.String(), .OpPos)
			.advanceLine(.OpPos.Line())
			.comments(.Y.Comments...)
			.newline(Pos{})
			.indent()
		}
		.advanceLine(.Y.Pos().Line())
		_, .nestedBinary = .Y.Cmd.(*BinaryCmd)
		.stmt(.Y)
		if  {
			.decLevel()
		}
		.nestedBinary = false
	case *FuncDecl:
		if .RsrvWord {
			.WriteString("function ")
		}
		.writeLit(.Name.Value)
		if !.RsrvWord || .Parens {
			.WriteString("()")
		}
		if .funcNextLine {
			.newline(Pos{})
			.indent()
		} else if !.Parens || !.minify {
			.space()
		}
		.advanceLine(.Body.Pos().Line())
		.comments(.Body.Comments...)
		.stmt(.Body)
	case *CaseClause:
		.WriteString("case ")
		.word(.Word)
		.WriteString(" in")
		.advanceLine(.In.Line())
		.wantSpace = spaceRequired
		if .swtCaseIndent {
			.incLevel()
		}
		if len(.Items) == 0 {
			// Apparently "case x in; esac" is invalid shell.
			.mustNewline = true
		}
		for ,  := range .Items {
			var  []Comment
			for ,  := range .Comments {
				if .Pos().After(.Pos()) {
					 = .Comments[:]
					break
				}
				.comments()
			}
			.newlines(.Pos())
			.spacePad(.Pos())
			.casePatternJoin(.Patterns)
			.WriteByte(')')
			if !.minify {
				.wantSpace = spaceRequired
			} else {
				.wantSpace = spaceNotRequired
			}

			 := stmtsPos(.Stmts, .Last)
			 := stmtsEnd(.Stmts, .Last)
			 := len(.Stmts) > 1 || .Line() > .line ||
				(.IsValid() && .OpPos.Line() > .Line())
			.nestedStmts(.Stmts, .Last, .OpPos)
			.level++
			if !.minify ||  != len(.Items)-1 {
				if  {
					.newlines(.OpPos)
					.wantNewline = true
				}
				.spacedToken(.Op.String(), .OpPos)
				.advanceLine(.OpPos.Line())
				// avoid ; directly after tokens like ;;
				.wroteSemi = true
			}
			.comments(...)
			.flushComments()
			.level--
		}
		.comments(.Last...)
		if .swtCaseIndent {
			.flushComments()
			.decLevel()
		}
		.semiRsrv("esac", .Esac)
	case *ArithmCmd:
		.WriteString("((")
		if .Unsigned {
			.WriteString("# ")
		}
		.arithmExpr(.X, false, false)
		.WriteString("))")
	case *TestClause:
		.WriteString("[[ ")
		.incLevel()
		.testExpr(.X)
		.decLevel()
		.spacedString("]]", .Right)
	case *DeclClause:
		.spacedString(.Variant.Value, .Pos())
		.assigns(.Args)
	case *TimeClause:
		.spacedString("time", .Pos())
		if .PosixFormat {
			.spacedString("-p", .Pos())
		}
		if .Stmt != nil {
			.stmt(.Stmt)
		}
	case *CoprocClause:
		.spacedString("coproc", .Pos())
		if .Name != nil {
			.space()
			.word(.Name)
		}
		.space()
		.stmt(.Stmt)
	case *LetClause:
		.spacedString("let", .Pos())
		for ,  := range .Exprs {
			.space()
			.arithmExpr(, true, false)
		}
	case *TestDecl:
		.spacedString("@test", .Pos())
		.space()
		.word(.Description)
		.space()
		.stmt(.Body)
	default:
		panic(fmt.Sprintf("syntax.Printer: unexpected node type %T", ))
	}
	return 
}

func ( *Printer) ( *IfClause,  bool) {
	if ! {
		.spacedString("if", .Pos())
	}
	.nestedStmts(.Cond, .CondLast, Pos{})
	.semiOrNewl("then", .ThenPos)
	 := .FiPos
	 := .Else
	if  != nil {
		 = .Position
	}
	.nestedStmts(.Then, .ThenLast, )

	if  != nil && .ThenPos.IsValid() {
		.comments(.Last...)
		.semiRsrv("elif", .Position)
		.(, true)
		return
	}
	if  == nil {
		.comments(.Last...)
	} else {
		var  []Comment
		for ,  := range .Last {
			if .Pos().After(.Position) {
				 = append(, )
				break
			}
			.comments()
		}
		.semiRsrv("else", .Position)
		.comments(...)
		.nestedStmts(.Then, .ThenLast, .FiPos)
		.comments(.Last...)
	}
	.semiRsrv("fi", .FiPos)
}

func ( *Printer) ( []*Stmt,  []Comment) {
	 := .wantNewline || (len() > 0 && [0].Pos().Line() > .line)
	for ,  := range  {
		if  > 0 && .singleLine && .wantNewline && !.wroteSemi {
			// In singleLine mode, ensure we use semicolons between
			// statements.
			.WriteByte(';')
			.wantSpace = spaceRequired
		}
		 := .Pos()
		var ,  []Comment
		for ,  := range .Comments {
			// Comments after the end of this command. Note that
			// this includes "<<EOF # comment".
			if .Cmd != nil && .End().After(.Cmd.End()) {
				 = append(, )
				break
			}
			// Comments between the beginning of the statement and
			// the end of the command.
			if .Pos().After() {
				 = append(, )
				continue
			}
			// The rest of the comments are before the entire
			// statement.
			.comments()
		}
		if .mustNewline || !.minify || .wantSpace == spaceRequired {
			.newlines()
		}
		.advanceLine(.Line())
		.comments(...)
		.stmt()
		.comments(...)
		.wantNewline = true
	}
	if len() == 1 && ! {
		.wantNewline = false
	}
	.comments(...)
}

func ( *Printer) ( []*Stmt,  []Comment,  Pos) {
	.incLevel()
	switch {
	case len() > 1:
		// Force a newline if we find:
		//     { stmt; stmt; }
		.wantNewline = true
	case .Line() > .line && len() > 0 &&
		stmtsEnd(, ).Line() < .Line():
		// Force a newline if we find:
		//     { stmt
		//     }
		.wantNewline = true
	case len(.pendingComments) > 0 && len() > 0:
		// Force a newline if we find:
		//     for i in a b # stmt
		//     do foo; done
		.wantNewline = true
	}
	.stmtList(, )
	if .IsValid() {
		.flushComments()
	}
	.decLevel()
}

func ( *Printer) ( []*Assign) {
	.incLevel()
	for ,  := range  {
		if .wantsNewline(.Pos(), true) {
			.bslashNewl()
		} else {
			.spacePad(.Pos())
		}
		if .Name != nil {
			.writeLit(.Name.Value)
			.wroteIndex(.Index)
			if .Append {
				.WriteByte('+')
			}
			if !.Naked {
				.WriteByte('=')
			}
		}
		if .Value != nil {
			// Ensure we don't use an escaped newline after '=',
			// because that can result in indentation, thus
			// splitting "foo=bar" into "foo= bar".
			.advanceLine(.Value.Pos().Line())
			.word(.Value)
		} else if .Array != nil {
			.wantSpace = spaceNotRequired
			.WriteByte('(')
			.elemJoin(.Array.Elems, .Array.Last)
			.rightParen(.Array.Rparen)
		}
		.wantSpace = spaceRequired
	}
	.decLevel()
}

type wantSpaceState uint8

const (
	spaceNotRequired wantSpaceState = iota
	spaceRequired                   // we should generally print a space or a newline next
	spaceWritten                    // we have just written a space or newline
)

// extraIndenter ensures that all lines in a '<<-' heredoc body have at least
// baseIndent leading tabs. Those that had more tab indentation than the first
// heredoc line will keep that relative indentation.
type extraIndenter struct {
	bufWriter
	baseIndent int

	firstIndent int
	firstChange int
	curLine     []byte
}

func ( *extraIndenter) ( byte) error {
	.curLine = append(.curLine, )
	if  != '\n' {
		return nil
	}
	 := bytes.TrimLeft(.curLine, "\t")
	if len() == 1 {
		// no tabs if this is an empty line, i.e. "\n"
		.bufWriter.Write()
		.curLine = .curLine[:0]
		return nil
	}

	 := len(.curLine) - len()
	if .firstIndent < 0 {
		// This is the first heredoc line we add extra indentation to.
		// Keep track of how much we indented.
		.firstIndent = 
		.firstChange = .baseIndent - 
		 = .baseIndent

	} else if  < .firstIndent {
		// This line did not have enough indentation; simply indent it
		// like the first line.
		 = .firstIndent
	} else {
		// This line had plenty of indentation. Add the extra
		// indentation that the first line had, for consistency.
		 += .firstChange
	}
	.bufWriter.WriteByte(tabwriter.Escape)
	for  := 0;  < ; ++ {
		.bufWriter.WriteByte('\t')
	}
	.bufWriter.WriteByte(tabwriter.Escape)
	.bufWriter.Write()
	.curLine = .curLine[:0]
	return nil
}

func ( *extraIndenter) ( string) (int, error) {
	for  := 0;  < len(); ++ {
		.WriteByte([])
	}
	return len(), nil
}

func startsWithLparen( Node) bool {
	switch node := .(type) {
	case *Stmt:
		return (.Cmd)
	case *BinaryCmd:
		return (.X)
	case *Subshell:
		return true // keep ( (
	case *ArithmCmd:
		return true // keep ( ((
	}
	return false
}