本文是PowerShell模组 PSComputerManagementZp 的使用案例分享。记录了通过该自定义模组,管理多个层级$Env:Path的过程。包括添加、删除、去重等。本文的案例非常适合于使用PowerShell进行系统管理,工具调用等行为的场景。

基本概念与动机

环境变量环境变量是一个动态命名,可以影响计算机上进程的行为方式。例如一个正在运行的进程可以查询TEMP环境变量的值,以发现一个合适的位置来存储临时文件,或者查询HOME或USERPROFILE变量,以找到运行该进程的用户所拥有的目录结构

$Env:Path: PATH类Unix系统DOSOS/2Microsoft Windows操作系统上的一个环境变量,用于设置一组包含可执行文件目录

在所有的环境变量中,我们关心的最多的,一般就是$Env:Path,因为其涉及诸多软件工具之间的相互配合与调用流程。当我们使用PowerShell等命令工具(或者说是shell)还安装使用 Miniconda, MSYS2 等带有包管理器,实现独立的包管理模式 的工具时,$Env:Path 将可能给我们带来一些不便之处。

Windows 系统环境变量的三个层级

参考 EnvironmentVariableTarget Enum (System),在Windows系统中,有三个级别的环境变量,分别是 Machine级别 、Process 级别和User 级别。如果将“控制和管理Windows系统”的过程抽象为“用户通过一定的接口与行为模式,与系统进行交互”,那么Windows系统的特殊性就在于,通过GUI和通过命令行,都可以实现大部分的用户需求。具体到环境变量时,我们至少有三种用户层面的控制环境变量的方案:

  1. 通过注册表(registry)编辑器GUI控制:修改和存储 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\EnvironmentHKEY_CURRENT_USER\Environment 的记录值。
  2. 通过Windows系统设置GUI控制:打开 设置>系统>系统信息>高级系统设置>环境变量 进行修改和存储。
  3. 通过命令行控制:可以通过cmd或者PowerShell,以及对应的命令,修改和存储环境变量。

上述三种方法之所以称之为用户层面,是因为其实现逻辑在系统层面可能是有重叠的、甚至是一致的。但是作为用户而言,我们不需要也不应当去关心系统层面的逻辑,只需要关注自己的行为模式是否有区分性即可。

$Env:Path 的不便之处

一般情况下,当我们在Windows终端使用PowerShell时,使用 Process 级别的环境变量,其会按照一定的优先级,包含并整合Machine级别和 User 级别环境变量中设置的内容。鉴于控制环境变量的方式很多,意味着环境变量值的来源就很多,在我们日常使用中,就很可能遇到一些问题,特别是环境变量中的路径$Env:Path

  • Machine级别和 User 级别环境变量$Env:Path中存在重复:安装某些软件时,会根据用户的选择,向系统注入Machine级别或者User 级别$Env:Path追加内容,但卸载软件时,不一定会自动清除。如果用户前后反复卸载,安装,且选择不一致,可能会导致重复。

    这种重复不会直接导致问题,但会构成隐患。可能会导致根据$Env:Path 寻找到软件或工具时,其版本不是目标版本等问题。

  • Machine级别和 User 级别环境变量$Env:Path中存在空值:某些软件卸载后,虽然会清除$Env:Path中的相应路径,但可能会引入空值。

    这种空值不会直接导致问题,且大部分情况下,都不影响使用。但是,如果基于$Env:Path的下游程序,需要对$Env:Path进行继承、修改时,空值可能会导致一些意外情况,尤其是空值出现在$Env:Path的中部而非首尾时。

再例如:当我们同时使用Miniconda, MSYS2 等带有包管理器,实现独立的包管理模式 的工具时,我们如果想安装Git,就可以有三种选择:

  • 安装到conda的虚拟环境中
  • 安装到msys2的环境中
  • 安装到一般的Windows程序文件夹‪Program Files

然后,我们可能会在$Env:Path的多个子条目路径中,寻找到git.exe。为了确保使用的是我们期望的git.exe, 我们可能会在PowerShell的配置文件$PROFILE.CurrentUserAllHosts中进行提前的处理,将我们期望的路径前置。如果我们每安装一个应用,调用时都要考虑类似于git.exe$Env:Path 中的前置问题,将会比较繁琐,麻烦。

动机总结

上述$Env:Path 中的处理过程尽管麻烦,但是其本身依旧是不可或缺的。但是,这套处理过程是可以整合的,以减少在$PROFILE.CurrentUserAllHosts配置文件中的配置行数,让用户从关心如何处理$Env:Path的过程中解放出来,只需要关心是否需要将某个路径加入到$Env:Path中。因此,PowerShell模组 PSComputerManagementZp 将一些处理$Env:Path的逻辑进行了打包和封装,参见EnvPath.ps1Manager.Env.ps1。后文就将介绍使用这套逻辑的案例,以更便捷的方式管理$Env:Path

准备工作

本节简述如何配置工具与环境

将PowerShell更新到7.0以上

可以直接从Release of PowerShell · PowerShell/PowerShell (github.com) 下载安装,或者使用 winget-cli:

