* Pattern 2: Check if this is a substitution command * Allows: sed 's/pattern/replacement/flags' where flags are only: g, p, i, I, m, M, 1-9 * When allowFileWrites is true, allows -i flag and file arguments for in-place editing * When allowFileWrites is false (default), requires stdout-only (no f
(
command: string,
expressions: string[],
hasFileArguments: boolean,
options?: { allowFileWrites?: boolean },
)
| 140 | * @internal Exported for testing |
| 141 | */ |
| 142 | function isSubstitutionCommand( |
| 143 | command: string, |
| 144 | expressions: string[], |
| 145 | hasFileArguments: boolean, |
| 146 | options?: { allowFileWrites?: boolean }, |
| 147 | ): boolean { |
| 148 | const allowFileWrites = options?.allowFileWrites ?? false |
| 149 | |
| 150 | // When not allowing file writes, must NOT have file arguments |
| 151 | if (!allowFileWrites && hasFileArguments) { |
| 152 | return false |
| 153 | } |
| 154 | |
| 155 | const sedMatch = command.match(/^\s*sed\s+/) |
| 156 | if (!sedMatch) return false |
| 157 | |
| 158 | const withoutSed = command.slice(sedMatch[0].length) |
| 159 | const parseResult = tryParseShellCommand(withoutSed) |
| 160 | if (!parseResult.success) return false |
| 161 | const parsed = parseResult.tokens |
| 162 | |
| 163 | // Extract all flags |
| 164 | const flags: string[] = [] |
| 165 | for (const arg of parsed) { |
| 166 | if (typeof arg === 'string' && arg.startsWith('-') && arg !== '--') { |
| 167 | flags.push(arg) |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | // Validate flags based on mode |
| 172 | // Base allowed flags for both modes |
| 173 | const allowedFlags = ['-E', '--regexp-extended', '-r', '--posix'] |
| 174 | |
| 175 | // When allowing file writes, also permit -i and --in-place |
| 176 | if (allowFileWrites) { |
| 177 | allowedFlags.push('-i', '--in-place') |
| 178 | } |
| 179 | |
| 180 | if (!validateFlagsAgainstAllowlist(flags, allowedFlags)) { |
| 181 | return false |
| 182 | } |
| 183 | |
| 184 | // Must have exactly one expression |
| 185 | if (expressions.length !== 1) { |
| 186 | return false |
| 187 | } |
| 188 | |
| 189 | const expr = expressions[0]!.trim() |
| 190 | |
| 191 | // STRICT ALLOWLIST: Must be exactly a substitution command starting with 's' |
| 192 | // This rejects standalone commands like 'e', 'w file', etc. |
| 193 | if (!expr.startsWith('s')) { |
| 194 | return false |
| 195 | } |
| 196 | |
| 197 | // Parse substitution: s/pattern/replacement/flags |
| 198 | // Only allow / as delimiter (strict) |
| 199 | const substitutionMatch = expr.match(/^s\/(.*?)$/) |
no test coverage detected