Carbon.Environment.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 $script:moduleDirPath 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.
$script:moduleDirPath = $PSScriptRoot

if (-not (Test-Path -Path 'variable:IsWindows'))
{
    $script:IsWindows = $true
    $script:IsLinux = $script:IsMacOS = $false
}

# 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 $script:moduleDirPath -ChildPath 'Functions\*.ps1'
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function Assert-Scope
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [Object] $Scope,

        [String] $Message
    )

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

        $writeError = $false
        $receivedCount = 0

        $seenScopes = [Collections.Generic.Hashset[EnvironmentVariableTarget]]::New()
    }

    process
    {
        if ($null -eq $Scope)
        {
            return
        }

        if ($Scope -isnot [EnvironmentVariableTarget])
        {
            $msg = "Failed to validate scope ""${Scope}"" because it is a [$($Scope.GetType().FullName)] object, but " +
                   'we expected [EnvironmentVariableTarget].'
            Write-Error -Message $msg -ErrorAction Stop
            return
        }

        $receivedCount += 1

        # PowerShell and .NET do not support user-level and computer-level environment variables on Linux and macOS.
        if (-not $IsWindows -and $Scope -ne [EnvironmentVariableTarget]::Process)
        {
            $writeError = $true
            return
        }

        if ($seenScopes.Contains($Scope))
        {
            return
        }

        $Scope | Write-Output
        [void]$seenScopes.Add($Scope)
    }

    end
    {
        # Operate on process-level environment variables by default, if the user specifies no scope.
        if ($receivedCount -eq 0)
        {
            return [EnvironmentVariableTarget]::Process
        }

        if ($writeError)
        {
            if (-not $Message)
            {
                $Message = 'PowerShell and .NET only support user-level and computer-level environment variables on ' +
                           'Windows.'
            }
            Write-Error -Message $Message -ErrorAction $ErrorActionPreference
        }
    }

}


