tokenizer: an escaped backtick opens a nested substitution (#732) - #742
Merged
Conversation
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:The double-quoted reader (
src/tokenizer.c) andlush_dequote_spanboth 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_backtick_unescaperemoves exactly the three escapes POSIX names and leaves every other\Xuntouched, 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:
echo xprintf 4y→xprintf 4y. No nesting needed;$( )and the quoted form both work. This is why the unquoted reproduction in the test uses no leading literal.echo [4]prints an empty line where bash and dash print[4]. The word disappears, so a command silently loses an argument.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.