使用PowerShell在不带BOM的UTF-8格式下编写文件

371

Out-File 在使用 UTF-8 时似乎会强制添加BOM:

$MyFile = Get-Content $MyPath
$MyFile | Out-File -Encoding "UTF8" $MyPath

如何使用PowerShell以UTF-8无BOM格式编写文件?

2021年更新

自我10年前提出此问题以来,PowerShell有了一些变化。请检查下面的多个答案,它们包含很多有用的信息!


35
BOM代表字节顺序标记。它是三个字符(0xEF、0xBB、0xBF)的组合,通常出现在文件开头,看起来像“”。 - Signal15
67
这真是令人沮丧。即使是第三方模块也会被污染,比如试图通过SSH上传文件?突然间就出现了BOM(字节顺序标记)!“没错,让我们破坏每一个文件;这听起来是个好主意。”--微软。 - MichaelGG
10
默认编码为UTF8NoBOM,从PowerShell6.0版本开始。https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/out-file?view=powershell-6#parameters - Paul Shiryaev
3
谈论破坏向后兼容性的问题... - Dragas
1
我觉得应该注意到,虽然在UTF-8文件中使用BOM会导致许多系统出现问题,但是在Unicode UTF-8规范中明确允许包含BOM。参考链接 - Bacon Bits
谢谢。我都快抓狂了——尝试了两种格式,但都无法满足要求使用UTF-8编码。1)$stdcaltxt | Out-File -encoding utf8 -FilePath $stdCalFileName 和 2)Set-Content -Path $stdCalFileName -Value $stdcaltxt -Encoding utf8 两者都产生了不同的编码 UTF8-BOMUSC2 LE BOM,根据 Notepad++ 编码检查! - JGFMK
20个回答

296

使用.NET的UTF8Encoding类并将$False传递给构造函数似乎是有效的:

$MyRawString = Get-Content -Raw $MyPath
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllLines($MyPath, $MyRawString, $Utf8NoBomEncoding)

66
希望那不是唯一的方法。 - Scott Muc
139
一行代码 [System.IO.File]::WriteAllLines($MyPath, $MyFile) 即可。这个 WriteAllLines 重载会精确地写入不带 BOM 的 UTF8。 - Roman Kuzmin
6
在此创建了一个 MSDN 功能请求:https://connect.microsoft.com/PowerShell/feedbackdetail/view/1137121,请求添加“nobom”标志到“out-file”。 - Groostav
8
请注意,“WriteAllLines”似乎需要将“$MyPath”设置为绝对路径。 - sschuberth
18
WriteAllLines 方法从 [System.Environment]::CurrentDirectory 获取当前目录。如果您在 PowerShell 中打开并更改了当前目录(使用 cdSet-Location 命令),则 [System.Environment]::CurrentDirectory 不会被更改,文件将保存在错误的目录中。为了避免这种情况,您可以使用 [System.Environment]::CurrentDirectory = (Get-Location).Path 来解决。 - Shayan Toqraee
显示剩余8条评论

109

目前来说,正确的做法是使用@Roman Kuzmin在评论中推荐的解决方案链接,以及@M. Dudley回答中提到的方法链接:

[IO.File]::WriteAllLines($filename, $content)

我还将它缩短了一点,去掉了不必要的System命名空间说明 - 它将自动被默认替换。


4
基于某种原因,这并没有为我去除字节顺序标记(BOM),而被采纳的答案却做到了。 - Liam
1
@Liam,可能是旧版的PowerShell或.NET? - ForNeVeR
2
我相信早期版本的.NET WriteAllLines函数默认会写入BOM。因此可能是版本问题。 - codewario
3
确认在PowerShell 3中带有BOM,但在PowerShell 4中没有BOM。我不得不使用M. Dudley的原始答案。 - chazbot7
6
它可在预设情况下安装在Windows 10上运行。 :) 另外,建议改进:[IO.File]::WriteAllLines(($filename | Resolve-Path), $content) - Johny Skovdal
显示剩余3条评论

79

我认为这不会是UTF,但我刚刚发现了一个似乎很简单的解决方案...

Get-Content path/to/file.ext | out-file -encoding ASCII targetFile.ext

对我来说,这将导致生成一个没有BOM的UTF-8文件,而不管源格式是什么。


18
这对我很有效,除了我使用了“-encoding utf8”来满足我的要求。 - Just Rudy
1
非常感谢。我正在处理一个工具的转储日志 - 在其中有标签。UTF-8无法正常工作。ASCII解决了问题。谢谢。 - user1529294
74
是的,-Encoding ASCII 可以避免 BOM 问题,但显然你只能得到 _7位ASCII字符_。鉴于ASCII是UTF-8的子集,因此生成的文件在技术上也是一个有效的UTF-8文件,但输入中的所有非ASCII字符都将被转换为文字 ? 字符。 - mklement0
10
警告:绝对不要这样做。这将删除所有非ASCII字符并用问号替换它们。不要这样做,否则您将丢失数据!(在Windows 10上尝试过PS 5.1) - ygoe
1
完全同意 @ygoe。这个解决方案应该避免使用。这里有更好的答案,比如被接受的答案 - 当然,新版本的跨平台 PowerShell 默认不使用 BOM;但对于使用桌面版的用户,请参考被接受的答案。 - undefined