function Remove-CEnvironmentVariable
{
    <#
    .SYNOPSIS
    Removes an environment variable.
 
    .DESCRIPTION
    The `Remove-CEnvironmentVariable` function deletes environment variables. Pass the names of the environment
    variables to delete to the `Name` parameter (or pipe the names into the function). If an environment variable does
    not exist at that scope, the function writes an error. Otherwise, the environment variable is deleted.
 
    By default, operates on the current process's environment variables. PowerShell and .NET do not support user-level
    and computer-level environment variables. On Windows, use the `Scope` parameter to remove user-level and/or
    machine-level environment variables. Multiple scopes are accepted. Changes to environment variables are not
    reflected in running processes, including the current PowerShell session. If you want the removal of the user-level
    or machine-level environment variable to be reflected in the current process, include `Process` in the list of
    scopes passed to the `Scope` parameter.
 
    To remove a user-level environment variable for a specific user on Windows, pass that user's credentials to the
    `-Credential` parameter. A PowerShell process is run as that user to remove the environment variable.
 
    On Windows, environment variable names are case-insensitive. On Linux and macOS, environment variable names are
    case-sensitive.
 
    .LINK
    Remove-CEnvironmentVariable
 
    .LINK
    Set-CEnvironmentVariable
 
    .LINK
    Test-CEnvironmentVariable
 
    .EXAMPLE
    Remove-CEnvironmentVariable -Name 'MyEnvironmentVariable'
 
    Demonstrates how to remove an environment variable from the current process. In this example, the
    `MyEnvironmentVariable` is removed. If it doesn't exist, an error is written.
 
    .EXAMPLE
    Remove-CEnvironmentVariable -Name 'SomeComputerVariable' -Scope Machine
 
    Demonstrates how to remove a computer-level environment variable. In this example, the `SomeComputerVariable`
    environment variable is removed from the computer's environment variables. If that computer-level variable doesn't
    exist, an error is written.
 
    .EXAMPLE
    Remove-CEnvironmentVariable -Name 'SomeUsersVariable' -Scope User
 
    Demonstrates how to remove a user-level environment variable for the current user. In this example, the
    `SomeUsersVariable` environment variable is removed from the current user's environment variables. If it doesn't
    exist at the user scope, an error is written.
 
    .EXAMPLE
    Remove-CEnvironmentVariable -Name 'SomeUsersVariable' -Scope Process,User
 
    Demonstrates how to have the change to a user-level or machine-level environment variable reflected in the current
    process by including `Process` in the list of scopes passed to `Scope`.
 
    .EXAMPLE
    Remove-CEnvironmentVariable -Name 'SomeUsersVariable' -Credential $user
 
    Demonstrates how to remove a user-level environment variable for a specific user. In this example, the
    `SomeUsersVariable` environment variable is removed from the `$user` user's environment variables. If that user
    doesn't have a `SomeUsersVariable` environment variable, an error is written.
 
    .EXAMPLE
    'Var1','Var2' | Remove-CEnvironmentVariable
 
    Demonstrates that you can pipe the environment variables to delete to `Remove-CEnvironmentVariable`.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ForCurrentUser')]
    param(
        # The environment variable to remove. Case-insensitive on Windows, case-sensitive on Linux and macOS.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String[]] $Name,

        # The scopes at which to remove the environment variable. Default is the current process.
        [Parameter(ParameterSetName='ForCurrentUser')]
        [EnvironmentVariableTarget[]] $Scope,

        # Remove an environment variable for a specific user.
        [Parameter(Mandatory, ParameterSetName='ForSpecificUser')]
        [pscredential] $Credential
    )

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

        $userEnvVars = [Collections.Generic.List[string]]::new()

        $Scope = $Scope | Assert-Scope
    }

    process
    {
        if ($Credential)
        {
            $userEnvVars.AddRange( $Name )
            return
        }

        foreach ($_name in $Name)
        {
            foreach ($_scope in $Scope)
            {
                $target = "$($_scope.ToString().ToLowerInvariant())-level environment variable ""${_name}"""

                if (-not (Test-CEnvironmentVariable -Name $_name -Scope $_scope))
                {
                    $msg = "Failed to delete ${target} because it does not exist."
                    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    continue
                }

                if (-not $PSCmdlet.ShouldProcess($target, "remove"))
                {
                    continue
                }

                Write-Information "Removing ${target}."
                [Environment]::SetEnvironmentVariable($_name, [NullString]::Value, $_scope)
            }
        }
    }

    end
    {
        if (-not $Credential -or -not $userEnvVars.Count)
        {
            return
        }

        if (-not $IsWindows)
        {
            $msg = 'PowerShell and .NET only support user-level environment variables on Windows.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $parameters = $PSBoundParameters
        [void]$parameters.Remove('Credential')
        [void]$parameters.Remove('Name')
        Start-Job -ScriptBlock {
                    Import-Module -Name (Join-Path -Path $using:moduleDirPath -ChildPath 'Carbon.Environment.psm1')
                    $VerbosePreference = $using:VerbosePreference
                    $ErrorActionPreference = $using:ErrorActionPreference
                    $DebugPreference = $using:DebugPreference
                    $WhatIfPreference = $using:WhatIfPreference
                    $InformationPreference = $using:InformationPreference
                    Remove-CEnvironmentVariable -Name $using:userEnvVars @using:parameters -Scope User
                } -Credential $Credential |
            Receive-Job -Wait -AutoRemoveJob
    }
}



