windowspowershellenvironment-variables

Adding path permanently to windows using powershell doesn't appear to work


I followed this procedure in order to permanently add a path to SumatraPDF using powershell. The last few commands from the link are meant to check that the path has indeed been added.

When I access the path using the following command,

(get-itemproperty -path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path.split(';')

the result includes the path to SumatraPDF

C:\Windows\system32
C:\Windows
C:\Windows\System32\Wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\OpenSSH\
C:\ProgramData\chocolatey\bin
C:\texlive\2021\bin\win32
C:\Users\921479\AppData\Local\SumatraPDF

However when I access it using the following command,

($env:path).split(';')

the result does not contain the path to SumatraPDF:

C:\Windows\system32
C:\Windows
C:\Windows\System32\Wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\OpenSSH\
C:\ProgramData\chocolatey\bin
C:\texlive\2021\bin\win32
C:\Users\921479\AppData\Local\Microsoft\WindowsApps

Finally, actually passing sumatrapdf does not works, which indicates to me that the real path is the one accessed using the get-itemproperty command.

Why does the path set in the registry not correspond to the one set in $env:path? Is there a mistake in the procedure shown in the link I followed? How can I correct it?

I should mention I have already tried restarting the shell but it doesn't help.


Solution

  • Preface:


    The procedure in the linked blog post is effective in principle, but is missing a crucial piece of information / additional step:

    If you modify environment variables directly via the registry - which, unfortunately, is the right way to do it for REG_EXPAND_SZ-based environment variables such as Path - you need to broadcast a WM_SETTINGCHANGE message so that the Windows (GUI) shell (and its components, File Explorer, the taskbar, the desktop, the Start Menu, all provided via explorer.exe processes) is notified of the environment change and reloads its environment variables from the registry. Applications launched afterwards then inherit the updated environment.

    Unfortunately, there's no direct way to do this from PowerShell, but there are workarounds:

    The approach in the blog post mentioned in the question has another problematic aspect:

    The helper function discussed in the next section addresses all issues, while also ensuring that the modification takes effect for the current session too.


    The following Add-Path helper function:

    Note: By definition (due to use of the registry), this function is Windows-only.

    With the function below defined, your desired Path addition could be performed as follows, modifying the current user's persistent Path definition:

    Add-Path C:\Users\921479\AppData\Local\SumatraPDF
    

    If you really want to update the machine-level definition (in the HKEY_LOCAL_MACHINE registry hive, which doesn't make sense with a user-specific path), add -Scope Machine, but note that you must then run with elevation (as admin).

    Add-Path source code:

    function Add-Path {
    
      param(
        [Parameter(Mandatory, Position=0)]
        [string] $LiteralPath,
        [ValidateSet('User', 'CurrentUser', 'Machine', 'LocalMachine')]
        [string] $Scope 
      )
    
      Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop'
    
      $isMachineLevel = $Scope -in 'Machine', 'LocalMachine'
      if ($isMachineLevel -and -not $($ErrorActionPreference = 'Continue'; net session 2>$null)) { throw "You must run AS ADMIN to update the machine-level Path environment variable." }  
    
      $regPath = 'registry::' + ('HKEY_CURRENT_USER\Environment', 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment')[$isMachineLevel]
    
      # Note the use of the .GetValue() method to ensure that the *unexpanded* value is returned.
      $currDirs = (Get-Item -LiteralPath $regPath).GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split ';' -ne ''
    
      if ($LiteralPath -in $currDirs) {
        Write-Verbose "Already present in the persistent $(('user', 'machine')[$isMachineLevel])-level Path: $LiteralPath"
        return
      }
    
      $newValue = ($currDirs + $LiteralPath) -join ';'
    
      # Update the registry.
      Set-ItemProperty -Type ExpandString -LiteralPath $regPath Path $newValue
    
      # Broadcast WM_SETTINGCHANGE to get the Windows shell to reload the
      # updated environment, via a dummy [Environment]::SetEnvironmentVariable() operation.
      $dummyName = [guid]::NewGuid().ToString()
      [Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
      [Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
    
      # Finally, also update the current session's `$env:Path` definition.
      # Note: For simplicity, we always append to the in-process *composite* value,
      #        even though for a -Scope Machine update this isn't strictly the same.
      $env:Path = ($env:Path -replace ';$') + ';' + $LiteralPath
    
      Write-Verbose "`"$LiteralPath`" successfully appended to the persistent $(('user', 'machine')[$isMachineLevel])-level Path and also the current-process value."
    
    }
    

    The limitations of setx.exe and why it shouldn't be used to update the Path environment variable:

    setx.exe has fundamental limitations that make it problematic, particularly for updating environment variables that are based on REG_EXPAND_SZ-typed registry values, such as Path:


    [1] A prototype implementation of new cmdlets for managing persistent environment variables is available via the PowerShell Gallery, though it seems to languish and, crucially, lacks support for expandable environment variables (REG_EXPAND_SZ) as of this writing, which makes it unsuitable for PATH updates.

    [2] That is, setx.exe decides whether to (re)create the underlying registry value as REG_SZ (static) or REG_EXPAND_SZ purely based on the presence of environment-variable references such as %SystemRoot% in the new value - irrespective of a preexisting registry value's current type.