使用PowerShell将路径添加到Windows永久性地似乎不起作用。

9

我按照此过程,通过powershell将路径添加到SumatraPDF,以实现永久性的更改。链接中的最后几个命令旨在检查是否确实已添加了路径。

当我使用以下命令访问路径时,

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

结果包括到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

然而,当我使用以下命令访问它时,

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

结果中不包含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

最后,实际传递sumatrapdf并没有起作用,这表明真正的路径是使用get-itemproperty命令访问的路径。

为什么注册表中设置的路径与在$env:path中设置的路径不对应?我在链接中遵循的步骤有误吗?我该如何纠正它?

我应该提到,我已经尝试重新启动shell,但没有帮助。

2个回答

20
注意:
请参考中间部分的“辅助函数Add-Path”。
请参考底部部分,了解为什么应避免使用setx.exe来更新Path环境变量。
链接的博客文章中的步骤原则上是有效的,但缺少了一个关键的信息/额外的步骤: 如果您直接通过注册表修改环境变量 - 不幸的是,这是对于基于REG_EXPAND_SZ的环境变量(如Path)来说是正确的方法 - 您需要发送一个WM_SETTINGCHANGE消息,以便通知Windows(GUI)shell(及其组件,文件资源管理器,任务栏,桌面,开始菜单,所有由explorer.exe进程提供)有关环境更改的信息,并从注册表重新加载其环境变量。然后启动的应用程序将继承更新后的环境。

  • 如果未发送此消息,则未来的PowerShell会话(和其他应用程序)在下次登录/重启之前都无法看到修改。