function Set-CEnvironmentVariable
{
    <#
    .SYNOPSIS
    Creates or sets an environment variable.
 
    .DESCRIPTION
    The `Set-CEnvironmentVariable` function creates or sets an environment variable. Pass the name of the environment
    variable to the `Name` parameter and the value to the `Value` parameter. An environment variable with that name and
    value is set for the current process. Use the `Scope` parameter to set user-level and/or machine-level variables.
    Uses `[Environment]::SetEnvironmentVariable` to create the variable if it doesn't exist, or update its value if the
    variable exists and its value is different from the value being set.
 
    By default, creates and sets the current process's environment variables. PowerShell and .NET do not support
    user-level and computer-level environment variables. On Windows, use the `Scope` parameter to remove user-level
    and/or machine-level environment variables. Multiple scopes are accepted. Changes to environment variables are not
    reflected in running processes, including the current PowerShell session. If you want a new or changed user-level or
    machine-level environment variable to be reflected in the current process, include `Process` in the list of scopes
    passed to the `Scope` parameter.
 
    To create or set an environment variable for a specific user on Windows, pass that user's credentials to the
    `-Credential` parameter. This will run a PowerShell process that creates or sets the environment variable.
 
    Writes an information message for each environment variable created or updated. The message includes the value being
    set. Use the `Sensitive` switch to omit the value from the information message.
 
    On Windows, environment variable names are case-insensitive. On Linux and macOS, environment variable names are
    case-sensitive.
 
    In PowerShell 7.4 and earlier, setting `Value` to an empty string deletes the variable. In newer versions of
    PowerShell, the variable is set to an empty value.
 
    .LINK
    Remove-CEnvironmentVariable
 
    .LINK
    Test-CEnvironmentVariable
 
    .EXAMPLE
    Set-CEnvironmentVariable -Name 'MyEnvironmentVariable' -Value 'Value1'
 
    Demonstrates how to create or set an environemnt variable for the current process. In this example, the current
    process's `MyEnvironmentVariable` variable is created or set with a value of `Value1`.
 
    .EXAMPLE
    Set-CEnvironmentVariable -Name 'MyEnvironmentVariable' -Value 'Value1' -Scope Machine
 
    Demonstrates how to create a computer-level environment variable by including `Machine` in the list of scopes passed
    to the `Scope` parameter. The current process's environment variables will not have the new or updated environment
    variable.
 
    .EXAMPLE
    Set-CEnvironmentVariable -Name 'MyEnvironmentVariable' -Value 'Value1' -Scope User
 
    Demonstrates how to create a user environment variable by including `User` in the list of scopes passed to the
    `Scope` parameter. The current process's environment variables will not have the new or updated environment
    variable.
 
    .EXAMPLE
    Set-CEnvironmentVariable -Name 'MyEnvironmentVariable' -Value 'Value1' -Scope Process,User
 
    Demonstrates how to have a change to a user or machine-level environment variable reflected in the current process
    by including `Process` in the list of scopes passed to the `Scope` parameter.
 
    .EXAMPLE
    Set-CEnvironmentVariable -Name 'SomeUsersEnvironmentVariable' -Value 'SomeValue' -Credential $userCreds
 
    Demonstrates how to set an environment variable for a specific user by passing that user's credentials to the
    `Credential` parameter.
 
    .EXAMPLE
    Set-CEnvironmentVariable -Name 'MySensitiveEnvironmentVariable' -Value 'SecretValue' -Sensitive
 
    Demonstrates how to omit the environment variable's value from the information message output by this function.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of environment variable to add/set. Case-insensitive on Windows. Case-sensitive on Linux and macOS.
        [Parameter(Mandatory)]
        [String] $Name,

        # The environment variable's value. In PowerShell 7.4 and earlier, setting this to an empty string deletes the
        # variable. In newer versions, the variable is created with an empty value.
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [String] $Value,

        # The scopes at which to set the variable. Default is the current process. Changes to user-level and
        # computer-level variables are not reflected in the current process's environment variables unless `Process` is
        # in this list.
        [Parameter(ParameterSetName='ForCurrentUser')]
        [EnvironmentVariableTarget[]] $Scope,

        [Parameter(Mandatory,ParameterSetName='ForSpecificUser')]
        # Set an environment variable for a specific user.
        [pscredential] $Credential,

        # Don't output the variable's value in information messages.
        [switch] $Sensitive
    )

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

    if( $PSCmdlet.ParameterSetName -eq 'ForSpecificUser' )
    {
        if (-not $IsWindows)
        {
            $msg = 'PowerShell and .NET only support user-level environment variables on Windows.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $parameters = $PSBoundParameters
        [void]$parameters.Remove('Credential')
        $job = Start-Job -ScriptBlock {
            Import-Module -Name (Join-Path -path $using:moduleDirPath -ChildPath 'Carbon.Environment.psm1' -Resolve)
            $VerbosePreference = $using:VerbosePreference
            $ErrorActionPreference = $using:ErrorActionPreference
            $DebugPreference = $using:DebugPreference
            $WhatIfPreference = $using:WhatIfPreference
            $InformationPreference = $using:InformationPreference
            Set-CEnvironmentVariable @using:parameters -Scope User
        } -Credential $Credential
        $job | Wait-Job | Receive-Job
        $job | Remove-Job -Force -ErrorAction Ignore
        return
    }

    $Scope = $Scope | Assert-Scope

    foreach ($_scope in $Scope)
    {
        # Only set the variable if its value has changed.
        if ($Value -eq [Environment]::GetEnvironmentVariable($Name, $_scope))
        {
            continue
        }

        $target = "$($_scope.ToString().ToLowerInvariant())-level environment variable ""${Name}"""

        if (-not $PSCmdlet.ShouldProcess($target, "set"))
        {
            continue
        }

        $valueMsg = " to ""${Value}"""
        if ($Sensitive)
        {
            $valueMsg = ''
        }

        Write-Information "Setting ${target}${valueMsg}."
        [Environment]::SetEnvironmentVariable($Name, $Value, $_scope)
    }
}