1
2
winget search powershell
winget install --id Microsoft.PowerShell

安装PowerShell模组PSComputerManagementZp

参考 PowerShell Gallery | PSComputerManagementZp 0.1.0:

1
Install-Module -Name PSComputerManagementZp -Force

使用案例

以管理员权限运行PowerShell,再下面的流程中不要退出该shell窗口。

以下所有函数,都基于一个自定义的类 EnvPath,该类被初始化时,会自动对每个层级的$Env:Path内部去重,删除空值和点值.

面向Process级别$Env:Path的函数,同样适用于Linux和Mac系统中,已被PSComputerManagementZp支持,并通过了测试。

Machine层级与User层级的$Env:Path中的重复项合并到User层级

1
Merge-RedundantEnvPathFromCurrentMachineToCurrentUser

此函数将帮助用户,检查Machine层级与User层级的$Env:Path中的重复项。对于重复项,将删除Machine层级中的,只保留User层级中的。这个方法非常适用于单用户的Windows系统,曾经多次以不同权限安装、卸载过某个软件,而$Env:Path残留了安装路径的情况。

对于这种行为,可能会有人不解、困惑甚至反对。但此处提现的思想是,希望用户配置对系统的影响范围最小。即使用户是管理员,我们也认为,用户的配置应当尽可能只影响以其账号登录的系统,而尽可能不影响其他账号登录的系统。如果保留Machine层级,删除User层级,那么,新用户将拥有其不知晓的$Env:Path内容,这在一般意义上,我们认为是不够好的。当然,这个做法不完美,最终用户是否选择使用,需要结合具体情况考虑。

将某个路径添加到$Env:Path

  • 添加到当前Process级别$Env:Path的开头:

    添加到当前Process级别意味着仅在当前运行的PowerShell的Scope有效,重启PowerShell等重置Scope的方法将会导致对当前Process级别$Env:Path的配置失效。但这也是我们在命令行中使用一些工具的一般做法,即,使用时再临时添加,不保存,以避免不同场景下的路径冲突。

    1
    Add-PathToCurrentProcessEnvPath -Path 'C:\Program Files\Git\cmd' # Default is prepend
  • 添加到当前Process级别$Env:Path的末尾:

    1
    Add-PathToCurrentProcessEnvPath -Path 'C:\Program Files\Git\cmd' -IsAppend
  • 添加到当前User级别$Env:Path的开头:

    1
    Add-PathToCurrentUserEnvPath -Path 'C:\Program Files\Git\cmd' # Default is prepend
  • 添加到当前User级别$Env:Path的末尾:

    1
    Add-PathToCurrentUserEnvPath -Path 'C:\Program Files\Git\cmd' -IsAppend
  • 添加到当前Machine级别$Env:Path的开头:

    1
    Add-PathToCurrentMachineEnvPath -Path 'C:\Program Files\Git\cmd' # Default is prepend
  • 添加到当前Machine级别$Env:Path的末尾:

    1
    Add-PathToCurrentMachineEnvPath -Path 'C:\Program Files\Git\cmd' -IsAppend

将某个路径从$Env:Path中删除

  • 从当前Process级别的$Env:Path中删除:

    1
    Remove-PathFromCurrentProcessEnvPath -Path 'C:\Program Files\Git\cmd'
  • 从当前User级别的$Env:Path中删除:

    1
    Remove-PathFromCurrentUserEnvPath -Path 'C:\Program Files\Git\cmd' 
  • 从当前Machine级别的$Env:Path中删除:

    1
    Remove-PathFromCurrentMachineEnvPath -Path 'C:\Program Files\Git\cmd'

上述命令会变量所有$Env:Path子项,当检测到子项值与给定的输入$Path一致时,对其进行删除操作。判断一致的逻辑与PowerShell字符串的相等运算符一致。

将匹配某个Pattern的路径从$Env:Path中删除

  • 从当前Process级别的$Env:Path中删除:

    1
    Remove-MatchedPathsFromCurrentProcessEnvPath -Pattern 'Git'
  • 从当前User级别的$Env:Path中删除:

    1
    Remove-MatchedPathsFromCurrentUserEnvPath -Pattern 'Git'
  • 从当前Machine级别的$Env:Path中删除:

    1
    Remove-MatchedPathsFromCurrentMachineEnvPath -Pattern 'Git'

上述命令会变量所有$Env:Path子项,当检测到子项值与给定的输入$Pattern匹配时,进行删除操作。判断匹配的逻辑与PowerShell字符串的匹配运算符一致。

总结与讨论

本文是PowerShell模组 PSComputerManagementZp 的使用案例分享。可以更方便地帮助用户,在使用PowerShell时,管理 $Env:Path。上述的案例非常适合于使用PowerShell、同时使用Miniconda, MSYS2 等带有包管理器,实现独立的包管理模式 的工具的场景。

  • 缺点:需要安装PowerShell模组
  • 优点:让用户从关心如何处理$Env:Path的过程中解放出来,只需要关心是否需要将某个路径加入到$Env:Path中。

References