using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using SabreTools.Collections.Extensions; using SabreTools.Text.Compare; namespace SabreTools.IO.Extensions { /// /// Methods around path operations /// public static class IOExtensions { #region Directory /// /// Ensure the output directory is a proper format and can be created /// /// Directory to check /// True if the directory should be created, false otherwise (default) /// Full path to the directory public static string EnsureDirectory(this string? dir, bool create = false) { // If the output directory is invalid if (string.IsNullOrEmpty(dir)) dir = PathTool.GetRuntimeDirectory(); // Get the full path for the output directory dir = Path.GetFullPath(dir!.Trim('"')); // If we're creating the output folder, do so if (create && !Directory.Exists(dir)) Directory.CreateDirectory(dir); return dir; } /// /// Retrieve a list of just directories from inputs /// /// List of strings representing directories and files /// True if the parent name should be included in the ParentablePath, false otherwise (default) /// List of strings representing just directories from the inputs public static List GetDirectoriesOnly(List inputs, bool appendParent = false) { var outputs = new List(); for (int i = 0; i < inputs.Count; i++) { string input = inputs[i]; // If we have a null or empty path if (string.IsNullOrEmpty(input)) continue; // If we have a wildcard string pattern = "*"; if (input.Contains("*") || input.Contains("?")) { pattern = Path.GetFileName(input); input = input.Substring(0, input.Length - pattern.Length); } // Get the parent path in case of appending string parentPath = Path.GetFullPath(input); if (Directory.Exists(input)) { List directories = input.GetDirectoriesOrdered(pattern); foreach (string dir in directories) { outputs.Add(new ParentablePath(Path.GetFullPath(dir), appendParent ? parentPath : string.Empty)); } } } return outputs; } /// /// Retrieve a list of directories from a directory recursively in proper order /// /// Directory to parse /// Optional pattern to search for directory names /// List with all new files public static List GetDirectoriesOrdered(this string dir, string pattern = "*") => GetDirectoriesOrderedHelper(dir, [], pattern); /// /// Retrieve a list of directories from a directory recursively in proper order /// /// Directory to parse /// List representing existing files /// Optional pattern to search for directory names /// List with all new files private static List GetDirectoriesOrderedHelper(string dir, List infiles, string pattern) { // Take care of the files in the top directory List toadd = [.. dir.SafeEnumerateDirectories(pattern, SearchOption.TopDirectoryOnly)]; toadd.Sort(new NaturalComparer()); infiles.AddRange(toadd); // Then recurse through and add from the directories foreach (string subDir in toadd) { infiles = GetDirectoriesOrderedHelper(subDir, infiles, pattern); } // Return the new list return infiles; } /// /// Get all empty folders within a root folder /// /// Root directory to parse /// IEumerable containing all directories that are empty, an empty enumerable if the root is empty, null otherwise public static List? ListEmpty(this string? root) { // Check null or empty first if (root is null) return null; // Then, check if the root exists if (!Directory.Exists(root)) return null; // Otherwise, get the complete list var empty = new List(); foreach (var dir in SafeGetDirectories(root, "*", SearchOption.AllDirectories)) { if (SafeGetFiles(dir).Length == 0) empty.Add(dir); } return empty; } #endregion #region File /// /// Concatenate all files in the order provided, if possible /// /// List of paths to combine /// Path to the output file /// True if the files were concatenated successfully, false otherwise public static bool Concatenate(List paths, string output) { // If the path list is empty if (paths.Count == 0) return false; // If the output filename is invalid if (string.IsNullOrEmpty(output)) return false; try { // Try to build the new output file using var ofs = File.Open(output, FileMode.Create, FileAccess.Write, FileShare.None); for (int i = 0; i < paths.Count; i++) { // Get the next file string next = paths[i]; if (!File.Exists(next)) break; // Copy the next input to the output using var ifs = File.Open(next, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); ifs.BlockCopy(ofs, blockSize: 3 * 1024 * 1024); } return true; } catch { // Absorb the exception right now return false; } } /// /// Determines a text file's encoding by analyzing its byte order mark (BOM). /// Defaults to ASCII when detection of the text file's endianness fails. /// /// The text file to analyze. /// The detected encoding. /// http://stackoverflow.com/questions/3825390/effective-way-to-find-any-files-encoding public static Encoding GetEncoding(this string filename) { if (string.IsNullOrEmpty(filename)) return Encoding.Default; if (!File.Exists(filename)) return Encoding.Default; // Try to open the file try { var file = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); if (file is null) return Encoding.Default; // Read the BOM var bom = new byte[4]; int read = file.Read(bom, 0, 4); file.Dispose(); // Disable warning about UTF7 usage #if NET5_0_OR_GREATER #pragma warning disable SYSLIB0001 #endif // Analyze the BOM if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) return Encoding.UTF7; if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) return Encoding.UTF8; if (bom[0] == 0xff && bom[1] == 0xfe) return Encoding.Unicode; //UTF-16LE if (bom[0] == 0xfe && bom[1] == 0xff) return Encoding.BigEndianUnicode; //UTF-16BE if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) return Encoding.UTF32; return Encoding.Default; // Restore warning about UTF7 usage #if NET5_0_OR_GREATER #pragma warning restore SYSLIB0001 #endif } catch { return Encoding.Default; } } /// /// Retrieve a list of just files from inputs /// /// List of strings representing directories and files /// True if the parent name should be be included in the ParentablePath, false otherwise (default) /// List of strings representing just files from the inputs public static List GetFilesOnly(List inputs, bool appendParent = false) { var outputs = new List(); for (int i = 0; i < inputs.Count; i++) { string input = inputs[i].Trim('"'); // If we have a null or empty path if (string.IsNullOrEmpty(input)) continue; // If we have a wildcard string pattern = "*"; if (input.Contains("*") || input.Contains("?")) { pattern = Path.GetFileName(input); input = input.Substring(0, input.Length - pattern.Length); } // Get the parent path in case of appending string parentPath = Path.GetFullPath(input); if (Directory.Exists(input)) { List files = input.GetFilesOrdered(pattern); foreach (string file in files) { outputs.Add(new ParentablePath(Path.GetFullPath(file), appendParent ? parentPath : string.Empty)); } } else if (File.Exists(input)) { outputs.Add(new ParentablePath(Path.GetFullPath(input), appendParent ? parentPath : string.Empty)); } } return outputs; } /// /// Retrieve a list of files from a directory recursively in proper order /// /// Directory to parse /// Optional pattern to search for directory names /// List with all new files public static List GetFilesOrdered(this string dir, string pattern = "*") => GetFilesOrderedHelper(dir, [], pattern); /// /// Retrieve a list of files from a directory recursively in proper order /// /// Directory to parse /// List representing existing files /// Optional pattern to search for directory names /// List with all new files private static List GetFilesOrderedHelper(string dir, List infiles, string pattern) { // Take care of the files in the top directory List toadd = [.. dir.SafeEnumerateFiles(pattern, SearchOption.TopDirectoryOnly)]; toadd.Sort(new NaturalComparer()); infiles.AddRange(toadd); // Then recurse through and add from the directories List subDirs = [.. dir.SafeEnumerateDirectories(pattern, SearchOption.TopDirectoryOnly)]; subDirs.Sort(new NaturalComparer()); foreach (string subdir in subDirs) { infiles = GetFilesOrderedHelper(subdir, infiles, pattern); } // Return the new list return infiles; } /// /// Helper to get the filesize from a path /// /// Size of the file path, -1 on error public static long GetFileSize(this string? filename) { // Invalid filenames are ignored if (string.IsNullOrEmpty(filename)) return -1; // Non-file paths are ignored if (!File.Exists(filename)) return -1; try { return new FileInfo(filename).Length; } catch { // Ignore errors return -1; } } /// /// Get the extension from the path, if possible /// /// Path to get extension from /// Extension, if possible public static string? GetNormalizedExtension(this string? path) { // Check null or empty first if (string.IsNullOrEmpty(path)) return null; // Get the extension from the path, if possible string? ext = Path.GetExtension(path)?.ToLowerInvariant(); // Check if the extension is null or empty if (string.IsNullOrEmpty(ext)) return null; // Make sure that extensions are valid ext = ext!.TrimStart('.'); return ext; } /// public static string NormalizeFilePath(this string? path) => path.NormalizeFilePath(fullPath: true); /// /// Normalize a file path /// /// Path value to normalize /// Indicates if the full path is used /// The normalized path on success, an empty path if null, or the original path on error /// /// This method performs the following steps: /// - Remove quotes and angle brackets from the start and end /// - Replaces invalid path characters with '_' /// - Remove spaces before and after directory separators /// public static string NormalizeFilePath(this string? path, bool fullPath) { try { // If we have an invalid path if (string.IsNullOrEmpty(path)) return string.Empty; // Remove quotes and angle brackets from path path = path!.Trim('\"'); path = path!.Trim('<'); path = path!.Trim('>'); // Remove invalid path characters foreach (char c in Path.GetInvalidPathChars()) { path = path.Replace(c, '_'); } // Try getting the combined path and returning that directly string usablePath = fullPath ? Path.GetFullPath(path) : path; var fullDirectory = Path.GetDirectoryName(usablePath)?.Trim(); string fullFile = Path.GetFileName(usablePath).Trim(); // Remove invalid filename characters foreach (char c in Path.GetInvalidFileNameChars()) { fullFile = fullFile.Replace(c, '_'); } // Rebuild the path, if necessary if (!string.IsNullOrEmpty(fullDirectory)) fullFile = Path.Combine(fullDirectory, fullFile); // Remove spaces before and after separators return Regex.Replace(fullFile, @"\s*([\\|/])\s*", @"$1"); } catch { } return path ?? string.Empty; } /// /// Resolve a file path that may be absolute, relative, /// within the runtime directory, or contained within a PATH directory. /// /// Raw value from the user's options /// The absolute path of the located files, or null on failure public static string? ResolvePath(this string? path) { // Invalid paths always return null if (string.IsNullOrEmpty(path)) return null; // If the configured path starts with a home character if (path!.StartsWith("~/") || path.StartsWith("~\\")) { string homeDirectory = PathTool.GetHomeDirectory(); path = path.Substring(2); path = Path.Combine(homeDirectory, path); return File.Exists(path) ? Path.GetFullPath(path) : null; } // Explicit location (absolute or relative path) if (path.Contains("/") || path.Contains("\\")) return File.Exists(path) ? Path.GetFullPath(path) : null; // Check the runtime directory if no directory path is provided string runtimeDir = PathTool.GetRuntimeDirectory(); if (!string.IsNullOrEmpty(runtimeDir)) { string candidate = Path.Combine(runtimeDir, path); if (File.Exists(candidate)) return candidate; } // Attempt to get the PATH variable for searching string? pathEnv = Environment.GetEnvironmentVariable("PATH"); if (string.IsNullOrEmpty(pathEnv)) return null; // Loop through all entries in PATH var pathParts = pathEnv!.Split(Path.PathSeparator); foreach (string dir in pathParts) { if (string.IsNullOrEmpty(dir)) continue; string candidate = Path.Combine(dir, path); if (File.Exists(candidate)) return candidate; } // All options failed return null; } #endregion #region Safe Directory Enumeration /// /// Returns an empty enumerable on any exception public static string[] SafeGetDirectories(this string path) { try { return Directory.GetDirectories(path); } catch { return []; } } /// /// Returns an empty enumerable on any exception public static string[] SafeGetDirectories(this string path, string searchPattern) { try { return Directory.GetDirectories(path, searchPattern); } catch { return []; } } /// /// Returns an empty enumerable on any exception public static string[] SafeGetDirectories(this string path, string searchPattern, SearchOption searchOption) { try { // Outside of Windows, linked directories are not followed if (SkipLinkedDirectories(searchPattern, searchOption)) return [.. WalkWithoutLinkedDirectories(path, searchPattern, Directory.GetDirectories, Directory.GetDirectories)]; return Directory.GetDirectories(path, searchPattern, searchOption); } catch { return []; } } /// /// Returns an empty enumerable on any exception public static string[] SafeGetFiles(this string path) { try { return Directory.GetFiles(path); } catch { return []; } } /// /// Returns an empty enumerable on any exception public static string[] SafeGetFiles(this string path, string searchPattern) { try { return Directory.GetFiles(path, searchPattern); } catch { return []; } } /// /// Returns an empty enumerable on any exception public static string[] SafeGetFiles(this string path, string searchPattern, SearchOption searchOption) { try { // Outside of Windows, linked directories are not followed if (SkipLinkedDirectories(searchPattern, searchOption)) return [.. WalkWithoutLinkedDirectories(path, searchPattern, Directory.GetFiles, Directory.GetDirectories)]; return Directory.GetFiles(path, searchPattern, searchOption); } catch { return []; } } /// /// Returns an empty enumerable on any exception public static string[] SafeGetFileSystemEntries(this string path) { try { return Directory.GetFileSystemEntries(path); } catch { return []; } } /// /// Returns an empty enumerable on any exception public static string[] SafeGetFileSystemEntries(this string path, string searchPattern) { try { return Directory.GetFileSystemEntries(path, searchPattern); } catch { return []; } } /// /// Returns an empty enumerable on any exception public static IEnumerable SafeGetFileSystemEntries(this string path, string searchPattern, SearchOption searchOption) { try { var enumerable = Directory.GetFileSystemEntries(path, searchPattern); return enumerable.SafeEnumerate(); } catch { return []; } } #if NET20 || NET35 /// /// Calls implementation public static IEnumerable SafeEnumerateDirectories(this string path) => path.SafeGetDirectories(); /// /// Calls implementation public static IEnumerable SafeEnumerateDirectories(this string path, string searchPattern) => path.SafeGetDirectories(searchPattern); /// /// Calls implementation public static IEnumerable SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption) => path.SafeGetDirectories(searchPattern, searchOption); /// /// Calls implementation public static IEnumerable SafeEnumerateFiles(this string path) => path.SafeGetFiles(); /// /// Calls implementation public static IEnumerable SafeEnumerateFiles(this string path, string searchPattern) => path.SafeGetFiles(searchPattern); /// /// Calls implementation public static IEnumerable SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption) => path.SafeGetFiles(searchPattern, searchOption); /// /// Calls implementation public static IEnumerable SafeEnumerateFileSystemEntries(this string path) => path.SafeGetFileSystemEntries(); /// /// Calls implementation public static IEnumerable SafeEnumerateFileSystemEntries(this string path, string searchPattern) => path.SafeGetFileSystemEntries(searchPattern); /// /// Calls implementation public static IEnumerable SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption) => path.SafeGetFileSystemEntries(searchPattern, searchOption); #elif NET40_OR_GREATER || NETSTANDARD2_0 /// public static IEnumerable SafeEnumerateDirectories(this string path) { try { var enumerable = Directory.EnumerateDirectories(path); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateDirectories(this string path, string searchPattern) { try { var enumerable = Directory.EnumerateDirectories(path, searchPattern); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption) { try { // Outside of Windows, linked directories are not followed if (SkipLinkedDirectories(searchPattern, searchOption)) return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateDirectories, SafeEnumerateDirectories); var enumerable = Directory.EnumerateDirectories(path, searchPattern, searchOption); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFiles(this string path) { try { var enumerable = Directory.EnumerateFiles(path); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFiles(this string path, string searchPattern) { try { var enumerable = Directory.EnumerateFiles(path, searchPattern); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption) { try { // Outside of Windows, linked directories are not followed if (SkipLinkedDirectories(searchPattern, searchOption)) return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateFiles, SafeEnumerateDirectories); var enumerable = Directory.EnumerateFiles(path, searchPattern, searchOption); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFileSystemEntries(this string path) { try { var enumerable = Directory.EnumerateFileSystemEntries(path); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFileSystemEntries(this string path, string searchPattern) { try { var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption) { try { // Outside of Windows, linked directories are not followed if (SkipLinkedDirectories(searchPattern, searchOption)) return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateFileSystemEntries, SafeEnumerateDirectories); var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, searchOption); return enumerable.SafeEnumerate(); } catch { return []; } } #else /// public static IEnumerable SafeEnumerateDirectories(this string path) { try { string searchPattern = "*"; SearchOption searchOption = SearchOption.TopDirectoryOnly; var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateDirectories(this string path, string searchPattern) { try { SearchOption searchOption = SearchOption.TopDirectoryOnly; var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption) { try { // Outside of Windows, linked directories are not followed if (SkipLinkedDirectories(searchPattern, searchOption)) return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateDirectories, SafeEnumerateDirectories); var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFiles(this string path) { try { string searchPattern = "*"; SearchOption searchOption = SearchOption.TopDirectoryOnly; var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFiles(this string path, string searchPattern) { try { SearchOption searchOption = SearchOption.TopDirectoryOnly; var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption) { try { // Outside of Windows, linked directories are not followed if (SkipLinkedDirectories(searchPattern, searchOption)) return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateFiles, SafeEnumerateDirectories); var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFileSystemEntries(this string path) { try { string searchPattern = "*"; SearchOption searchOption = SearchOption.TopDirectoryOnly; var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFileSystemEntries(this string path, string searchPattern) { try { SearchOption searchOption = SearchOption.TopDirectoryOnly; var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// public static IEnumerable SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption) { try { // Outside of Windows, linked directories are not followed if (SkipLinkedDirectories(searchPattern, searchOption)) return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateFileSystemEntries, SafeEnumerateDirectories); var enumerationOptions = FromSearchOption(searchOption); enumerationOptions.IgnoreInaccessible = true; var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions); return enumerable.SafeEnumerate(); } catch { return []; } } /// Initializes a new instance of the class with the recommended default options. /// private static EnumerationOptions FromSearchOption(SearchOption searchOption) { if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) throw new ArgumentOutOfRangeException(nameof(searchOption)); return searchOption == SearchOption.AllDirectories ? new EnumerationOptions { RecurseSubdirectories = true, MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false } : new EnumerationOptions { MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false }; } #endif /// /// Lists the entries of one directory that match a pattern /// private delegate IEnumerable DirectoryListing(string directory, string searchPattern); /// /// Indicates if a path is a linked directory /// /// Path to check /// True if the path is a directory that carries FileAttributes.ReparsePoint, false otherwise /// /// Returns false if the directory cannot be read, such as below a directory without /// execute permission, so the directory is entered and fails like in the recursive search /// private static bool IsLinkedDirectory(string path) { // Exists returns false instead of throwing if the directory cannot be read var info = new DirectoryInfo(path); if (!info.Exists) return false; #if NET20 || NET35 return (info.Attributes & FileAttributes.ReparsePoint) != 0; #else return info.Attributes.HasFlag(FileAttributes.ReparsePoint); #endif } /// /// Indicates if a search walks the directories itself to skip linked directories /// /// Pattern to match the entry names against /// Whether to search the top directory only or all subdirectories /// True if the walk is used, false if the recursive search is used private static bool SkipLinkedDirectories(string searchPattern, SearchOption searchOption) { // Only a search of all subdirectories enters subdirectories if (searchOption != SearchOption.AllDirectories) return false; // Linked directories on discs are only read as links outside of Windows if (SystemEnvironment.IsWindows()) return false; // A directory part of the pattern is matched once from the top directory, but the walk would match it in each directory if (!string.IsNullOrEmpty(Path.GetDirectoryName(searchPattern))) return false; return true; } /// /// Walk a directory and all subdirectories without following linked directories /// /// Directory to search /// Pattern to match the entry names against /// Lists the matching entries of one directory /// Lists the subdirectories of one directory /// The same entries in the same order as the recursive search, minus those under linked directories /// /// A linked directory that points back into the tree, such as a UDF symlink whose /// target reads as empty on Linux, makes the recursive search run without end /// private static IEnumerable WalkWithoutLinkedDirectories(string path, string searchPattern, DirectoryListing getEntries, DirectoryListing getDirectories) { var pending = new Queue(); pending.Enqueue(path); while (pending.Count > 0) { string directory = pending.Dequeue(); foreach (string entry in getEntries(directory, searchPattern)) { yield return entry; } foreach (string subdirectory in getDirectories(directory, "*")) { if (!IsLinkedDirectory(subdirectory)) pending.Enqueue(subdirectory); } } } #endregion } }