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
20 changes: 20 additions & 0 deletions crates/bashkit/src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,26 @@ impl<'a> Lexer<'a> {
self.advance();
}
}
Some('\\') => {
// Backslash escape after a quoted segment: `'a'\''b'`.
self.advance();
match self.peek_char() {
// Line continuation.
Some('\n') => {
self.advance();
}
Some(next) => {
let start = content.len();
if next == '$' {
content.push('\x00');
}
content.push(next);
self.advance();
flags.quoted_ranges.push((start, content.len()));
}
None => content.push('\\'),
}
}
Some(ch) if self.is_word_char(ch) => {
if matches!(ch, '*' | '?' | '[') {
flags.has_unquoted_glob = true;
Expand Down
33 changes: 33 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/quote.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,36 @@ show "${x}"
### expect
$'a\nb'
### end

### quote_backslash_escape_after_quoted_segment
# `'\''` (close, escaped quote, reopen) and other escapes right after a
# quoted segment.
echo 'a'\''b'
echo 'it'\''s' "x"\'
echo "a"\$HOME 'a'\$HOME
echo "a"\ b 'c'\ d
echo "g"\* 'h'\?
echo 'p'\"q
### expect
a'b
it's x'
a$HOME a$HOME
a b c d
g* h?
p"q
### end

### quote_nested_sh_c_quote_idiom
sh -c 'sh -c '\''echo deep'\'''
sh -c 'echo '\''one two'\'''
### expect
deep
one two
### end

### quote_backslash_newline_after_quoted_segment
echo 'a'\
'b'
### expect
ab
### 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 escape right after a quoted segment broke lexing: `'a'\''b'` (the standard way to put a quote inside single quotes, as in `sh -c '...'\''...'\''...'`) failed with "unterminated single quote", and `"a"\$x`, `'a'\ b`, `"g"\*` kept the backslash. The quote-continuation reader now treats `\c` as a quoted `c` and `\<newline>` as a continuation. Still open: single quotes inside an unquoted `${x:-'d'}` operand are not removed.
* **Fix**: `$SECONDS` counts whole seconds since the `Bash` instance was built (it was always 0), and `SECONDS=N` restarts the count from N; subshells keep their own. The rootfs gained `/proc/sys/kernel/random/uuid`, a fresh v4 UUID per read from the same CSPRNG as `uuidgen`. See [VFS](foundations/vfs.md).
* **Feature**: Pipelines stream. From the first stage that runs shell code, stages run concurrently on forked shells over 4 KiB pipes, with backpressure and SIGPIPE: `while :; do echo x; done | head -n 2` stops the loop (`PIPESTATUS` `141 0`) instead of running it into the command limit. Single-builtin leading stages keep running in sequence (no fork). The pipe is 4 KiB rather than Linux's 64 KiB because every byte an endless producer runs ahead costs budgeted commands. Nested streaming is capped at 4 levels after a debug-build stack measurement (TM-DOS-124). L-PIPE-001 narrowed. See [Parallel Execution](foundations/parallel-execution.md#pipelines).
* **Fix**: Pipeline stages are subshells, as in bash: `echo x | read v` leaves `v` unset and `cd /tmp | true` keeps the cwd; `shopt -s lastpipe` keeps the last stage in the shell. Streaming callers no longer see a non-last stage's output (`for ...; done | tac` streamed the loop's lines in input order). Stages still run sequentially (new L-PIPE-001). See [Parallel Execution](foundations/parallel-execution.md#pipelines).
Expand Down
Loading