using System; using System.IO; using System.Collections; using System.Diagnostics; namespace CxxBuilder; static class FileMatcher { public enum Error { ExpectedEscapeSequence, CharacterClassNotClosed, } public static Result HandleMatches(Span patterns, StringView directory, delegate void(StringView match) callback) { Runtime.Assert(Directory.Exists(directory), scope $"No such directory {directory}"); void Dir(StringView dir) { dir: for (let element in Directory.Enumerate(dir)) { let path = element.GetFilePath(..scope .()); path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); if (element.IsDirectory) { Dir(path); continue; } let relpath = Path.GetRelativePath(path, directory, ..scope .()); for (let pattern in patterns) { if (!pattern.StartsWith('^') && !pattern.StartsWith('!')) continue; if (IsMatch(pattern[1...], relpath)) continue dir; } for (let pattern in patterns) { if (pattern.StartsWith('^') || pattern.StartsWith('!')) continue; if (!IsMatch(pattern, relpath)) continue; callback(relpath); break; } } } Dir(directory); return .Ok; } public static Result IsMatch(StringView pattern, StringView path) { var pattern, path; Result Matches(StringView pattern, char8 c) { if (pattern.IsEmpty) return false; switch (pattern[0]) { case '*': if (pattern.Length > 1 && pattern[1] == '*') return true; return !Path.IsDirectorySeparatorChar(c); case '?': return true; case '\\': if (pattern.Length <= 1) return .Err(.ExpectedEscapeSequence); return c == pattern[1]; case '/': return Path.IsDirectorySeparatorChar(c); case '[': int i = 0; char8 Next() { if (pattern.Length <= ++i) return (.)7; return pattern[i]; } bool negated = false; char8 current; switch (Next()) { case (.)7: return .Err(.CharacterClassNotClosed); case '^', '!': negated = true; fallthrough; case '\\': current = Next(); case ']': return false; default: current = _; } while (true) { if (c == current) return !negated; switch (Next()) { case ']': return negated; case '-': let end = Next(); if (c >= current && c <= end) return !negated; current = Next(); case '\\': current = Next(); default: current = _; } } default: return c == _; } } reduce: while (true) { if (path.IsEmpty) return true; if (pattern.IsEmpty) return false; switch (Matches(pattern, path[0])) { case .Err: return _; case .Ok(false): return false; case .Ok: } switch (pattern[0]) { case '*': StringView lazy; if (pattern.Length > 1) { lazy = pattern; lazy.RemoveFromStart(pattern[1] == '*' ? 2 : 1); } else lazy = .(); defer { pattern = lazy; } while (true) { if (Try!(IsMatch(lazy, path))) continue reduce; if (!Try!(Matches(pattern, path[0]))) continue reduce; path.RemoveFromStart(1); if (path.IsEmpty) return lazy.IsEmpty; } case '[': bool escaped = false; while (true) { let c = pattern[0]; pattern.RemoveFromStart(1); if (c == ']' && !escaped) { path.RemoveFromStart(1); continue reduce; } escaped = c == '\\'; } case '\\': pattern.RemoveFromStart(2); path.RemoveFromStart(1); default: pattern.RemoveFromStart(1); path.RemoveFromStart(1); } } } }