* Normalize a script by stripping leading whitespace from lines, * while preserving whitespace inside heredoc content. * * This allows writing indented bash scripts in template literals: * ``` * await bash.exec(` * if [ -f foo ]; then * echo "yes" * fi * `); * ``` * * Heredocs ar
(script: string)
| 877 | * Heredocs are detected by looking for << or <<- operators and their delimiters. |
| 878 | */ |
| 879 | function normalizeScript(script: string): string { |
| 880 | const lines = script.split("\n"); |
| 881 | const result: string[] = []; |
| 882 | |
| 883 | // Stack of pending heredoc delimiters (for nested heredocs) |
| 884 | const pendingDelimiters: { delimiter: string; stripTabs: boolean }[] = []; |
| 885 | |
| 886 | for (let i = 0; i < lines.length; i++) { |
| 887 | const line = lines[i]; |
| 888 | |
| 889 | // If we're inside a heredoc, check if this line ends it |
| 890 | if (pendingDelimiters.length > 0) { |
| 891 | const current = pendingDelimiters[pendingDelimiters.length - 1]; |
| 892 | // For <<-, strip leading tabs when checking delimiter |
| 893 | // For <<, require exact match (no leading whitespace allowed) |
| 894 | const lineToCheck = current.stripTabs ? line.replace(/^\t+/, "") : line; |
| 895 | if (lineToCheck === current.delimiter) { |
| 896 | // End of heredoc - this line can be normalized |
| 897 | result.push(line.trimStart()); |
| 898 | pendingDelimiters.pop(); |
| 899 | continue; |
| 900 | } |
| 901 | // Inside heredoc - preserve the line exactly as-is |
| 902 | result.push(line); |
| 903 | continue; |
| 904 | } |
| 905 | |
| 906 | // Not inside a heredoc - normalize the line and check for heredoc starts |
| 907 | const normalizedLine = line.trimStart(); |
| 908 | result.push(normalizedLine); |
| 909 | |
| 910 | // Check for heredoc operators in this line |
| 911 | // Match: <<DELIM, <<-DELIM, << 'DELIM', <<- "DELIM", etc. |
| 912 | // Multiple heredocs on one line are possible: cmd <<EOF1 <<EOF2 |
| 913 | const heredocPattern = /<<(-?)\s*(['"]?)([\w-]+)\2/g; |
| 914 | for (const match of normalizedLine.matchAll(heredocPattern)) { |
| 915 | const stripTabs = match[1] === "-"; |
| 916 | const delimiter = match[3]; |
| 917 | pendingDelimiters.push({ delimiter, stripTabs }); |
| 918 | } |
| 919 | } |
| 920 | |
| 921 | return result.join("\n"); |
| 922 | } |
| 923 | |
| 924 | /** |
| 925 | * Strict UTF-8 decoder that throws on invalid byte sequences. |