PowerShell 入门实战:环境变量怎么设、profile 放什么、5.1 和 7 差在哪
从一条查程序路径的命令出发,把三件常被混着用的事讲清:$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。它存在的意义是让“我这台机器上的终端”和“刚装好的终端”不是同一个东西。
它有四个作用域,按顺序加载:
| 作用域 | 路径 | 影响谁 |
|---|---|---|
| AllUsersAllHosts | WindowsPowerShell\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]::OutputEncoding | CP936 | 仍是 CP936 | 不能 |
$OutputEncoding | us-ascii | UTF-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):

条形长度是冷启动耗时,右侧一列是加载 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 的历史预测已启用。
五个坑,都是真踩过的
- profile 报错是静默的。 写错一个字符,窗口照样开,但后面的配置全部不生效,你还以为成功了。改完一定要
. $PROFILE手动重载看报错。 - 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))) - 改 PATH 只能读一个作用域再拼,也永远别用
setx。 混着读 User 和 Machine 再写回其中一个,会把另一个作用域的路径整片抹掉;而setx有 1024 字符静默截断。 - 改完环境变量,老进程看不到。 已经开着的终端、编辑器都读的是旧值。Explorer 也会缓存,最保险是重开终端,或者注销重来。
- 装了 7 之后,
powershell和pwsh是两个世界。 你在 5.1 里配的东西不会自动出现在 7 里(除非像上面那样共享),在 7 里Set-ExecutionPolicy也要单独设。别以为装完就统一了。
收尾
这三件事说到底是一句话:PowerShell 是两套系统拼起来的,你得知道自己在跟哪一套说话。
$env: 是 shell 的、临时的、本进程的;[Environment]:: 是 .NET 的、可以写进注册表的;profile 是每次启动时自动执行的那层“个人化补丁”。分清了,那些看起来奇怪的长命令就都有了解释。
真要动手,顺序建议这样:先把编码两行加进 profile,解决最烦的乱码;再用两周时间往里加 senv 这类每天都会用的小东西;最后才考虑升级版本或者装主题。顺序反过来做,你只会得到一个启动很慢、但什么都没解决的终端。
参考资料
- Microsoft Docs:about_Environment_Variables / about_Profiles —— 作用域、Provider 机制与 profile 加载顺序
- Microsoft Docs:about_PowerShell_Editions —— Desktop 与 Core 的区别
- PowerShell/PowerShell GitHub Release —— 7.6.7 安装包与 MSIX 安装说明
本文的版本号、编码状态与启动耗时均来自本机实测(Windows 26100,PowerShell 5.1.26100.9711 与 7.6.7)。耗时数据为单次测量,存在约 ±50ms 浮动,不作为性能基准。
微信公众号
欢迎关注「字与码」
如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。
X / Twitter
关注 @ax2_zicode
更即时的技术观察、新文章提醒和一些短想法会发在 X 上。