Skip to content

在非拉丁语言环境的 Windows PowerShell 5.1 下,13 个脚本全部无法使用(编码 / 语法 / StrictMode) #8

Description

@bluesxu

概述

在一台 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-ObjectGenericMeasurePropertyNotFound —— 又因为 _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:20Invoke-Stage5-MoveApps.ps1:32

顺带一提,_Common.ps1:85 是对的(带了 -ErrorAction SilentlyContinue),只是另外三处漏了。

6. Test-Path 对内核锁定文件返回「不存在」—— 漏掉了收益最大的一项

这条影响最大。Get-DiskReport.ps1:40Invoke-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.sys12.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-ObjectUninstallString / 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 上、shellpowershell(5.1)而不是 pwsh,这样 PS7 专有语法也能一并抓到。


这是在用这个 skill 做真实清理的过程中发现的 —— 打上补丁之后它确实干成了活(回收了 27.5 GB),感谢作者。PR 风格如果想调整,我随时配合。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions