source dump of claude code
at main 128 lines 4.7 kB view raw
1import { quote } from './shellQuote.js' 2 3/** 4 * Detects if a command contains a heredoc pattern 5 * Matches patterns like: <<EOF, <<'EOF', <<"EOF", <<-EOF, <<-'EOF', <<\EOF, etc. 6 */ 7function containsHeredoc(command: string): boolean { 8 // Match heredoc patterns: << followed by optional -, then optional quotes or backslash, then word 9 // Matches: <<EOF, <<'EOF', <<"EOF", <<-EOF, <<-'EOF', <<\EOF 10 // Check for bit-shift operators first and exclude them 11 if ( 12 /\d\s*<<\s*\d/.test(command) || 13 /\[\[\s*\d+\s*<<\s*\d+\s*\]\]/.test(command) || 14 /\$\(\(.*<<.*\)\)/.test(command) 15 ) { 16 return false 17 } 18 19 // Now check for heredoc patterns 20 const heredocRegex = /<<-?\s*(?:(['"]?)(\w+)\1|\\(\w+))/ 21 return heredocRegex.test(command) 22} 23 24/** 25 * Detects if a command contains multiline strings in quotes 26 */ 27function containsMultilineString(command: string): boolean { 28 // Check for strings with actual newlines in them 29 // Handle escaped quotes by using a more sophisticated pattern 30 // Match single quotes: '...\n...' where content can include escaped quotes \' 31 // Match double quotes: "...\n..." where content can include escaped quotes \" 32 const singleQuoteMultiline = /'(?:[^'\\]|\\.)*\n(?:[^'\\]|\\.)*'/ 33 const doubleQuoteMultiline = /"(?:[^"\\]|\\.)*\n(?:[^"\\]|\\.)*"/ 34 35 return ( 36 singleQuoteMultiline.test(command) || doubleQuoteMultiline.test(command) 37 ) 38} 39 40/** 41 * Quotes a shell command appropriately, preserving heredocs and multiline strings 42 * @param command The command to quote 43 * @param addStdinRedirect Whether to add < /dev/null 44 * @returns The properly quoted command 45 */ 46export function quoteShellCommand( 47 command: string, 48 addStdinRedirect: boolean = true, 49): string { 50 // If command contains heredoc or multiline strings, handle specially 51 // The shell-quote library incorrectly escapes ! to \! in these cases 52 if (containsHeredoc(command) || containsMultilineString(command)) { 53 // For heredocs and multiline strings, we need to quote for eval 54 // but avoid shell-quote's aggressive escaping 55 // We'll use single quotes and escape only single quotes in the command 56 const escaped = command.replace(/'/g, "'\"'\"'") 57 const quoted = `'${escaped}'` 58 59 // Don't add stdin redirect for heredocs as they provide their own input 60 if (containsHeredoc(command)) { 61 return quoted 62 } 63 64 // For multiline strings without heredocs, add stdin redirect if needed 65 return addStdinRedirect ? `${quoted} < /dev/null` : quoted 66 } 67 68 // For regular commands, use shell-quote 69 if (addStdinRedirect) { 70 return quote([command, '<', '/dev/null']) 71 } 72 73 return quote([command]) 74} 75 76/** 77 * Detects if a command already has a stdin redirect 78 * Match patterns like: < file, </path/to/file, < /dev/null, etc. 79 * But not <<EOF (heredoc), << (bit shift), or <(process substitution) 80 */ 81export function hasStdinRedirect(command: string): boolean { 82 // Look for < followed by whitespace and a filename/path 83 // Negative lookahead to exclude: <<, <( 84 // Must be preceded by whitespace or command separator or start of string 85 return /(?:^|[\s;&|])<(?![<(])\s*\S+/.test(command) 86} 87 88/** 89 * Checks if stdin redirect should be added to a command 90 * @param command The command to check 91 * @returns true if stdin redirect can be safely added 92 */ 93export function shouldAddStdinRedirect(command: string): boolean { 94 // Don't add stdin redirect for heredocs as it interferes with the heredoc terminator 95 if (containsHeredoc(command)) { 96 return false 97 } 98 99 // Don't add stdin redirect if command already has one 100 if (hasStdinRedirect(command)) { 101 return false 102 } 103 104 // For other commands, stdin redirect is generally safe 105 return true 106} 107 108/** 109 * Rewrites Windows CMD-style `>nul` redirects to POSIX `/dev/null`. 110 * 111 * The model occasionally hallucinates Windows CMD syntax (e.g., `ls 2>nul`) 112 * even though our bash shell is always POSIX (Git Bash / WSL on Windows). 113 * When Git Bash sees `2>nul`, it creates a literal file named `nul` — a 114 * Windows reserved device name that is extremely hard to delete and breaks 115 * `git add .` and `git clone`. See anthropics/claude-code#4928. 116 * 117 * Matches: `>nul`, `> NUL`, `2>nul`, `&>nul`, `>>nul` (case-insensitive) 118 * Does NOT match: `>null`, `>nullable`, `>nul.txt`, `cat nul.txt` 119 * 120 * Limitation: this regex does not parse shell quoting, so `echo ">nul"` 121 * will also be rewritten. This is acceptable collateral — it's extremely 122 * rare and rewriting to `/dev/null` inside a string is harmless. 123 */ 124const NUL_REDIRECT_REGEX = /(\d?&?>+\s*)[Nn][Uu][Ll](?=\s|$|[|&;)\n])/g 125 126export function rewriteWindowsNullRedirect(command: string): string { 127 return command.replace(NUL_REDIRECT_REGEX, '$1/dev/null') 128}