Files
mvdan-sh/syntax/printer_test.go
T

1419 lines
33 KiB
Go

// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package syntax
import (
"bytes"
"cmp"
"fmt"
"os"
"regexp"
"strings"
"testing"
)
func TestPrintFiles(t *testing.T) {
t.Parallel()
for lang := range langResolvedVariants.bits() {
t.Run(lang.String(), func(t *testing.T) {
parser := NewParser(Variant(lang), KeepComments(true))
printer := NewPrinter()
for _, c := range append(fileTests, fileTestsKeepComments...) {
want, _ := c.byLangIndex[lang.index()].(*File)
if want == nil {
continue
}
if wantBash, _ := c.byLangIndex[LangBash.index()].(*File); wantBash != nil &&
lang != LangBash && want != wantBash {
// Skip cases where a non-Bash language parses differently than Bash.
// For example, `((foo))` prints as `( (foo))` only when in POSIX.
continue
}
t.Run("", func(t *testing.T) {
in := c.inputs[0]
printTest(t, parser, printer, in, cmp.Or(c.printedAs, in))
})
}
})
}
}
func strPrint(p *Printer, node Node) (string, error) {
var buf bytes.Buffer
err := p.Print(&buf, node)
return buf.String(), err
}
type printCase struct {
in, want string
}
func samePrint(s string) printCase { return printCase{in: s, want: s} }
var printTests = []printCase{
samePrint(`fo○ b\år`),
samePrint(`"fo○ b\år"`),
samePrint(`'fo○ b\år'`),
samePrint(`${a#fo○ b\år}`),
samePrint(`#fo○ b\år`),
samePrint("<<EOF\nfo○ b\\år\nEOF"),
samePrint(`$'○ b\år'`),
samePrint("${a/b//○}"),
{strings.Repeat(" ", bufSize-3) + "○", "○"}, // at the end of a chunk
{strings.Repeat(" ", bufSize-0) + "○", "○"}, // at the start of a chunk
{strings.Repeat(" ", bufSize-2) + "○", "○"}, // split after 1st byte
{strings.Repeat(" ", bufSize-1) + "○", "○"}, // split after 2nd byte
// peekByte that would (but cannot) go to the next chunk
{strings.Repeat(" ", bufSize-2) + ">(a)", ">(a)"},
// escaped newline at end of chunk
{"a" + strings.Repeat(" ", bufSize-2) + "\\\nb", "a \\\n\tb"},
// panics if padding is only 4 (utf8.UTFMax)
{strings.Repeat(" ", bufSize-10) + "${a/b//○}", "${a/b//○}"},
// multiple p.fill calls
{"a" + strings.Repeat(" ", bufSize*4) + "b", "a b"},
// newline at the beginning of second chunk
{"a" + strings.Repeat(" ", bufSize-2) + "\nb", "a\nb"},
{"foo; bar", "foo\nbar"},
{"foo\n\n\nbar", "foo\n\nbar"},
{"foo\n\n", "foo"},
{"\n\nfoo", "foo"},
{"# foo \n # bar\t", "# foo\n# bar"},
samePrint("#"),
samePrint("#c1\\\n#c2"),
samePrint("#\\\n#"),
{"#\\\r\n#", "#\\\n#"},
samePrint("{\n\tfoo\n\t# bar \\\n}"),
samePrint("foo\\\\\nbar"),
samePrint("a=b # inline\nbar"),
samePrint("a=$(b) # inline"),
samePrint("foo # inline\n# after"),
samePrint("$(a) $(b)"),
{"if a\nthen\n\tb\nfi", "if a; then\n\tb\nfi"},
samePrint("if a; then\n\tb\nelse\n\tc\nfi"),
{"if a; then b\nelse c\nfi", "if a; then\n\tb\nelse\n\tc\nfi"},
samePrint("foo >&2 <f bar"),
samePrint("foo >&2 bar <f"),
samePrint(">&2 foo bar <f"),
samePrint(">&2 foo"),
samePrint(">&2 foo 2>&1 bar <f"),
{"foo >&2>/dev/null", "foo >&2 >/dev/null"},
samePrint("exec {foo[1]}>&-"),
samePrint("exec {foo}>&-"),
{"foo <<EOF bar\nl1\nEOF", "foo bar <<EOF\nl1\nEOF"},
samePrint("foo <<\\\\\\\\EOF\nbar\n\\\\EOF"),
samePrint("foo <<\"\\EOF\"\nbar\n\\EOF"),
samePrint("foo <<\"EOF\"\nbar\nEOF\nbar"),
samePrint("foo <<EOF && bar\nl1\nEOF"),
samePrint("foo <<EOF &&\nl1\nEOF\n\tbar"),
samePrint("foo <<EOF\nl1\nEOF\n\nfoo2"),
samePrint("<<EOF\nfoo\\\nbar\nEOF"),
samePrint("<<'EOF'\nfoo\\\nbar\nEOF"),
samePrint("<<EOF\n\\\n$foo\nEOF"),
samePrint("<<'EOF'\n\\\nEOF"),
samePrint("{\n\t<<EOF\nfoo\\\nbar\nEOF\n}"),
samePrint("{\n\t<<'EOF'\nfoo\\\nbar\nEOF\n}"),
samePrint("<<EOF\nEOF"),
samePrint("foo <<EOF\nEOF\n\nbar"),
samePrint("foo <<'EOF'\nEOF\n\nbar"),
samePrint("if cmd <<EOF\nbody\nEOF\nthen\n\tfoo\nfi"),
samePrint("while cmd <<EOF\nbody\nEOF\ndo\n\tfoo\ndone"),
samePrint("if true; then\n\tcat <<-EOF # comment\n\t\tcontent\n\tEOF\nfi"),
{
"{ foo; bar; }",
"{\n\tfoo\n\tbar\n}",
},
{
"{ foo; bar; }\n#etc",
"{\n\tfoo\n\tbar\n}\n#etc",
},
{
"{\n\tfoo; }",
"{\n\tfoo\n}",
},
{
"{ foo\n}",
"{\n\tfoo\n}",
},
{
"(foo\n)",
"(\n\tfoo\n)",
},
{
"$(foo\n)",
"$(\n\tfoo\n)",
},
{
"a\n\n\n# etc\nb",
"a\n\n# etc\nb",
},
{
"a b\\\nc d",
"a bc d",
},
{
"a bb\\\ncc d",
"a bbcc d",
},
samePrint("a \\\n\tb \\\n\t\"c\" \\\n\t;"),
samePrint("a=1 \\\n\tb=2 \\\n\tc=\"3\" \\\n\t;"),
{
"a=\\\nfoo\nb=\\\n\"bar\"\nc=\\\n'baz'",
"a=foo\nb=\"bar\"\nc='baz'",
},
samePrint("if a \\\n\t; then b; fi"),
samePrint("a > \\\n\tfoo"),
samePrint("a <<< \\\n\t\"foo\""),
samePrint("a 'b\nb' c"),
samePrint("a $'b\nb' c"),
{
"(foo; bar)",
"(\n\tfoo\n\tbar\n)",
},
{
"{\nfoo\nbar; }",
"{\n\tfoo\n\tbar\n}",
},
samePrint("\"$foo\"\n{\n\tbar\n}"),
{
"{\nbar\n# extra\n}",
"{\n\tbar\n\t# extra\n}",
},
{
"foo\nbar # extra",
"foo\nbar # extra",
},
{
"foo # 1\nfooo # 2\nfo # 3",
"foo # 1\nfooo # 2\nfo # 3",
},
{
" foo # 1\n fooo # 2\n fo # 3",
"foo # 1\nfooo # 2\nfo # 3",
},
{
"foo # 1\nfooo # 2\nfo # 3",
"foo # 1\nfooo # 2\nfo # 3",
},
{
"foooooa\nfoo # 1\nfooo # 2\nfo # 3\nfooooo",
"foooooa\nfoo # 1\nfooo # 2\nfo # 3\nfooooo",
},
{
"foo\nbar\nfoo # 1\nfooo # 2",
"foo\nbar\nfoo # 1\nfooo # 2",
},
samePrint("foobar # 1\nfoo\nfoo # 2"),
samePrint("foobar # 1\n#foo\nfoo # 2"),
samePrint("foobar # 1\n\nfoo # 2"),
{
"foo # 2\nfoo2 bar # 1",
"foo # 2\nfoo2 bar # 1",
},
{
"foo bar # 1\n! foo # 2",
"foo bar # 1\n! foo # 2",
},
{
"aa #b\nc #d\ne\nf #g",
"aa #b\nc #d\ne\nf #g",
},
{
"{ a; } #x\nbbb #y\n{ #z\nc\n}",
"{ a; } #x\nbbb #y\n{ #z\n\tc\n}",
},
{
"foo; foooo # 1",
"foo\nfoooo # 1",
},
{
"aaa; b #1\nc #2",
"aaa\nb #1\nc #2",
},
{
"a #1\nbbb; c #2\nd #3",
"a #1\nbbb\nc #2\nd #3",
},
samePrint("aa #c1\n{ #c2\n\tb\n}"),
{
"aa #c1\n{ b; c; } #c2",
"aa #c1\n{\n\tb\n\tc\n} #c2",
},
samePrint("a #c1\n'b\ncc' #c2"),
{
"(\nbar\n# extra\n)",
"(\n\tbar\n\t# extra\n)",
},
{
"for a in 1 2\ndo\n\t# bar\n\tx\ndone",
"for a in 1 2; do\n\t# bar\n\tx\ndone",
},
samePrint("#before\nfoo | bar"),
samePrint("#before\nfoo && bar"),
samePrint("foo | bar # inline"),
samePrint("foo && bar # inline"),
samePrint("foo `# inline` \\\n\tbar"),
samePrint("for a in 1 2; do\n\n\tbar\ndone"),
{
"a \\\n\t&& b",
"a &&\n\tb",
},
{
"a \\\n\t&& b\nc",
"a &&\n\tb\nc",
},
{
"{\n(a \\\n&& b)\nc\n}",
"{\n\t(a &&\n\t\tb)\n\tc\n}",
},
{
"a && b \\\n&& c",
"a && b &&\n\tc",
},
{
"a \\\n&& $(b) && c \\\n&& d",
"a &&\n\t$(b) && c &&\n\td",
},
{
"a \\\n&& b\nc \\\n&& d",
"a &&\n\tb\nc &&\n\td",
},
{
"a \\\n&&\n#c\nb",
"a &&\n\t#c\n\tb",
},
{
"a | {\nb \\\n| c\n}",
"a | {\n\tb |\n\t\tc\n}",
},
{
"a \\\n\t&& if foo; then\nbar\nfi",
"a &&\n\tif foo; then\n\t\tbar\n\tfi",
},
{
"if\nfoo\nthen\nbar\nfi",
"if\n\tfoo\nthen\n\tbar\nfi",
},
{
"if foo \\\nbar\nthen\nbar\nfi",
"if foo \\\n\tbar; then\n\tbar\nfi",
},
{
"if foo \\\n&& bar\nthen\nbar\nfi",
"if foo &&\n\tbar; then\n\tbar\nfi",
},
{
"a |\nb |\nc",
"a |\n\tb |\n\tc",
},
samePrint("a |\n\tb | c |\n\td"),
samePrint("a | b |\n\tc |\n\td"),
{
"foo |\n# misplaced\nbar",
"foo |\n\t# misplaced\n\tbar",
},
samePrint("{\n\tfoo\n\t#a\n\tbar\n} | etc"),
{
"foo &&\n#a1\n#a2\n$(bar)",
"foo &&\n\t#a1\n\t#a2\n\t$(bar)",
},
{
"{\n\tfoo\n\t#a\n} |\n# misplaced\nbar",
"{\n\tfoo\n\t#a\n} |\n\t# misplaced\n\tbar",
},
samePrint("foo | bar\n#after"),
{
"a |\nb | #c2\nc",
"a |\n\tb | #c2\n\tc",
},
{
"{\nfoo &&\n#a1\n#a2\n$(bar)\n}",
"{\n\tfoo &&\n\t\t#a1\n\t\t#a2\n\t\t$(bar)\n}",
},
{
"foo | while read l; do\nbar\ndone",
"foo | while read l; do\n\tbar\ndone",
},
samePrint("while x; do\n\t#comment\n\ty\ndone"),
{
"while x\ndo\n\ty\ndone",
"while x; do\n\ty\ndone",
},
samePrint("\"\\\nfoo\""),
samePrint("'\\\nfoo'"),
samePrint("\"foo\\\n bar\""),
samePrint("'foo\\\n bar'"),
samePrint("v=\"\\\nfoo\""),
{
"v=foo\\\nbar",
"v=foobar",
},
{
"v='foo'\\\n'bar'",
"v='foo''bar'",
},
{
"v=\\\n\"foo\"",
"v=\"foo\"",
},
{
"v=\\\nfoo\\\n$bar",
"v=foo$bar",
},
samePrint("\"\\\n\\\nfoo\\\n\\\n\""),
samePrint("'\\\n\\\nfoo\\\n\\\n'"),
{
"foo \\\n>bar\netc",
"foo \\\n\t>bar\netc",
},
{
"foo \\\nfoo2 \\\n>bar",
"foo \\\n\tfoo2 \\\n\t>bar",
},
samePrint("> >(foo)"),
samePrint("x > >(foo) y"),
samePrint("a | (x) |\n\tb"),
samePrint("a | (\n\tx\n\ty\n) |\n\tb"),
samePrint("a |\n\tif foo; then\n\t\tbar\n\tfi |\n\tb"),
samePrint("a | if foo; then\n\tbar\nfi"),
samePrint("a | b | if foo; then\n\tbar\nfi"),
{
"case $i in\n1)\nfoo\n;;\nesac",
"case $i in\n1)\n\tfoo\n\t;;\nesac",
},
{
"case $i in\n1)\nfoo\nesac",
"case $i in\n1)\n\tfoo\n\t;;\nesac",
},
{
"case $i in\n1) foo\nesac",
"case $i in\n1) foo ;;\nesac",
},
{
"case $i in\n1) foo; bar\nesac",
"case $i in\n1)\n\tfoo\n\tbar\n\t;;\nesac",
},
{
"case $i in\n1) foo; bar;;\nesac",
"case $i in\n1)\n\tfoo\n\tbar\n\t;;\nesac",
},
{
"case $i in\n1)\n#foo \t\n;;\nesac",
"case $i in\n1)\n\t#foo\n\t;;\nesac",
},
samePrint("case $i in\n1)\n\t;;\n\n2)\n\t;;\nesac"),
{
"case $i\nin\n1)\n\t;;\nesac",
"case $i in\n1)\n\t;;\nesac",
},
samePrint("case $i in\n1)\n\ta\n\t#b\n\t;;\nesac"),
samePrint("case $i in\n1) foo() { bar; } ;;\nesac"),
samePrint("case $i in\n1) ;; #foo\nesac"),
samePrint("case $i in\n#foo\nesac"),
samePrint("case $i in\n#before\n1) ;;\nesac"),
samePrint("case $i in\n#bef\n1) ;; #inl\nesac"),
samePrint("case $i in\n#before1\n'1') ;;\n#before2\n'2') ;;\nesac"),
samePrint("case $i in\n1) ;; #inl1\n2) ;; #inl2\nesac"),
samePrint("case $i in\n#bef\n1) #inl\n\tfoo\n\t;;\nesac"),
samePrint("case $i in\n1) #inl\n\t;;\nesac"),
samePrint("case $i in\n1) a \\\n\tb ;;\nesac"),
samePrint("case $i in\n1 | 2 | \\\n\t3 | 4) a b ;;\nesac"),
samePrint("case $i in\n1 | 2 | \\\n\t3 | 4)\n\ta b\n\t;;\nesac"),
samePrint("case $i in\nx) ;;\ny) for n in 1; do echo $n; done ;;\nesac"),
samePrint("case a in b) [[ x =~ y ]] ;; esac"),
samePrint("case a in b) [[ a =~ b$ || c =~ d$ ]] ;; esac"),
samePrint("case a in b) [[ a =~ (b) ]] ;; esac"),
samePrint("[[ (a =~ b$) ]]"),
samePrint("[[ a && ((b || c) && d) ]]"),
samePrint("[[ a &&\n\tb ]]"),
samePrint("[[ a ||\n\tb ]]"),
{
"[[ -f \\\n\tfoo ]]",
"[[ -f foo ]]",
},
{
"[[ foo \\\n\t-ef \\\n\tbar ]]",
"[[ foo -ef bar ]]",
},
{
"[[ a && \\\nb \\\n && c ]]",
"[[ a &&\n\tb &&\n\tc ]]",
},
samePrint("{\n\t[[ a || b ]]\n}"),
{
"a=(\nb\nc\n) b=c",
"a=(\n\tb\n\tc\n) b=c",
},
samePrint("a=(\n\t#before\n\tb #inline\n)"),
samePrint("a=(\n\tb #foo\n\tc #bar\n)"),
samePrint("a=(\n\tb\n\n\t#foo\n\t#bar\n\tc\n)"),
samePrint("a=(\n\t#foo\n\t#bar\n\tc\n)"),
samePrint("a=(\n\t#lone\n)"),
samePrint("a=(\n\n)"),
samePrint("a=(\n\tx\n\n\ty\n)"),
samePrint("foo <<EOF | $(bar)\n3\nEOF"),
{
"a <<EOF\n$(\n\tb\n\tc)\nEOF",
"a <<EOF\n$(\n\tb\n\tc\n)\nEOF",
},
samePrint("<<EOF1\n$(\n\t<<EOF2\ninner\nEOF2\n)\nEOF1"),
{
"<(<<EOF\nbody\nEOF\n)",
"<(\n\t<<EOF\nbody\nEOF\n)",
},
{
"( (foo) )\n$( (foo) )\n<( (foo) )",
"( (foo) )\n$( (foo) )\n<((foo))",
},
{
"if ( ((foo)) || bar ); then baz; fi",
"if ( ((foo)) || bar); then baz; fi",
},
samePrint("if x; then (\n\ty\n) & fi"),
samePrint("\"foo\n$(bar)\""),
samePrint("\"foo\\\n$(bar)\""),
samePrint("\"foo\\\nbar\""),
samePrint("((foo++)) || bar"),
{
"(( ! 0 )) && echo true",
"((! 0)) && echo true",
},
{
"a=b \\\nc=d \\\nfoo",
"a=b \\\n\tc=d \\\n\tfoo",
},
{
"a=b \\\nc=d \\\nfoo \\\nbar",
"a=b \\\n\tc=d \\\n\tfoo \\\n\tbar",
},
samePrint("a $(x) \\\n\tb"),
samePrint("\"foo\nbar\"\netc"),
samePrint("\"foo\nbar\nbar2\"\netc"),
samePrint("a=\"$b\n\"\nd=e"),
samePrint("\"\n\"\n\nfoo"),
samePrint("$\"\n\"\n\nfoo"),
samePrint("'\n'\n\nfoo"),
samePrint("$'\n'\n\nfoo"),
samePrint("foo <<EOF\na\nb\nc\nd\nEOF\n{\n\tbar\n}"),
samePrint("foo bar # one\nif a; then\n\tb\nfi # two"),
{
"# foo\n\n\nbar",
"# foo\n\nbar",
},
{
"# foo\n\n\nbar\nbaz",
"# foo\n\nbar\nbaz",
},
samePrint("#foo\n#\n#bar"),
{
"(0 #\n0)#\n0",
"(\n\t0 #\n\t0\n) #\n0",
},
samePrint("a | #c1\n\t(\n\t\tb\n\t)"),
samePrint("a | #c1\n\t{\n\t\tb\n\t}"),
samePrint("a | #c1\n\tif b; then\n\t\tc\n\tfi"),
samePrint("a | #c1\n\t#c2\n\t#c3\n\tb"),
samePrint("a && #c1\n\t(\n\t\tb\n\t)"),
samePrint("f() body # comment"),
samePrint("f <<EOF\nbody\nEOF"),
samePrint("f <<EOF\nEOF"),
samePrint("f <<-EOF\n\tbody\nEOF"),
{
"f <<-EOF\nbody\nEOF",
"f <<-EOF\n\tbody\nEOF",
},
samePrint("f <<-EOF\nEOF"),
samePrint("f <<-EOF\n\n\nEOF"),
samePrint("f <<-EOF\n\n\tindented\n\nEOF"),
samePrint("{\n\tf <<EOF\nEOF\n}"),
samePrint("{\n\tf <<-EOF\n\t\tbody\n\tEOF\n}"),
samePrint("{\n\tf <<-EOF\n\t\tbody\n\tEOF\n\tf2\n}"),
samePrint("f <<-EOF\n\t{\n\t\tnicely indented\n\t}\nEOF"),
samePrint("f <<-EOF\n\t{\n\t\tnicely indented\n\t}\nEOF"),
{
"f <<-EOF\n\t{\nbadly indented\n\t}\nEOF",
"f <<-EOF\n\t{\n\tbadly indented\n\t}\nEOF",
},
{
"f <<-EOF\n\t\t{\n\t\t\ttoo indented\n\t\t}\nEOF",
"f <<-EOF\n\t{\n\t\ttoo indented\n\t}\nEOF",
},
{
"f <<-EOF\n{\n\ttoo little indented\n}\nEOF",
"f <<-EOF\n\t{\n\t\ttoo little indented\n\t}\nEOF",
},
samePrint("<<-EOF\n\t$foo\nEOF\n\n{\n\tbar\n}"),
samePrint("f <<EOF\nEOF\n# comment"),
samePrint("f <<EOF\nEOF\n# comment\nbar"),
samePrint("f <<EOF # inline\n$(\n\t# inside\n)\nEOF\n# outside\nbar"),
samePrint("while foo; do\n\tbar\ndone <<-EOF # inline\n\tbaz\nEOF"),
samePrint("{\n\tcat <<EOF\nEOF\n\t# comment\n}"),
{
"if foo # inline\nthen\n\tbar\nfi",
"if foo; then # inline\n\tbar\nfi",
},
samePrint("for i; do echo $i; done"),
samePrint("for i in; do echo $i; done"),
{
"for foo in a b # inline\ndo\n\tbar\ndone",
"for foo in a b; do # inline\n\tbar\ndone",
},
{
"if x # inline\nthen bar; fi",
"if x; then # inline\n\tbar\nfi",
},
{
"for i in a b # inline\ndo bar; done",
"for i in a b; do # inline\n\tbar\ndone",
},
{
"for i #a\n\tin 1; do #b\nx\ndone",
"for i in \\\n\t1; do #a\n\t#b\n\tx\ndone",
},
{
"foo() # inline\n{\n\tbar\n}",
"foo() { # inline\n\tbar\n}",
},
{
"foo() #before\n(\n\tbar #inline\n)",
"foo() ( #before\n\tbar #inline\n)",
},
{
"foo() (#before\n\tbar #inline\n)",
"foo() ( #before\n\tbar #inline\n)",
},
{
"foo()\n#before-1\n(#before-2\n\tbar #inline\n)",
"foo() ( #before-1\n\t#before-2\n\tbar #inline\n)",
},
{
"(#before\n\tbar #inline\n)",
"( #before\n\tbar #inline\n)",
},
{
"(\n#before\n\tbar #inline\n)",
"(\n\t#before\n\tbar #inline\n)",
},
{
"foo=$(#before\n\tbar #inline\n)",
"foo=$( #before\n\tbar #inline\n)",
},
{
"foo=`#before\nbar`",
"foo=$( #before\n\tbar\n)",
},
samePrint("if foo; then\n\tbar\n\t# comment\nfi"),
samePrint("if foo; then\n\tbar\n# else commented out\nfi"),
samePrint("if foo; then\n\tx\nelse\n\tbar\n\t# comment\nfi"),
samePrint("if foo; then\n\tx\n#comment\nelse\n\ty\nfi"),
samePrint("if foo; then\n\tx\n\t#comment\nelse\n\ty\nfi"),
{
"if foo; then\n\tx\n#a\n\t#b\n\t#c\nelse\n\ty\nfi",
"if foo; then\n\tx\n\t#a\n\t#b\n\t#c\nelse\n\ty\nfi",
},
samePrint("if foo; then\n\tx\nelse #comment\n\ty\nfi"),
samePrint("if foo; then\n\tx\n#comment\nelif bar; then\n\ty\nfi"),
samePrint("if foo; then\n\tx\n\t#comment\nelif bar; then\n\ty\nfi"),
samePrint("case i in\nx)\n\ta\n\t;;\n#comment\ny) ;;\nesac"),
samePrint("case i in\nx)\n\ta\n\t;;\n\t#comment\ny) ;;\nesac"),
{
"case i in\nx)\n\ta\n\t;;\n\t#a\n#b\n\t#c\ny) ;;\nesac",
"case i in\nx)\n\ta\n\t;;\n\t#a\n\t#b\n\t#c\ny) ;;\nesac",
},
samePrint("'foo\tbar'\n'foooo\tbar'"),
samePrint("\"foo\tbar\"\n\"foooo\tbar\""),
samePrint("foo\\\tbar\nfoooo\\\tbar"),
samePrint("#foo\tbar\n#foooo\tbar"),
{
"array=('one'\n\t\t# 'two'\n\t\t'three')",
"array=('one'\n\t# 'two'\n\t'three')",
},
samePrint("#comment\n>redir"),
{
">redir \\\n\tfoo",
">redir foo",
},
samePrint("$(declare)"),
{
"`declare`",
"$(declare)",
},
{
"(\n(foo >redir))",
"(\n\t(foo >redir)\n)",
},
samePrint("( (foo) )"),
samePrint("$( (foo) )"),
samePrint("$( ((foo++)) )"),
{
"$( (foo); bar )",
"$(\n\t(foo)\n\tbar\n)",
},
{
"( (foo); bar )",
"(\n\t(foo)\n\tbar\n)",
},
samePrint("( ((foo++)) )"),
{
"( ((foo++)); bar )",
"(\n\t((foo++))\n\tbar\n)",
},
samePrint("(foo && (bar) )"),
samePrint("( (foo) >redir)"),
samePrint("(\n\t((foo++))\n)"),
samePrint("(foo && bar)"),
samePrint(`$foo#bar ${foo}#bar 'foo'#bar "foo"#bar`),
samePrint("case $i in\n1)\n\ta\n\t;;\n(esac)\n\tb\n\t;;\nesac"),
samePrint("case $i in\n1)\n\ta\n\t;;\n(esac | 2)\n\tb\n\t;;\nesac"),
samePrint("case $i in\n1)\n\ta\n\t;;\n2 | esac)\n\tb\n\t;;\nesac"),
// TODO: support cases with command substitutions as well
// {
// "`foo`#bar",
// "$(foo)#bar",
// },
// samePrint(`$("foo"#bar)#bar`),
samePrint("${array[dash - string]}"),
samePrint("array[dash - string]=123"),
samePrint("${arr[0,1]}"),
samePrint("arr[0,1]=x"),
}
func TestPrintTable(t *testing.T) {
t.Parallel()
parser := NewParser(KeepComments(true))
printer := NewPrinter()
for i, tc := range printTests {
t.Run(fmt.Sprintf("#%03d", i), func(t *testing.T) {
printTest(t, parser, printer, tc.in, tc.want)
})
t.Run(fmt.Sprintf("#%03d-nl", i), func(t *testing.T) {
printTest(t, parser, printer, "\n"+tc.in+"\n", tc.want)
})
t.Run(fmt.Sprintf("#%03d-redo", i), func(t *testing.T) {
printTest(t, parser, printer, tc.want, tc.want)
})
}
}
func parsePath(tb testing.TB, path string) *File {
f, err := os.Open(path)
if err != nil {
tb.Fatal(err)
}
defer f.Close()
prog, err := NewParser(KeepComments(true)).Parse(f, "")
if err != nil {
tb.Fatal(err)
}
return prog
}
const canonicalPath = "canonical.sh"
func TestPrintMultiline(t *testing.T) {
t.Parallel()
prog := parsePath(t, canonicalPath)
got, err := strPrint(NewPrinter(), prog)
if err != nil {
t.Fatal(err)
}
wantBs, err := os.ReadFile(canonicalPath)
if err != nil {
t.Fatal(err)
}
// If we're on Windows and it was set up to automatically replace LF
// with CRLF, that might make this test fail. Just ignore \r characters.
want := strings.ReplaceAll(string(wantBs), "\r", "")
got = strings.ReplaceAll(got, "\r", "")
if got != want {
t.Fatalf("Print mismatch in canonical.sh")
}
}
func TestPrintSpaces(t *testing.T) {
t.Parallel()
spaceFormats := [...]struct {
spaces uint
in, want string
}{
{
0,
"{\nfoo \\\nbar\n}",
"{\n\tfoo \\\n\t\tbar\n}",
},
{
2,
"{\nfoo \\\nbar\n}",
"{\n foo \\\n bar\n}",
},
{
4,
"{\nfoo \\\nbar\n}",
"{\n foo \\\n bar\n}",
},
{
2,
"if foo; then # inline1\nbar # inline2\n# withfi\nfi",
"if foo; then # inline1\n bar # inline2\n# withfi\nfi",
},
{
2,
"array=('one'\n # 'two'\n 'three')",
"array=('one'\n # 'two'\n 'three')",
},
}
parser := NewParser(KeepComments(true))
for _, tc := range spaceFormats {
t.Run("", func(t *testing.T) {
printer := NewPrinter(Indent(tc.spaces))
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
var errBadWriter = fmt.Errorf("write: expected error")
type badWriter struct{}
func (b badWriter) Write(p []byte) (int, error) { return 0, errBadWriter }
func TestWriteErr(t *testing.T) {
t.Parallel()
f := &File{Stmts: []*Stmt{
{
Redirs: []*Redirect{{
Op: RdrOut,
Word: litWord("foo"),
}},
Cmd: &Subshell{},
},
}}
err := NewPrinter().Print(badWriter{}, f)
if err == nil {
t.Fatalf("Expected error with bad writer")
}
if err != errBadWriter {
t.Fatalf("Error mismatch with bad writer:\nwant: %v\ngot: %v",
errBadWriter, err)
}
}
func TestPrintBinaryNextLine(t *testing.T) {
t.Parallel()
tests := [...]printCase{
{
"foo <<EOF &&\nl1\nEOF\nbar",
"foo <<EOF && bar\nl1\nEOF",
},
samePrint("a \\\n\t&& b"),
samePrint("a \\\n\t&& b\nc"),
{
"{\n(a \\\n&& b)\nc\n}",
"{\n\t(a \\\n\t\t&& b)\n\tc\n}",
},
{
"a && b \\\n&& c",
"a && b \\\n\t&& c",
},
{
"a \\\n&& $(b) && c \\\n&& d",
"a \\\n\t&& $(b) && c \\\n\t&& d",
},
{
"a \\\n&& b\nc \\\n&& d",
"a \\\n\t&& b\nc \\\n\t&& d",
},
{
"a | {\nb \\\n| c\n}",
"a | {\n\tb \\\n\t\t| c\n}",
},
{
"a \\\n\t&& if foo; then\nbar\nfi",
"a \\\n\t&& if foo; then\n\t\tbar\n\tfi",
},
{
"if foo \\\n&& bar\nthen\nbar\nfi",
"if foo \\\n\t&& bar; then\n\tbar\nfi",
},
{
"a |\nb |\nc",
"a \\\n\t| b \\\n\t| c",
},
{
"foo |\n# misplaced\nbar",
"foo \\\n\t|\n\t# misplaced\n\tbar",
},
samePrint("{\n\tfoo\n\t#a\n\tbar\n} | etc"),
{
"foo &&\n#a1\n#a2\n$(bar)",
"foo \\\n\t&&\n\t#a1\n\t#a2\n\t$(bar)",
},
{
"{\n\tfoo\n\t#a\n} |\n# misplaced\nbar",
"{\n\tfoo\n\t#a\n} \\\n\t|\n\t# misplaced\n\tbar",
},
samePrint("foo | bar\n#after"),
{
"a |\nb | #c2\nc",
"a \\\n\t| b \\\n\t|\n\t#c2\n\tc",
},
samePrint("a \\\n\t&"),
{
"a \\\n\tb \\\n\tc \\\n\t# EOC",
"a \\\n\tb \\\n\tc\n# EOC",
},
{
"a \\\n\t| b \\\n\t| c \\\n\t# EOC",
"a \\\n\t| b \\\n\t| c\n# EOC",
},
}
parser := NewParser(KeepComments(true))
printer := NewPrinter(BinaryNextLine(true))
for _, tc := range tests {
t.Run("", func(t *testing.T) {
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
func TestPrintSwitchCaseIndent(t *testing.T) {
t.Parallel()
tests := [...]printCase{
{
"case $i in\n1)\nfoo\n;;\nesac",
"case $i in\n\t1)\n\t\tfoo\n\t\t;;\nesac",
},
{
"case $i in\n1)\na\n;;\n2)\nb\n;;\nesac",
"case $i in\n\t1)\n\t\ta\n\t\t;;\n\t2)\n\t\tb\n\t\t;;\nesac",
},
samePrint("case $i in\n\t#foo\nesac"),
}
parser := NewParser(KeepComments(true))
printer := NewPrinter(SwitchCaseIndent(true))
for _, tc := range tests {
t.Run("", func(t *testing.T) {
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
func TestPrintFunctionNextLine(t *testing.T) {
t.Parallel()
tests := [...]printCase{
{
"foo() { bar; }",
"foo()\n{\n\tbar\n}",
},
{
"foo()\n{ bar; }",
"foo()\n{\n\tbar\n}",
},
{
"foo()\n\n{\n\n\tbar\n}",
"foo()\n{\n\n\tbar\n}",
},
{
"function foo {\n\tbar\n}",
"function foo\n{\n\tbar\n}",
},
{
"function foo() {\n\tbar\n}",
"function foo()\n{\n\tbar\n}",
},
{
"{ foo() { bar; }; }",
"{\n\tfoo()\n\t{\n\t\tbar\n\t}\n}",
},
}
parser := NewParser(KeepComments(true))
printer := NewPrinter(FunctionNextLine(true))
for _, tc := range tests {
t.Run("", func(t *testing.T) {
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
func TestPrintSpaceRedirects(t *testing.T) {
t.Parallel()
tests := [...]printCase{
samePrint("echo foo bar > f"),
samePrint("echo > f foo bar"),
samePrint("echo >(cmd)"),
samePrint("echo > >(cmd)"),
samePrint("<< EOF\nfoo\nEOF"),
samePrint("<<- EOF\n\t$(< foo)\nEOF"),
samePrint("echo 2> f"),
samePrint("echo foo bar >&1"),
samePrint("echo 2<&1 foo bar"),
}
parser := NewParser(KeepComments(true))
printer := NewPrinter(SpaceRedirects(true))
for _, tc := range tests {
t.Run("", func(t *testing.T) {
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
func TestPrintKeepPadding(t *testing.T) {
t.Parallel()
tests := [...]printCase{
samePrint("echo foo bar"),
samePrint("echo foo bar"),
samePrint("a=b c=d bar"),
samePrint("echo foo >bar"),
samePrint("echo foo 2>bar"),
samePrint("{ foo; }"),
samePrint("a() { foo; }"),
samePrint("a && b"),
samePrint("a | b"),
samePrint("a | b"),
samePrint("{ a b c; }"),
samePrint("foo # x\nbaaar # y"),
samePrint("{ { a; }; }"),
samePrint("{ a; }"),
samePrint("( a )"),
samePrint("'foo\nbar' # x"),
{"\tfoo", "foo"},
{" if foo; then bar; fi", "if foo; then bar; fi"},
samePrint("echo '★' || true"),
{
"1234 || { x; y; }",
"1234 || {\n\tx\n\ty\n}",
},
{
"array=('one'\n\t\t# 'two'\n\t\t'three')",
"array=('one'\n\t# 'two'\n\t'three')",
},
}
parser := NewParser(KeepComments(true))
printer := NewPrinter(KeepPadding(true))
for _, tc := range tests {
t.Run("", func(t *testing.T) {
// ensure that Reset does properly reset colCounter
printer.w.WriteByte('x')
printer.w.Reset(nil)
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
func TestPrintKeepPaddingSpaces(t *testing.T) {
t.Parallel()
tests := [...]printCase{
samePrint("array=('one'\n # 'two'\n 'three')"),
samePrint(" abc=123"),
samePrint("foo \\\n bar \\\n baz"),
samePrint("{\n foo\n bar\n}"),
samePrint("# foo\n # bar"),
}
parser := NewParser(KeepComments(true))
printer := NewPrinter(KeepPadding(true), Indent(2))
for _, tc := range tests {
t.Run("", func(t *testing.T) {
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
func TestPrintMinify(t *testing.T) {
t.Parallel()
tests := [...]printCase{
samePrint("echo foo bar $a $(b)"),
{
"#comment",
"",
},
{
"foo #comment",
"foo",
},
{
"foo\n\nbar",
"foo\nbar",
},
{
"foo &",
"foo&",
},
samePrint("foo >bar 2>baz <etc"),
{
"{\n\tfoo\n}",
"{\nfoo\n}",
},
{
"(\n\ta\n)\n(\n\tb\n\tc\n)",
"(a)\n(b\nc)",
},
{
"$(\n\ta\n)\n$(\n\tb\n\tc\n)",
"$(a)\n$(b\nc)",
},
{
"f() { x; }",
"f(){ x;}",
},
{
"((1 + 2))",
"((1+2))",
},
// TODO: see https://github.com/mvdan/sh/issues/1169
{
"((\nfoo\n))",
"((\\\nfoo))",
},
{
"((\na > 0\n&&\na < 10\n))",
"((\\\na>0&& \\\na<10))",
},
{
"((a > 0 &&\na < 10))",
"((a>0&& \\\na<10))",
},
{
"echo $a ${b} ${c}-d ${e}f ${g}_h",
"echo $a $b $c-d ${e}f ${g}_h",
},
{
"echo ${0} ${3} ${10} ${22}",
"echo $0 $3 ${10} ${22}",
},
{
"case $a in\nx) c ;;\ny | z)\n\td\n\t;;\nesac",
"case $a in\nx)c;;\ny|z)d\nesac",
},
{
"a && b | c",
"a&&b|c",
},
{
"a &&\n\tb |\n\tc",
"a&&b|c",
},
{
"${0/${a}\\\n}",
"${0/$a/}",
},
{
"#!/bin/sh\necho 1\n#!/bin/sh\necho 2",
"#!/bin/sh\necho 1\necho 2",
},
samePrint("foo >bar 2>baz <etc"),
samePrint("<<-EOF\n$(a|b)\nEOF"),
{
"a=$(\n\tcat <<'EOF'\n hello\nEOF\n)",
"a=$(cat <<'EOF'\n hello\nEOF\n)",
},
{
"(\n\tcat <<EOF\n hello\nEOF\n)",
"(cat <<EOF\n hello\nEOF\n)",
},
samePrint("diff -y <(cat <<EOF\n1\n2\n3\nEOF\n) <(cat <<EOF\n1\n4\n3\nEOF\n)"),
}
parser := NewParser(KeepComments(true))
printer := NewPrinter(Minify(true))
for _, tc := range tests {
t.Run("", func(t *testing.T) {
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
func TestPrintSingleLine(t *testing.T) {
t.Parallel()
tests := [...]printCase{
samePrint("echo foo bar $a $(b)"),
samePrint("foo #comment"),
{
"foo\n\nbar",
"foo; bar",
},
samePrint("foo &"),
samePrint("foo >bar 2>baz <etc"),
{
"{\n\tfoo\n}",
"{ foo; }",
},
{
"(\n\ta\n)\n(\n\tb\n\tc\n)",
"(a); (b; c)",
},
{
"$(\n\ta\n)\n$(\n\tb\n\tc\n)",
"$(a); $(b; c)",
},
samePrint("f() { x; }"),
samePrint("((1 + 2))"),
samePrint("echo $a ${b} ${c}-d ${e}f ${g}_h"),
samePrint("echo ${0} ${3} ${10} ${22}"),
{
"case $a in\nx)c;;\ny|z)d\nesac",
"case $a in x) c ;; y | z) d ;; esac",
},
samePrint("a && b | c"),
{
"a &&\n\tb |\n\tc",
"a && b | c",
},
{
"if\nfoo\nthen\nbar\nfi",
"if foo; then bar; fi",
},
{
"a \\\n >b",
"a >b",
},
samePrint("foo >bar 2>baz <etc"),
samePrint("<<-EOF\n\t$(a | b)\nEOF"),
}
parser := NewParser(KeepComments(true))
printer := NewPrinter(SingleLine(true))
for _, tc := range tests {
t.Run("", func(t *testing.T) {
printTest(t, parser, printer, tc.in, tc.want)
})
}
}
func TestPrintOptionsNotBroken(t *testing.T) {
t.Parallel()
singleLineException := regexp.MustCompile(`#|<<|'|"`)
checkSingleLine := func(t *testing.T, got string) {
if singleLineException.MatchString(got) {
return
}
got = strings.TrimSuffix(got, "\n") // trailing newline is expected
if strings.Contains(got, "\n") {
t.Fatalf("unexpected newline with SingleLine: %q", got)
}
}
for _, opts := range []struct {
name string
list []PrinterOption
}{
{"Minify", []PrinterOption{Minify(true)}},
{"SingleLine", []PrinterOption{SingleLine(true)}},
} {
printer := NewPrinter(opts.list...)
for lang := range langResolvedVariants.bits() {
parser := NewParser(Variant(lang), KeepComments(true))
for _, tc := range append(fileTests, fileTestsNoPrint...) {
want, _ := tc.byLangIndex[lang.index()].(*File)
if want == nil {
continue
}
t.Run(fmt.Sprintf("File%s/%s", opts.name, lang), func(t *testing.T) {
in := tc.inputs[0]
t.Logf("input: %s", in)
prog, err := parser.Parse(strings.NewReader(in), "")
if err != nil {
t.Skipf("skipping due to error; TestParsePass should catch this")
}
got, err := strPrint(printer, prog)
if err != nil {
t.Fatal(err)
}
if opts.name == "SingleLine" {
checkSingleLine(t, got)
}
_, err = parser.Parse(strings.NewReader(got), "")
if err != nil {
t.Fatalf("program was broken: %v\noriginal:\n%s\nfinal:\n%s", err, in, got)
}
})
}
}
parser := NewParser(KeepComments(true))
for _, tc := range printTests {
t.Run(fmt.Sprintf("Print%s", opts.name), func(t *testing.T) {
prog, err := parser.Parse(strings.NewReader(tc.in), "")
if err != nil {
t.Skipf("skipping due to error; TestPrintTable should catch this")
}
got, err := strPrint(printer, prog)
if err != nil {
t.Fatal(err)
}
if opts.name == "SingleLine" {
checkSingleLine(t, got)
}
_, err = parser.Parse(strings.NewReader(got), "")
if err != nil {
t.Fatalf("program was broken: %v\noriginal:\n%s\nfinal:\n%s", err, tc.in, got)
}
})
}
}
}
func printTest(t *testing.T, parser *Parser, printer *Printer, in, want string) {
t.Helper()
t.Logf("input: %s", in)
prog, err := parser.Parse(strings.NewReader(in), "")
if err != nil {
t.Fatalf("parsing got an error: %s:\n%s", err, in)
}
want += "\n"
got, err := strPrint(printer, prog)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("Print mismatch:\nwant:\n%q\ngot:\n%q", want, got)
}
// With the "want" output string, including the added trailing newline,
// make sure that it's idempotent when formatted again.
progAgain, err := parser.Parse(strings.NewReader(want), "")
if err != nil {
t.Fatalf("Result is not valid shell:\n%s", want)
}
gotAgain, err := strPrint(printer, progAgain)
if err != nil {
t.Fatal(err)
}
if gotAgain != want {
t.Fatalf("Re-print mismatch:\nwant:\n%q\ngot:\n%q", want, gotAgain)
}
}
func TestPrintNodeTypes(t *testing.T) {
t.Parallel()
multiline, err := NewParser().Parse(strings.NewReader(`
echo foo
`), "")
if err != nil {
t.Fatal(err)
}
tests := [...]struct {
in Node
want string
wantErr bool
}{
{
in: &File{Stmts: litStmts("foo")},
want: "foo\n",
},
{
in: &File{Stmts: litStmts("foo", "bar")},
want: "foo\nbar\n",
},
{
in: litStmt("foo", "bar"),
want: "foo bar",
},
{
in: litCall("foo", "bar"),
want: "foo bar",
},
{
in: litWord("foo"),
want: "foo",
},
{
in: lit("foo"),
want: "foo",
},
{
in: sglQuoted("foo"),
want: "'foo'",
},
{
in: &Comment{},
wantErr: true,
},
{
in: multiline.Stmts[0],
want: "echo foo",
},
{
in: multiline.Stmts[0].Cmd,
want: "echo foo",
},
{
in: multiline.Stmts[0].Cmd.(*CallExpr).Args[0],
want: "echo",
},
{
in: multiline.Stmts[0].Cmd.(*CallExpr).Args[0].Parts[0],
want: "echo",
},
}
printer := NewPrinter()
for _, tc := range tests {
t.Run("", func(t *testing.T) {
got, err := strPrint(printer, tc.in)
if err == nil && tc.wantErr {
t.Fatalf("wanted an error but found none")
} else if err != nil && !tc.wantErr {
t.Fatalf("didn't want an error but got %v", err)
}
if got != tc.want {
t.Fatalf("Print mismatch:\nwant:\n%s\ngot:\n%s",
tc.want, got)
}
})
}
}
func TestPrintManyStmts(t *testing.T) {
t.Parallel()
tests := [...]struct {
in, want string
}{
{"foo; bar", "foo\nbar\n"},
{"foo\nbar", "foo\nbar\n"},
{"\n\nfoo\nbar\n\n", "foo\nbar\n"},
{"foo\nbar <<EOF\nbody\nEOF\n", "foo\nbar <<EOF\nbody\nEOF\n"},
{"foo\nbar # inline", "foo\nbar # inline\n"},
{"# comment before\nfoo bar", "# comment before\nfoo bar\n"},
}
parser := NewParser(KeepComments(true))
printer := NewPrinter()
for _, tc := range tests {
t.Run("", func(t *testing.T) {
f, err := parser.Parse(strings.NewReader(tc.in), "")
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
for _, stmt := range f.Stmts {
printer.Print(&buf, stmt)
buf.WriteByte('\n')
}
got := buf.String()
if got != tc.want {
t.Fatalf("Print mismatch:\nwant:\n%s\ngot:\n%s",
tc.want, got)
}
})
}
}
func TestKeepPaddingRepeated(t *testing.T) {
t.Parallel()
parser := NewParser()
printer := NewPrinter()
// Enable the KeepPadding option twice. This used to crash, since the
// option made an invalid type assertion the second time.
KeepPadding(true)(printer)
KeepPadding(true)(printer)
// Ensure the option is enabled.
printTest(t, parser, printer, "foo bar", "foo bar")
// Disable the option, and ensure it's disabled.
KeepPadding(false)(printer)
printTest(t, parser, printer, "foo bar", "foo bar")
}