Skip to content

Nested tab heredoc formatting bug #1403

Closed

Description

@milhnl

Hi! First of all, thanks for the amazing project!

shfmt reformats my nested tab-prefixed heredoc the wrong way. An example:

sudo systemd-tmpfiles --create - <<-EOF
	f+~ /etc/configfile $(
		base64 -w0 <<-INNER
			content
		INNER
	)
EOF

is reformatted into

sudo systemd-tmpfiles --create - <<-EOF
	f+~ /etc/configfile $(
		base64 -w0 <<-INNER
content
				INNER
	)
EOF

with shfmt -i0 (because those are tabs, take care when copying). The actual content of the nested heredoc is completely deindented, and the INNER end marker is indented too far (seems to gain an extra tab for every line in the inner heredoc).

I'm not in a hurry, but this seems wrong. shfmt support is important enough for me that I wont use this construct until it works 😄. Once again thanks for the project, and maybe for fixing this!

Activity

  1. milhnl commented on Aug 28, 2026

    @milhnl
    ContributorAuthor

    What seems to happen is the extraIndenter has an embedded bufWriter of which the Write method is called by (e *extraIndenter) WriteByte(b byte). I added an override which forwards it to the WriteByte method like WriteString. I have to be honest, I don't know exactly why it happens on a nested heredoc only. I saw the policy on LLM PR's and this is a bit of skirting the line, as I wrote the code myself, but the debugging was assisted. It does fix the issue, without breaking any tests.

    diff --git i/syntax/printer.go w/syntax/printer.go
    index d11e6146..3678b6ab 100644
    --- i/syntax/printer.go
    +++ w/syntax/printer.go
    @@ -1620,6 +1620,13 @@ func (e *extraIndenter) WriteString(s string) (int, error) {
        return len(s), nil
     }
    
    +func (e *extraIndenter) Write(b []byte) (int, error) {
    +   for i := range len(b) {
    +       e.WriteByte(b[i])
    +   }
    +   return len(b), nil
    +}
    +
     func startsWithLparen(node Node) bool {
        switch node := node.(type) {
        case *Stmt:
    diff --git i/syntax/printer_test.go w/syntax/printer_test.go
    index a3231b03..ae23a93c 100644
    --- i/syntax/printer_test.go
    +++ w/syntax/printer_test.go
    @@ -544,6 +544,7 @@ var printTests = []printCase{
            "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 <<-A\n\ta $(\n\t\tg <<-B\n\t\t\tb1\n\t\t\tb2\n\t\tB\n\t)\nA"),
        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"),
  2. mvdan commented on Aug 28, 2026

    @mvdan
    Owner

    Thanks! Please feel free to send a PR, if your model is convinced that this solution is right (and why). The guidance against PRs are aimed at github users who send PRs indiscriminately to resolve existing issues, aiming only to pad their resumes.

  3. milhnl commented on Aug 31, 2026

    @milhnl
    ContributorAuthor

    Fascinating what happened there. Anyway, I now fully understand the change I made and why it was necessary, and made a pull request. Thanks for the comments!

  4. added a commit that references this issue on Aug 31, 2026
    b5502bf
  5. added a commit that references this issue on Sep 19, 2026
    fa356dc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      ⌂ 换网址