Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
4.7 kB · 128 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129import { quote } from './shellQuote.js'
/** * Detects if a command contains a heredoc pattern * Matches patterns like: <<EOF, <<'EOF', <<"EOF", <<-EOF, <<-'EOF', <<\EOF, etc. */function containsHeredoc(command: string): boolean { // Match heredoc patterns: << followed by optional -, then optional quotes or backslash, then word // Matches: <<EOF, <<'EOF', <<"EOF", <<-EOF, <<-'EOF', <<\EOF // Check for bit-shift operators first and exclude them if ( /\d\s*<<\s*\d/.test(command) || /\[\[\s*\d+\s*<<\s*\d+\s*\]\]/.test(command) || /\$\(\(.*<<.*\)\)/.test(command) ) { return false }
// Now check for heredoc patterns const heredocRegex = /<<-?\s*(?:(['"]?)(\w+)\1|\\(\w+))/ return heredocRegex.test(command)}
/** * Detects if a command contains multiline strings in quotes */function containsMultilineString(command: string): boolean { // Check for strings with actual newlines in them // Handle escaped quotes by using a more sophisticated pattern // Match single quotes: '...\n...' where content can include escaped quotes \' // Match double quotes: "...\n..." where content can include escaped quotes \" const singleQuoteMultiline = /'(?:[^'\\]|\\.)*\n(?:[^'\\]|\\.)*'/ const doubleQuoteMultiline = /"(?:[^"\\]|\\.)*\n(?:[^"\\]|\\.)*"/
return ( singleQuoteMultiline.test(command) || doubleQuoteMultiline.test(command) )}
/** * Quotes a shell command appropriately, preserving heredocs and multiline strings * @param command The command to quote * @param addStdinRedirect Whether to add < /dev/null * @returns The properly quoted command */export function quoteShellCommand( command: string, addStdinRedirect: boolean = true,): string { // If command contains heredoc or multiline strings, handle specially // The shell-quote library incorrectly escapes ! to \! in these cases if (containsHeredoc(command) || containsMultilineString(command)) { // For heredocs and multiline strings, we need to quote for eval // but avoid shell-quote's aggressive escaping // We'll use single quotes and escape only single quotes in the command const escaped = command.replace(/'/g, "'\"'\"'") const quoted = `'${escaped}'`
// Don't add stdin redirect for heredocs as they provide their own input if (containsHeredoc(command)) { return quoted }
// For multiline strings without heredocs, add stdin redirect if needed return addStdinRedirect ? `${quoted} < /dev/null` : quoted }
// For regular commands, use shell-quote if (addStdinRedirect) { return quote([command, '<', '/dev/null']) }
return quote([command])}
/** * Detects if a command already has a stdin redirect * Match patterns like: < file, </path/to/file, < /dev/null, etc. * But not <<EOF (heredoc), << (bit shift), or <(process substitution) */export function hasStdinRedirect(command: string): boolean { // Look for < followed by whitespace and a filename/path // Negative lookahead to exclude: <<, <( // Must be preceded by whitespace or command separator or start of string return /(?:^|[\s;&|])<(?![<(])\s*\S+/.test(command)}
/** * Checks if stdin redirect should be added to a command * @param command The command to check * @returns true if stdin redirect can be safely added */export function shouldAddStdinRedirect(command: string): boolean { // Don't add stdin redirect for heredocs as it interferes with the heredoc terminator if (containsHeredoc(command)) { return false }
// Don't add stdin redirect if command already has one if (hasStdinRedirect(command)) { return false }
// For other commands, stdin redirect is generally safe return true}
/** * Rewrites Windows CMD-style `>nul` redirects to POSIX `/dev/null`. * * The model occasionally hallucinates Windows CMD syntax (e.g., `ls 2>nul`) * even though our bash shell is always POSIX (Git Bash / WSL on Windows). * When Git Bash sees `2>nul`, it creates a literal file named `nul` — a * Windows reserved device name that is extremely hard to delete and breaks * `git add .` and `git clone`. See anthropics/claude-code#4928. * * Matches: `>nul`, `> NUL`, `2>nul`, `&>nul`, `>>nul` (case-insensitive) * Does NOT match: `>null`, `>nullable`, `>nul.txt`, `cat nul.txt` * * Limitation: this regex does not parse shell quoting, so `echo ">nul"` * will also be rewritten. This is acceptable collateral — it's extremely * rare and rewriting to `/dev/null` inside a string is harmless. */const NUL_REDIRECT_REGEX = /(\d?&?>+\s*)[Nn][Uu][Ll](?=\s|$|[|&;)\n])/g
export function rewriteWindowsNullRedirect(command: string): string { return command.replace(NUL_REDIRECT_REGEX, '$1/dev/null')}