PowerShell 入门实战:环境变量怎么设、profile 放什么、5.1 和 7 差在哪
原创 · 约 30 分钟阅读 · 阅读 --

PowerShell 入门实战:环境变量怎么设、profile 放什么、5.1 和 7 差在哪

作者: Alex Xiang


从一条查程序路径的命令出发,把三件常被混着用的事讲清:$env: 和 [Environment] 到底谁是谁、profile 该放什么不该放什么、5.1 和 7 的真实差异在哪。

Alex Xiang · 2026.10.21 · 约 3200 字,阅读 9 分钟 · Windows / 终端配置

在 Windows 上查一个程序的安装路径,我原本以为是一条命令的事。结果一路上撞到三个问题:$env:PATH 和 [Environment] 到底谁是谁,profile 是什么,以及为什么终端里的中文老是变成问号。

这三个问题看着零散,其实是同一件事的三个切面:PowerShell 同时长着 .NET 的骨架和 shell 的皮,你得先知道每句话是对谁说的,才谈得上记住它。

环境变量有三种寿命,混着用就会踩坑

PowerShell 里改环境变量不是一个动作,是三个。它们的区别不在写法,在活多久。

作用域写法关掉窗口后
进程级$env:FOO = 'bar'丢失,只有当前这个 PowerShell 看得见
用户级SetEnvironmentVariable('FOO','bar','User')保留,你以后开的所有程序都看得见
系统级同上,第三参数换成 'Machine'保留,全机器所有人

试错时永远先用第一行。它只影响当前窗口,搞坏了关掉就干净,这是最便宜的安全带。

真正要写死的时候才动后两个。这里有个容易忽略的点:$env:FOO 这种写法天生没有持久化能力,不是它不够用,是它的设计范围本来就在本进程内。想让配置活过这次关机,绕不开 .NET 那边的方法。

方括号和冒号:PowerShell 的两套世界观

[Environment]::SetEnvironmentVariable('X','v','User') 这行字,很多人是复制粘贴用的,从没拆开看过。拆开其实就三段:

  • [Environment] —— 类型字面量,告诉解析器“接下来这个词是一个 .NET 类型”
  • :: —— 静态成员访问符,意思是“访问类本身的成员,不是某个对象的”
  • SetEnvironmentVariable —— 静态方法名

为什么非要方括号? 因为 PowerShell 是命令优先的语言。行首出现一个裸词,它会默认当成命令名去 PATH 里翻。Environment 单独写在行首,PowerShell 会先去找有没有一个叫这个的命令。方括号是唯一能让解析器闭嘴、老实承认“这是类型”的记号。

Environment 是 System.Environment 的简写,PowerShell 会自动在 System 命名空间里搜索,所以写全称和简写等价。

顺便把 :: 和 . 分清,这俩天天用:

[Environment]::SetEnvironmentVariable('X','v','User')   # 静态:属于类
(Get-Date).AddDays(1)                                   # 实例:属于某个对象

Environment 是个工具类,不用 new 出对象,所以用 ::。写成 [Environment].SetEnvironmentVariable 会直接报错。

那 $env: 又是什么

它压根不走 .NET,走的是 PowerShell 自己的 Provider(虚拟驱动器)机制。

PowerShell 把各种数据源都伪装成盘符:C: 是文件系统,HKLM: 是注册表,Function: 是函数表,env: 就是环境变量盘。所以:

$env:PATH                    # 语法糖,等价于 ↓(Get-Item Env:PATH).Value

完整读法是:$(我要取值)+ env:(去哪个盘找)+ PATH(项名)。这是 PowerShell 特有的 $drive:item 简写。

两个都能改环境变量,凭什么

因为 $env: 的背后也调了 .NET,只是少传了作用域参数:$env:X = 'v' 实际就是 SetEnvironmentVariable('X','v'),没有第三个参数,所以只改当前进程;而带 'User' 的版本会写进 HKCU\Environment 并广播通知。

记住三句话就够用了:方括号是对类型说话,$xxx: 是对驱动器说话,要持久化就必须进 .NET 世界。

