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-CEnvVariable
{
    <#
    .SYNOPSIS
    Removes an environment variable or items from an environment variable that's a list.
 
    .DESCRIPTION
    The `Remove-CEnvVariable` function deletes environment variables or items from an environment variable that
    is a list.
 
    To delete an environment variable, 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.
 
    To delete an item from an environment variable that is a list (e.g. `PATH`, `PSModulePath`, etc.), pass the
    environment variable's name to the `Name` parameter and the items to remove from the environment variable's list to
    the `Item` parameter. If an item doesn't exist in the list, the function deletes items that are in the list and
    writes an error if any items to remove are missing. By default, creates the list by splitting the environment
    variable's value using `[IO.Path]::PathSeparator` (`;` on Windows, `:` on Linux and macOS). To use a different
    separator, pass it to the `Separator` parameter.
 
    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
    Set-CEnvVariable
 
    .LINK
    Test-CEnvVariable
 
    .LINK
    Uninstall-CEnvVariable
 
    .EXAMPLE
    Remove-CEnvVariable -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-CEnvVariable -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-CEnvVariable -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-CEnvVariable -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-CEnvVariable -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-CEnvVariable
 
    Demonstrates that you can pipe the environment variables to delete to `Remove-CEnvVariable`.
 
    .EXAMPLE
    Remove-CEnvVariable -Name 'PATH' -Item 'C:\Some\Obsolete\Path'
 
    Demonstrates how to remove items from an environment variable whose value is a list. In this example, the
    `C:\Some\Obsolete\Path` path is removed from the `PATH` enviornment variable.
 
    .EXAMPLE
    Remove-CEnvVariable -Name 'PATH' -Item 'C:\Some\Obsolete\Path','C:\Some\Other\Obsolete\Path'
 
    Demonstrates that you can pass multiple items to the `Item` parameter to remove multiple items from an environment
    variable.
 
    .EXAMPLE
    Remove-CEnvVariable -Name 'MyPipeVar' -Item 'a' -Separator '|'
 
    Demonstrates how to remove items from an environment variable whose value is a list that uses a custom separator. In
    this case the `MyPipeVar` environment variable is split using a `|` character, `a` is removed, the list is joined
    with `|` character, and the environment variable is set to the new value.
    #>

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

        # Items to remove from the environment variable.
        [Parameter(Mandatory, ParameterSetName='List_ForCurrentUser')]
        [Parameter(Mandatory, ParameterSetName='List_ForSpecificUser')]
        [String[]] $Item,

        # The separator for the items in the environment variable. Default is `[IO.Path]::PathSeparator`, `;` on
        # Windows, `:` on Linux and macOS.
        [Parameter(ParameterSetName='List_ForCurrentUser')]
        [Parameter(ParameterSetName='List_ForSpecificUser')]
        [String] $Separator,

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

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

        [Parameter(ParameterSetName='List_ForCurrentUser')]
        [Parameter(ParameterSetName='List_ForSpecificUser')]
        [switch] $Sensitive
    )

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

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

        $Scope = $Scope | Assert-Scope

        if (-not $Separator)
        {
            $Separator = [IO.Path]::PathSeparator
        }
    }

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

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

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

                    $currentItems = Split-CEnvVariable -Name $_name -Scope $_scope -Separator $Separator
                    $itemsMissing = $Item | Where-Object { $_ -notin $currentItems }
                    if ($itemsMissing)
                    {
                        $suffix = ''
                        if (($itemsMissing | Measure-Object).Count -gt 1)
                        {
                            $suffix = 's'
                        }

                        $itemsMsg = """$($itemsMissing -join $Separator)"" item${suffix}"
                        $thoseThe = 'those'
                        if ($Sensitive)
                        {
                            $itemsMsg = "sensitive item${suffix}"
                            $thoseThe = 'the'
                        }
                        $msg = "Failed to remove ${itemsMsg} from ${target} because ${thoseThe} item${suffix} do not " +
                               'exist.'
                        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    }

                    $itemsToRemove = $Item | Where-Object { $_ -in $currentItems }
                    $suffix = ''
                    if (($itemsToRemove | Measure-Object).Count -gt 1)
                    {
                        $suffix = 's'
                    }
                    $itemsMsg = "item${suffix} ""$($itemsToRemove -join $Separator)"""
                    if ($Sensitive)
                    {
                        $itemsMsg = "sensitive item${suffix}"
                    }

                    $newItems = $currentItems | Where-Object { $_ -notin $itemsToRemove }
                    $newValue = $newItems -join $Separator

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

                    Write-Information "Removing ${itemsMsg} from ${target}."
                    [Environment]::SetEnvironmentVariable($_name, $newValue, $_scope)
                    continue
                }

                if (-not (Test-CEnvVariable -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-CEnvVariable -Name $using:userEnvVars @using:parameters -Scope User
                } -Credential $Credential |
            Receive-Job -Wait -AutoRemoveJob
    }
}



