| 229 | let scanPendingBackslashes = 0 |
| 230 | |
| 231 | const advanceScan = (target: number): void => { |
| 232 | for (let i = scanPos; i < target; i++) { |
| 233 | const ch = command[i]! |
| 234 | |
| 235 | // Any physical newline clears comment state. The old isInsideComment |
| 236 | // used `lineStart = lastIndexOf('\n', pos-1)+1` (quote-blind), so a |
| 237 | // `\n` inside quotes still advanced lineStart. Match that here by |
| 238 | // clearing BEFORE the quote branches. |
| 239 | if (ch === '\n') scanInComment = false |
| 240 | |
| 241 | if (scanInSingleQuote) { |
| 242 | if (ch === "'") scanInSingleQuote = false |
| 243 | continue |
| 244 | } |
| 245 | |
| 246 | if (scanInDoubleQuote) { |
| 247 | if (scanDqEscapeNext) { |
| 248 | scanDqEscapeNext = false |
| 249 | continue |
| 250 | } |
| 251 | if (ch === '\\') { |
| 252 | scanDqEscapeNext = true |
| 253 | continue |
| 254 | } |
| 255 | if (ch === '"') scanInDoubleQuote = false |
| 256 | continue |
| 257 | } |
| 258 | |
| 259 | // Unquoted context. Quote tracking is COMMENT-BLIND (same as the old |
| 260 | // isInsideQuotedString): we do NOT skip chars for being inside a |
| 261 | // comment. Only the `#` detection itself is gated on not-in-comment. |
| 262 | if (ch === '\\') { |
| 263 | scanPendingBackslashes++ |
| 264 | continue |
| 265 | } |
| 266 | const escaped = scanPendingBackslashes % 2 === 1 |
| 267 | scanPendingBackslashes = 0 |
| 268 | if (escaped) continue |
| 269 | |
| 270 | if (ch === "'") scanInSingleQuote = true |
| 271 | else if (ch === '"') scanInDoubleQuote = true |
| 272 | else if (!scanInComment && ch === '#') scanInComment = true |
| 273 | } |
| 274 | scanPos = target |
| 275 | } |
| 276 | |
| 277 | while ((match = heredocStartPattern.exec(command)) !== null) { |
| 278 | const startIndex = match.index |