File System File System
File System File System
DocFX + Singulink = ♥

Search Results for

    Searching and Enumeration

    Overview

    Every absolute directory exposes a complete set of enumeration methods (twelve in total) that fall into a clean grid:

    Files Directories Entries (either)
    Cached info GetChildFilesInfo GetChildDirectoriesInfo GetChildEntriesInfo
    Absolute paths GetChildFiles GetChildDirectories GetChildEntries
    Relative to this directory GetRelativeChildFiles GetRelativeChildDirectories GetRelativeChildEntries
    Relative to a sub-location GetRelativeFiles GetRelativeDirectories GetRelativeEntries

    All return IEnumerable<T> and stream lazily; iteration touches the file system as you go.

    Picking the Right Method

    Decide along two axes:

    1. What do you want back?
      • Just paths → Get* (returns IAbsolute*Path or IRelative*Path).
      • Paths plus metadata in a single call → Get*Info (returns CachedFileInfo / CachedDirectoryInfo).
    2. What kind of entries?
      • Files only / directories only / either: pick the matching variant.

    Use the Info variants when you'll read attributes, sizes or timestamps from each result; they save a stat per entry compared to calling GetInfo() afterwards.

    // Just paths:
    foreach (IAbsoluteFilePath f in dir.GetChildFiles("*.cs"))
        Console.WriteLine(f.PathDisplay);
    
    // Paths + metadata in one call:
    foreach (CachedFileInfo info in dir.GetChildFilesInfo("*.cs"))
        Console.WriteLine($"{info.Path.Name}  {info.Length}  {info.LastWriteTimeUtc:O}");
    

    Search Patterns

    The optional searchPattern argument supports two wildcards:

    • *: matches any sequence of characters (including empty).
    • ?: matches any single character.

    The default pattern (when omitted) is "*", which matches everything.

    dir.GetChildFiles();              // all files
    dir.GetChildFiles("*.json");      // matches "config.json", "users.json"
    dir.GetChildFiles("data?.bin");   // matches "data1.bin" but not "data10.bin"
    
    Note

    Search patterns are matched against the entry name, not the full path. Recursive searches still apply the pattern to each entry's name as they descend.

    SearchOptions

    Tune behavior with SearchOptions. All properties are optional and have sensible defaults:

    Property Default Purpose
    Recursive false Descend into subdirectories.
    MaxRecursionDepth int.MaxValue Cap recursion depth (only meaningful when Recursive is true).
    MatchCasing CaseInsensitive Filename matching mode.
    AttributesToSkip None Skip entries with any of the specified FileAttributes.
    BufferSize 0 (default) Suggested OS buffer size.
    InaccessibleSearchBehavior ThrowForSearchDir How to handle inaccessible directories; see below.
    Tip

    The MatchCasing default is case-insensitive: the same on Windows and Unix. System.IO's default differs by platform, which leads to platform-specific bugs that this library deliberately avoids.

    var opts = new SearchOptions
    {
        Recursive = true,
        MaxRecursionDepth = 3,
        AttributesToSkip = FileAttributes.Hidden | FileAttributes.System,
    };
    
    foreach (IAbsoluteFilePath cs in repoRoot.GetChildFiles("*.cs", opts))
        Process(cs);
    

    Inaccessible Directories

    When recursive searches encounter a directory the process can't read, you have three behaviors via InaccessibleSearchBehavior:

    Value Behavior
    ThrowForSearchDir (default) Throw if the directory you started searching is inaccessible; silently skip inaccessible nested directories.
    ThrowForAll Throw the moment any inaccessible directory is encountered.
    IgnoreAll Skip everything inaccessible without complaint.
    var resilient = new SearchOptions
    {
        Recursive = true,
        InaccessibleSearchBehavior = InaccessibleSearchBehavior.IgnoreAll,
    };
    
    long total = profile.GetChildFiles("*", resilient).Sum(f => f.Length);
    
    Note

    ThrowForSearchDir is the friendliest default: you reliably get notified if the path you handed in is unreadable, but missing permissions deep in the tree don't blow up an otherwise-valid search. While this is the least surprising behavior, it does incur an extra file system call for searches that yield no matches. This should not be an issue for the vast majority of applications, but is worth noting.

    When a search throws because of inaccessibility, the exception is UnauthorizedIOAccessException. See Exception Handling.

    Relative Enumeration

    The GetRelative* methods return relative paths instead of absolute ones, useful when you'll move/copy/store the results relative to the search root:

    IAbsoluteDirectoryPath src = appBase.CombineDirectory("source");
    IAbsoluteDirectoryPath dst = appBase.CombineDirectory("dist");
    
    var opts = new SearchOptions { Recursive = true };
    foreach (IRelativeFilePath rel in src.GetRelativeChildFiles("*", opts))
    {
        IAbsoluteFilePath target = dst + rel;
        target.ParentDirectory.Create();
        (src + rel).CopyTo(target, overwrite: true);
    }
    

    GetRelative* with a Search Location

    GetRelativeFiles(searchLocation, ...) (and the directory/entry variants) start the search inside a searchLocation that is relative to the outer directory, while keeping every returned path relative to that outer directory.

    When searchLocation points to a sub-directory of the outer directory, the behavior is straightforward:

    // outerDir = /app
    IRelativeDirectoryPath sub = DirectoryPath.ParseRelative("config/users");
    
    foreach (IRelativeFilePath f in outerDir.GetRelativeFiles(sub, "*.json"))
    {
        // Search runs inside /app/config/users.
        // f is relative to /app, e.g. "config/users/admin.json".
    }
    
    Searching in a Parent Directory

    searchLocation may also point upward via .. segments. The search then runs outside of the outer directory, but results are still expressed relative to it. Because some matches may live inside the same subtree the outer directory is in, the library applies the following rule when constructing each returned path:

    • For a match whose absolute path is inside the outer directory, the upward navigation cancels out and the result is a clean forward-only relative path.
    • For a match whose absolute path is not inside the outer directory, the result is a relative path that begins with as many .. segments as needed to walk back out to the search location.

    Example: searching the parent directory of /repo/src with searchLocation = "..":

    // outerDir = /repo/src
    IRelativeDirectoryPath up = DirectoryPath.ParseRelative("..");
    
    foreach (IRelativeFilePath f in outerDir.GetRelativeFiles(up, "*.md", new SearchOptions { Recursive = true }))
    {
        // Search runs inside /repo (the parent of /repo/src).
        //
        // /repo/README.md        -> "../README.md"        (sibling of outerDir)
        // /repo/docs/intro.md    -> "../docs/intro.md"    (sibling subtree)
        // /repo/src/notes.md     -> "notes.md"            (inside outerDir)
        // /repo/src/api/x.md     -> "api/x.md"            (inside outerDir)
    }
    

    The same rule scales to deeper navigation (../.., ../../..) and to a rooted search location: searchLocation rooted to the file system root means the search runs from the root, and each result is given as many leading .. segments as required to reach the outer directory.

    Tip

    Use this overload when you have a stable "anchor" directory (your outer directory) but the actual search location is computed at runtime. Every result you store, log or compare is anchored against the same reference point regardless of where the search ran.

    Lazy Iteration and Exceptions

    Enumeration is lazy. The file system call that produces the next batch of entries can throw at any iteration step, typically IOException, DirectoryNotFoundException or UnauthorizedIOAccessException. Wrap the foreach (or materialization with .ToList()) in a try/catch if you need to handle errors:

    try
    {
        foreach (var f in dir.GetChildFiles("*", new SearchOptions { Recursive = true }))
            Process(f);
    }
    catch (UnauthorizedIOAccessException ex)
    {
        log.Warn($"Search interrupted: {ex.Message}");
    }
    

    Next Steps

    • Cached Entry Info: make the most of the *Info variants.
    • Exception Handling: handle search-time errors cleanly.
    © Singulink. All rights reserved.