62

注意:本答案适用于 Windows PowerShell;相比之下,在跨平台的 PowerShell Core 版本(v6+)中,UTF-8 无 BOM 是所有 cmdlet 的 默认编码

  • 换句话说:如果您使用的是 PowerShell [Core] 版本 6 或更高版本,则默认情况下会获得无 BOM 的 UTF-8 文件(您还可以使用 -Encoding utf8 / -Encoding utf8NoBOM 显式请求,而使用 -utf8BOM 则会获得带有 BOM 的编码)。

  • 如果您正在运行 Windows 10 或更高版本,并且您愿意在整个系统范围内切换到无 BOM 的 UTF-8 编码 - 这将产生深远的影响,但是 - 即使是 Windows PowerShell 也可以始终使用无 BOM 的 UTF-8 - 请参见 this answer


为了补充M. Dudley的简单而实用的回答(以及ForNeVeR更简洁的重新表述):
  • 一个简单的、(非流式) PowerShell 本地替代方案是使用 New-Item,它 (奇怪的是) 即使在 Windows PowerShell 中默认创建没有 BOM 的 UTF-8 文件:

    # 注意使用 -Raw 将文件作为整体读取。
    # 与 Set-Content / Out-File 不同,不会添加任何尾随换行符。
    $null = New-Item -Force $MyPath -Value (Get-Content -Raw $MyPath)
    
    • 注意: 要将任意命令的输出保存为与 Out-File 相同格式的输出,请先将其管道传输到 Out-String; 例如:

       $null = New-Item -Force Out.txt -Value (Get-ChildItem | Out-String) 
      
  • 为了方便起见,下面是高级的自定义函数 Out-FileUtf8NoBom,一种基于管道的替代方案,模仿 Out-File,这意味着:

    • 您可以像在管道中使用 Out-File 一样使用它。
    • 不是字符串的输入对象会被格式化,就像使用 Out-File 一样发送到控制台。
    • 额外的 -UseLF 开关允许您使用 Unix 格式的 LF-only 换行符 ("`n"),而不是通常获得的 Windows 格式的 CRLF 换行符 ("`r`n")。

例子:

(Get-Content $MyPath) | Out-FileUtf8NoBom $MyPath # Add -UseLF for Unix newlines

请注意,(Get-Content $MyPath)被括在(...)中,这确保了整个文件在通过管道发送结果之前被打开、完全读取并关闭。这是必要的,以便能够将更新写回到相同文件(就地更新)。

然而,总的来说,这种技术不建议使用,有两个原因:(a) 整个文件必须适合内存,(b) 如果命令被中断,数据将会丢失。

关于内存使用的说明:

  • M. 都德利的答案 和上方的New-Item替代品需要首先在内存中构建整个文件内容,这可能会在大输入集的情况下造成问题。
  • 下面的函数不需要这样做,因为它是作为代理(包装器)函数实现的(有关如何定义这种函数的简洁摘要,请参见此答案)。

Out-FileUtf8NoBom 函数的源代码:

注意:该函数也可作为 MIT 许可的 Gist 使用, 未来只有后者将得到维护。

您可以使用以下命令直接安装它(尽管我可以亲自保证这样做是安全的,但在直接执行脚本之前,您应始终检查脚本内容):

# Download and define the function.
irm https://gist.github.com/mklement0/8689b9b5123a9ba11df7214f82a673be/raw/Out-FileUtf8NoBom.ps1 | iex

