Private/Update-ProcessPath.ps1

function Update-ProcessPath {
    <#
        .SYNOPSIS
        Brings this process's PATH up to date with the machine and user PATH.
 
        .DESCRIPTION
        A step that installs a manager writes to the machine or user PATH in the
        registry, and a process keeps the PATH it started with. Without this, a
        tool installed by one step is found by the next run rather than the next
        step.
 
        The result is the process-only entries first, in their existing order,
        then the machine entries, then the user entries, each once. Process-only
        entries are the ones neither hive lists: an activated virtual environment,
        a per-session prepend. They keep their place at the front so what they
        shadow stays shadowed.
 
        Returns the entries that were in a hive and not in the process PATH
        before, so the caller can say what the run gained. Returns nothing, and
        changes nothing, when neither hive could be read.
 
        .PARAMETER MachinePath
        The machine PATH, unexpanded. Read from the registry when not given.
 
        .PARAMETER UserPath
        The user PATH, unexpanded. Read from the registry when not given.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Changes only this process''s PATH, to match what the registry already says. Nothing on the machine is touched, and the run that calls it reports every entry gained.')]
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [string] $MachinePath,
        [string] $UserPath
    )

    # DoNotExpandEnvironmentNames, then expand: Get-ItemProperty expands the
    # REG_EXPAND_SZ value itself, but against this process's environment, which is
    # the thing being brought up to date.
    $read = {
        param($Key)
        try {
            $item = Get-Item -LiteralPath $Key -ErrorAction Stop
            [string] $item.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
        } catch {
            Write-Verbose "Could not read PATH from ${Key}: $($_.Exception.Message)"
            ''
        }
    }
    if (-not $PSBoundParameters.ContainsKey('MachinePath')) {
        $MachinePath = & $read 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
    }
    if (-not $PSBoundParameters.ContainsKey('UserPath')) {
        $UserPath = & $read 'HKCU:\Environment'
    }
    if ([string]::IsNullOrWhiteSpace($MachinePath) -and [string]::IsNullOrWhiteSpace($UserPath)) {
        return
    }

    $split = {
        param($Text)
        @(([Environment]::ExpandEnvironmentVariables([string] $Text) -split ';') |
            ForEach-Object { $_.Trim() } |
            Where-Object { $_ })
    }
    $before = @(& $split $env:PATH)
    $hive   = @(& $split $MachinePath) + @(& $split $UserPath)

    # Trailing separators are the usual reason one directory appears as two.
    $key = { param($Entry) $Entry.TrimEnd('\', '/') }
    $comparer = [System.StringComparer]::OrdinalIgnoreCase

    $inHive = [System.Collections.Generic.HashSet[string]]::new($comparer)
    foreach ($entry in $hive) { $null = $inHive.Add((& $key $entry)) }
    $inBefore = [System.Collections.Generic.HashSet[string]]::new($comparer)
    foreach ($entry in $before) { $null = $inBefore.Add((& $key $entry)) }

    $seen = [System.Collections.Generic.HashSet[string]]::new($comparer)
    $ordered = [System.Collections.Generic.List[string]]::new()
    foreach ($entry in $before) {
        if (-not $inHive.Contains((& $key $entry)) -and $seen.Add((& $key $entry))) { $ordered.Add($entry) }
    }
    $gained = [System.Collections.Generic.List[string]]::new()
    foreach ($entry in $hive) {
        if ($seen.Add((& $key $entry))) {
            $ordered.Add($entry)
            if (-not $inBefore.Contains((& $key $entry))) { $gained.Add($entry) }
        }
    }

    $env:PATH = $ordered -join ';'
    $gained
}