Reputation: 5877
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.
Upvotes: 12
Views: 17068
Reputation: 437428
Preface:
See the middle section for helper function Add-Path
See the bottom section for why use of setx.exe
should be avoided for updating the Path
environment variable.
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:
Brute-force workaround - simple, but visually disruptive and closes all open File Explorer windows:
# Kills all explorer.exe processes, which restarts the Windows shell
# components, forcing a reload of the environment from the registry.
Stop-Process -Name explorer
Workaround via .NET APIs:
# Create a random name assumed to be unique
[string] $dummyName = New-Guid
# Set an environment variable by that name, which makes .NET
# send a WM_SETTINGCHANGE broadcast
[Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
# Now that the dummy variable has served its purpose, remove it again.
# (This will trigger another broadcast, but its performance impact is negligible.)
[Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
[Environment]::SetEnvironmentVariable()
to directly update the PATH
variable is not a proper solution as of .NET 8 - though you may get away with it.Workaround by calling the Windows API via an ad hoc-compiled P/Invoke call to SendMessageTimeout()
in C#, via Add-Type
:
While this is a proper solution, it invariably incurs a noticeable performance penalty due to the ad hoc-compilation the first time it is run in a session.
For details, see this blog post.
The approach in the blog post mentioned in the question has another problematic aspect:
Get-ItemProperty
and Get-ItemPropertyValue
invariably do. That is, if directories in the value are defined in terms of other environment variables (e.g., %SystemRoot%
or %JAVADIR%
), the returned value no longer contains these variables, but their current values. See the bottom section for why this can be problematic.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:
Adds (appends) a given, single directory path to the persistent user-level Path
environment variable by default; use -Scope Machine
to target the machine-level definition, which requires elevation (run as admin).
If the directory is already present in the target variable, no action is taken.
The relevant registry value is updated (rather than recreated), which preserves its REG_EXPAND_SZ
data type, based on the existing unexpanded value - that is, references to other environment variables are preserved as such (e.g., %SystemRoot%
), and may also be used in the new entry being added.
Triggers a WM_SETTINGCHANGE
message broadcast to inform the Windows shell of the change.
Also updates the current session's $env:Path
variable value.
It is unfortunate that PowerShell doesn't ship with this functionality; a previous discussion about providing a cmdlet for robustly and selectively updating $env:PATH
has gone nowhere - see the abandoned GitHub RFC #92 (which covered providing cmdlets for managing persistent environment variables in general).[1]
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."
}
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
:
Values are limited to 1024 characters, with additional ones getting truncated, albeit with a warning (as of at least Windows 10).
The environment variable that is (re)created is with type REG_SZ
if the new value happens NOT to contain environment-variable references such as %SystemRoot%
.[2] However, Path
is originally of type REG_EXPAND_SZ
and indeed contains directory paths based on other environment variables, such as %SystemRoot%
and %JAVADIR%
.
%JAVADIR%
will stop working if the value of %JAVADIR%
is later changed. Similarly, if you later add an entry that is expressed in terms of other environment-variables, such entries will not be expanded.Additionally, if you base the updated value on the current session's $env:Path
value, you'll end up duplicating entries, because the process-level $env:Path
value is a composite of the machine-level and current-user-level values.
This increases the risk of running into the 1024-character limit, especially if the technique is used repeatedly. It also bears the risk of duplicate values lingering after the original entry is removed from the original scope.
While you can avoid this particular problem by retrieving the scope-specific value either directly from the registry or - invariably in expanded form - via [Environment]::GetEnvironmentVariable('Path', 'User')
or [Environment]::GetEnvironmentVariable('Path', 'Machine')
, that still doesn't solve the REG_EXPAND_SZ
problem discussed above.
[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.
Upvotes: 28
Reputation: 104514
Use setx to permanently update an environment variable. Don't hack the registry.
After you invoke setx, just update the Path environment manually in the current session. Powershell: Reload the path in PowerShell
Upvotes: -2