将源代码转换为UTF-8无BOM格式

4

我尝试将目标文件夹中的所有源文件转换为UTF-8(无BOM)编码。

我使用以下PowerShell脚本:

$MyPath = "D:\my projects\etc\"
Get-ChildItem $MyPath\* -Include *.h, *.cpp, *.c | Foreach-Object {
    $content = Get-Content $_.FullName  
    $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
    [System.IO.File]::WriteAllLines($_.FullName, $content, $Utf8NoBomEncoding)    
}
cmd /c pause | out-null

如果文件不是UTF-8编码,那么它可以正常工作。但是,如果某个文件已经是UTF-8无BOM格式,所有的国际字符就会被转换成未知符号(例如,如果我再次运行脚本)。如何修改脚本以解决这个问题?


2
这听起来像是文件实际上采用了其他编码方式。你能否展示一下这种损坏的例子(输入和输出中的实际十六进制字节,可能只需要几个代表性字符)? - tripleee
1
没有BOM,Get-Content就无法将文件内容识别为UTF-8格式,因此会将文件读取为ANSI格式,从而错误地解释特殊字符。您首先要通过删除BOM来解决什么问题? - Ansgar Wiechers
“官方不推荐”?由谁决定的?此外,相关链接 - Ansgar Wiechers
1
@AnsgarWiechers:一个实用的总结是:Unicode标准_允许_使用UTF-8“BOM”(Unicode签名),但既不推荐也不反对其使用。 在实践中,许多类Unix平台上的工具和例如Java的标准库既不期望也不知道如何处理这样的BOM,因此最好避免在跨平台使用中使用它。 相反,在Windows上,传统的工具和Windows PowerShell(而不是PowerShell_Core_)需要这个BOM才能正确识别和处理UTF-8文件。 - mklement0
1
@mklement0 我的重点主要是在 Windows 环境中的实际应用,但是没错。 - Ansgar Wiechers
显示剩余3条评论
2个回答

7
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 {
    # Note:
    #  * the use of -Encoding Utf8 to first try to read the file as UTF-8.
    #  * the use of -Raw to read the entire file as a *single string*.
    $content = Get-Content -Raw -Encoding Utf8 $_.FullName  

    # If the replacement char. is found in the content, the implication
    # is that the file is NOT UTF-8, so read it again *without -Encoding*,
    # which interprets the files as "ANSI" encoded (Windows-1251, in your case).
    if ($content.Contains([char] 0xfffd)) {
      $content = Get-Content -Raw $_.FullName  
    }

    # Note the use of WriteAllText() in lieu of WriteAllLines()
    # and that no explicit encoding object is passed, given that
    # .NET *defaults* to BOM-less UTF-8.
    # CAVEAT: There's a slight risk of data loss if writing back to the input
    #         file is interrupted.
    [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:

  • PowerShell Core 默认使用(无 BOM 的)UTF-8 编码,因此仅省略 -Encoding 参数不能用于读取 ANSI 编码的文件。

  • 同样,在 .NET Core 中,[System.Text.Encoding]::Default 总是报告 UTF-8 编码。

因此,您必须手动确定活动系统区域设置的 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 {
    # Note:
    #  * the use of -Encoding Utf8 to ensure the correct interpretation of the input file
    #  * the use of -Raw to read the entire file as a *single string*.
    $content = Get-Content -Raw -Encoding Utf8 $_.FullName  

    # Note the use of WriteAllText() in lieu of WriteAllLines()
    # and that no explicit encoding object is passed, given that
    # .NET *defaults* to BOM-less UTF-8.
    # CAVEAT: There's a slight risk of data loss if writing back to the input
    #         file is interrupted.
    [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-ContentSet-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 {
  # CAVEAT: There's a slight risk of data loss if writing back to the input
  #         file is interrupted.
  [System.IO.File]::WriteAllText(
    $_.FullName,
    [System.IO.File]::ReadAllText($_.FullName)
  )   
}

@VladimirBershov:另外说一句:我已经从“-like”转换为“.Contains()”来测试替换字符,这样更简单,而且可能更快。 - mklement0

0

正确检查 BOM 是否存在,例如使用以下模板(在关于 BOM 的注释处应用操作):

$ps1scripts = Get-ChildItem .\*.ps1 -Recurse      # change to match your circumstances
foreach ( $ps1script in $ps1scripts ) {
    $first3 = $ps1script | Get-Content -Encoding byte -TotalCount 3
    $first3Hex = '{0:X2}{1:X2}{2:X2}' -f $first3[0],$first3[1],$first3[2]
    $first2Hex = '{0:x2}{1:x2}'       -f $first3[0],$first3[1]

    if ( $first3Hex -eq 'EFBBBF' )     {
        # UTF-8 BOM

    } elseif ( $first2Hex -eq 'fffe' ) {
        # UCS-2LE BOM

    } elseif ( $first2Hex -eq 'feff' ) {
        # UCS-2BE BOM

    } else {
        # unknown (no BOM)

    }
}

请注意,上述模板是从我的旧脚本中派生出来的;您可以按照以下方式更改第一行:
$MyPath = "D:\my projects\etc\"
$ps1scripts = Get-ChildItem $MyPath\* -Include *.h, *.cpp, *.c

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接