function Test-CEnvironmentVariable
{
    <#
    .SYNOPSIS
    Tests if an environment variable exists.
 
    .DESCRIPTION
    The `Test-CEnvironmentVariable` function tests if an environment variable exists. Pass the name of the variable to
    the `Name` parameter (or pipe in multiple names). If a variable with that name exists in the current process,
    returns `$true`. Otherwise, returns `$false`.
 
    By default, checks in the current process's environment variables. PowerShell and .NET do not support user-level and
    computer-level environment variables. On Windows, use the `Scope` parameter to check if user-level or computer-level
    environment variables exist.
 
    To check if a specific user has an environment variable on Windows, pass that user's credentials to the `Credential`
    parameter.
 
    On Windows, environment variable names are case-insenstive. On Linux and macOS, they are case-sensitive.
 
    .EXAMPLE
    Test-CEnvironmentVariable -Name 'PATH'
 
    Demonstrates how to check that an environment variable exists. In this case, will return `$true` if the `PATH`
    environment variable exists at any scope.
 
    .EXAMPLE
    Test-CEnvironmentVariable -Name 'MY_VAR' -Scope User
 
    Demonstrates how to check that an environment variable exists at a specific scope. In this case, will return `$true`
    if the user has a `MY_VAR` environment variable.
 
    .EXAMPLE
    'PATH' | Test-CEnvironmentVariable
 
    Demonstrates that you can pipe environment variable names to `Test-CEnvironmentVariable`.
    #>

    [CmdletBinding(DefaultParameterSetName='CurrentUser')]
    param(
        # The name of the environment variable to check. Case-insensitive on Windows. Cse-sensitive on Linux and MacOS.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String] $Name,

        # The specific scope to check. By default, checks the current process.
        [Parameter(ParameterSetName='CurrentUser')]
        [EnvironmentVariableTarget] $Scope,

        [Parameter(Mandatory, ParameterSetName='ForUser')]
        [pscredential] $Credential
    )

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

        $userEnvVars = [Collections.Generic.List[String]]::New()

        $validScope = $Scope | Assert-Scope
    }

    process
    {
        if ($Credential)
        {
            $userEnvVars.Add($Name)
            return
        }

        if ($null -eq $validScope)
        {
            return
        }

        return ($null -ne [Environment]::GetEnvironmentVariable($Name, $validScope))
    }

    end
    {
        if (-not $Credential -or -not $userEnvVars.Count)
        {
            return
        }

        if (-not $IsWindows)
        {
            $msg = 'PowerShell and .NET only support user-level environment variables on Windows.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $parameters = $PSBoundParameters
        [void]$parameters.Remove('Credential')
        [void]$parameters.Remove('Name')
        Start-Job -ScriptBlock {
                Import-Module -Name (Join-Path -path $using:moduleDirPath -ChildPath 'Carbon.Environment.psm1' -Resolve)
                $VerbosePreference = $using:VerbosePreference
                $ErrorActionPreference = $using:ErrorActionPreference
                $DebugPreference = $using:DebugPreference
                $WhatIfPreference = $using:WhatIfPreference
                $InformationPreference = $using:InformationPreference
                $using:userEnvVars | Test-CEnvironmentVariable @using:parameters -Scope User
            } -Credential $Credential |
            Receive-Job -Wait -AutoRemoveJob |
            Write-Output
    }
}


