Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion crates/bashkit/src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,12 @@ impl<'a> Lexer<'a> {
self.advance();
} else {
// Escaped character: backslash quotes the next char
// (quote removal — only the literal char survives)
// (quote removal — only the literal char survives).
// `\$` and `\`` must stay literal through parse_word, so
// they carry the NUL sentinel like quoted `\$` does.
if matches!(next, '$' | '`') {
word.push('\x00');
}
word.push(next);
self.advance();
}
Expand Down
36 changes: 36 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/escaped-dollar.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# A backslash outside quotes makes the next character literal: `\$` is a
# dollar sign, not the start of a parameter or command substitution.

### escaped_dollar_not_expanded
HOME=/h
echo \$HOME
echo a\$HOME
echo \${HOME}
x=\$HOME; echo "$x"
### expect
$HOME
a$HOME
${HOME}
$HOME
### end

### escaped_dollar_paren_not_substituted
echo \$\(echo hi\)
echo a\$\(b\)
### expect
$(echo hi)
a$(b)
### end

### escaped_backtick_literal
echo \`echo hi\`
### expect
`echo hi`
### end

### escaped_dollar_printf_args
printf '%s\n' a\$1 \$PATH
### expect
a$1
$PATH
### end
1 change: 1 addition & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2026-10-07

* **Fix**: A backslash outside quotes keeps `$` and `` ` `` literal. `echo \$HOME` printed the home directory; it now prints `$HOME`, as do `a\$x`, `\${x}` and `x=\$y` (the lexer emits the same NUL literal marker as quoted `"\$"`). Found while working gap telemetry.
* **Fix**: `$'\x1e'` and `$'\x1f'` bytes (record/unit separators) are kept as data. They collided with the lexer's quote-boundary markers and were silently dropped, which broke the common `SEP=$'\x1f'` composite-key idiom for associative arrays (`v["$section"$'\x1f'"$key"]`). Decoded ANSI-C content now NUL-escapes those bytes like `$`. Found by gap telemetry (`config_ini_merge` replay printed nothing).
* **Fix**: A usage error in a bundled builtin no longer aborts the script. `grep -E '('`, `grep -P` with a bad pattern, `rg` with a bad regex, an awk syntax error, `grep` with no pattern and `grep -f missing` now fail that one command with exit 2 and the message on stderr, and the script continues (bash semantics; `set -e` still stops). `Err` from custom builtins (`BashBuilder::builtin`, registry, resolver) keeps aborting, per the public contract. See foundations/builtins "Errors: Failed Command vs Aborted Execution".
* **Feature**: `gawk`, `mawk` and `nawk` run the awk builtin (same engine, including gawk extensions it already had such as `gensub`, `toupper`). Gap telemetry showed agents probing `gawk` by name. `--version` still identifies as bashkit awk.
Expand Down
Loading