function Out-FileUtf8NoBom {

  <#
  .SYNOPSIS
    Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark).

  .DESCRIPTION

    Mimics the most important aspects of Out-File:
      * Input objects are sent to Out-String first.
      * -Append allows you to append to an existing file, -NoClobber prevents
        overwriting of an existing file.
      * -Width allows you to specify the line width for the text representations
        of input objects that aren't strings.
    However, it is not a complete implementation of all Out-File parameters:
      * Only a literal output path is supported, and only as a parameter.
      * -Force is not supported.
      * Conversely, an extra -UseLF switch is supported for using LF-only newlines.

  .NOTES
    The raison d'être for this advanced function is that Windows PowerShell
    lacks the ability to write UTF-8 files without a BOM: using -Encoding UTF8 
    invariably prepends a BOM.

    Copyright (c) 2017, 2022 Michael Klement <mklement0@gmail.com> (http://same2u.net), 
    released under the [MIT license](https://spdx.org/licenses/MIT#licenseText).

  #>

  [CmdletBinding(PositionalBinding=$false)]
  param(
    [Parameter(Mandatory, Position = 0)] [string] $LiteralPath,
    [switch] $Append,
    [switch] $NoClobber,
    [AllowNull()] [int] $Width,
    [switch] $UseLF,
    [Parameter(ValueFromPipeline)] $InputObject
  )

  begin {

    # Convert the input path to a full one, since .NET's working dir. usually
    # differs from PowerShell's.
    $dir = Split-Path -LiteralPath $LiteralPath
    if ($dir) { $dir = Convert-Path -ErrorAction Stop -LiteralPath $dir } else { $dir = $pwd.ProviderPath }
    $LiteralPath = [IO.Path]::Combine($dir, [IO.Path]::GetFileName($LiteralPath))
    
    # If -NoClobber was specified, throw an exception if the target file already
    # exists.
    if ($NoClobber -and (Test-Path $LiteralPath)) {
      Throw [IO.IOException] "The file '$LiteralPath' already exists."
    }
    
    # Create a StreamWriter object.
    # Note that we take advantage of the fact that the StreamWriter class by default:
    # - uses UTF-8 encoding
    # - without a BOM.
    $sw = New-Object System.IO.StreamWriter $LiteralPath, $Append
    
    $htOutStringArgs = @{}
    if ($Width) { $htOutStringArgs += @{ Width = $Width } }

    try { 
      # Create the script block with the command to use in the steppable pipeline.
      $scriptCmd = { 
        & Microsoft.PowerShell.Utility\Out-String -Stream @htOutStringArgs | 
          . { process { if ($UseLF) { $sw.Write(($_ + "`n")) } else { $sw.WriteLine($_) } } }
      }  
      
      $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
      $steppablePipeline.Begin($PSCmdlet)
    }
    catch { throw }

  }

  process
  {
    $steppablePipeline.Process($_)
  }

  end {
    $steppablePipeline.End()
    $sw.Dispose()
  }

}

1
将utf8BOM文件转换为普通的utf8格式:$null = New-Item -Force "\$env:ProgramData\ssh\administrators_authorized_keys" -Value (Get-Content -Path "\$env:ProgramData\ssh\administrators_authorized_keys" | Out-String) - nhooyr
2
@nhooyr,最好使用$null = New-Item -Force $MyPath -Value (Get-Content -Raw $MyPath)(速度更快,并保留现有的换行格式)- 我已更新答案。 - mklement0

32

版本6开始,PowerShell支持UTF8NoBOM编码,包括set-contentout-file,甚至将其作为默认编码。

所以在上面的示例中,它应该是这样的:

$MyFile | Out-File -Encoding UTF8NoBOM $MyPath

5
好的。顺便提一下,请使用“$PSVersionTable.PSVersion”检查版本。 - KCD
1
值得注意的是,在 PowerShell [Core] v6+ 中,“-Encoding UTF8NoBOM”从未被“要求”,因为它是“默认”的编码方式。 - mklement0
似乎https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-content?view=powershell-7.3#-encoding证实了这一点,但是在将重定向的dos输出到文件并发出时遇到了问题。当我从CSV中驱动不同参数的相同命令,并在各处设置编码时,Notepad ++会在两个文件中发出不同的编码(一个为UCS-2 LE BOM,另一个为UTF8-BOM!) - 但是Powershell似乎忽略了它。我正在从CSV中驱动屏幕抓取自动化/重定向到临时输出文件并提取子字符串。 - JGFMK
如果您为PowerShell编码与预期不符而苦恼(即使明确要求utf8,Set-Content和Out-File给出不同的编码答案!),请务必查看此答案以及我在问题本身下面的评论!https://dev59.com/SG035IYBdhLWcg3wJcjT#5596984 - JGFMK

20
这是一个替代方案。
这种方法的优点是它与IO.FileInfo对象(来自诸如Get-Item的函数)和相对路径兼容。
1. 创建一个Text.UTF8Encoding对象 - 虽然Text.UTF8Encoding能够插入BOM,但默认情况下不会插入。
2. 调用对象的GetBytes方法将字符串转换为字节 - 确保目标字符串实际上不是一个字符串数组 - $stringVar.Count应该等于1。
3. 使用Set-Content -Encoding Byte将字节数组写入目标位置。
# This is a reusable class instance object
$utf8 = New-Object Text.UTF8Encoding

$GCRaw = Get-Content -Raw -PSPath $MyPath
Set-Content -Value $utf8.GetBytes($GCRaw) -Encoding Byte -PSPath $MyPath

这可以通过让-Value根据位置推断,并且通过在参数内部创建Text.UTF8Encoding对象来进一步缩短。
$GCRaw = Get-Content $MyPath -Raw

Set-Content ([Text.UTF8Encoding]::new().GetBytes($GCRaw)) -Encoding Byte -PSPath $MyPath

#NOTE#
# (New-Object Text.UTF8Encoding).GetBytes($GCRaw))
# can be used instead of
# ([Text.UTF8Encoding]::new().GetBytes($GCRaw))
# For code intended to be compact, I recommend the latter,
# not just because it's not as long, but also because its
# lack of whitespace makes it visually more distinct.

1
很好 - 在字符串方面工作得很好(这可能是所有需要的,肯定满足问题的要求)。如果您需要利用 Out-File 提供的格式化功能(与 Set-Content 不同),请先将其管道传输到 Out-String;例如, $MyFile = Get-ChildItem | Out-String - mklement0

7

重要提示!:仅当文件开头的额外空格或换行符对您的使用情况没有问题时才有效,例如,如果它是SQL文件、Java文件或可读文本文件。
可以使用创建一个空(非UTF8或ASCII(UTF8兼容))文件并附加到它的组合(如果源是文件,则用 gc $src 替换 $ str ):

" "    |  out-file  -encoding ASCII  -noNewline  $dest
$str  |  out-file  -encoding UTF8   -append     $dest

一行代码实现

根据您的用例替换$dest$str

$_ofdst = $dest ; " " | out-file -encoding ASCII -noNewline $_ofdst ; $src | out-file -encoding UTF8 -append $_ofdst

作为简单函数

function Out-File-UTF8-noBOM { param( $str, $dest )
  " "    |  out-file  -encoding ASCII  -noNewline  $dest
  $str  |  out-file  -encoding UTF8   -append     $dest
}

使用源文件进行操作:

Out-File-UTF8-noBOM  (gc $src),  $dest

使用字符串进行操作:

Out-File-UTF8-noBOM  $str,  $dest
  • optionally: continue appending with Out-File:

    "more foo bar"  |  Out-File -encoding UTF8 -append  $dest
    

UTF8不等于ASCII。所以,不,这在所有情况下都不起作用。所有的ASCII都可以转换为UTF8,但不是所有的UTF8都可以转换为ASCII再转回去,也就是说,UTF8并不是(全部)可逆转的到ASCII再转回去。事实上,这可能是危险的。 - undefined
@fourpastmidnight 这就是为什么我一开始就提到了这个 :) - undefined

