Skip to content

syntax: printer drops a trailing backslash at EOF, making shfmt non-idempotent #1354

Closed

Description

@sarathfrancis90

shfmt isn't idempotent on a word ending in a trailing backslash at EOF — the second pass drops the backslash:

$ printf 'echo foo\' | shfmt
echo foo\
$ printf 'echo foo\' | shfmt | shfmt
echo foo

The parser keeps the trailing backslash as a literal: echo foo\ parses to a Lit with value foo\, and the interpreter expands it to foo\ (matching dash/POSIX). But the printer writes that literal verbatim right before the newline it always adds at the end of a file, so the emitted echo foo\<newline> re-parses as a line continuation and the backslash is gone. So the very first format already changes meaning (the literal backslash is dropped), and formatting isn't a fixed point.

It only shows up with an odd number of trailing backslashes at the very end of the input — echo foo\\ and quoted heredoc bodies are fine, since only \<EOF> ends up as a lone literal backslash.

FuzzParsePrint doesn't catch it because it discards the printed output and only checks that Print doesn't error; the printTest helper sidesteps it too, since its idempotency check re-parses the want string without the trailing newline (the comment there already notes that the added newline "can change the meaning of trailing backslashes").

A few directions, and I'm not sure which you'd prefer:

  1. Have the printer emit foo\\ instead of foo\ — same value, survives the newline, idempotent. Downside: it rewrites the literal (foo\ in, foo\\ out).
  2. Drop the final newline when the output would otherwise end in a lone backslash. Downside: that one file wouldn't end in a newline.
  3. Discard the trailing \<EOF> in the parser in Bash mode (Bash drops it, dash keeps it), so echo foo\ formats to echo foo in a single pass. Changes parsing/interp and is variant-specific.

I have (1) working locally with a test, but I'm happy to send whichever approach you'd rather take.

Activity

  1. added a commit that references this issue on Aug 13, 2026
  2. added a commit that references this issue on Aug 13, 2026
    d3da88a
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