长命令的三种化简方式

[Environment]::SetEnvironmentVariable 敲起来确实烦。三条路,各有取舍。

第一条,setx。 系统自带,最短:setx FOO bar(用户级),加 /M 是系统级。代价是它有个 1024 字符的静默截断——值太长会被悄悄切掉,不报错。所以永远不要用 setx 改 PATH,这条路专门坑人。

第二条,写进 profile 包成函数。 一次配置,终身可用:

function senv($n, $v, $s = 'User') {[Environment]::SetEnvironmentVariable($n, $v, $s)}

之后 senv FOO bar 就完事,senv FOO $null 是删除。附带的 PATH 追加函数值得单独写一个,因为手工拼 PATH 是最容易写错的:

function addpath($p, $s = 'User') {
    $old = [Environment]::GetEnvironmentVariable('Path', $s)
    if (($old -split ';') -notcontains $p) {
        [Environment]::SetEnvironmentVariable('Path', ($old.TrimEnd(';') + ';' + $p), $s)
    }
}

它自带去重,重复执行不会把 PATH 越写越长。

第三条,干脆不背,靠补全。 敲 [Env 再按 Tab 得到 [Environment],接着 ::SetEnv 按 Tab 补全方法名,全程十几个键。比背短名稳,也不污染任何配置。

在终端里敲命令的人

每天要敲几十遍的东西,值得花十分钟变成三个字母。图:Pexels

profile 是什么

一句话:你每次开终端时,PowerShell 都会先偷偷跑一遍的脚本,跑完才把提示符交给你。

类比 bash 的 .bashrc、zsh 的 .zshrc、Vim 的 .vimrc。它存在的意义是让“我这台机器上的终端”和“刚装好的终端”不是同一个东西。

它有四个作用域,按顺序加载:

作用域路径影响谁
AllUsersAllHostsWindowsPowerShell\v1.0\profile.ps1全机器、所有宿主
AllUsersCurrentHost同目录 Microsoft.PowerShell_profile.ps1全机器、仅控制台
CurrentUserAllHosts~\Documents\WindowsPowerShell\profile.ps1只有你、所有宿主
CurrentUserCurrentHost~\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1只有你、仅控制台

$PROFILE 这个变量默认指向最后一个,也是绝大多数人该用的那个。用它的好处是不用记路径:

Test-Path $PROFILE        # 有没有
notepad $PROFILE          # 直接编辑
. $PROFILE                # 手动重载

两个前提:文件必须是 .ps1 后缀,执行策略得放行。RemoteSigned 就够,不用开到 Unrestricted。

该往 profile 里放什么

判断标准只有一条:这东西你会不会在每次开窗口时都想有? 会就放,只是一次性操作就单独写个脚本。

按收益排序,值得放的是这三类:

  • 修环境坑的。 首推编码,Windows 上的 PowerShell 5.1 默认拿 GBK 处理输出,git log、npm 的中文经常直接变问号。两行代码的事,收益最大。
  • 省按键的。senv 这类函数、上箭头改成前缀搜索历史、Ctrl+D 目录补全。这些每天用几十次,攒起来比任何别名都值。
  • 补能力的。 代理开关、自检函数,偶尔用一次但省事。

反过来,有四类东西我建议一律别放:

不要放进 profile 的四样东西

API Key、token、密码。 profile 是明文文件,会被各种工具读、被备份同步。存密钥用 SetEnvironmentVariable(...,'User'),至少还躺在注册表里。

Import-Module 一大堆漂亮主题。 每次开窗口都付一次钱。装个花哨提示符能把启动从 200ms 拉到 2 秒,开个终端等两秒非常难受。

Set-Location C:\some\project。 会覆盖你从资源管理器右键启动时所在的目录,看起来方便,实际很烦。

大段一次性脚本。 profile 是长期住宅,不是临时停车场。

我实际放进去的核心就这一段:

if ([Console]::OutputEncoding.CodePage -ne 65001) {
    [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
}
if ($OutputEncoding.CodePage -ne 65001) {
    $OutputEncoding = [System.Text.Encoding]::UTF8
}

为什么要判断 CodePage 而不是直接赋值?因为这两行在 5.1 和 7 上的必要性不一样,判断一下就能两边通用,后面会讲。

5.1 和 7 到底差在哪

先说怎么查。最靠谱的是 $PSVersionTable:

$PSVersionTable.PSVersion          # 5.1.26100.9711
$PSVersionTable.PSEdition          # Desktop = 5.1;Core = 7+
$PSVersionTable                    # 全量

判断在跑哪个版本,看 PSEdition 最干脆,或者看进程名:powershell.exe 是 5.1,pwsh.exe 是 7。这俩在 Windows 上是并存的独立程序,不是升级关系,装 7 不会覆盖 5.1。

这里有个必须避开的坑:(Get-Host).Version 在 PowerShell 7 里会返回 5.1.17763.1007 这种假的兼容版本号,专门坑习惯老写法的人。别用它判断版本。

编码:一条最容易搞错的差异

很多人以为“升级到 7 就不用管编码了”。不成立。这两行代码来自两个完全不同的系统:前者是 .NET / Windows 控制台的值,跟系统区域走;后者是 PowerShell 自己的变量。

代码5.1 默认7 默认7 下能否删
[Console]::OutputEncodingCP936仍是 CP936不能
$OutputEncodingus-asciiUTF-8可以

$OutputEncoding 是 PowerShell 6.0 才改成默认 UTF-8 的;而 [Console]::OutputEncoding 认的是 Windows 系统区域设置(中文系统就是 936),PowerShell 7 并没有去改它。所以 PS 7 上照样会遇到中文乱码,只是没 5.1 那么严重。

us-ascii 这个默认值有多坑,值得单独说一句:它意味着 "中文" | findstr xxx、Get-Content a.txt | node 这类管道传给外部程序的中文,出 PowerShell 那一刻就变成 ? 了。这是比屏幕显示乱码更隐蔽的问题——文件写出来才发现问号。

顺带量一下启动成本

配置加进去了,得知道自己付了多少钱。本机实测(各测一次,浮动约 ±50ms):

PowerShell 5.1 与 7.6 冷启动耗时

条形长度是冷启动耗时,右侧一列是加载 profile 的额外开销。启动成本才是判断“该不该往 profile 里加东西”的唯一依据。

结论很清楚:一份克制的 profile 大约花 150–180ms,完全在可接受范围。而一旦开始 Import-Module 主题和一堆增强模块,这个数字会成倍往上走。超过 300ms 就该减肥了,正常应该在 100ms 出头。

PSReadLine 的版本有三个不同答案

PSReadLine 是 PowerShell 的命令行编辑器(补全、历史、语法高亮全靠它)。查它版本时,三个命令会给你三个不同的数字,别搞混:

(Get-Module PSReadLine).Version              # A. 当前会话「已加载」的
Get-Module PSReadLine -ListAvailable         # B. 磁盘上「已安装」的全部
Get-PSReadLineOption                         # C. 当前生效的配置

A 和 B 经常不一样。更麻烦的是 A 可能是空的——PSReadLine 只在交互式会话里自动加载,用脚本或非交互方式跑命令时它根本没被载入,拿到空值很正常。

所以判断某个功能能不能用,别猜版本号,直接问命令本身:

(Get-Command Set-PSReadLineOption).Parameters.ContainsKey('PredictionSource')

返回 True 就是能用。这招比记“2.1 以上支持某某”可靠得多,因为它问的是事实,不是代号。

本机的实际结果很有代表性:PowerShell 5.1 自带的 PSReadLine 是 2.0.0,PredictionSource 返回 False;而 PowerShell 7.6 自带 2.4.5,返回 True。也就是说,换到 7 之后,“敲几个字就有灰字提示上次的命令”这个功能自动就有了,不用额外装任何模块。

实战:非管理员怎么装 7.6

这段是踩出来的,值得记一下。

第一反应是 winget install --id Microsoft.PowerShell --scope user。它跑了四分半,中途毫无输出(静默下载),最后报成功。但去 C:\Program Files\PowerShell\7\ 一看——目录根本不存在。

原因是权限。当前不是管理员,--scope user 让 winget 走了 MSIX 包而不是传统 MSI,装完落在:

C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.7.0_x64__8wekyb3d8bbwe\pwsh.exe

启动入口是 App Execution Alias:%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe,这个目录本来就在用户 PATH 里,所以终端里直接敲 pwsh 也能起来。

有管理员权限的话,MSI 版会装到 C:\Program Files\PowerShell\7\,是更标准的选择。非管理员就只能接受 MSIX,功能上没差别,只是更新走 Store 那套。

让两个版本共用一份配置

装完 7 之后有个新问题:5.1 的 profile 在 Documents\WindowsPowerShell\,7 的在 Documents\PowerShell\,是两个独立目录。同样一份配置写两遍,改一次要同步两处,迟早忘。

解法是把公共部分抽出来:

Documents\PowerShell\common.ps1                               ← 真正的配置
Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1  ← 只有一行,引它
Documents\PowerShell\Microsoft.PowerShell_profile.ps1         ← 同样一行

两个 profile 的内容一模一样:

$common = Join-Path $HOME 'Documents\PowerShell\common.ps1'
if (Test-Path $common) { . $common }

common.ps1 里放所有实际配置。用 Test-Path 包一层是有意的——万一哪天这个文件被挪走或删了,profile 不会报错,只是安静地什么都不做,不至于让终端起不来。

一处维护,两个版本同时生效。在 7.6 上验证过:函数在、编码是 utf-8、PSReadLine 2.4.5 的历史预测已启用。

五个坑,都是真踩过的

  1. profile 报错是静默的。 写错一个字符,窗口照样开,但后面的配置全部不生效,你还以为成功了。改完一定要 . $PROFILE 手动重载看报错。
  2. profile 文件必须存成带 BOM 的 UTF-8。 5.1 读无 BOM 的 UTF-8 会把中文注释当 GBK 解析,直接乱码甚至语法错误。用记事本另存为“UTF-8 带 BOM”,或者用 .NET 显式写:[System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding($true)))
  3. 改 PATH 只能读一个作用域再拼,也永远别用 setx。 混着读 User 和 Machine 再写回其中一个,会把另一个作用域的路径整片抹掉;而 setx 有 1024 字符静默截断。
  4. 改完环境变量,老进程看不到。 已经开着的终端、编辑器都读的是旧值。Explorer 也会缓存,最保险是重开终端,或者注销重来。
  5. 装了 7 之后,powershell 和 pwsh 是两个世界。 你在 5.1 里配的东西不会自动出现在 7 里(除非像上面那样共享),在 7 里 Set-ExecutionPolicy 也要单独设。别以为装完就统一了。

收尾

这三件事说到底是一句话:PowerShell 是两套系统拼起来的,你得知道自己在跟哪一套说话。

$env: 是 shell 的、临时的、本进程的;[Environment]:: 是 .NET 的、可以写进注册表的;profile 是每次启动时自动执行的那层“个人化补丁”。分清了,那些看起来奇怪的长命令就都有了解释。

真要动手,顺序建议这样:先把编码两行加进 profile,解决最烦的乱码;再用两周时间往里加 senv 这类每天都会用的小东西;最后才考虑升级版本或者装主题。顺序反过来做,你只会得到一个启动很慢、但什么都没解决的终端。

参考资料

  1. Microsoft Docs:about_Environment_Variables / about_Profiles —— 作用域、Provider 机制与 profile 加载顺序
  2. Microsoft Docs:about_PowerShell_Editions —— Desktop 与 Core 的区别
  3. PowerShell/PowerShell GitHub Release —— 7.6.7 安装包与 MSIX 安装说明

本文的版本号、编码状态与启动耗时均来自本机实测(Windows 26100,PowerShell 5.1.26100.9711 与 7.6.7)。耗时数据为单次测量,存在约 ±50ms 浮动,不作为性能基准。

打开原图 ↗