Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
9.6 kB · 322 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323/** * Parser for sed edit commands (-i flag substitutions) * Extracts file paths and substitution patterns to enable file-edit-style rendering */
import { randomBytes } from 'crypto'import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
// BRE→ERE conversion placeholders (null-byte sentinels, never appear in user input)const BACKSLASH_PLACEHOLDER = '\x00BACKSLASH\x00'const PLUS_PLACEHOLDER = '\x00PLUS\x00'const QUESTION_PLACEHOLDER = '\x00QUESTION\x00'const PIPE_PLACEHOLDER = '\x00PIPE\x00'const LPAREN_PLACEHOLDER = '\x00LPAREN\x00'const RPAREN_PLACEHOLDER = '\x00RPAREN\x00'const BACKSLASH_PLACEHOLDER_RE = new RegExp(BACKSLASH_PLACEHOLDER, 'g')const PLUS_PLACEHOLDER_RE = new RegExp(PLUS_PLACEHOLDER, 'g')const QUESTION_PLACEHOLDER_RE = new RegExp(QUESTION_PLACEHOLDER, 'g')const PIPE_PLACEHOLDER_RE = new RegExp(PIPE_PLACEHOLDER, 'g')const LPAREN_PLACEHOLDER_RE = new RegExp(LPAREN_PLACEHOLDER, 'g')const RPAREN_PLACEHOLDER_RE = new RegExp(RPAREN_PLACEHOLDER, 'g')
export type SedEditInfo = { /** The file path being edited */ filePath: string /** The search pattern (regex) */ pattern: string /** The replacement string */ replacement: string /** Substitution flags (g, i, etc.) */ flags: string /** Whether to use extended regex (-E or -r flag) */ extendedRegex: boolean}
/** * Check if a command is a sed in-place edit command * Returns true only for simple sed -i 's/pattern/replacement/flags' file commands */export function isSedInPlaceEdit(command: string): boolean { const info = parseSedEditCommand(command) return info !== null}
/** * Parse a sed edit command and extract the edit information * Returns null if the command is not a valid sed in-place edit */export function parseSedEditCommand(command: string): SedEditInfo | null { const trimmed = command.trim()
// Must start with sed const sedMatch = trimmed.match(/^\s*sed\s+/) if (!sedMatch) return null
const withoutSed = trimmed.slice(sedMatch[0].length) const parseResult = tryParseShellCommand(withoutSed) if (!parseResult.success) return null const tokens = parseResult.tokens
// Extract string tokens only const args: string[] = [] for (const token of tokens) { if (typeof token === 'string') { args.push(token) } else if ( typeof token === 'object' && token !== null && 'op' in token && token.op === 'glob' ) { // Glob patterns are too complex for this simple parser return null } }
// Parse flags and arguments let hasInPlaceFlag = false let extendedRegex = false let expression: string | null = null let filePath: string | null = null
let i = 0 while (i < args.length) { const arg = args[i]!
// Handle -i flag (with or without backup suffix) if (arg === '-i' || arg === '--in-place') { hasInPlaceFlag = true i++ // On macOS, -i requires a suffix argument (even if empty string) // Check if next arg looks like a backup suffix (empty, or starts with dot) // Don't consume flags (-E, -r) or sed expressions (starting with s, y, d) if (i < args.length) { const nextArg = args[i] // If next arg is empty string or starts with dot, it's a backup suffix if ( typeof nextArg === 'string' && !nextArg.startsWith('-') && (nextArg === '' || nextArg.startsWith('.')) ) { i++ // Skip the backup suffix } } continue } if (arg.startsWith('-i')) { // -i.bak or similar (inline suffix) hasInPlaceFlag = true i++ continue }
// Handle extended regex flags if (arg === '-E' || arg === '-r' || arg === '--regexp-extended') { extendedRegex = true i++ continue }
// Handle -e flag with expression if (arg === '-e' || arg === '--expression') { if (i + 1 < args.length && typeof args[i + 1] === 'string') { // Only support single expression if (expression !== null) return null expression = args[i + 1]! i += 2 continue } return null } if (arg.startsWith('--expression=')) { if (expression !== null) return null expression = arg.slice('--expression='.length) i++ continue }
// Skip other flags we don't understand if (arg.startsWith('-')) { // Unknown flag - not safe to parse return null }
// Non-flag argument if (expression === null) { // First non-flag arg is the expression expression = arg } else if (filePath === null) { // Second non-flag arg is the file path filePath = arg } else { // More than one file - not supported for simple rendering return null }
i++ }
// Must have -i flag, expression, and file path if (!hasInPlaceFlag || !expression || !filePath) { return null }
// Parse the substitution expression: s/pattern/replacement/flags // Only support / as delimiter for simplicity const substMatch = expression.match(/^s\//) if (!substMatch) { return null }
const rest = expression.slice(2) // Skip 's/'
// Find pattern and replacement by tracking escaped characters let pattern = '' let replacement = '' let flags = '' let state: 'pattern' | 'replacement' | 'flags' = 'pattern' let j = 0
while (j < rest.length) { const char = rest[j]!
if (char === '\\' && j + 1 < rest.length) { // Escaped character if (state === 'pattern') { pattern += char + rest[j + 1] } else if (state === 'replacement') { replacement += char + rest[j + 1] } else { flags += char + rest[j + 1] } j += 2 continue }
if (char === '/') { if (state === 'pattern') { state = 'replacement' } else if (state === 'replacement') { state = 'flags' } else { // Extra delimiter in flags - unexpected return null } j++ continue }
if (state === 'pattern') { pattern += char } else if (state === 'replacement') { replacement += char } else { flags += char } j++ }
// Must have found all three parts (pattern, replacement delimiter, and optional flags) if (state !== 'flags') { return null }
// Validate flags - only allow safe substitution flags const validFlags = /^[gpimIM1-9]*$/ if (!validFlags.test(flags)) { return null }
return { filePath, pattern, replacement, flags, extendedRegex, }}
/** * Apply a sed substitution to file content * Returns the new content after applying the substitution */export function applySedSubstitution( content: string, sedInfo: SedEditInfo,): string { // Convert sed pattern to JavaScript regex let regexFlags = ''
// Handle global flag if (sedInfo.flags.includes('g')) { regexFlags += 'g' }
// Handle case-insensitive flag (i or I in sed) if (sedInfo.flags.includes('i') || sedInfo.flags.includes('I')) { regexFlags += 'i' }
// Handle multiline flag (m or M in sed) if (sedInfo.flags.includes('m') || sedInfo.flags.includes('M')) { regexFlags += 'm' }
// Convert sed pattern to JavaScript regex pattern let jsPattern = sedInfo.pattern // Unescape \/ to / .replace(/\\\//g, '/')
// In BRE mode (no -E flag), metacharacters have opposite escaping: // BRE: \+ means "one or more", + is literal // ERE/JS: + means "one or more", \+ is literal // We need to convert BRE escaping to ERE for JavaScript regex if (!sedInfo.extendedRegex) { jsPattern = jsPattern // Step 1: Protect literal backslashes (\\) first - in both BRE and ERE, \\ is literal backslash .replace(/\\\\/g, BACKSLASH_PLACEHOLDER) // Step 2: Replace escaped metacharacters with placeholders (these should become unescaped in JS) .replace(/\\\+/g, PLUS_PLACEHOLDER) .replace(/\\\?/g, QUESTION_PLACEHOLDER) .replace(/\\\|/g, PIPE_PLACEHOLDER) .replace(/\\\(/g, LPAREN_PLACEHOLDER) .replace(/\\\)/g, RPAREN_PLACEHOLDER) // Step 3: Escape unescaped metacharacters (these are literal in BRE) .replace(/\+/g, '\\+') .replace(/\?/g, '\\?') .replace(/\|/g, '\\|') .replace(/\(/g, '\\(') .replace(/\)/g, '\\)') // Step 4: Replace placeholders with their JS equivalents .replace(BACKSLASH_PLACEHOLDER_RE, '\\\\') .replace(PLUS_PLACEHOLDER_RE, '+') .replace(QUESTION_PLACEHOLDER_RE, '?') .replace(PIPE_PLACEHOLDER_RE, '|') .replace(LPAREN_PLACEHOLDER_RE, '(') .replace(RPAREN_PLACEHOLDER_RE, ')') }
// Unescape sed-specific escapes in replacement // Convert \n to newline, & to $& (match), etc. // Use a unique placeholder with random salt to prevent injection attacks const salt = randomBytes(8).toString('hex') const ESCAPED_AMP_PLACEHOLDER = `___ESCAPED_AMPERSAND_${salt}___` const jsReplacement = sedInfo.replacement // Unescape \/ to / .replace(/\\\//g, '/') // First escape \& to a placeholder .replace(/\\&/g, ESCAPED_AMP_PLACEHOLDER) // Convert & to $& (full match) - use $$& to get literal $& in output .replace(/&/g, '$$&') // Convert placeholder back to literal & .replace(new RegExp(ESCAPED_AMP_PLACEHOLDER, 'g'), '&')
try { const regex = new RegExp(jsPattern, regexFlags) return content.replace(regex, jsReplacement) } catch { // If regex is invalid, return original content return content }}