Processing symbolic links and junctions safely in PowerShell

PowerShell's Get-ChildItem -Recurse will recurse into symbolic links and junctions, unless prohibited by ACLs. This can easily result in pseudo-infinite loops where PowerShell returns paths such as C:\Users\All Users\Application Data\Application Data\Application Data. The C:\Users directory is particularly prone to such problems, as it contains both a symbolic link and a junction:

C:\>dir c:\Users /a
 Volume in drive C has no label.
 Volume Serial Number is 787B-CFFB

 Directory of c:\Users

2019-03-14  14:26    <DIR>          .
2019-03-14  14:26    <DIR>          ..
2019-03-12  00:41    <DIR>          Administrator
2018-04-12  00:45    <SYMLINKD>     All Users [C:\ProgramData]
2019-01-09  03:50    <DIR>          Default
2018-04-12  00:45    <JUNCTION>     Default User [C:\Users\Default]
2018-09-24  00:16    <DIR>          Public
2018-04-12  00:36               174 desktop.ini
               1 File(s)            174 bytes
               8 Dir(s)  390.996.451.328 bytes free

The following function Find-Files – both in name and behavior inspired by the Unix find program – avoids this problem as it does not recurse into symbolic links or junctions:

function Find-Files
{
    <#
    .SYNOPSIS

    Lists the contents of a directory. Unlike Get-ChildItem, this function does not recurse into symbolic links or junctions in order to avoid infinite loops.

    #>

    param (
        [Parameter( Mandatory=$false )]
        [string]
        # Specifies the path to the directory whose contents are to be listed. By default, the current working directory is used.
        $LiteralPath = (Get-Location),

        [Parameter( Mandatory=$false )]
        # Specifies a filter that is applied to each file or directory. Wildcards ? and * are supported.
        $Filter,

        [Parameter( Mandatory=$false )]
        [boolean]
        # Specifies if file objects should be returned. By default, all file system objects are returned.
        $File = $true,

        [Parameter( Mandatory=$false )]
        [boolean]
        # Specifies if directory objects should be returned. By default, all file system objects are returned.
        $Directory = $true,

        [Parameter( Mandatory=$false )]
        [boolean]
        # Specifies if reparse point objects should be returned. By default, all file system objects are returned.
        $ReparsePoint = $true,

        [Parameter( Mandatory=$false )]
        [boolean]
        # Specifies if the top directory should be returned. By default, all file system objects are returned.
        $Self = $true
    )

    function Enumerate( [System.IO.FileSystemInfo] $Item ) {
        $Item;
        if ( $Item.GetType() -eq [System.IO.DirectoryInfo] -and ! $Item.Attributes.HasFlag( [System.IO.FileAttributes]::ReparsePoint ) ) {
            foreach ($ChildItem in $Item.EnumerateFileSystemInfos() ) {
                Enumerate $ChildItem;
            }
        }
    }

    function FilterByName {
        process {
            if ( ( $Filter -eq $null ) -or ( $_.Name -ilike $Filter ) ) {
                $_;
            }
        }
    }

    function FilterByType {
        process {
            if ( $_.GetType() -eq [System.IO.FileInfo] ) {
                if ( $File ) { $_; }
            } elseif ( $_.Attributes.HasFlag( [System.IO.FileAttributes]::ReparsePoint ) ) {
                if ( $ReparsePoint ) { $_; }
            } else {
                if ( $Directory ) { $_; }
            }
        }
    }
    
    $Skip = if ($Self) { 0 } else { 1 };
    Enumerate ( Get-Item -LiteralPath $LiteralPath -Force ) | Select-Object -Skip $Skip | FilterByName | FilterByType;
}

Download Find-Files.ps1