Prism.psm1

# Copyright WebMD Health Services
#
# 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

#Requires -Version 5.1
Set-StrictMode -Version 'Latest'

# Functions should use $moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$moduleRoot = $PSScriptRoot

# Store each of your module's functions in its own file in the Functions
# directory. On the build server, your module's functions will be appended to
# this file, so only dot-source files that exist on the file system. This allows
# developers to work on a module without having to build it first. Grab all the
# functions that are in their own files.
$functionsPath = Join-Path -Path $moduleRoot -ChildPath 'Functions\*.ps1'
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function Get-PackageManagementPreference
{
    [CmdletBinding()]
    param(
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $deepPrefs = @{}
    if( (Test-Path -Path 'env:PRISM_DISABLE_DEEP_DEBUG') -and `
        'Continue' -in @($Global:DebugPreference, $DebugPreference) )
    {
        $deepPrefs['Debug'] = $false
    }

    if( (Test-Path -Path 'env:PRISM_DISABLE_DEEP_VERBOSE') -and `
        'Continue' -in @($Global:VerbosePreference, $VerbosePreference))
    {
        $deepPrefs['Verbose'] = $false
    }

    return $deepPrefs
}



# Ugh. I hate this name, but it interferes with Install-Module in one of the package management modules.
function Install-PrivateModule
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Configuration,

        # A subset of the required modules to install or update.
        [String[]] $Name
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $pkgMgmtPrefs = Get-PackageManagementPreference

        $repoByLocation = @{}
        foreach ($repo in (Get-PSRepository))
        {
            $repoUrl = $repo.SourceLocation
            $repoByLocation[$repoUrl] = $repo.Name

            # Ignore slashes at the end of URLs.
            $trimmedRepoUrl = $repoUrl.TrimEnd('/')
            $repoByLocation[$trimmedRepoUrl] = $repo.Name
        }
    }

    process
    {
        if (-not (Test-Path -Path $Configuration.LockPath))
        {
            $Configuration | Update-ModuleLock | Out-Null
        }

        $installDirPath = $Configuration.InstallDirectoryPath
        $locks = Get-Content -Path $Configuration.LockPath | ConvertFrom-Json
        $locks | Add-Member -Name 'PSModules' -MemberType NoteProperty -Value @() -ErrorAction Ignore

        if ($Name)
        {
            $locks.PSModules = $locks.PSModules | Where-Object {$_.Name -in $Name}
        }

        $installedModules =
            & {
                if ($Configuration.Nested)
                {
                    Get-ChildItem -Path $Configuration.InstallDirectoryPath -Recurse | Out-String | Write-Debug
                    Get-ChildItem -Path (Join-Path -Path $Configuration.InstallDirectoryPath -ChildPath '*\*.psd1') |
                        ForEach-Object { Get-Module -Name $_.FullName -ListAvailable -ErrorAction Ignore }
                }
                else
                {
                    $origPSModulePath = $env:PSModulePath
                    $env:PSModulePath = $Configuration.InstallDirectoryPath
                    try
                    {
                        Write-Debug $env:PSModulePath
                        Get-Module -ListAvailable -ErrorAction Ignore
                    }
                    finally
                    {
                        $env:PSModulePath = $origPSModulePath
                    }
                }
            } |
            Add-Member -Name 'SemVer' -MemberType ScriptProperty -PassThru -Value {
                $prerelease = $this.PrivateData['PSData']['PreRelease']
                if ($prerelease)
                {
                    $prerelease = "-$($prerelease)"
                }
                return "$($this.Version)$($prerelease)"
            }

            $installedModules | Format-Table -Auto | Out-String | Write-Debug

        foreach ($module in $locks.PSModules)
        {
            $module | Format-List | Out-String | Write-Debug
            $installedModule =
                $installedModules | Where-Object 'Name' -EQ $module.Name | Where-Object 'SemVer' -EQ $module.version
            if ($installedModule)
            {
                Write-Debug 'Module already installed.'
                continue
            }

            $sourceUrl = $module.repositorySourceLocation
            $repoName = $repoByLocation[$sourceUrl]
            if (-not $repoName)
            {
                # Ignore slashes at the end of URLs.
                $sourceUrl = $sourceUrl.TrimEnd('/')
            }
            $repoName = $repoByLocation[$sourceUrl]
            if (-not $repoName)
            {
                $msg = "PowerShell repository at ""$($module.repositorySourceLocation)"" does not exist. Use " +
                       '"Get-PSRepository" to see the current list of repositories, "Register-PSRepository" ' +
                       'to add a new repository, or "Set-PSRepository" to update an existing repository.'
                Write-Debug "Unknown repo."
                Write-Error $msg
                continue
            }

            if (-not (Test-Path -Path $installDirPath))
            {
                New-Item -Path $installDirPath -ItemType 'Directory' -Force | Out-Null
            }

            # How many versions of this module will we be installing?
            $moduleVersionCount = ($locks.PSModules | Where-Object 'Name' -EQ $module.name | Measure-Object).Count

            $nestedSingleVersion = $Configuration.Nested -and $moduleVersionCount -eq 1
            Write-Debug "Nested $($Configuration.Nested)"
            Write-Debug "moduleVersionCount ${moduleVersionCount}"
            Write-Debug "nestedSingleVersion ${nestedSingleVersion}"

            $moduleDirPath = Join-Path -Path $installDirPath -ChildPath $module.Name
            Write-Debug "moduleDirPath ${moduleDirPath}"
            if ($nestedSingleVersion -and (Test-Path -Path $moduleDirPath))
            {
                Write-Debug "Removing ${moduleDirPath}"
                Remove-Item -Path $moduleDirPath -Recurse -Force
                if (Test-Path -Path $moduleDirPath)
                {
                    $msg = "Failed to save PowerShell module ""$($module.name)"" $($module.version) to " +
                           "destination ""${moduleDirPath}"" because that destination already exists and deletion " +
                           'failed.'
                    Write-Debug "Failed to delete module."
                    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    continue
                }
            }

            Save-Module -Name $module.name `
                        -Path $installDirPath `
                        -RequiredVersion $module.version `
                        -AllowPrerelease `
                        -Repository $repoName `
                        @pkgMgmtPrefs

            # PowerShell has a 10 directory limit for nested modules, so reduce the number of nested directories
            # when installing a nested module by installing directly in the module root directory and moving
            # everything out of the version module directory.
            if ($nestedSingleVersion)
            {
                $modulePath = Join-Path -Path $installDirPath -ChildPath $module.name
                $versionDirName = $module.version
                if ($versionDirName -match '^(\d+\.\d+\.\d+)')
                {
                    $versionDirName = $Matches[1]
                }
                $moduleVersionPath = Join-Path -Path $modulePath -ChildPath $versionDirName
                Get-ChildItem -Path $moduleVersionPath -Force | Move-Item -Destination $modulePath
                Get-Item -Path $moduleVersionPath | Remove-Item
            }

            $modulePath = Join-Path -Path $installDirPath -ChildPath $module.name | Resolve-Path -Relative
            $installedModule = [pscustomobject]@{
                Name = $module.name;
                Version = $module.version;
                Path = $modulePath;
                RepositorySourceLocation = $module.repositorySourceLocation;
            }
            $installedModule.pstypenames.Add('Prism.InstalledModule')
            $installedModule | Write-Output
        }
    }
}

function Invoke-Prism
{
    <#
    .SYNOPSIS
    Invokes Prism.
 
    .DESCRIPTION
    A tool similar to nuget but for PowerShell modules. A config file in the root of a repository that specifies
    what modules should be installed into the PSModules directory of the repository. If a path is provided for the
    module it will be installed at the specified path instead of the PSModules directory.
 
    .EXAMPLE
    Invoke-Prism 'install'
 
    Demonstrates how to call this function to install required PSModules.
 
    .EXAMPLE
    Invoke-Prism 'install' -Name 'Module1', 'Module2'
 
    Demonstrates how to install a subset of the required PSModules.
 
    .EXAMPLE
    Invoke-Prism 'update' -Name 'Module1', 'Module2'
 
    Demonstrates how to update a subset of the required PSModules.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position=0)]
        [ValidateSet('install', 'update')]
        [String] $Command,

        # A subset of the required modules to install or update.
        [Parameter(Position=1)]
        [String[]] $Name,

        # The path to a prism.json file or a directory where Prism can find a prism.json file. If path is to a file,
        # the "FileName" parameter is ignored, if given.
        #
        # If the path is a directory, Prism will look for a "prism.json' file in that directory. If the -Recurse switch
        # is given and the path is to a directory, Prism will recursively search in and under that directory and run for
        # each prism.json file it finds. If Path is to a directory and FileName is given, Prism will look for a file
        # with that name instead.
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [String] $Path,

        # The name of the Prism configuration file to use. Defaults to `prism.json`. Ignored if "Path" is given and is
        # the path to a file.
        [String] $FileName = 'prism.json',

        # If given, searches the current directory and all sub-directories for prism.json files and runs the command
        # for each file. If the Path parameter is given and is to a directory, Prism will start searching in that
        # directory instead of the current directory.
        [switch] $Recurse
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $origModulePath = $env:PSModulePath

        $pkgMgmtPrefs = Get-PackageManagementPreference

        Import-Module -Name 'PackageManagement' `
                      -MinimumVersion '1.3.2' `
                      -MaximumVersion '1.4.8.1' `
                      -Global `
                      -ErrorAction Stop `
                      @pkgMgmtPrefs
        Import-Module -Name 'PowerShellGet' `
                      -MinimumVersion '2.0.0' `
                      -MaximumVersion '2.2.5' `
                      -Global `
                      -ErrorAction Stop `
                      @pkgMgmtPrefs
    }

    process
    {
        try
        {
            $startIn = '.'
            if ($Path)
            {
                if ((Test-Path -Path $Path -PathType Leaf))
                {
                    $FileName = $Path | Split-Path -Leaf
                    $startIn = $Path | Split-Path -Parent
                }
                elseif ((Test-Path -Path $Path -PathType Container))
                {
                    $startIn = $Path
                }
                else
                {
                    Write-Error -Message "Path ""$($Path)"" does not exist."
                    return
                }
            }

            $Force = $FileName.StartsWith('.')
            $prismJsonFiles = Get-ChildItem -Path $startIn -Filter $FileName -Recurse:$Recurse -Force:$Force -ErrorAction Ignore
            if (-not $prismJsonFiles)
            {
                $msg = ''
                $suffix = ''
                if ($Recurse)
                {
                    $suffix = 's'
                    $msg = ' or any of its sub-directories'
                }

                $locationMsg = 'the current directory'
                if ($startIn -ne '.' -and $startIn -ne (Get-Location).Path)
                {
                    $locationMsg = """$($startIn | Resolve-Path -Relative)"""
                }

                $msg = "No $($FileName) file$($suffix) found in $($locationMsg)$($msg)."
                Write-Error -Message $msg -ErrorAction Stop
                return
            }

            foreach ($prismJsonFile in $prismJsonFiles)
            {
                $prismJsonPath = $prismJsonFile.FullName
                $config = Get-Content -Path $prismJsonPath | ConvertFrom-Json
                if (-not $config)
                {
                    Write-Warning "File ""$($prismJsonPath | Resolve-Path -Relative) is empty."
                    continue
                }

                $lockBaseName = [IO.Path]::GetFileNameWithoutExtension($prismJsonPath)
                $lockExtension = [IO.Path]::GetExtension($prismJsonPath)
                # Hidden file with no extension, e.g. `.prism`
                if (-not $lockBaseName -and $lockExtension)
                {
                    $lockBaseName = $lockExtension
                    $lockExtension = ''
                }

                $ignore = @{ 'ErrorAction' = 'Ignore' }
                # public configuration that users can customize.
                # Add-Member doesn't return an object if the member already exists, so these can't be part of one pipeline.
                $config | Add-Member -Name 'PSModules' -MemberType NoteProperty -Value @() @ignore
                $config | Add-Member -Name 'PSModulesDirectoryName' -MemberType NoteProperty -Value 'PSModules' @ignore

                if ($config.PSModulesDirectoryName.Contains('\') -or `
                    $config.PSModulesDirectoryName.Contains('/') -or `
                    $config.PSModulesDirectoryName -eq '..')
                {
                    $msg = "Failed to run ``prism ${Command}`` because the ""PSModulesDirectoryName"" configuration " +
                           "value, ""$($config.PSModulesDirectoryName)"", in ""${primsJsonPath}"" is invalid. It can " +
                           'not contain the "\" or "/" characters or be "..".'
                    Write-Error -Message $msg -ErrorAction Stop
                    return
                }

                $isNested = (Test-Path -Path (Join-Path -Path $prismJsonFile.DirectoryName -ChildPath '*.psd1')) -or `
                            (Test-Path -Path (Join-Path -Path $prismJsonFile.DirectoryName -ChildPath '*.psm1'))

                $installDirPath =
                    Join-Path -Path $prismJsonFile.DirectoryName -ChildPath $config.PSModulesDirectoryName
                if ($isNested -or $config.PSModulesDirectoryName -eq '.')
                {
                    $installDirPath = $prismJsonFile.DirectoryName
                }

                $lockPath =
                    Join-Path -Path ($prismJsonPath |
                    Split-Path -Parent) -ChildPath "$($lockBaseName).lock$($lockExtension)"
                $addMemberArgs = @{
                    MemberType = 'NoteProperty';
                    PassThru = $true;
                    # Force so users can't customize these properties.
                    Force = $true;
                }
                # Members that users aren't allowed to customize/override.
                $config |
                    Add-Member -Name 'Path' -Value $prismJsonPath @addMemberArgs |
                    Add-Member -Name 'File' -Value $prismJsonFile @addMemberArgs |
                    Add-Member -Name 'LockPath' -Value $lockPath @addMemberArgs |
                    Add-Member -Name 'Nested' -Value $isNested @addMemberArgs |
                    Add-Member -Name 'InstallDirectoryPath' -Value $installDirPath @addMemberArgs |
                    Out-Null

                # This makes it so we can use PowerShell's module cmdlets as much as possible.
                $privateModulePath =  & {
                    # Prism's private module path, PSModules, or a module directory, if installing nested modules.
                    $config.InstallDirectoryPath | Write-Output

                    # PackageManagement needs to be able to find and load PowerShellGet so it can get repositoriees,
                    # package sources, etc, so it and PowerShellGet have to be in PSModulePath, unfortunately.
                    Get-Module -Name 'PackageManagement','PowerShellGet' |
                        Select-Object -ExpandProperty 'Path' | # **\PSModules\MODULE\VERSION\MODULE.psd1
                        Split-Path -Parent |                   # **\PSModules\MODULE\VERSION
                        Split-Path -Parent |                   # **\PSModules\MODULE
                        Split-Path -Parent |                   # **\PSModules
                        Select-Object -Unique |
                        Write-Output
                }
                $env:PSModulePath = $privateModulePath -join [IO.Path]::PathSeparator
                Write-Debug -Message "env:PSModulePath $($env:PSModulePath)"

                switch ($Command)
                {
                    'install'
                    {
                        $config | Install-PrivateModule -Name $Name
                    }
                    'update'
                    {
                        $config | Update-ModuleLock -Name $Name
                    }
                }
            }
        }
        finally
        {
            $env:PSModulePath = $origModulePath
        }
    }
}

