Glob.psm1


$functionDir = Join-Path -Path $PSScriptRoot -ChildPath 'Functions'
if( (Test-Path -Path $functionDir) )
{
    foreach( $item in (Get-ChildItem -Path $functionDir -Filter '*.ps1') )
    {
        . $item.FullName
    }
}



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.
 
    By default, hidden files and directories are not searched. To include hidden files, use the `Force` switch.
 
    To troubleshoot which files `Find-GlobFile` is including/excluding/finding, set your `$DebugPreference` to `Continue`. You'll see three columns of output:
     
    1. Either empty to indicate a pattern included a file, or contains an exclamation mark, "!", to indicate a file was excluded by a pattern.
    2. The pattern.
    3. The file that matched the pattern.
 
    Here's an example:
 
        DEBUG: **/* file.txt
        DEBUG: ! **/*.orig file.txt.orig
 
    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.
 
    .EXAMPLE
    Find-GlobFile -Path '.' -Include '**/*.txt' -Force
 
    Demonstrates how to search hidden files and directories by using the `Force`.
    #>

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

        # The files to include. By default all files in and under each directory in `Path` are returned.
        [String[]]$Include = ('**{0}*' -f [IO.Path]::DirectorySeparatorChar),

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

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

        # Include hidden files, too.
        [switch]$Force
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    $errorActionParam = @{ }
    if( $ErrorActionPreference -eq [Management.Automation.ActionPreference]::Ignore )
    {
        $errorActionParam['ErrorAction'] = [Management.Automation.ActionPreference]::Ignore
    }

    function Test-GlobMatch
    {
        param(
            [object]$InputObject,

            [switch]$IsDirectory
        )

        $relativePath = Resolve-Path -LiteralPath $InputObject.FullName -Relative @errorActionParam
        if( -not $relativePath )
        {
            return $false
        }

        if( $relativePath.Length -ge 2 -and `
            $relativePath[0] -eq '.' -and `
            ($relativePath[1] -eq [IO.Path]::DirectorySeparatorChar -or `
            $relativePath[1] -eq [IO.Path]::AltDirectorySeparatorChar))
        {
            # Remove the .\ or ./ at the beginning of the path, as the glob library doesn't like it.
            $relativePath = $relativePath.Substring(2)
        }
        
        $result = ' '
        $whatMatched = ''
        $showMessage = -not $IsDirectory
        try
        {
            if( -not $IsDirectory )
            {
                $isIncluded = $false
                foreach( $includeGlob in $includeGlobs )
                {
                    if( $includeGlob.IsMatch($relativePath) )
                    {
                        $whatMatched = $includeGlob
                        $isIncluded = $true
                        break
                    }
                }

                if( -not $isIncluded )
                {
                    return $false
                }
            }

            $isIncluded = $true
            foreach( $excludeGlob in $excludeGlobs )
            {
                if( $excludeGlob.IsMatch($relativePath) )
                {
                    $showMessage = $true
                    $result = '!'
                    $whatMatched = $excludeGlob
                    $isIncluded = $false
                    break
                }
            }

            return $isIncluded
        }
        finally
        {
            if( $showMessage )
            {
                Write-Debug ($outputFormat -f $result, $whatMatched, $relativePath)
            }
        }
    }

    function Find-GlobFileMatch
    {
        param(
            [object[]]$Item
        )

        foreach( $info in (Get-ChildItem -LiteralPath $Item.FullName -Force:$Force) )
        {
            # Recurse into directories that aren't exluded.
            if( $info.PSIsContainer )
            {
                if( -not (Test-GlobMatch $info -IsDirectory) )
                {
                    continue
                }

                Find-GlobFileMatch $info
                continue
            }

            if( (Test-GlobMatch $info) )
            {
                Write-Output $info
            }
        }
    }

    $stats = 
        & {
            $Include
            $Exclude
        } |
        Where-Object { $_ } |
        Measure-Object -Property 'Length' -Maximum

    $outputFormat = '{{0}} {{1,-{0}}} {{2}}' -f $stats.Maximum
    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

        function ConvertTo-Glob
        {
            param(
                [Parameter(Mandatory,ValueFromPipeline)]
                [String]$InputObject
            )

            process
            {
                return [DotNet.Globbing.Glob]::Parse($InputObject,$options) 
            }
        }

        $includeGlobs = $Include | Where-Object { $_ } | ConvertTo-Glob
        $excludeGlobs = $Exclude | Where-Object { $_ } | ConvertTo-Glob

        Push-Location -Path $rootPath
        try
        {
            Find-GlobFileMatch -Item (Get-Item -Path $rootPath)
        }
        finally
        {
            Pop-Location
        }
    }
}


# Copyright 2012 Aaron Jensen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every function/cmdlet call in your function. Please vote up this issue so it can get fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        $Cmdlet,

        [Parameter(Mandatory)]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        [Management.Automation.SessionState]$SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }

}