Skip to content

tokenizer: an escaped backtick opens a nested substitution (#732) - #742

Merged
berrym merged 1 commit into
masterfrom
fix/732-nested-backticks
Aug 16, 2026
Merged

tokenizer: an escaped backtick opens a nested substitution (#732)#742
berrym merged 1 commit into
masterfrom
fix/732-nested-backticks

Conversation

@berrym

@berrym berrym commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Closes #732.

The rule

POSIX 2.6.3: inside `...` a backslash keeps its literal meaning except before $, a backtick or another backslash, where it escapes that byte. An embedded \` is therefore the delimiter of a substitution one level down, and removing those backslashes is what lets the body parse as one.

Two independent defects, which is why the spellings failed differently

1. The unquoted scanner had no escape handling at all, so the span ended at the first \` and the rest of the line mis-parsed:

$ lush -c 'echo `echo \`printf 4\``'
error[E1004]: unterminated backtick command substitution
                          # bash, zsh, dash: 4

The double-quoted reader (src/tokenizer.c) and lush_dequote_span both already consumed an escaped byte correctly. Only this path did not — which is also why the quoted spelling produced the milder symptom.

2. The body was parsed verbatim, so a surviving \` reached the sub-parse as a literal backtick and the nested command was echoed as its own source:

$ lush -c 'echo "[`echo \`printf 4\``]"'
[`printf 4`]                # bash, zsh, dash: [4]

lush_backtick_unescape removes exactly the three escapes POSIX names and leaves every other \X untouched, so a body like `printf 'a\tb'` is unaffected.

bash, zsh and dash all evaluate these forms, and lush's own model agrees that one level of quoting is removed on the way into a substitution — mode-invariant, gates nothing.

Two pre-existing defects found while probing (filed, not folded in)

Both verified identical against a build of the parent commit:

Worth noting the interaction: with nesting involved, the input in #740 used to be a parse error, and now degrades to that same pre-existing literal-text behavior. That is progress in the same direction, not a regression.

Verification

  • tests/integration/test_nested_backticks.c — 16 checks: both spellings, an assignment, three levels of nesting, the exact escape set, and the single-level / $( ) forms that must not change. Two checks pin that an unterminated backtick is still diagnosed, since the scanner now skips a byte and must not be able to skip past the end.
  • 5 of 16 fail against the parent build.
  • Full suite 194/194. ASan 65/65; the new test 1/1 under ASan.

POSIX 2.6.3: inside `...` a backslash keeps its literal meaning EXCEPT before
`$`, a backtick or another backslash, where it escapes that byte. An embedded
`\`` is therefore the delimiter of a substitution one level down, and removing
those backslashes is what lets the body parse as one.

lush got this wrong in two independent places, which is why the two spellings
failed differently.

The UNQUOTED backtick scanner had no escape handling at all, so the span ended
at the first `\`` and the rest of the line mis-parsed:

    echo `echo \`printf 4\``
      before: error[E1004]: unterminated backtick command substitution
      bash, zsh, dash: 4

The double-quoted reader and lush_dequote_span both already scanned it the
right way -- an escaped byte is consumed and does not close the span. Only
this one path did not, which is also why the quoted spelling produced the
milder symptom.

The substitution body was then parsed VERBATIM. A surviving `\`` reached the
sub-parse as a LITERAL backtick, so the nested command was echoed as its own
source text instead of being run:

    echo "[`echo \`printf 4\``]"
      before: [`printf 4`]        bash, zsh, dash: [4]

lush_backtick_unescape removes exactly the three escapes POSIX names, leaving
every other `\X` untouched so a body like `printf 'a\tb'` is unaffected.

bash, zsh and dash all evaluate these forms and lush's own model agrees -- one
level of quoting is removed on the way into a substitution -- so the fix is
mode-invariant and gates nothing.

Two PRE-EXISTING defects surfaced while probing and are filed rather than
folded in, both verified identical against a build of the parent commit:
#740 (an unquoted backtick preceded by literal text is emitted literally --
`echo x`printf 4`y`, no nesting needed) and #741 (an unmatched bracket glob
expands to nothing instead of staying literal -- `echo [4]`). The first is why
the unquoted reproduction in this test uses no leading literal.

tests/integration/test_nested_backticks.c: 16 checks -- both spellings, an
assignment, three levels of nesting, the exact escape set, and the single-level
and `$( )` forms that must not change. Two of them pin that an unterminated
backtick is still diagnosed, since the scanner now skips a byte and must not be
able to skip past the end. 5 fail against the parent build.
@berrym
berrym merged commit daad8f4 into master Aug 16, 2026
6 checks passed
@berrym
berrym deleted the fix/732-nested-backticks branch August 16, 2026 09:28
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nested backticks inside a backtick substitution are not processed (escaped inner form)

1 participant