6

此脚本将把DIRECTORY1中所有的.txt文件转换为UTF-8(无BOM),并输出到DIRECTORY2。

foreach ($i in ls -name DIRECTORY1\*.txt)
{
    $file_content = Get-Content "DIRECTORY1\$i";
    [System.IO.File]::WriteAllLines("DIRECTORY2\$i", $file_content);
}

这个程序没有任何警告就失败了。我应该使用哪个版本的PowerShell来运行它? - darksoulsong
5
WriteAllLines方法对于小文件效果很好。然而,我需要一种适用于大文件的解决方案。每次我尝试在较大的文件上使用它时,都会出现OutOfMemory错误。 - BermudaLamb

6

旧问题,新答案:

虽然“旧”PowerShell会写入BOM,但新的跨平台变体行为不同:默认值为“无BOM”,可以通过开关进行配置:

-Encoding

指定目标文件的编码类型。 默认值为utf8NoBOM。

此参数的可接受值如下:

  • ascii: 使用ASCII(7位)字符集的编码。
  • bigendianunicode: 使用大端字节顺序对UTF-16格式进行编码。
  • oem: 使用MS-DOS和控制台程序的默认编码。
  • unicode: 使用小端字节顺序对UTF-16格式进行编码。
  • utf7: 使用UTF-7格式进行编码。
  • utf8: 使用UTF-8格式进行编码。
  • utf8BOM: 使用带有字节顺序标记(BOM)的UTF-8格式进行编码
  • utf8NoBOM: 使用不带字节顺序标记(BOM)的UTF-8格式进行编码
  • utf32: 使用UTF-32格式进行编码。

来源:https://learn.microsoft.com/de-de/powershell/module/Microsoft.PowerShell.Utility/Out-File?view=powershell-7 重点是我的


3

对于PowerShell 5.1,启用以下设置:

控制面板,区域,管理,更改系统区域设置,使用Unicode UTF-8来支持全球语言

然后在PowerShell中输入以下内容:

$PSDefaultParameterValues['*:Encoding'] = 'Default'

另外,您可以升级到 PowerShell 6 或更高版本。

https://github.com/PowerShell/PowerShell


1
具体来说,这是一个系统级设置,使得Windows PowerShell在所有命令中默认使用无BOM的UTF-8编码,这可能是需要的,也可能不需要,尤其是因为该功能仍处于测试阶段(截至本文撰写时),并且可能会破坏传统控制台应用程序 - 有关背景信息,请参见此答案。 - mklement0

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