File size: 3,658 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | /**
* Command semantics configuration for interpreting exit codes in different contexts.
*
* Many commands use exit codes to convey information other than just success/failure.
* For example, grep returns 1 when no matches are found, which is not an error condition.
*/
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
export type CommandSemantic = (
exitCode: number,
stdout: string,
stderr: string,
) => {
isError: boolean
message?: string
}
/**
* Default semantic: treat only 0 as success, everything else as error
*/
const DEFAULT_SEMANTIC: CommandSemantic = (exitCode, _stdout, _stderr) => ({
isError: exitCode !== 0,
message:
exitCode !== 0 ? `Command failed with exit code ${exitCode}` : undefined,
})
/**
* Command-specific semantics
*/
const COMMAND_SEMANTICS: Map<string, CommandSemantic> = new Map([
// grep: 0=matches found, 1=no matches, 2+=error
[
'grep',
(exitCode, _stdout, _stderr) => ({
isError: exitCode >= 2,
message: exitCode === 1 ? 'No matches found' : undefined,
}),
],
// ripgrep has same semantics as grep
[
'rg',
(exitCode, _stdout, _stderr) => ({
isError: exitCode >= 2,
message: exitCode === 1 ? 'No matches found' : undefined,
}),
],
// find: 0=success, 1=partial success (some dirs inaccessible), 2+=error
[
'find',
(exitCode, _stdout, _stderr) => ({
isError: exitCode >= 2,
message:
exitCode === 1 ? 'Some directories were inaccessible' : undefined,
}),
],
// diff: 0=no differences, 1=differences found, 2+=error
[
'diff',
(exitCode, _stdout, _stderr) => ({
isError: exitCode >= 2,
message: exitCode === 1 ? 'Files differ' : undefined,
}),
],
// test/[: 0=condition true, 1=condition false, 2+=error
[
'test',
(exitCode, _stdout, _stderr) => ({
isError: exitCode >= 2,
message: exitCode === 1 ? 'Condition is false' : undefined,
}),
],
// [ is an alias for test
[
'[',
(exitCode, _stdout, _stderr) => ({
isError: exitCode >= 2,
message: exitCode === 1 ? 'Condition is false' : undefined,
}),
],
// wc, head, tail, cat, etc.: these typically only fail on real errors
// so we use default semantics
])
/**
* Get the semantic interpretation for a command
*/
function getCommandSemantic(command: string): CommandSemantic {
// Extract the base command (first word, handling pipes)
const baseCommand = heuristicallyExtractBaseCommand(command)
const semantic = COMMAND_SEMANTICS.get(baseCommand)
return semantic !== undefined ? semantic : DEFAULT_SEMANTIC
}
/**
* Extract just the command name (first word) from a single command string.
*/
function extractBaseCommand(command: string): string {
return command.trim().split(/\s+/)[0] || ''
}
/**
* Extract the primary command from a complex command line;
* May get it super wrong - don't depend on this for security
*/
function heuristicallyExtractBaseCommand(command: string): string {
const segments = splitCommand_DEPRECATED(command)
// Take the last command as that's what determines the exit code
const lastCommand = segments[segments.length - 1] || command
return extractBaseCommand(lastCommand)
}
/**
* Interpret command result based on semantic rules
*/
export function interpretCommandResult(
command: string,
exitCode: number,
stdout: string,
stderr: string,
): {
isError: boolean
message?: string
} {
const semantic = getCommandSemantic(command)
const result = semantic(exitCode, stdout, stderr)
return {
isError: result.isError,
message: result.message,
}
}
|