如
Ansgar Wiechers在评论中指出的,问题在于
Windows PowerShell在没有BOM的情况下,默认将文件解释为“ANSI”编码,即遗留系统区域设置(ANSI代码页)所暗示的编码,这反映在.NET Framework中(但不是.NET
Core)的
[System.Text.Encoding] ::Default
中。
考虑到你后续的评论,
在没有BOM的输入文件中,既有使用Windows-1251编码的文件,也有使用UTF-8编码的文件,因此
必须检查它们的内容以确定它们的具体编码:
使用-Encoding Utf8
读取每个文件,并测试结果字符串是否包含Unicode替换字符(U+FFFD
) 。如果包含,则说明该文件不是UTF-8格式,因为这个特殊字符用来表示遇到了在UTF-8中无效的字节序列。
如果文件不是有效的UTF-8格式,则再次读取文件,不要使用-Encoding
参数,这会使Windows PowerShell将文件解释为Windows-1251编码,因为这是你系统区域设置所暗示的编码(代码页)。
$MyPath = "D:\my projects\etc"
Get-ChildItem $MyPath\* -Include *.h, *.cpp, *.c | Foreach-Object {
$content = Get-Content -Raw -Encoding Utf8 $_.FullName
if ($content.Contains([char] 0xfffd)) {
$content = Get-Content -Raw $_.FullName
}
[System.IO.File]::WriteAllText($_.FullName, $content)
}
使用 [IO.File]::ReadAllText()
方法并搭配一个UTF-8编码对象可以更快地完成操作(PSv5+语法)。当遇到无效的UTF-8字节时,该编码对象会抛出异常:
$utf8EncodingThatThrows = [Text.UTF8Encoding]::new($false, $true)
try {
$content = [IO.File]::ReadAllText($_.FullName, $utf8EncodingThatThrows)
} catch [Text.DecoderFallbackException] {
$content = [IO.File]::ReadAllText($_.FullName, [Text.Encoding]::Default)
}
将上述解决方案适应于 PowerShell Core / .NET Core:
因此,您必须手动确定活动系统区域设置的 ANSI 代码页并获取相应的编码对象:
$ansiEncoding = [Text.Encoding]::GetEncoding(
[int] (Get-ItemPropertyValue HKLM:\SYSTEM\CurrentControlSet\Control\Nls\CodePage ACP)
)
您需要将此编码明确传递给
Get-Content -Encoding
(
Get-Content -Raw -Encoding $ansiEncoding $_.FullName
)或.NET方法(
[IO.File]::ReadAllText($_.FullName, $ansiEncoding)
)。
答案的原始形式:对于所有已经使用UTF-8编码的输入文件:
因此,如果您的一些UTF-8编码的文件(已经)没有BOM,则必须显式地指示Get-Content
将它们视为UTF-8,使用-Encoding Utf8
- 否则,如果它们包含7位ASCII范围之外的字符,它们将被错误解释:
$MyPath = "D:\my projects\etc"
Get-ChildItem $MyPath\* -Include *.h, *.cpp, *.c | Foreach-Object {
$content = Get-Content -Raw -Encoding Utf8 $_.FullName
[System.IO.File]::WriteAllText($_.FullName, $content)
}
注意:在您的场景中,无BOM的UTF-8文件不需要重写,但这样做是良性的并简化了代码;“替代方案”是“测试每个文件的前3个字节是否为UTF-8 BOM”,并跳过这样的文件:
$hasUtf8Bom = "$(Get-Content -Encoding Byte -First 3 $_.FullName)" -eq '239 187 191'
(Windows PowerShell)或
$hasUtf8Bom = "$(Get-Content -AsByteStream -First 3 $_.FullName)" -eq '239 187 191'
(PowerShell Core)。
顺便说一句:如果有非UTF8编码的输入文件(例如UTF-16),只要这些文件有BOM,解决方案仍然适用,因为PowerShell(安静地)优先考虑BOM而不是通过-Encoding指定的编码。
请注意,使用-Raw / WriteAllText()作为整体读取/写入文件(单个字符串)不仅可以稍微加快处理速度,还可以确保每个输入文件的以下特征得到保留:
- 特定的换行符样式(CRLF(Windows)vs.LF-only(Unix))
- 最后一行是否有尾随换行符。
相比之下,不使用-Raw(逐行阅读)并使用.WriteAllLines()不会保留这些特征:您总是会获得平台适当的换行符(在Windows PowerShell中,始终是CRLF),并且您总会得到一个尾随换行符。
请注意,多平台的PowerShell Core版本在读取没有BOM的文件时合理地默认为UTF-8,并且默认情况下创建不带BOM的UTF-8文件-创建带有BOM的UTF-8文件需要使用-Encoding utf8BOM进行显式选择。
因此,“PowerShell Core解决方案要简单得多”。
# PowerShell Core only.
$MyPath = "D:\my projects\etc"
Get-ChildItem $MyPath\* -Include *.h, *.cpp, *.c | Foreach-Object {
# * Read the file at hand (UTF8 files both with and without BOM are
# read correctly).
# * Simply rewrite it with the *default* encoding, which in
# PowerShell Core is BOM-less UTF-8.
# Note the (...) around the Get-Content call, which is necessary in order
# to write back to the *same* file in the same pipeline.
# CAVEAT: There's a slight risk of data loss if writing back to the input
# file is interrupted.
(Get-Content -Raw $_.FullName) | Set-Content -NoNewline $_.FullName
}
更快的基于.NET类型的解决方案
上述解决方案可以工作,但是Get-Content
和Set-Content
相对较慢,因此使用.NET类型来读取和重写文件将会更加高效。
与上述一样,在以下解决方案中不需要显式地指定任何编码方式(即使在Windows PowerShell中),因为.NET自身从一开始就默认为无BOM UTF-8(同时仍然识别UTF-8 BOM 如果存在):
$MyPath = "D:\my projects\etc"
Get-ChildItem $MyPath\* -Include *.h, *.cpp, *.c | Foreach-Object {
[System.IO.File]::WriteAllText(
$_.FullName,
[System.IO.File]::ReadAllText($_.FullName)
)
}
Get-Content
就无法将文件内容识别为UTF-8格式,因此会将文件读取为ANSI格式,从而错误地解释特殊字符。您首先要通过删除BOM来解决什么问题? - Ansgar Wiechers