1

What specific syntax must be changed in the PowerShell 5 script below in order to successfully add $HOME\\myapphome\\ to the system PATH in a Windows 11 server?

The following script named .\addMyPath.ps1 successfully prints out the correct value for myapp version and also prints out the correct value for Write-Output $newpath including $HOME\\myapphome\\ when the following script is run as .\addMyPath.ps1.

$env:Path += ";$HOME\\myapphome\\"
Write-Output "About to prepend myapp to path permanently. "
$aloc="$HOME\\myapphome\\"
$oldpath = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).path
$newpath = "$aloc;$oldpath"
Write-Output "About to print newpath "
Write-Output $newpath
Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $newpath
echo "About to print myapp cli version command output: "
myapp version

But when we close that PowerShell window and then open a new PowerShell window, we get the following error, which indicates that myapp was not permanently added to the PATH.

PS C:\Users\Administrator> myapp version
myapp : The term 'myapp' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ myapp version
+ ~~~
    + CategoryInfo          : ObjectNotFound: (myapp:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException
PS C:\Users\Administrator>

Similarly, running $env:PATH from the new PowerShell window also fails to include $HOME\\myapphome\\ in the results.

CodeMed
  • 9,527
  • 70
  • 212
  • 364

1 Answers1

1

tl;dr

There are two problems with your code, which the snippet below corrects:

  • After modifying the persistent definition of the PATH environment variable, you forgot to notify the Windows shell of the update.

  • By using Get-ItemProperty to retrieve the old persisted value from the registry and by basing the new persisted value on that, you inadvertently converted the expandable old value to a static one.

$aloc="$HOME\myapphome"

# Update the in-process PATH value.
$env:Path += ";$aloc"

"About to prepend myapp to path permanently. "
# Get the *unexpanded* value of the machine-level PATH environment variable 
# from the registry.
$oldpath = (Get-Item 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment').GetValue('Path', $null, 'DoNotExpandEnvironmentNames')
$newpath = "$aloc;$oldpath"

"About to print newpath "
$newpath
Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $newpath
echo "About to print myapp cli version command output: "
myapp version

# Now that the registry has been updated, the Windows shell must be
# notified of the update.
# A non-disruptive approach is to create and delete a persistent dummy variable 
# via [Environment]::SetEnvironmentVariable().
[string] $dummyEnvVarName = New-Guid
[Environment]::SetEnvironmentVariable($dummyEnvVarName, 'dummy', 'User')
[Environment]::SetEnvironmentVariable($dummyEnvVarName, $null, 'User')

Note:

  • Read on for an explanation and background information.

  • For a custom convenience function (Add-Path) that robustly updates the persistent PATH environment variable (in either scope) with automatic in-process updating and notification of the Windows shell, see this answer.


Explanation and background information:

  • As an aside (the problem is benign, because duplicated path separators are quietly tolerated):

    • \ has no special meaning to PowerShell, so it never needs escaping as \\ (see this answer for background information).
  • If you modify environment variables via the registry, you need to ensure that the Windows shell is notified of the update. (The Windows shell is the system GUI component that comprises the Start Menu, the Desktop, the taskbar, and File Explorer.)

    • After such a notification, new processes launched via the Window shell see the modified environment (as opposed to new processes launched from preexisting processes that predate the modification, in which case they inherit this old environment).

Because updating the registry with Set-ItemProperty does not perform this notification, your newly launched processes didn't see the modification.

Solution options:

  • A simple, but visually disruptive solution that also makes you lose the state of your File Explorer windows is to forcefully terminate all explorer processes, which forces the Windows shell to reload, in the course of which the updated environment-variable definitions are read from the registry:

    Stop-Process -Name explorer 
    
  • A less disruptive, but more cumbersome solution is to rely on the fact that [Environment]::SetEnvironmentVariable() does implicitly perform this notification, and that that it causes the Windows shell to reload all environment variables.

    # Create and delete a persistent dummy variable via
    # [Environment]::SetEnvironmentVariable(), which causes the 
    # Windows shell to reload the environment.
    [string] $dummyEnvVarName = New-Guid
    [Environment]::SetEnvironmentVariable($dummyEnvVarName, 'dummy', 'User')
    [Environment]::SetEnvironmentVariable($dummyEnvVarName, $null, 'User')
    
    • As discussed next, directly using [Environment]::SetEnvironmentVariable() to update REG_EXPAND_SZ-based environment variables is unfortunately currently not an option.
  • As of .NET 8, direct use of [Environment]::SetEnvironmentVariable() is only safe for static persistent environment variables (stored in the registry as REG_SZ), not for expandable ones such as PATH (stored as REG_EXPAND_SZ):

    # !! AVOID for REG_EXPAND_SZ env. vars. such as PATH
    # !! This informs the shell of an updated environment,
    # !! but converts *expandable* (REG_EXPAND_SZ-based_
    # !! environment variables to *static* ones (REG_SZ).
    [Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine')
    
    • The PATH environment variables are originally created as registry values of type REG_EPXAND_SZ, which means that they may be defined in terms of other environment variables. E.g., the unexpanded value of the TEMP user-level variable in the registry is %USERPROFILE%\AppData\Local\Temp.

    • As of .NET 8, [Environment]::SetEnvironmentVariable() does not support REG_EPXAND_SZ environment variables and potentially corrupts them - see next section.


Incomplete support for managing persistent environment variables in .NET and PowerShell:

  • The .NET environment-variable APIs have incomplete and potentially destructive support for REG_EXPAND_SZ environment variables (which PATH is a notable example of), as of .NET 8:

    • [Environment]::GetEnvironmentVariable() invariably returns the already expanded value of a given REG_EXPAND_SZ environment variable. While that is appropriate for using the variable value, it isn't for updating it, because in the latter case you want to modify the unexpanded value.

    • What's worse, [Environment]::SetEnvironmentVariable() invariably (re)creates REG_SZ values, i.e. non-expandable environment variables, so if you use this method to update Path, the latter becomes a static value.

      • While replacing an expandable value with a statically expanded one may or may not cause problems later, you will break things if you set an unexpanded value (because the embedded environment-variable references then won't get expanded).
    • This problematic behavior is the subject of GitHub issue #1442 - unfortunately, no action has been taken since 2019, when the issue was reported.

    • The workaround is to use the registry .NET APIs instead, which, however, is not only more cumbersome, but also requires manually notifying the Windows shell of the update.

  • On the PowerShell side, support is incomplete too, as of PowerShell (Core) 7.3.6:

    • In terms of PowerShell-native environment-variable features:

      • Updating the persistent definitions of environment variables is unsupported.

      • The Env: drive and its associated namespace variable notation (e.g., $env:PATH) support querying the expanded, in-process values only.

      • A previous discussion about providing cmdlets that manage persistent environment variables has not (yet) led to a solution that ships with PowerShell itself. A prototype implementation of new cmdlets is available via the PowerShell Gallery, but it seems to languish and, most importantly, it too lacks support for REG_EXPAND_SZ environment variables.

    • In terms of PowerShell-native registry features:

      • Get-ItemProperty and Get-ItemPropertyValue also currently invariably return the expanded value.

        • However, a future improvement to allow requesting the unexpanded value has been green-lit, but no one has stepped up to implement it yet - see GitHub issue #16812.

        • For now, to get the unexpanded value you must use the .NET registry APIs directly, though you can combine them with Get-Item:

           (Get-Item `
              'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment'
           ).GetValue('Path', $null, 'DoNotExpandEnvironmentNames')
          
      • On the plus side, Set-ItemProperty does implicitly preserve an existing REG_EXPAND_SZ registry value as such on updating (to create a value as such, use -Type ExpandString).

mklement0
  • 382,024
  • 64
  • 607
  • 775