// Copyright (c) 2016, Daniel Martí // 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("<(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 &2 bar &2 foo bar &2 foo"), samePrint(">&2 foo 2>&1 bar &2>/dev/null", "foo >&2 >/dev/null"}, samePrint("exec {foo[1]}>&-"), samePrint("exec {foo}>&-"), {"foo < \\\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 <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 < 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 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 bar 2>baz b", "a >b", }, samePrint("foo >bar 2>baz