概述
在一台 zh-CN Windows 10 22H2(19045)、Windows PowerShell 5.1、ACP/OEMCP 936 的机器上,所有脚本开箱即不可用:3 个无法解析,另有 4 个运行时报错,还有 1 处会悄悄漏掉磁盘上最大的一笔可回收空间。
我是在实际用这个 skill 清理 C 盘时踩到的,下面每一条都来自真实运行,不是读代码猜的。修复见文末分支。
环境
|
|
| 系统 |
Microsoft Windows 10 专业版, 10.0.19045 |
| Shell |
Windows PowerShell 5.1(未安装 pwsh) |
| ACP / OEMCP |
936 (GBK) |
| 内存 |
31.7 GB |
1. 编码:全部脚本是 UTF-8 无 BOM → ParserError
13 个 .ps1 都没有 BOM。Windows PowerShell 5.1 读取 .ps1 时用的是系统 ANSI 代码页而非 UTF-8。在 CP936 下所有中文字符串变成乱码,乱码字节还会吞掉字符串终止符:
At ...\Get-DiskReport.ps1:50 char:54
+ ... { $_ -match 'Component Store|Actual Size|Reclaimable|缁勪欢瀛樺偍|瀹為檯澶у皬|鍙 ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The string is missing the terminator: '.
缁勪欢瀛樺偍 就是 组件存储 按 GBK 解读的结果。这一条影响全部 13 个文件,意味着在任何中日韩 / 西里尔等非拉丁语言的 Windows 上,这个 skill 完全不能用。
修法:给每个 .ps1 加 UTF-8 BOM。
2–4. 三个脚本在任何语言环境下都无法解析
这三处与编码无关,是 5.1 下的硬语法错误:
Get-DuplicateDrivers.ps1:43,48
OriginalName = ($original ? $original.ToLowerInvariant() : '') # ?: 是 PS7+ 专有
VersionParsed = (try { [Version](...) } catch { [Version]'0.0' }) # try/catch 是语句不是表达式
Unexpected token '?' in expression or statement.
Invoke-Stage5-MoveApps.ps1:53 —— 子表达式里反引号位置错误:
Write-Host "... 选目标盘$(if($TargetDrive) { ` (建议 $TargetDrive)`})"
Unexpected token '`}' in expression or statement.
Invoke-Stage7-OemBloat.ps1:64 —— 把 foreach 语句直接接了管道:
$found = foreach ($m in $matchList) { ... } | Sort-Object DisplayName -Unique
An empty pipe element is not allowed.
复现用的静态检查:
Get-ChildItem .\scripts -Filter *.ps1 | ForEach-Object {
$e = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$null, [ref]$e)
if ($e.Count) { "[FAIL] $($_.Name): $($e[0].Message)" }
}
5. Measure-Object Length -Sum 撞上无文件目录会中断整个脚本
$size = (Get-ChildItem -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object Length -Sum).Sum
Get-ChildItem 不加 -File 会返回 DirectoryInfo,而它没有 Length 属性。当整棵树里一个文件都没有时,Measure-Object 抛 GenericMeasurePropertyNotFound —— 又因为 _Common.ps1 设了 $ErrorActionPreference = 'Stop',整个脚本直接中断:
Measure-Object : The property "Length" cannot be found in the input for any objects.
At ...\Get-DiskReport.ps1:60 char:14
C:\ 下必然存在这样的目录,所以 Get-DiskReport.ps1 每次都会在主表格中途死掉。同样的写法还出现在 Get-AppDataTopConsumers.ps1:20 和 Invoke-Stage5-MoveApps.ps1:32。
顺带一提,_Common.ps1:85 是对的(带了 -ErrorAction SilentlyContinue),只是另外三处漏了。
6. Test-Path 对内核锁定文件返回「不存在」—— 漏掉了收益最大的一项
这条影响最大。Get-DiskReport.ps1:40 和 Invoke-Stage2-HiberPagefile.ps1:43 都用 Test-Path 做判断:
if (Test-Path -LiteralPath 'C:\hiberfil.sys') { ... }
FileSystem provider 打不开被独占锁定的分页 / 休眠文件,于是报告「不存在」。在一台 pagefile.sys 确实存在且有 32 GB 的机器上实测:
| 方法 |
结果 |
Test-Path -LiteralPath 'D:\pagefile.sys' |
False ❌ |
Get-Item -LiteralPath ... -Force |
ItemNotFoundException ❌ |
[System.IO.File]::Exists(...) |
True ✅ |
Get-ChildItem D:\ -Force -File -Filter pagefile.sys |
找到,32 GB ✅ |
这不是隐藏 / 系统属性的问题 —— 一个同样是隐藏+系统但没被锁定的文件(C:\DumpStack.log),Test-Path 返回 True 一切正常。
后果:磁盘报告的「已知大户文件」一节渲染为空,阶段 2 也从不显示休眠文件大小。在我这台机器上 hiberfil.sys 有 12.7 GB —— 是全盘最大的一笔可回收空间,而工具自己的报告在那一栏什么都没显示。
修法:改用 Get-ChildItem -Force -File -Filter <name> 枚举父目录。
7. 阶段 7 遇到没有 DisplayName 的注册表项直接死掉
Get-ItemProperty -Path $p -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName }
Where-Object : The property 'DisplayName' cannot be found on this object.
FullyQualifiedErrorId : PropertyNotFoundStrict
卸载表里没有 DisplayName 的键很常见。在 Set-StrictMode -Version Latest 下这个访问是终止性错误,所以阶段 7 在列出任何东西之前就挂了。后面 Select-Object 里 UninstallString / QuietUninstallString 的计算属性有同样的隐患。
8. 阶段 8 的白名单和它实际扫描的路径不一致
Invoke-Stage8-RemovePpkg.ps1:34 同时枚举 C:\Recovery\Customizations 和 C:\Recovery\OEM,但 _Common.ps1 的 $Script:SafePathPrefixes 里只有 C:\Recovery\Customizations。于是 C:\Recovery\OEM 下的 .ppkg 会被复制到备份目录,然后在删除时被 Test-SafePath 拒绝,留下一份没用的副本,空间一点没省。
方向是安全的(失败倾向于不删),但属于静默错误。
9. DISM 输出用错代码页解码 —— 非英文 Windows 上 WinSxS 分析永远空白
Get-DiskReport.ps1:49 用本地化关键词去匹配 DISM 输出,但原生命令的 stdout 是按 [Console]::OutputEncoding 解码的。任何把它保持在 UTF-8 的宿主都会让 DISM 的 OEM 代码页输出变成乱码:
����洢(WinSxS)��Ϣ:
Windows ��Դ���������������洢��С : 9.71 GB
于是 -match 一条都匹配不上,这一节输出为空。把 [Console]::OutputEncoding 在调用期间固定到 OEM 代码页之后:
匹配行数: 5
组件存储(WinSxS)信息:
组件存储的实际大小 : 9.61 GB
可回收的程序包数 : 0
推荐使用组件存储清理 : 否
Invoke-Stage3-DismCleanup.ps1:21 把 DISM 输出直接管道到控制台,同样是乱码(那里只影响显示)。
建议加个回归防线
上面 2–4 那三个解析失败,用一行 CI 就能拦住 —— 仓库里已经有 .github/workflows/ 了,值得加:
$bad = 0
Get-ChildItem -Recurse -Filter *.ps1 | ForEach-Object {
$e = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$null, [ref]$e)
if ($e.Count) { Write-Host "[FAIL] $($_.Name): $($e[0].Message)"; $bad++ }
}
exit $bad
跑在 windows-latest 上、shell 用 powershell(5.1)而不是 pwsh,这样 PS7 专有语法也能一并抓到。
这是在用这个 skill 做真实清理的过程中发现的 —— 打上补丁之后它确实干成了活(回收了 27.5 GB),感谢作者。PR 风格如果想调整,我随时配合。
概述
在一台 zh-CN Windows 10 22H2(19045)、Windows PowerShell 5.1、ACP/OEMCP 936 的机器上,所有脚本开箱即不可用:3 个无法解析,另有 4 个运行时报错,还有 1 处会悄悄漏掉磁盘上最大的一笔可回收空间。
我是在实际用这个 skill 清理 C 盘时踩到的,下面每一条都来自真实运行,不是读代码猜的。修复见文末分支。
环境
pwsh)1. 编码:全部脚本是 UTF-8 无 BOM → ParserError
13 个
.ps1都没有 BOM。Windows PowerShell 5.1 读取.ps1时用的是系统 ANSI 代码页而非 UTF-8。在 CP936 下所有中文字符串变成乱码,乱码字节还会吞掉字符串终止符:缁勪欢瀛樺偍就是组件存储按 GBK 解读的结果。这一条影响全部 13 个文件,意味着在任何中日韩 / 西里尔等非拉丁语言的 Windows 上,这个 skill 完全不能用。修法:给每个
.ps1加 UTF-8 BOM。2–4. 三个脚本在任何语言环境下都无法解析
这三处与编码无关,是 5.1 下的硬语法错误:
Get-DuplicateDrivers.ps1:43,48Invoke-Stage5-MoveApps.ps1:53—— 子表达式里反引号位置错误:Invoke-Stage7-OemBloat.ps1:64—— 把foreach语句直接接了管道:复现用的静态检查:
5.
Measure-Object Length -Sum撞上无文件目录会中断整个脚本Get-ChildItem不加-File会返回DirectoryInfo,而它没有Length属性。当整棵树里一个文件都没有时,Measure-Object抛GenericMeasurePropertyNotFound—— 又因为_Common.ps1设了$ErrorActionPreference = 'Stop',整个脚本直接中断:C:\下必然存在这样的目录,所以Get-DiskReport.ps1每次都会在主表格中途死掉。同样的写法还出现在Get-AppDataTopConsumers.ps1:20和Invoke-Stage5-MoveApps.ps1:32。顺带一提,
_Common.ps1:85是对的(带了-ErrorAction SilentlyContinue),只是另外三处漏了。6.
Test-Path对内核锁定文件返回「不存在」—— 漏掉了收益最大的一项这条影响最大。
Get-DiskReport.ps1:40和Invoke-Stage2-HiberPagefile.ps1:43都用Test-Path做判断:FileSystem provider 打不开被独占锁定的分页 / 休眠文件,于是报告「不存在」。在一台
pagefile.sys确实存在且有 32 GB 的机器上实测:Test-Path -LiteralPath 'D:\pagefile.sys'Get-Item -LiteralPath ... -Force[System.IO.File]::Exists(...)Get-ChildItem D:\ -Force -File -Filter pagefile.sys这不是隐藏 / 系统属性的问题 —— 一个同样是隐藏+系统但没被锁定的文件(
C:\DumpStack.log),Test-Path返回True一切正常。后果:磁盘报告的「已知大户文件」一节渲染为空,阶段 2 也从不显示休眠文件大小。在我这台机器上
hiberfil.sys有 12.7 GB —— 是全盘最大的一笔可回收空间,而工具自己的报告在那一栏什么都没显示。修法:改用
Get-ChildItem -Force -File -Filter <name>枚举父目录。7. 阶段 7 遇到没有
DisplayName的注册表项直接死掉卸载表里没有
DisplayName的键很常见。在Set-StrictMode -Version Latest下这个访问是终止性错误,所以阶段 7 在列出任何东西之前就挂了。后面Select-Object里UninstallString/QuietUninstallString的计算属性有同样的隐患。8. 阶段 8 的白名单和它实际扫描的路径不一致
Invoke-Stage8-RemovePpkg.ps1:34同时枚举C:\Recovery\Customizations和C:\Recovery\OEM,但_Common.ps1的$Script:SafePathPrefixes里只有C:\Recovery\Customizations。于是C:\Recovery\OEM下的.ppkg会被复制到备份目录,然后在删除时被Test-SafePath拒绝,留下一份没用的副本,空间一点没省。方向是安全的(失败倾向于不删),但属于静默错误。
9. DISM 输出用错代码页解码 —— 非英文 Windows 上 WinSxS 分析永远空白
Get-DiskReport.ps1:49用本地化关键词去匹配 DISM 输出,但原生命令的 stdout 是按[Console]::OutputEncoding解码的。任何把它保持在 UTF-8 的宿主都会让 DISM 的 OEM 代码页输出变成乱码:于是
-match一条都匹配不上,这一节输出为空。把[Console]::OutputEncoding在调用期间固定到 OEM 代码页之后:Invoke-Stage3-DismCleanup.ps1:21把 DISM 输出直接管道到控制台,同样是乱码(那里只影响显示)。建议加个回归防线
上面 2–4 那三个解析失败,用一行 CI 就能拦住 —— 仓库里已经有
.github/workflows/了,值得加:跑在
windows-latest上、shell用powershell(5.1)而不是pwsh,这样 PS7 专有语法也能一并抓到。这是在用这个 skill 做真实清理的过程中发现的 —— 打上补丁之后它确实干成了活(回收了 27.5 GB),感谢作者。PR 风格如果想调整,我随时配合。