UNPKG

lsh-framework

Version:

A powerful, extensible shell with advanced job management, database persistence, and modern CLI features

412 lines (411 loc) 14.5 kB
/** * Extended Globbing Implementation * Provides ZSH-compatible extended globbing patterns */ import * as fs from 'fs'; import * as path from 'path'; export class ExtendedGlobber { cwd; constructor(cwd = process.cwd()) { this.cwd = cwd; } /** * Expand extended glob patterns */ async expandPattern(pattern, options = {}) { const opts = { cwd: this.cwd, includeHidden: false, followSymlinks: false, extendedGlob: true, ...options, }; // Handle exclusion patterns: *.txt~*backup* if (pattern.includes('~')) { return this.expandExclusionPattern(pattern, opts); } // Handle alternation patterns: (foo|bar).txt if (pattern.includes('(') && pattern.includes('|') && pattern.includes(')')) { return this.expandAlternationPattern(pattern, opts); } // Handle numeric ranges: <1-10>.txt if (pattern.includes('<') && pattern.includes('-') && pattern.includes('>')) { return this.expandNumericRange(pattern, opts); } // Handle qualifiers: *.txt(.L+10) if (pattern.includes('(') && pattern.includes('.')) { return this.expandQualifiedPattern(pattern, opts); } // Handle negation patterns: ^*.backup if (pattern.startsWith('^')) { return this.expandNegationPattern(pattern, opts); } // Handle recursive patterns: **/*.txt if (pattern.includes('**')) { return this.expandRecursivePattern(pattern, opts); } // Fall back to regular globbing return this.expandRegularPattern(pattern, opts); } /** * Expand exclusion patterns: *.txt~*backup* */ async expandExclusionPattern(pattern, options) { const [includePattern, excludePattern] = pattern.split('~'); const includeResults = await this.expandRegularPattern(includePattern, options); const excludeResults = await this.expandRegularPattern(excludePattern, options); return includeResults.filter(file => !excludeResults.includes(file)); } /** * Expand alternation patterns: (foo|bar).txt */ async expandAlternationPattern(pattern, options) { const results = []; // Find alternation groups const alternationRegex = /\(([^)]+)\)/g; let match; while ((match = alternationRegex.exec(pattern)) !== null) { const alternatives = match[1].split('|'); const prefix = pattern.substring(0, match.index); const suffix = pattern.substring(match.index + match[0].length); for (const alt of alternatives) { const altPattern = prefix + alt + suffix; const altResults = await this.expandPattern(altPattern, options); results.push(...altResults); } } return [...new Set(results)]; // Remove duplicates } /** * Expand numeric ranges: <1-10>.txt */ async expandNumericRange(pattern, options) { const results = []; const rangeRegex = /<(\d+)-(\d+)>/g; let match; while ((match = rangeRegex.exec(pattern)) !== null) { const start = parseInt(match[1], 10); const end = parseInt(match[2], 10); for (let i = start; i <= end; i++) { const altPattern = pattern.replace(match[0], i.toString()); const altResults = await this.expandPattern(altPattern, options); results.push(...altResults); } } return [...new Set(results)]; } /** * Expand patterns with qualifiers: *.txt(.L+10) */ async expandQualifiedPattern(pattern, options) { const qualifierMatch = pattern.match(/^(.+)\(([^)]+)\)$/); if (!qualifierMatch) return []; const [, basePattern, qualifierStr] = qualifierMatch; const baseResults = await this.expandRegularPattern(basePattern, options); const qualifiers = this.parseQualifiers(qualifierStr); return this.filterByQualifiers(baseResults, qualifiers); } /** * Expand negation patterns: ^*.backup */ async expandNegationPattern(pattern, options) { const negatedPattern = pattern.substring(1); // Remove ^ const allFiles = await this.getAllFiles(options.cwd || this.cwd, options); const negatedFiles = await this.expandRegularPattern(negatedPattern, options); return allFiles.filter(file => !negatedFiles.includes(file)); } /** * Expand recursive patterns: **\/*.txt */ async expandRecursivePattern(pattern, options) { const results = []; const searchDir = options.cwd || this.cwd; // Convert **/*.txt to recursive search const recursivePattern = pattern.replace(/\*\*\//g, ''); await this.searchRecursively(searchDir, recursivePattern, results, options); return results; } /** * Expand regular glob patterns */ async expandRegularPattern(pattern, options) { const results = []; const searchDir = options.cwd || this.cwd; // Handle tilde expansion const expandedPattern = this.expandTilde(pattern); // Split pattern into segments const segments = expandedPattern.split('/').filter(seg => seg.length > 0); if (segments.length === 0) { return [searchDir]; } await this.matchSegments(searchDir, segments, results, options); return results.sort(); } /** * Parse qualifiers from string */ parseQualifiers(qualifierStr) { const qualifiers = []; // Parse size qualifiers: L+10, L-5, L=100 const sizeMatch = qualifierStr.match(/L([+\-=])(\d+)/); if (sizeMatch) { qualifiers.push({ type: 'size', operator: sizeMatch[1], value: sizeMatch[2], }); } // Parse time qualifiers: m-1 (modified within 1 day) const timeMatch = qualifierStr.match(/m([+\-=])(\d+)/); if (timeMatch) { qualifiers.push({ type: 'time', operator: timeMatch[1], value: timeMatch[2], }); } // Parse type qualifiers: f (file), d (directory) const typeMatch = qualifierStr.match(/[fd]/); if (typeMatch) { qualifiers.push({ type: 'type', operator: '=', value: typeMatch[0], }); } return qualifiers; } /** * Filter files by qualifiers */ filterByQualifiers(files, qualifiers) { return files.filter(file => { try { const stats = fs.statSync(file); for (const qualifier of qualifiers) { if (!this.matchesQualifier(file, stats, qualifier)) { return false; } } return true; } catch { return false; } }); } /** * Check if file matches a qualifier */ matchesQualifier(file, stats, qualifier) { switch (qualifier.type) { case 'size': { const size = stats.size; const targetSize = parseInt(qualifier.value, 10); switch (qualifier.operator) { case '=': return size === targetSize; case '+': return size > targetSize; case '-': return size < targetSize; case '>': return size >= targetSize; default: return false; } } case 'time': { const now = Date.now(); const fileTime = stats.mtime.getTime(); const daysDiff = (now - fileTime) / (1000 * 60 * 60 * 24); const targetDays = parseInt(qualifier.value, 10); switch (qualifier.operator) { case '=': return Math.abs(daysDiff) <= targetDays; case '+': return daysDiff > targetDays; case '-': return daysDiff < targetDays; case '>': return daysDiff >= targetDays; default: return false; } } case 'type': switch (qualifier.value) { case 'f': return stats.isFile(); case 'd': return stats.isDirectory(); default: return false; } default: return true; } } /** * Search recursively for files */ async searchRecursively(dir, pattern, results, options) { try { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); // Skip hidden files unless explicitly included if (!options.includeHidden && entry.name.startsWith('.')) { continue; } if (entry.isDirectory()) { // Recursively search subdirectories await this.searchRecursively(fullPath, pattern, results, options); } else if (entry.isFile()) { // Check if file matches pattern if (this.matchesPattern(entry.name, pattern)) { results.push(fullPath); } } } } catch { // Directory doesn't exist or not readable } } /** * Get all files in directory */ async getAllFiles(dir, options) { const files = []; try { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (!options.includeHidden && entry.name.startsWith('.')) { continue; } if (entry.isFile()) { files.push(fullPath); } else if (entry.isDirectory()) { const subFiles = await this.getAllFiles(fullPath, options); files.push(...subFiles); } } } catch { // Directory doesn't exist or not readable } return files; } /** * Match segments recursively */ async matchSegments(currentPath, remainingSegments, results, options) { if (remainingSegments.length === 0) { results.push(currentPath); return; } const [currentSegment, ...restSegments] = remainingSegments; try { const entries = await fs.promises.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { if (!options.includeHidden && entry.name.startsWith('.')) { continue; } if (this.matchesPattern(entry.name, currentSegment)) { const fullPath = path.join(currentPath, entry.name); if (restSegments.length === 0) { results.push(fullPath); } else if (entry.isDirectory()) { await this.matchSegments(fullPath, restSegments, results, options); } } } } catch { // Directory doesn't exist or not readable } } /** * Check if filename matches pattern */ matchesPattern(filename, pattern) { const regex = this.patternToRegex(pattern); return regex.test(filename); } /** * Convert glob pattern to regex */ patternToRegex(pattern) { let regexStr = ''; let i = 0; while (i < pattern.length) { const char = pattern[i]; switch (char) { case '*': regexStr += '.*'; break; case '?': regexStr += '.'; break; case '[': { const closeIdx = this.findClosingBracket(pattern, i); if (closeIdx === -1) { regexStr += '\\['; } else { let charClass = pattern.slice(i + 1, closeIdx); if (charClass.startsWith('!') || charClass.startsWith('^')) { charClass = '^' + charClass.slice(1); } regexStr += '[' + charClass + ']'; i = closeIdx; } break; } case '\\': if (i + 1 < pattern.length) { regexStr += '\\' + pattern[i + 1]; i++; } else { regexStr += '\\\\'; } break; default: regexStr += this.escapeRegex(char); break; } i++; } return new RegExp('^' + regexStr + '$'); } /** * Find closing bracket */ findClosingBracket(str, startIdx) { let depth = 1; for (let i = startIdx + 1; i < str.length; i++) { if (str[i] === '[') depth++; else if (str[i] === ']') { depth--; if (depth === 0) return i; } } return -1; } /** * Expand tilde */ expandTilde(pattern) { if (pattern.startsWith('~/')) { const homeDir = process.env.HOME || '/'; return path.join(homeDir, pattern.slice(2)); } if (pattern === '~') { return process.env.HOME || '/'; } return pattern; } /** * Escape regex special characters */ escapeRegex(str) { return str.replace(/[.+^$()|[\]{}\\]/g, '\\$&'); } } export default ExtendedGlobber;