π File detail
utils/bash/shellQuoting.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes quoteShellCommand, hasStdinRedirect, shouldAddStdinRedirect, and rewriteWindowsNullRedirect β mainly functions, hooks, or classes. It composes internal code from shellQuote (relative imports). What the file header says: 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.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
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
π€ Exports (heuristic)
quoteShellCommandhasStdinRedirectshouldAddStdinRedirectrewriteWindowsNullRedirect
π₯οΈ Source preview
import { 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')
}