* Parse a single line of commands.
(command: string)
| 540 | * Parse a single line of commands. |
| 541 | */ |
| 542 | function parseCommandLine(command: string): string[] { |
| 543 | if (!command?.trim()) return [] |
| 544 | |
| 545 | // Storage for replaced content |
| 546 | const redirections: string[] = [] |
| 547 | const subshells: string[] = [] |
| 548 | const quotes: string[] = [] |
| 549 | const singleQuotes: string[] = [] |
| 550 | const arithmeticExpressions: string[] = [] |
| 551 | const variables: string[] = [] |
| 552 | const parameterExpansions: string[] = [] |
| 553 | |
| 554 | // First handle PowerShell redirections by temporarily replacing them |
| 555 | let processedCommand = command.replace(/\d*>&\d*/g, (match) => { |
| 556 | redirections.push(match) |
| 557 | return `__REDIR_${redirections.length - 1}__` |
| 558 | }) |
| 559 | |
| 560 | // Handle arithmetic expressions: $((...)) pattern |
| 561 | // Match the entire arithmetic expression including nested parentheses |
| 562 | processedCommand = processedCommand.replace(/\$\(\([^)]*(?:\)[^)]*)*\)\)/g, (match) => { |
| 563 | arithmeticExpressions.push(match) |
| 564 | return `__ARITH_${arithmeticExpressions.length - 1}__` |
| 565 | }) |
| 566 | |
| 567 | // Handle $[...] arithmetic expressions (alternative syntax) |
| 568 | processedCommand = processedCommand.replace(/\$\[[^\]]*\]/g, (match) => { |
| 569 | arithmeticExpressions.push(match) |
| 570 | return `__ARITH_${arithmeticExpressions.length - 1}__` |
| 571 | }) |
| 572 | |
| 573 | // Handle parameter expansions: ${...} patterns (including array indexing) |
| 574 | // This covers ${var}, ${var:-default}, ${var:+alt}, ${#var}, ${var%pattern}, etc. |
| 575 | processedCommand = processedCommand.replace(/\$\{[^}]+\}/g, (match) => { |
| 576 | parameterExpansions.push(match) |
| 577 | return `__PARAM_${parameterExpansions.length - 1}__` |
| 578 | }) |
| 579 | |
| 580 | // Handle process substitutions: <(...) and >(...) |
| 581 | processedCommand = processedCommand.replace(/[<>]\(([^)]+)\)/g, (_, inner) => { |
| 582 | subshells.push(inner.trim()) |
| 583 | return `__SUBSH_${subshells.length - 1}__` |
| 584 | }) |
| 585 | |
| 586 | // Handle locale quoting: $"...". This must run before variable masking for |
| 587 | // the same reason as ANSI-C: without it the generic double-quote masker would |
| 588 | // strip the outer quotes leaving a bare $ that the variable regex absorbs, |
| 589 | // corrupting the placeholder. The whole token (including $") is stored in the |
| 590 | // double-quote bucket so it is restored verbatim. |
| 591 | processedCommand = processedCommand.replace(/\$"(?:[^"\\]|\\.)*"/g, (match) => { |
| 592 | quotes.push(match) |
| 593 | return `__QUOTE_${quotes.length - 1}__` |
| 594 | }) |
| 595 | |
| 596 | // Handle ANSI-C quoting: $'...'. This must run before variable masking so the |
| 597 | // leading $ is captured as part of the quoted unit rather than being treated |
| 598 | // as a variable expansion (which would corrupt a following placeholder). |
| 599 | // ANSI-C strings interpret backslash escapes, so the pattern is escape-aware. |
no test coverage detected