很遗憾,从PowerShell没有直接的方法来做到这一点,但有一些变通方法:
  • Brute-force workaround - simple, but visually disruptive and closes all open File Explorer windows:

    # 杀死所有的 explorer.exe 进程,这将重新启动 Windows shell
    # 组件,强制从注册表重新加载环境。
    Stop-Process -Name explorer
    
  • 通过.NET API的解决方法:

    # 创建一个假定为唯一的随机名称
    [string] $dummyName = New-Guid
    # 通过该名称设置一个环境变量,这会导致 .NET 发送 WM_SETTINGCHANGE 广播
    [Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
    # 现在假变量已经完成了它的目的,再次删除它。
    # (这将触发另一个广播,但其性能影响可以忽略不计。)
    [Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
    
    • 注意:这个答案解释了为什么使用 [Environment]::SetEnvironmentVariable() 直接更新 PATH 变量作为 .NET 8 的选项。
  • 通过调用 Windows API来绕过,通过 C# 中的临时编译的 P/Invoke 调用 SendMessageTimeout(),通过 Add-Type

    • 虽然这是一个正确的解决方案,但由于在会话中第一次运行时需要进行临时编译,它总是会产生明显的性能损耗

    • 详细信息请参阅 此博客文章

博客文章中的方法有另一个问题

  • 它从注册表中检索扩展的环境变量值,因为Get-ItemPropertyGet-ItemPropertyValue总是这样做。也就是说,如果值中的目录是用其他环境变量(例如%SystemRoot%%JAVADIR%)定义的,则返回的值不再包含这些变量,而是它们的当前值。请参阅底部部分以了解为什么这可能会有问题。

下一节讨论的辅助函数解决了所有问题,并确保修改对当前会话也生效。


以下是Add-Path助手函数:
  • 默认情况下,将给定的单个目录路径添加(附加)到持久的用户级别Path环境变量中;使用-Scope Machine来定位机器级别的定义,这需要提权(以管理员身份运行)。

    • 如果目录已经存在于目标变量中,则不采取任何操作。

    • 相关的注册表值会被更新,它保留了其REG_EXPAND_SZ数据类型,基于现有的未展开值——也就是说,对其他环境变量的引用被保留为原样(例如,%SystemRoot%),并且可以在新添加的条目中使用。

  • 触发一个WM_SETTINGCHANGE消息广播,通知Windows shell发生了变化。

  • 同时更新当前会话的$env:Path变量值。

  • 很遗憾的是,PowerShell没有内置此功能;之前讨论过为稳健且可选择地更新$env:PATH提供一个cmdlet的问题没有进展——请参阅被放弃的GitHub RFC #92(其中涵盖了一般持久环境变量管理的cmdlet)。[1]

注意:根据定义(由于使用注册表),此函数仅适用于Windows。

通过下面定义的函数,您可以执行所需的Path添加操作,修改当前用户的持久性Path定义:

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

如果你真的想要更新机器级别的定义(在HKEY_LOCAL_MACHINE注册表中,这与用户特定路径不符合逻辑),请添加-Scope Machine,但是请注意你必须以提权方式运行(作为管理员)。
Add-Path源代码:
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的局限性以及为什么不应该用它来更新Path环境变量:

setx.exe存在根本性的限制,使其在更新基于REG_EXPAND_SZ类型的注册表值(如Path)的环境变量时存在问题:

  • 值被限制在1024个字符以内,超出部分将被截断,尽管会有警告(至少适用于Windows 10)。

  • 如果新值不包含环境变量引用(例如%SystemRoot%),则重新创建的环境变量类型为REG_SZ。[2] 然而,Path最初是REG_EXPAND_SZ类型,并且确实包含基于其他环境变量(如%SystemRoot%和%JAVADIR%)的目录路径。

    • 如果新值只包含文字路径(没有环境变量引用),可能不会立即产生任何负面影响。但是,如果原本依赖于%JAVADIR%的条目稍后停止工作,则会出现问题,如果%JAVADIR%的值稍后更改。同样地,如果您稍后添加一个以其他环境变量表示的条目,这些条目将不会被展开。
  • 此外,如果您基于当前会话的$env:Path值来更新值,您将会复制条目,因为进程级别的$env:Path值是机器级别和当前用户级别值的组合。

    • 这增加了遇到1024字符限制的风险,特别是如果该技术被重复使用。它还存在着原始条目从原始范围中删除后,重复值仍然存在的风险。

    • 虽然您可以通过直接从注册表检索特定范围的值或通过[Environment]::GetEnvironmentVariable('Path', 'User')或[Environment]::GetEnvironmentVariable('Path', 'Machine')以展开的形式获取该值来避免此特定问题,但这仍然无法解决上述讨论的REG_EXPAND_SZ问题。


[1] 通过原型实现,可以使用PowerShell Gallery来管理持久环境变量的新命令。然而,目前它似乎不受关注,并且关键是缺乏对可扩展环境变量(REG_EXPAND_SZ)的支持,这使得它不适用于PATH更新。

[2] 换句话说,setx.exe根据新值中是否存在环境变量引用(如%SystemRoot%)来决定是否(重新)创建基础注册表值为REG_SZ(静态)或REG_EXPAND_SZ(可扩展),而不考虑预先存在的注册表值的当前类型。


1
我建议你们将这个功能作为PowerShell的标准功能添加进去。我们不应该考虑所有这些底层技巧,来执行简单的用户或系统路径添加操作,这是没有意义的。(有什么阻止你们这样做的吗?) - not2qubit
1
@not2qubit,我完全同意,但不幸的是,以前的讨论没有结果,而且我对于 PowerShell 本身添加了什么或者不添加什么毫无发言权(每个人都可以通过 PowerShell Gallery 贡献安装-on-demand 模块)。请参见https://github.com/PowerShell/PowerShell-RFC/pull/92#issuecomment-429451171。请注意,讨论的背景是提供管理持久化环境变量的 cmdlet,这也从未实现;我强烈感觉不需要新的 cmdlet,并且现有的 * -Item 可以增强。 - mklement0
1
很遗憾,因为您的“Update-PathVariable”建议本来是一个非常好的具体且广泛使用的补充,可以扩展更一般(且直观模糊)的“*-Item”扩展,尽管任何添加的功能都比当前状态更好。通过扩展您上面的代码,并包括一个更新的refreshenv版本即可。 - not2qubit
1
感谢@not2qubit。不幸的是,不能直接使用System.Environment] :: SetEnvironmentVariable('PATH',$ SPATH,[System.EnvironmentVariableTarget] :: Machine),因为它将无论如何创建一个REG_SZ值,而保留REG_EXPAND_SZ值以及带有对其他环境变量的嵌入引用的条目非常重要-因此需要进行直接注册表操作。 - mklement0
1
请请请让它变得标准! - Daniel Williams
显示剩余3条评论

-2

我用 setx 命令使其正常工作,命令如下:setx PATH "$env:path;C:\Users\921479\AppData\Local\SumatraPDF"。我猜想那个编写我遵循的过程的人基本上不知道他们在说什么。 - user32882
3
使用 setx.exe 是很诱人的,但应该避免使用,因为 setx.exe 具有根本性的限制,这使得它成为一个问题,特别是它不支持更新基于 REG_EXPAND_SZ 类型的注册表值的环境变量,如 Path,而且通常不支持超过 1024 个字符的值。 - mklement0
1
回顾来看,@mklement0的答案显然更加完整。 - user32882

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接