function Set-CEnvVariable
{
    <#
    .SYNOPSIS
    Creates or sets an environment variable.
 
    .DESCRIPTION
    The `Set-CEnvVariable` 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.
 
    For environment variable's that are lists (e.g. `PATH`, `PSModulesPath`, etc.), `Set-CEnvVariable` can add
    items to the beginning or end of the list. Pass the item(s) to add to the list to the `Item` parameter. Any item not
    already in the list is added to the beginning. To append items instead, use the `Append` switch. By default, uses
    `[IO.Path]::PathSeparator` as the item separator. Use the `Separator` parameter to use a custom separator.
 
    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-CEnvVariable
 
    .LINK
    Test-CEnvVariable
 
    .LINK
    Uninstall-CEnvVariable
 
    .EXAMPLE
    Set-CEnvVariable -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-CEnvVariable -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-CEnvVariable -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-CEnvVariable -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-CEnvVariable -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-CEnvVariable -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, Position=0)]
        [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, ParameterSetName='Single_CurrentUser')]
        [Parameter(Mandatory, ParameterSetName='Single_ForSpecificUser')]
        [AllowEmptyString()]
        [String] $Value,

        # Adds an item in an environment variable this is a list of items.
        [Parameter(Mandatory, ParameterSetName='List_CurrentUser')]
        [Parameter(Mandatory, ParameterSetName='List_ForSpecificUser')]
        [String[]] $Item,

        # The separator between items in the list. Default is `[IO.Path]::PathSeparator` (`;` on Windows; `:` on Linux
        # and macOS).
        [Parameter(ParameterSetName='List_CurrentUser')]
        [Parameter(ParameterSetName='List_ForSpecificUser')]
        [String] $Separator,

        # When adding an item to an environment variable that is a list, add it to the end of the list. By default, it
        # is added to the beginning.
        [Parameter(ParameterSetName='List_CurrentUser')]
        [Parameter(ParameterSetName='List_ForSpecificUser')]
        [switch] $Append,

        # 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='Single_CurrentUser')]
        [Parameter(ParameterSetName='List_CurrentUser')]
        [EnvironmentVariableTarget[]] $Scope,

        [Parameter(Mandatory,ParameterSetName='List_ForSpecificUser')]
        [Parameter(Mandatory,ParameterSetName='Single_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 ($Credential)
    {
        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-CEnvVariable @using:parameters -Scope User
        } -Credential $Credential
        $job | Wait-Job | Receive-Job
        $job | Remove-Job -Force -ErrorAction Ignore
        return
    }

    $Scope = $Scope | Assert-Scope

    if (-not $Separator)
    {
        $Separator = [IO.Path]::PathSeparator
    }

    foreach ($_scope in $Scope)
    {
        $target = "$($_scope.ToString().ToLowerInvariant())-level environment variable ""${Name}"""
        $action = 'set'
        $actionMsg = 'Setting'

        # Are we adding an item to an environment variable that is a list?
        if ($Item)
        {
            $items = Split-CEnvVariable -Name $Name -Scope $_scope -Separator $Separator
            $itemsToAdd = $Item | Where-Object { $items -notcontains $_ }
            if (-not $itemsToAdd)
            {
                continue
            }

            if ($itemsToAdd)
            {
                $newItems = $itemsToAdd -join $Separator

                $location = 'beginning'
                if ($Append)
                {
                    $location = 'end'
                }

                $action = "add ""${newItems}"""
                $actionMsg = "Adding ""${newItems}"" to ${location} of"
                if ($Sensitive)
                {
                    $itemCount = ($itemsToAdd | Measure-Object).Count
                    $suffix = ''
                    if ($itemCount -gt 1)
                    {
                        $suffix = 's'
                    }
                    $action = "adding ${itemCount} item${suffix}"
                    $actionMsg = "Adding ${itemCount} item${suffix} to ${location} of"
                }

                $items = & {
                    if (-not $Append)
                    {
                        $newItems | Write-Output
                    }

                    $items | Write-Output

                    if ($Append)
                    {
                        $newItems | Write-Output
                    }
                }

                $Value = $items -join $Separator
            }
        }

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

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

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

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



function Split-CEnvVariable
{
    <#
    .SYNOPSIS
    Splits an environment variable value into a list.
 
    .DESCRIPTION
    The `Split-CEnvVariable` function splits an environment variable into a list. Pass the name of the
    environment variable to the `Name` parameter. By default, will split the environment variable using the
    `[IO.Path]::PathSeparator` character (`;` on Windows, `:` on Linux and macOS). Pass a custom separator to the
    `Separator` parameter.
 
    By default, splits process-level environment variables. To operate on user-level and computer-level environment
    variables, use the `Scope` parameter. Note that user-level and computer-level environment variables are only
    supported on Windows.
 
    If the environment variable doesn't exist, writes an error and returns an empty array.
 
    Environment variable names are case-insensitive on Windows and are case-sensitive on Linux and macOS.
 
    .EXAMPLE
    Split-CEnvVariable -Name 'PATH'
 
    Demonstrates how to split the current process's `PATH` environment variable using the `[IO.Path]::PathSeparator`.
 
    .EXAMPLE
    Split-CEnvVariable -Name 'PATH' -Scope Machine
 
    Demonstrates how to operate on a machine-level environment variable by passing `Machine` to the `Scope` parameter.
 
    .EXAMPLE
    Split-CEnvVariable -Name 'MyPipeVar' -Separator '|'
 
    Demonstrates how to split an environment variable using a custom separator.
    #>

    [CmdletBinding()]
    param(
        # The name of the environmen variable whose value to split. If the variable doesn't exist, writes an error.
        [Parameter(Mandatory)]
        [String] $Name,

        # The scope/level of environment variable to split. By default, uses process-level environment variables. Pass
        # `User` or `Machine` to split a user-level or machine-level environment variable.
        [EnvironmentVariableTarget] $Scope,

        # The string to use that separates items in the environment variable's values. By default, splits the
        # environment variable's value using `[IO.Path]::PathSeparator` (`;` on Windows, `:` on Linux and macOS).
        [String] $Separator
    )

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

    $Scope = $Scope | Assert-Scope

    if (-not (Test-CEnvVariable -Name $Name -Scope $Scope))
    {
        $msg = "Failed to split $($Scope.ToString().ToLowerInvariant())-level environment variable ""${Name}"" " +
               'it doesn''t exist.'
        Write-Error -Message $msg -ErrorAction Ignore
        return @()
    }

    $value = [Environment]::GetEnvironmentVariable($Name, $Scope)

    if ($null -eq $value)
    {
        return @()
    }

    if (-not $Separator)
    {
        $Separator = [IO.Path]::PathSeparator
    }

    return $value.Split($Separator, [StringSplitOptions]::None)
}



function Test-CEnvVariable
{
    <#
    .SYNOPSIS
    Tests if an environment variable exists or contains an item.
 
    .DESCRIPTION
    The `Test-CEnvVariable` function tests if an environment variable exists or, if an environment variable is a
    list, if the list contains an item.
 
    To check if an environment variable exits, 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`.
 
    To check if an item exists in an environment variable that is a list (e.g. `PATH`, `PSModulePath`, etc.), pass the
    name of the environment variable to the `Name` parameter and the item to check to the `Item` parameter. Splits the
    environment variable using `[IO.Path]::PathSeparator` (`;` on Windows, `:` on Linux and macOS) and returns true if
    the item is in that list. Returns false if the environment variable doesn't exist or doesn't have the item. Use the
    `Separator` parameter to use custom separator.
 
    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 user-level or computer-level
    environment variables.
 
    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.
 
    .LINK
    Remove-CEnvVariable
 
    .LINK
    Set-CEnvVariable
 
    .LINK
    Uninstall-CEnvVariable
 
    .EXAMPLE
    Test-CEnvVariable -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-CEnvVariable -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-CEnvVariable
 
    Demonstrates that you can pipe environment variable names to `Test-CEnvVariable`.
 
    .EXAMPLE
    Test-CEnvVariable -Name 'PATH' -Item 'C:\Some\path'
 
    Demonstrates how to test if an environment variable that is a list contains an item. In this example, tests if the
    `PATH` environment variable contains `C:\Some\path`.
    #>

    [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 item in the environment variable to test for.
        [String] $Item,

        # The separator to use to split the environment variable into a list. By default, uses
        # `[IO.Path]::PathSeparator` (`;` on Windows, `:` on Linux and macOS).
        [String] $Separator,

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

        # The credential of the user whose user-level environment variables to check.
        [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
        }

        if ($Item)
        {
            return $Item -in (Split-CEnvVariable -Name $Name -Scope $validScope -Separator $Separator)
        }

        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-CEnvVariable @using:parameters -Scope User
            } -Credential $Credential |
            Receive-Job -Wait -AutoRemoveJob |
            Write-Output
    }
}


function Uninstall-CEnvVariable
{
    <#
    .SYNOPSIS
    Removes an environment variable or an item from an environment variable, if it exists.
 
    .DESCRIPTION
    The `Uninstall-CEnvVariable` function deletes environment variables or items from an environment variable.
    When deleting an environment variable, ignores if the environment variable no longer exists. When deleting an item
    from an environment variables, ignores if the item is no longer in the environment variable.
 
    To delete environment variables, pass their names to the `Name` parameter (or pipe in the names). Each environment
    variable that exists is deleted.
 
    To delete an item from an environment variable that is a list (e.g. `PATH`, `PSModulePath`, etc.), pass the name of
    the environment variable to the `Name` parameter, and the items to remove from the environment variable to the
    `Item` parameter. Each item that exists in the environment variable is removed. By default, the environment variable
    is split using `[IO.Path]::PathSeparator` (`;` on Windows, `:` on Linux and macOS). Pass a custom separator to the
    `Separator` parameter.
 
    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-CEnvVariable
 
    .LINK
    Set-CEnvVariable
 
    .LINK
    Test-CEnvVariable
 
    .EXAMPLE
    Uninstall-CEnvVariable -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-CEnvVariable -Name 'MyUserVariable' -Scope User
 
    Demonstrates how to remove a user-level environment by including `User` in the list of scopes.
 
    .EXAMPLE
    Uninstall-CEnvVariable -Name 'MyComputerVariable' -Scope Machine
 
    Demonstrates how to remove a computer-level environment by including `Machine` in the list of scopes.
 
    .EXAMPLE
    Uninstall-CEnvVariable -Name 'MyComputerVariable' -Scope User,Machine
 
    Demonstrates that you can pass multiple scopes to the `Scope` parameter.
 
    .EXAMPLE
    Uninstall-CEnvVariable -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-CEnvVariable
 
    Demonstrates that you can pipe names to `Uninstall-CEnvVariable`.
 
    .EXAMPLE
    Uninstall-CEnvVariable Name 'Var1','Var2'
 
    Demonstrates that you can pass an array of names to the `Name` parameter.
 
    .EXAMPLE
    Uninstall-CEnvVariable -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.
 
    .EXAMPLE
    Uninstall-CEnvVariable -Name 'PATH' -Item 'C:\Some\Obsolete\Path'
 
    Demonstrates how to remove items from an environment variable whose value is a list. In this example, the
    `C:\Some\Obsolete\Path` path is removed from the `PATH` enviornment variable.
 
    .EXAMPLE
    Uninstall-CEnvVariable -Name 'PATH' -Item 'C:\Some\Obsolete\Path','C:\Some\Other\Obsolete\Path'
 
    Demonstrates that you can pass multiple items to the `Item` parameter to remove multiple items from an environment
    variable.
 
    .EXAMPLE
    Uninstall-CEnvVariable -Name 'MyPipeVar' -Item 'a' -Separator '|'
 
    Demonstrates how to remove items from an environment variable whose value is a list that uses a custom separator. In
    this case the `MyPipeVar` environment variable is split using a `|` character, `a` is removed, the list is joined
    with `|` character, and the environment variable is set to the new value.
    #>

    [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,

        # Items to remove from the environment variable. By default, the entire environment variable is removed if it
        # exists. If one or more items are specified, the environment variable's value is split using
        # `[IO.Path]::PathSeparator` (`;` on Windows, `:` on Linux and macOS), and each item in the list is removed. The
        # list is joined with the path separator, and the environment variable's value is set.
        #
        # Use the `Separator` parameter to customize the separator to use use.
        [String[]] $Item,

        # The separator for items in the list. Ignored unless `Item` has a value.
        [String] $Separator,

        # If set and removing items from an environment variable's value, omits the values being removed from
        # information messages.
        [switch] $Sensitive,

        # 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

        if (-not $PSBoundParameters.ContainsKey('Separator'))
        {
            $Separator = [IO.Path]::PathSeparator
        }
    }

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

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

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

                if ($Item)
                {
                    $currentItems = Split-CEnvVariable -Name $_name -Scope $_scope -Separator $Separator
                    $itemsToRemove = $currentItems | Where-Object { $_ -in $Item }
                    if (-not $itemsToRemove)
                    {
                        continue
                    }

                    $newItems = $currentItems | Where-Object { $_ -notin $itemsToRemove }
                    $newValue = $newItems -join $Separator
                    $itemsToRemoveMsg = $itemsToRemove -join $Separator
                    $infoItemsMsg = 'items'
                    $targetItemsMsg = ''
                    if (-not $Sensitive)
                    {
                        $infoItemsMsg = """${itemsToRemoveMsg}"""
                        $targetItemsMsg = " '${itemsToRemoveMsg}'"
                    }

                    $suffix = ''
                    if (($itemsToRemove | Measure-Object).Count -gt 1)
                    {
                        $suffix = 's'
                    }

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

                    Write-Information "Removing ${infoItemsMsg} from ${target}."
                    [Environment]::SetEnvironmentVariable($_name, $newValue, $_scope)
                    continue
                }

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