function Uninstall-CEnvironmentVariable
{
    <#
    .SYNOPSIS
    Removes an environment variable if it exists.
 
    .DESCRIPTION
    The `Uninstall-CEnvironmentVariable` function deletes environment variables, but only if they exist. Pass the names
    of the environment variables to delete to the `Name` parameter (or pipe in the names). Each environment variable
    that exists is deleted. No errors are written if an environment variable doesn't exist.
 
    By default, removes the current process's environment variables. PowerShell and .NET do not support user-level and
    computer-level environment variables. On Windows, use the `Scope` parameter to remove user-level and/or
    machine-level environment variables. Multiple scopes are accepted. Changes to environment variables are not
    reflected in running processes, including the current PowerShell session. If you want the removal of user-level
    and/or machine-level environment variable to be reflected in the current process, include `Process` in the list of
    scopes passed to the `Scope` parameter.
 
    To remove a specific user's user-level environment variable on Windows, pass that user's credentials to the
    `-Credential` parameter.
 
    On Windows, environment variable names are case-insensitive. On Linux and macOS, environment variable names are
    case-sensitive.
 
    .LINK
    Remove-CEnvironmentVariable
 
    .LINK
    Set-CEnvironmentVariable
 
    .LINK
    Test-CEnvironmentVariable
 
    .EXAMPLE
    Uninstall-CEnvironmentVariable -Name 'MyEnvironmentVariable'
 
    Demonstrates how to remove an environment variable from the current process if it exists. In this case, will remove
    the `MyEnvironmentVariable` environment variable, if it exists.
 
    .EXAMPLE
    Uninstall-CEnvironmentVariable -Name 'MyUserVariable' -Scope User
 
    Demonstrates how to remove a user-level environment by including `User` in the list of scopes.
 
    .EXAMPLE
    Uninstall-CEnvironmentVariable -Name 'MyComputerVariable' -Scope Machine
 
    Demonstrates how to remove a computer-level environment by including `Machine` in the list of scopes.
 
    .EXAMPLE
    Uninstall-CEnvironmentVariable -Name 'MyComputerVariable' -Scope User,Machine
 
    Demonstrates that you can pass multiple scopes to the `Scope` parameter.
 
    .EXAMPLE
    Uninstall-CEnvironmentVariable -Name 'MyComputerVariable' -Scope Process,Machine
 
    Demonstrates how to have the removal of a computer-level environment reflected in the current process by including
    `Process` in the list of scopes.
 
    .EXAMPLE
    'Var1','Var2' | Uninstall-CEnvironmentVariable
 
    Demonstrates that you can pipe names to `Uninstall-CEnvironmentVariable`.
 
    .EXAMPLE
    Uninstall-CEnvironmentVariable Name 'Var1','Var2'
 
    Demonstrates that you can pass an array of names to the `Name` parameter.
 
    .EXAMPLE
    Uninstall-CEnvironmentVariable -Name 'SomeUsersVariable' -Credential $credential
 
    Demonstrates that you can remove another user's user-level environment variable by passing its credentials to the
    `Credential` parameter. This runs a separate PowerShell process as that user to remove the variable.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ForCurrentUser')]
    param(
        # The environment variable to remove. Case-insensitive on Windows, case-sensitive on Linux and macOS.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String[]] $Name,

        # The scopes at which to remove the environment variable. Defaults to the current process.
        [Parameter(ParameterSetName='ForCurrentUser')]
        [EnvironmentVariableTarget[]] $Scope,

        # Remove an environment variable for a specific user.
        [Parameter(Mandatory, ParameterSetName='ForSpecificUser')]
        [pscredential] $Credential
    )

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

        $userEnvVarsToDelete = [Collections.Generic.List[String]]::New()

        $Scope = $Scope | Assert-Scope
    }

    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'ForSpecificUser')
        {
            $userEnvVarsToDelete.AddRange($Name)
            return
        }

        foreach ($_name in $Name)
        {
            foreach ($_scope in $Scope)
            {
                if (-not (Test-CEnvironmentVariable -Name $_name -Scope $_scope))
                {
                    continue
                }

                $target = "$($_scope.ToString().ToLowerInvariant())-level environment variable ""${_name}"""

                if (-not $PSCmdlet.ShouldProcess($target, 'remove'))
                {
                    continue
                }

                Write-Information "Removing ${target}."
                [Environment]::SetEnvironmentVariable($_name, [NullString]::Value, $_scope)
            }
        }
    }

    end
    {
        if (-not $userEnvVarsToDelete.Count)
        {
            return
        }

        if (-not $IsWindows)
        {
            $msg = 'PowerShell and .NET only support user-level environment variables on Windows.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $uninstallArgs = $PSBoundParameters
        [void]$uninstallArgs.Remove('Credential')
        [void]$uninstallArgs.Remove('Name')
        Start-Job -ScriptBlock {
                    Import-Module -Name (Join-Path -Path $using:moduleDirPath -ChildPath 'Carbon.Environment.psm1')
                    $VerbosePreference = $using:VerbosePreference
                    $ErrorActionPreference = $using:ErrorActionPreference
                    $DebugPreference = $using:DebugPreference
                    $WhatIfPreference = $using:WhatIfPreference
                    $InformationPreference = $using:InformationPreference
                    Uninstall-CEnvironmentVariable -Name $using:userEnvVarsToDelete -Scope User @using:uninstallArgs
                } -Credential $Credential |
            Receive-Job -Wait -AutoRemoveJob
    }
}



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)
        }
    }
}