public static HashSet <string> GetFiles( ResultCache cache, IEnumerable <string> searchablePaths, IEnumerable <IPattern> includePaths, IEnumerable <IPattern> excludePaths, string basePath = null) { searchablePaths = searchablePaths as string[] ?? searchablePaths.ToArray(); includePaths = includePaths as IPattern[] ?? includePaths.ToArray(); if (excludePaths != null) { excludePaths = excludePaths as IPattern[] ?? excludePaths.ToArray(); } else { excludePaths = new List <IPattern>(); } basePath = basePath ?? "./"; bool returnAbsolutePaths = Path.IsPathRooted(basePath); string absBasePath; if (basePath == "./" || basePath == ".") { absBasePath = new DirectoryInfo(Directory.GetCurrentDirectory()).FullName; } else { absBasePath = new DirectoryInfo(basePath).FullName; } (int hash, bool hasResults, HashSet <string> files)cacheRequest = cache?.GetResultsOrReserve( searchablePaths, includePaths, excludePaths, basePath) ?? (0, false, null); if (cacheRequest.hasResults) { Log.Debug("Getting files for query hash {{{0}}} from cached results for base path '{1}':", cacheRequest.hash, basePath); Log.IndentedCollection(cacheRequest.files, Log.Debug); return(cacheRequest.files); } Log.Debug("Getting files for query hash {{{0}}} using base path '{1}' and provided include/exclude paths:", cacheRequest.hash, basePath); using (new CompositeDisposable( new Log.ScopedIndent(), new Log.ScopedTimer(Log.Level.Debug, "GetFiles(...)", $"{{{cacheRequest.hash,-11}}} {basePath}"))) { Log.Debug("search paths:"); Log.IndentedCollection(searchablePaths, Log.Debug); HashSet <string> includeFiles; HashSet <GLOB> includeGlobs; HashSet <RegexPattern> includeRegexes; HashSet <string> excludeFiles; HashSet <GLOB> excludeGlobs; HashSet <RegexPattern> excludeRegexes; (includeFiles, includeGlobs, includeRegexes) = ProcessFileValues(includePaths); (excludeFiles, excludeGlobs, excludeRegexes) = ProcessFileValues(excludePaths); var searchPathMatches = new Dictionary <string, List <PatternMatch> >(); foreach (string currentSearchPath in searchablePaths) { var currentMatches = new List <PatternMatch>(); var dir = new DirectoryInfo(new DirectoryInfo( currentSearchPath == "./" || currentSearchPath == "." ? Directory.GetCurrentDirectory() : currentSearchPath) .FullName); bool makeCandidatesAbsolute = Path.IsPathRooted(currentSearchPath); if (!dir.Exists) { Log.Debug("Skipping searchable directory because it does not exist: {0}'", currentSearchPath); continue; } string[] candidates = dir.GetFilesSafeRecursive("*") .Select(f => GetPath(makeCandidatesAbsolute, f.FullName, absBasePath)) .Reverse() .ToArray(); DirectoryInfo[] allDirs = dir.GetDirectoriesSafeRecursive("*") .Reverse() .Concat(new[] { dir }) .ToArray(); #region includes if (includeGlobs != null && includeGlobs.Count > 0) { foreach (GLOB glob in includeGlobs) { IEnumerable <string> matchesForGlob = candidates.Where(f => glob.IsMatch(f)); currentMatches.AddRange(matchesForGlob.Select( f => new PatternMatch(f, currentSearchPath, "glob \"" + glob.Pattern + "\""))); } } if (includeRegexes != null && includeRegexes.Count > 0) { IEnumerable <PatternMatch> matchesForRegex = includeRegexes.SelectMany( r => r.FilterMatches(candidates).Select( f => new PatternMatch(f, currentSearchPath, r.ToString()))); currentMatches.AddRange(matchesForRegex); } var literalPatternMatches = new Dictionary <string, PatternMatch[]>(); if (includeFiles != null && includeFiles.Count > 0) { foreach (string filePattern in includeFiles.ToArray()) { string[] matchesForFile = (from dirInfo in allDirs select Path.Combine(dirInfo.FullName, filePattern) into includeFilePath where File.Exists(includeFilePath) select GetPath(makeCandidatesAbsolute, includeFilePath, absBasePath)) .ToArray(); PatternMatch[] patternMatchesForFile = matchesForFile.Select( f => new PatternMatch(f, currentSearchPath, filePattern)) .ToArray(); if (patternMatchesForFile.Length > 0) { literalPatternMatches[filePattern] = patternMatchesForFile; currentMatches.AddRange(patternMatchesForFile); } } } #endregion #region excludes IEnumerable <PatternMatch> tempMatches = currentMatches; if (excludeGlobs != null && excludeGlobs.Count > 0) { var excludeGlob = new CompositeGlob(excludeGlobs, null); tempMatches = tempMatches.Where(m => !excludeGlob.IsMatch(m.File)) .ToList(); // Helps with debugging } if (excludeRegexes != null && excludeRegexes.Count > 0) { tempMatches = tempMatches.Where(m => excludeRegexes.All(r => !r.Regex.IsMatch(m.File))) .ToList(); // Helps with debugging } if (excludeFiles != null && excludeFiles.Count > 0) { tempMatches = tempMatches.Where(m => excludeFiles.All(x => x != Path.GetFileName(m.File))) .ToList(); // Helps with debugging } #endregion #region literal validation currentMatches = tempMatches.ToList(); foreach (KeyValuePair <string, PatternMatch[]> kvp in literalPatternMatches) { PatternMatch[] patternMatchesForFile = kvp.Value; if (patternMatchesForFile.Length > 1 && currentMatches.Intersect(patternMatchesForFile).Count() > 1) { Log.Warn("The literal pattern '{0}' matched more than one file.", kvp.Key); Log.Warn("Only the first match '{0}' will be returned. See all matches below.", patternMatchesForFile.First().File); int count = 0; Log.IndentedCollection(patternMatchesForFile, p => $"{p.File} (from {p.SearchPath}) {(count++ == 0 ? "*" : "")}", Log.Warn); currentMatches = currentMatches.Except(patternMatchesForFile.Skip(1)).ToList(); } } searchPathMatches[currentSearchPath] = currentMatches; #endregion } #region validation IEnumerable <PatternMatch> allMatches = searchPathMatches.Values.SelectMany(v => v); IEnumerable <PatternMatch> finalMatches = ValidateMatches(allMatches, includePaths); #endregion #region results HashSet <string> allMatchedFiles = finalMatches.Select(m => GetPath(returnAbsolutePaths, m.File, absBasePath)).ToHashSet(); Log.Debug("include globs:"); Log.IndentedCollection(includeGlobs, s => s.Pattern, Log.Debug); Log.Debug("include regexes:"); Log.IndentedCollection(includeRegexes, r => r.Value, Log.Debug); Log.Debug("include literals:"); Log.IndentedCollection(includeFiles, Log.Debug); Log.Debug("exclude globs:"); Log.IndentedCollection(excludeGlobs, s => s.Pattern, Log.Debug); Log.Debug("exclude regexes:"); Log.IndentedCollection(excludeRegexes, r => r.Value, Log.Debug); Log.Debug("exclude literals:"); Log.IndentedCollection(excludeFiles, Log.Debug); Log.Debug("matched files:"); Log.IndentedCollection(allMatchedFiles, Log.Debug); cache?.CacheResults(allMatchedFiles, cacheRequest.hash); return(allMatchedFiles); #endregion } }