UNPKG

@probelabs/probe

Version:

Node.js wrapper for the probe code search tool

153 lines (138 loc) 5 kB
/** * Grep functionality for the probe package * @module grep */ import { execFile } from 'child_process'; import { promisify } from 'util'; import { getBinaryPath } from './utils.js'; const execFileAsync = promisify(execFile); /** * Flag mapping for grep options * Maps option keys to command-line flags */ const GREP_FLAG_MAP = { ignoreCase: '-i', lineNumbers: '-n', count: '-c', filesWithMatches: '-l', filesWithoutMatches: '-L', invertMatch: '-v', beforeContext: '-B', afterContext: '-A', context: '-C', noGitignore: '--no-gitignore', color: '--color', maxCount: '-m' }; /** * Standard grep-style search across files (works with any file type, not just code) * * This provides a cross-platform grep interface that works on any OS and file type. * Use this for searching non-code files (logs, config files, text files, etc.) that * are not supported by probe's semantic search. * * For code files, prefer using the `search()` function which provides semantic, * AST-aware search capabilities. * * @param {Object} options - Grep options * @param {string} options.pattern - Pattern to search for (regex) * @param {string|string[]} options.paths - Path(s) to search in * @param {boolean} [options.ignoreCase] - Case-insensitive search (-i) * @param {boolean} [options.lineNumbers] - Show line numbers (-n) * @param {boolean} [options.count] - Only show count of matches (-c) * @param {boolean} [options.filesWithMatches] - Only show filenames with matches (-l) * @param {boolean} [options.filesWithoutMatches] - Only show filenames without matches (-L) * @param {boolean} [options.invertMatch] - Invert match, show non-matching lines (-v) * @param {number} [options.beforeContext] - Lines of context before match (-B) * @param {number} [options.afterContext] - Lines of context after match (-A) * @param {number} [options.context] - Lines of context before and after match (-C) * @param {boolean} [options.noGitignore] - Don't respect .gitignore files (--no-gitignore) * @param {string} [options.color] - Colorize output: 'always', 'never', 'auto' (--color) * @param {number} [options.maxCount] - Stop after N matches per file (-m) * @param {Object} [options.binaryOptions] - Options for getting the binary * @param {boolean} [options.binaryOptions.forceDownload] - Force download even if binary exists * @param {string} [options.binaryOptions.version] - Specific version to download * @returns {Promise<string>} - Grep results as string * @throws {Error} If the grep operation fails * * @example * // Search for "error" in log files (case-insensitive) * const results = await grep({ * pattern: 'error', * paths: '/var/log', * ignoreCase: true, * lineNumbers: true * }); * * @example * // Count occurrences of "TODO" in project * const count = await grep({ * pattern: 'TODO', * paths: '.', * count: true * }); * * @example * // Find files containing "config" with context * const matches = await grep({ * pattern: 'config', * paths: '/etc', * context: 2, * filesWithMatches: true * }); */ export async function grep(options) { if (!options || !options.pattern) { throw new Error('Pattern is required'); } if (!options.paths) { throw new Error('Path(s) are required'); } // Get the binary path const binaryPath = await getBinaryPath(options.binaryOptions || {}); // Build CLI arguments array for grep subcommand // Using an array prevents command injection vulnerabilities const cliArgs = ['grep']; // Add flags from GREP_FLAG_MAP for (const [key, flag] of Object.entries(GREP_FLAG_MAP)) { const value = options[key]; if (value === undefined || value === null) continue; if (typeof value === 'boolean' && value) { // Boolean flag cliArgs.push(flag); } else if (typeof value === 'number') { // Numeric option cliArgs.push(flag, String(value)); } else if (typeof value === 'string') { // String option (like color) cliArgs.push(flag, value); } } // Add pattern (no need to escape - execFile handles it securely) cliArgs.push(options.pattern); // Add paths (can be single string or array) const paths = Array.isArray(options.paths) ? options.paths : [options.paths]; cliArgs.push(...paths); try { // Use execFile instead of exec to prevent command injection // execFile does not spawn a shell and passes arguments as an array const { stdout, stderr } = await execFileAsync(binaryPath, cliArgs, { maxBuffer: 10 * 1024 * 1024, // 10MB buffer env: { ...process.env, // Disable colors in stderr for cleaner output NO_COLOR: '1' } }); // Return stdout (grep results) return stdout; } catch (error) { // Grep exit code 1 means "no matches found", which is not an error if (error.code === 1 && !error.stderr) { return error.stdout || ''; } // Other errors are real failures const errorMessage = error.stderr || error.message || 'Unknown error'; throw new Error(`Grep failed: ${errorMessage}`); } }