Set-Alias -Name 'prism' -Value 'Invoke-Prism'


function Select-Module
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Module,

        [Parameter(Mandatory)]
        [String] $Name,

        [String] $Version,

        [switch] $AllowPrerelease
    )

    process
    {
        if( $Module.Name -ne $Name )
        {
            return
        }

        if( $Version -and $Module.Version -notlike $Version )
        {
            return
        }

        if( $AllowPrerelease )
        {
            return $Module
        }

        [Version]$moduleVersion = $null
        if( [Version]::TryParse($Module.Version, [ref]$moduleVersion) )
        {
            return $Module
        }
    }
}


function Update-ModuleLock
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Configuration,

        # A subset of the required modules to install or update.
        [String[]] $Name
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $pkgMgmtPrefs = Get-PackageManagementPreference
    }

    process
    {
        $modulesNotFound = [Collections.ArrayList]::New()
        $moduleNames = $Configuration.PSModules | Select-Object -ExpandProperty 'Name'
        if (-not $moduleNames)
        {
            Write-Warning "There are no modules listed in ""$($Configuration.Path | Resolve-Path -Relative)""."
            return
        }

        $numFinds = $moduleNames | Measure-Object | Select-Object -ExpandProperty 'Count'
        $numFinds = $numFinds + 2
        Write-Debug " numSteps $($numFinds)"
        $curStep = 0
        $uniqueModuleNames =
            $moduleNames |
            Select-Object -Unique |
            Where-Object {
                if (-not $Name)
                {
                    return $true
                }

                $moduleName = $_
                return $Name | Where-Object { $moduleName -like $_ }
            }

        if (-not $uniqueModuleNames)
        {
            return
        }

        $status = "Find-Module -Name '$($uniqueModuleNames -join "', '")'"
        $percentComplete = ($curStep++/$numFinds * 100)
        $activity = @{ Activity = 'Resolving Module Versions' }
        Write-Progress @activity -Status $status -PercentComplete $percentComplete

        $currentLocks = [pscustomobject]@{
            'PSModules' = @()
        }

        $lockDisplayPath = $Configuration.LockPath
        if ($Configuration.LockPath -and (Test-Path -Path $Configuration.LockPath))
        {
            $numErrors = $Global:Error.Count
            try
            {
                $currentLocks = Get-Content -Path $Configuration.LockPath | ConvertFrom-Json -ErrorAction Ignore
            }
            catch
            {
                $numErrorsToDelete = $Global:Error.Count - $numErrors
                for ($idx = 0 ; $idx -lt $numErrorsToDelete; ++$idx)
                {
                    $Global:Error.RemoveAt(0)
                }
            }

            $lockDisplayPath = $lockDisplayPath | Resolve-Path -Relative
        }

        try
        {
            $modules = Find-Module -Name $uniqueModuleNames -ErrorAction Ignore @pkgMgmtPrefs

            # Find-Module is expensive. Limit calls as much as possible.
            $findModuleCache = @{}

            $env:PSModulePath =
                Join-Path -Path $Configuration.File.DirectoryName -ChildPath $Configuration.PSModulesDirectoryName

            $locksUpdated = $false

            foreach ($module in $Configuration.PSModules)
            {
                if ($Name -and -not ($Name | Where-Object { $module.Name -like $_ }))
                {
                    continue
                }

                $optionalParams = @{}

                # Make sure these members are present and have default values.
                $module | Add-Member -Name 'Version' -MemberType NoteProperty -Value '' -ErrorAction Ignore
                $module |
                    Add-Member -Name 'AllowPrerelease' -MemberType NoteProperty -Value $false -ErrorAction Ignore

                $versionDesc = 'latest'
                if ($module.Version)
                {
                    $versionDesc = $optionalParams['Version'] = $module.Version
                }

                $allowPrerelease = $false
                if ($module.AllowPrerelease -or $module.Version -match '-|\+')
                {
                    $allowPrerelease = $optionalParams['AllowPrerelease'] = $true
                }

                $curStep += 1

                Write-Debug " curStep $($curStep)"
                $moduleToInstall =
                    $modules | Select-Module -Name $module.Name @optionalParams | Select-Object -First 1
                if (-not $moduleToInstall)
                {
                    $status = "Find-Module -Name '$($module.Name)' -AllVersions"
                    if ($allowPrerelease)
                    {
                        $status = "$($status) -AllowPrerelease"
                    }

                    if (-not $findModuleCache.ContainsKey($status))
                    {
                        Write-Progress @activity -Status $status -PercentComplete ($curStep/$numFinds * 100)
                        $findModuleCache[$status] = Find-Module -Name $module.Name `
                                                                -AllVersions `
                                                                -AllowPrerelease:$allowPrerelease `
                                                                -ErrorAction Ignore `
                                                                @pkgMgmtPrefs
                    }
                    $moduleToInstall =
                        $findModuleCache[$status] |
                        Select-Module -Name $module.Name @optionalParams |
                        Select-Object -First 1
                }

                if (-not $moduleToInstall)
                {
                    [void]$modulesNotFound.Add($module.Name)
                    continue
                }

                $lockUpdated = $false
                $oldVersion = ''

                $lock = $currentLocks.PSModules | Where-Object 'name' -eq $moduleToInstall.name
                if ($lock)
                {
                    $oldVersion = $lock.version
                }
                else
                {
                    $lock = [pscustomobject]@{
                        name = $moduleToInstall.Name;
                        version = $moduleToInstall.Version;
                        repositorySourceLocation = $moduleToInstall.RepositorySourceLocation;
                    }
                    $currentLocks.PSModules += $lock
                    $lockUpdated = $true
                }

                if ($moduleToInstall.Version -ne $lock.version)
                {
                    $lock.version = $moduleToInstall.Version
                    $lockUpdated = $true
                }

                if (-not $lockUpdated)
                {
                    continue
                }

                $locksUpdated = $lockUpdated

                $moduleLock = [pscustomobject]@{
                    ModuleName = $lock.Name;
                    Version = $versionDesc;
                    PreviousLockedVersion = $oldVersion;
                    LockedVersion = $lock.version;
                    RepositorySourceLocation = $lock.repositorySourceLocation;
                    Path = $lockDisplayPath;
                }
                $moduleLock.pstypenames.Add('Prism.ModuleLock')
                $moduleLock | Write-Output
            }

            if ($locksUpdated)
            {
                Write-Progress @activity -Status "Saving lock file ""$($Configuration.LockPath)""." -PercentComplete 100
                [Object[]] $sortedPSModules = $currentLocks.PSModules | Sort-Object -Property 'Name','Version'
                $currentLocks.PSModules = $sortedPSModules
                $currentLocks | ConvertTo-Json -Depth 2 | Set-Content -Path $Configuration.LockPath -NoNewline
            }

            if ($modulesNotFound.Count)
            {
                $suffix = ''
                if ($modulesNotFound.Count -gt 1)
                {
                    $suffix = 's'
                }
                $msg = "$($Path | Resolve-Path -Relative): Module$($suffix) ""$($modulesNotFound -join '", "')"" not " +
                       'found.'
                Write-Error $msg
            }
        }
        finally
        {
            Write-Progress @activity -Completed
        }
    }
}


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 `Write-Error` call. 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)
        }
    }
}