Skip to content

executor: a backtick anywhere in a word is a substitution (#740) - #745

Merged
berrym merged 1 commit into
masterfrom
fix/740-backtick-concatenation
Aug 16, 2026
Merged

executor: a backtick anywhere in a word is a substitution (#740)#745
berrym merged 1 commit into
masterfrom
fix/740-backtick-concatenation

Conversation

@berrym

@berrym berrym commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Closes #740.

The defect

A `...` substitution that was not the first thing in a word never reached a substitution handler, so it was emitted as its own source text:

$ lush -c 'echo x`printf 4`y'
x`printf 4`y                      # bash, zsh, dash: x4y

The $ forms above already handle this — they route a mid-word construct to the general expander on first_dollar != text. The backtick branch tested only text[0], so a word with a leading literal fell through to the plain-text path, which removes backslashes and nothing else.

A trailing literal always worked, because the backtick was still first. That asymmetry is the whole shape of the bug: PREFIX=`date` shipped its own source text while `date`-suffix did not.

Two details the fix has to get right

  • The scan looks for an unescaped backtick — x\`y is a literal and must not become a substitution.
  • Finding one is not enough to treat the word as a bare substitution. cmdsub_spans_whole_word assumes the construct starts at text[0], so the position test gates that call too. Without it the whole word went to expand_command_substitution and was executed verbatim, reporting x: command not found — which is exactly what my first cut did.

All four modes were identical here and every one disagreed with its own reference, so this is a defect rather than curation; the fix is mode-invariant.

Three adversarial results that are NOT this bug

Both filed defects are byte-identical against a build of the parent commit.

Verification

  • 13 new checks in tests/integration/test_nested_backticks.c: leading literal with and without a trailing one, assignment, two substitutions in one word, combination with a nested substitution, empty and no-output bodies, redirection and for-list positions, and three pinning an escaped or single-quoted backtick as a literal.
  • 10 of the file's 29 checks fail against the parent build.
  • Full suite 194/194. ASan 65/65; the backtick test 1/1 under ASan.

A `...` substitution that was not the FIRST thing in a word never reached a
substitution handler, so it was emitted as its own source text:

    echo x`printf 4`y
      before: x`printf 4`y        bash, zsh, dash: x4y

The `$` forms above already handle this: they route a mid-word construct to
the general expander on `first_dollar != text`. The backtick branch tested
only `text[0]`, so a word with a LEADING literal fell through to the
plain-text path and had its backslashes removed and nothing else. A TRAILING
literal worked, because the backtick was still first -- which is the whole
shape of the bug, and why `PREFIX=`date`` shipped its own source while
``date`-suffix` did not.

The scan looks for an UNESCAPED backtick, since `x\`y` is a literal and must
not become a substitution. Finding one is not on its own enough to treat the
word as a bare substitution: cmdsub_spans_whole_word assumes the construct
starts at text[0], so the position test has to gate that call as well. Without
it the whole word went to expand_command_substitution and was executed
verbatim, reporting `x: command not found`.

All four modes were identical here and every one of them disagreed with its
own reference, so this is a defect rather than curation, and the fix is
mode-invariant.

Three adversarial results were checked and are NOT this bug:
  - `IFS=,; v=`printf 'a,b'`; echo x${v}y` differs from bash because lush mode
    has word splitting off by design; `mode bash` and `mode posix` both
    reproduce bash exactly.
  - an escaped `\"` inside a backtick inside double quotes truncates the word
    (#743, filed).
  - a backtick in a `case` pattern is a parse error (#744, filed) -- the
    backtick sibling of #494.
Both filed defects are byte-identical against a build of the parent commit.

tests/integration/test_nested_backticks.c gains 13 checks: a leading literal
with and without a trailing one, an assignment, two substitutions in one word,
the combination with a nested substitution, empty and no-output bodies,
redirection and for-list positions, and three that pin an escaped or
single-quoted backtick as a literal. 10 of the file's 29 checks fail against
the parent build.
@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!

@berrym
berrym merged commit 0bf19e5 into master Aug 16, 2026
7 checks passed
@berrym
berrym deleted the fix/740-backtick-concatenation branch August 16, 2026 10:05
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.

an unquoted backtick substitution preceded by literal text is emitted literally

1 participant