Functions/Find-GlobFile.ps1


function Find-GlobFile
{
    <#
    .SYNOPSIS
    Searches for files using advanced wildcard/glob syntax.
 
    .DESCRIPTION
    The `Find-GlobFile` function searches directories for files using `*` and `**` wildcard/glob patterns. Pass the top-level directories to search to the `Path` parameter. Only files under these directories will be returned. By default, all files are returned (i.e. the function uses `**/*` as the pattern).
 
    Pass glob/wildcard patterns to the `Include` pattern. Only files that match that pattern will be included. To exclude files, pass glob/wildcard patterns to the `Exclude` parameter. Supported patterns are:
 
    * `*`: matches zero or more characters in a directory or file name *except* the directory separator character
    * `**`: matches zero or more characters in a directory or file name's path, i.e. it matches the directory separator character
    * `?`: match exactly one character
    * `[abc]`: match one of the characters inside the brackets
    * `[a-z]`: matches one character from the range inside the brackets
    * `[!abc]`: matches any one character *not* inside the brackets
    * `[!a-z]`: matches one character that is *not* in the range defined in the brackets
 
    By default, the search is case-insensitive. To peform a case-sensitive search, use the `CaseSensitive` switch.
 
    The `Find-GlobFile` function uses the [DotNet.Glob library](https://www.nuget.org/packages/DotNet.Glob).
 
    .EXAMPLE
    Find-GlobFile -Path 'dir1','dir2'
 
    Returns all files under `dir`` and `dir2` in the current directory.
 
    .EXAMPLE
    Find-GlobFile -Path 'dir1' -Include '*.ps1'
 
    Returns all `*.ps1` files in the `dir1` directory.
 
    .EXAMPLE
    Find-GlobFile -Path '.' -Include '**/*.ps1'
 
    Returns all `*.ps1` files under the current directory and all its sub-directories.
 
    .EXAMPLE
    Find-GlobFile -Path '.' -Include '**/*.ps1' -Exclude '**/*.Tests.ps1'
 
    Returns all `*.ps1` files except files that match `*.Tests.ps1` under the current directory and all its sub-directories.
 
    .EXAMPLE
    Find-GlobFile -Path '.' -Include 'Find-GlobFile.ps1' -CaseSensitive
 
    Demonstrates how to do a case-sensitive search.
    #>

    [CmdletBinding()]
    [OutputType([IO.FileInfo])]
    param(
        [Parameter(Mandatory)]
        [string[]]
        # The directories to search. Relative paths are evaluated from the current directory.
        $Path,

        [string[]]
        # The files to include. By default all files in and under each directory in `Path` are returned.
        $Include = '**/*',

        [string[]]
        # Any files to exclude. By default, no files are excluded. Any file that gets included that matches an exclude pattern is not returned.
        $Exclude,

        [Switch]
        # By default, the search is case-insensitive. To perform a case-sensitive search, use this switch.
        $CaseSensitive
    )

    Set-StrictMode -Version 'Latest'

    foreach( $rootPath in $Path )
    {
        $rootPath = Resolve-Path -Path $rootPath | Select-Object -ExpandProperty 'ProviderPath'
        if( -not $rootPath )
        {
            continue
        }

        $options = New-Object 'DotNet.Globbing.GlobOptions'
        $options.Evaluation.CaseInsensitive = -not $CaseSensitive

        $includeGlobs = $Include | Where-Object { $_ } | ForEach-Object { [DotNet.Globbing.Glob]::Parse($_,$options) }
        $excludeGlobs = $Exclude | Where-Object { $_ } | ForEach-Object { [DotNet.Globbing.Glob]::Parse($_,$options) }

        Push-Location -Path $rootPath
        try
        {
            foreach( $fileInfo in (Get-ChildItem -Path $rootPath -File -Recurse) )
            {
                $relativePath = $fileInfo.FullName | Resolve-Path -Relative | ForEach-Object { $_ -replace '^\.(\\|//)','' }
                $matches = $false
                foreach( $includeGlob in $includeGlobs )
                {
                    if( $includeGlob.IsMatch($relativePath) )
                    {
                        $matches = $true
                        break
                    }
                }

                if( -not $matches )
                {
                    continue
                }

                foreach( $excludeGlob in $excludeGlobs )
                {
                    if( $excludeGlob.IsMatch($relativePath) )
                    {
                        $matches = $false
                        break
                    }
                }

                if( $matches )
                {
                    $fileInfo
                }
            }
        }
        finally
        {
            Pop-Location
        }
    }
}