Usando PowerShell para escribir un archivo en UTF-8 sin la lista de materiales

246

Out-File parece forzar la lista de materiales cuando se usa UTF-8:

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

¿Cómo puedo escribir un archivo en UTF-8 sin BOM usando PowerShell?

M. Dudley
fuente
23
BOM = Marca de orden de bytes. Tres caracteres colocados al comienzo de un archivo (0xEF, 0xBB, 0xBF) que se parecen a "ï» ¿"
Señal15
40
Esto es increíblemente frustrante. ¿Incluso los módulos de terceros se contaminan, como intentar cargar un archivo a través de SSH? BOM! "Sí, corrompamos cada archivo; eso suena como una buena idea". -Microsoft.
MichaelGG
3
La codificación predeterminada es UTF8NoBOM a partir de Powershell versión 6.0 docs.microsoft.com/en-us/powershell/module/…
Paul Shiryaev
Hable acerca de romper la compatibilidad con versiones anteriores ...
Dragas

Respuestas:

220

Usar la UTF8Encodingclase de .NET y pasar $Falseal constructor parece funcionar:

$MyRawString = Get-Content -Raw $MyPath
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllLines($MyPath, $MyRawString, $Utf8NoBomEncoding)
M. Dudley
fuente
42
Ugh, espero que esa no sea la única forma.
Scott Muc
114
Una línea [System.IO.File]::WriteAllLines($MyPath, $MyFile)es suficiente. Esta WriteAllLinessobrecarga escribe exactamente UTF8 sin BOM.
Roman Kuzmin
66
Creé una solicitud de función de MSDN aquí: connect.microsoft.com/PowerShell/feedbackdetail/view/1137121/…
Groostav
3
Tenga en cuenta que WriteAllLinesparece requerir $MyPathser absoluto.
sschuberth
10
@xdhmoore WriteAllLinesobtiene el directorio actual de [System.Environment]::CurrentDirectory. Si abre PowerShell y luego cambia su directorio actual (usando cdo Set-Location), [System.Environment]::CurrentDirectoryno se cambiará y el archivo terminará en el directorio incorrecto. Puedes evitar esto por [System.Environment]::CurrentDirectory = (Get-Location).Path.
Shayan Toqraee
79

La forma correcta a partir de ahora es utilizar una solución recomendada por @Roman Kuzmin en los comentarios a @M. Dudley responde :

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

(También lo acorté un poco eliminando Systemaclaraciones innecesarias del espacio de nombres; se sustituirá automáticamente de forma predeterminada).

Para nunca
fuente
2
Esto (por cualquier razón) no eliminó la lista de materiales para mí, como lo hizo la respuesta aceptada
Liam
@Liam, ¿probablemente alguna versión antigua de PowerShell o .NET?
ForNeVeR
1
Creo que las versiones anteriores de la función .NET WriteAllLines escribieron la lista de materiales por defecto. Por lo tanto, podría ser un problema de versión.
Bender the Greatest
2
Confirmado con escrituras con una lista de materiales en Powershell 3, pero sin una lista de materiales en Powershell 4. Tuve que usar la respuesta original de M. Dudley.
chazbot7
2
Por lo tanto, funciona en Windows 10 donde está instalado de forma predeterminada. :) Además, mejora sugerida:[IO.File]::WriteAllLines(($filename | Resolve-Path), $content)
Johny Skovdal
50

Pensé que esto no sería UTF, pero acabo de encontrar una solución bastante simple que parece funcionar ...

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

Para mí, esto da como resultado un archivo utf-8 sin bom independientemente del formato de origen.

Lenny
fuente
8
Esto funcionó para mí, excepto que solía -encoding utf8cumplir con mis requisitos.
Chim Chimz
1
Muchas gracias. Estoy trabajando con los registros de volcado de una herramienta, que tenía pestañas dentro. UTF-8 no estaba funcionando. ASCII resolvió el problema. Gracias.
user1529294
44
Sí, -Encoding ASCIIevita el problema de la lista de materiales, pero obviamente solo obtienes caracteres ASCII de 7 bits . Dado que ASCII es un subconjunto de UTF-8, el archivo resultante es técnicamente también un archivo UTF-8 válido, pero todos los caracteres no ASCII en su entrada se convertirán en ?caracteres literales .
mklement0
44
@ChimChimz Accidentalmente voté por tu comentario, pero -encoding utf8aún saca UTF-8 con una lista de materiales. :(
TheDudeAbides
33

Nota: esta respuesta se aplica a Windows PowerShell ; por el contrario, en la edición multiplataforma de PowerShell Core (v6 +), UTF-8 sin BOM es la codificación predeterminada , en todos los cmdlets.
En otras palabras: si está utilizando PowerShell [Core] versión 6 o superior , obtendrá archivos UTF-8 sin BOM de forma predeterminada (que también puede solicitar explícitamente con -Encoding utf8/ -Encoding utf8NoBOM, mientras que obtiene con la codificación -BOM con -utf8BOM).


Para complementar la propia respuesta simple y pragmática de M. Dudley (y la reformulación más concisa de ForNeVeR ):

Por conveniencia, aquí está la función avanzada Out-FileUtf8NoBom, una alternativa basada en canalización que imitaOut-File , lo que significa:

  • puedes usarlo como Out-Fileen una tubería.
  • Los objetos de entrada que no son cadenas están formateados como lo estarían si los enviara a la consola, al igual que con Out-File.

Ejemplo:

(Get-Content $MyPath) | Out-FileUtf8NoBom $MyPath

Tenga en cuenta cómo (Get-Content $MyPath)se incluye (...), lo que garantiza que todo el archivo se abra, se lea por completo y se cierre antes de enviar el resultado a través de la canalización. Esto es necesario para poder volver a escribir en el mismo archivo (actualizarlo en su lugar ).
En general, sin embargo, esta técnica no es aconsejable por 2 razones: (a) todo el archivo debe caber en la memoria y (b) si se interrumpe el comando, se perderán datos.

Una nota sobre el uso de la memoria :

  • La propia respuesta de M. Dudley requiere que todo el contenido del archivo se acumule primero en la memoria, lo que puede ser problemático con archivos grandes.
  • La siguiente función mejora ligeramente esto: todos los objetos de entrada todavía se almacenan en el búfer primero, pero sus representaciones de cadena se generan y se escriben en el archivo de salida una por una.

Código fuente deOut-FileUtf8NoBom (también disponible como Gist con licencia MIT ):

<#
.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-String parameters:
  * Only a literal output path is supported, and only as a parameter.
  * -Force is not supported.

  Caveat: *All* pipeline input is buffered before writing output starts,
          but the string representations are generated and written to the target
          file one by one.

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

#>
function Out-FileUtf8NoBom {

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

  #requires -version 3

  # Make sure that the .NET framework sees the same working dir. as PS
  # and resolve the input path to a full path.
  [System.IO.Directory]::SetCurrentDirectory($PWD.ProviderPath) # Caveat: Older .NET Core versions don't support [Environment]::CurrentDirectory
  $LiteralPath = [IO.Path]::GetFullPath($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 IO.StreamWriter $LiteralPath, $Append

  $htOutStringArgs = @{}
  if ($Width) {
    $htOutStringArgs += @{ Width = $Width }
  }

  # Note: By not using begin / process / end blocks, we're effectively running
  #       in the end block, which means that all pipeline input has already
  #       been collected in automatic variable $Input.
  #       We must use this approach, because using | Out-String individually
  #       in each iteration of a process block would format each input object
  #       with an indvidual header.
  try {
    $Input | Out-String -Stream @htOutStringArgs | % { $sw.WriteLine($_) }
  } finally {
    $sw.Dispose()
  }

}
mklement0
fuente
16

A partir de la versión 6, powershell admite la UTF8NoBOMcodificación tanto para el contenido del conjunto como para el archivo externo e incluso lo utiliza como codificación predeterminada.

Entonces, en el ejemplo anterior, simplemente debería ser así:

$MyFile | Out-File -Encoding UTF8NoBOM $MyPath
sc911
fuente
@ RaúlSalinas-Monteagudo ¿en qué versión estás?
John Bentley
Agradable. FYI verifique la versión con$PSVersionTable.PSVersion
KCD
14

Cuando se usa en Set-Contentlugar de Out-File, puede especificar la codificación Byte, que se puede usar para escribir una matriz de bytes en un archivo. Esto en combinación con una codificación UTF8 personalizada que no emite la lista de materiales da el resultado deseado:

# This variable can be reused
$utf8 = New-Object System.Text.UTF8Encoding $false

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

La diferencia con el uso [IO.File]::WriteAllLines()o similar es que debería funcionar bien con cualquier tipo de elemento y ruta, no solo rutas de archivos reales.

Lucero
fuente
5

Este script convertirá, a UTF-8 sin BOM, todos los archivos .txt en DIRECTORIO1 y los enviará a DIRECTORIO2

foreach ($i in ls -name DIRECTORY1\*.txt)
{
    $file_content = Get-Content "DIRECTORY1\$i";
    [System.IO.File]::WriteAllLines("DIRECTORY2\$i", $file_content);
}
jamhan
fuente
Este falla sin ninguna advertencia. ¿Qué versión de powershell debo usar para ejecutarlo?
darksoulsong
3
La solución WriteAllLines funciona muy bien para archivos pequeños. Sin embargo, necesito una solución para archivos más grandes. Cada vez que intento usar esto con un archivo más grande obtengo un error OutOfMemory.
BermudaLamb
2
    [System.IO.FileInfo] $file = Get-Item -Path $FilePath 
    $sequenceBOM = New-Object System.Byte[] 3 
    $reader = $file.OpenRead() 
    $bytesRead = $reader.Read($sequenceBOM, 0, 3) 
    $reader.Dispose() 
    #A UTF-8+BOM string will start with the three following bytes. Hex: 0xEF0xBB0xBF, Decimal: 239 187 191 
    if ($bytesRead -eq 3 -and $sequenceBOM[0] -eq 239 -and $sequenceBOM[1] -eq 187 -and $sequenceBOM[2] -eq 191) 
    { 
        $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($False) 
        [System.IO.File]::WriteAllLines($FilePath, (Get-Content $FilePath), $utf8NoBomEncoding) 
        Write-Host "Remove UTF-8 BOM successfully" 
    } 
    Else 
    { 
        Write-Warning "Not UTF-8 BOM file" 
    }  

Fuente Cómo eliminar la marca de orden de bytes UTF8 (BOM) de un archivo usando PowerShell

bronceado franco
fuente
2

Si desea usar [System.IO.File]::WriteAllLines(), debe convertir el segundo parámetro a String[](si el tipo de $MyFilees Object[]), y también especificar la ruta absoluta con $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath), como:

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
Get-ChildItem | ConvertTo-Csv | Set-Variable MyFile
[System.IO.File]::WriteAllLines($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath), [String[]]$MyFile, $Utf8NoBomEncoding)

Si desea usar [System.IO.File]::WriteAllText(), a veces debe canalizar el segundo parámetro | Out-String |para agregar CRLF al final de cada línea explícitamente (especialmente cuando los usa con ConvertTo-Csv):

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
Get-ChildItem | ConvertTo-Csv | Out-String | Set-Variable tmp
[System.IO.File]::WriteAllText("/absolute/path/to/foobar.csv", $tmp, $Utf8NoBomEncoding)

O puedes usar [Text.Encoding]::UTF8.GetBytes()con Set-Content -Encoding Byte:

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
Get-ChildItem | ConvertTo-Csv | Out-String | % { [Text.Encoding]::UTF8.GetBytes($_) } | Set-Content -Encoding Byte -Path "/absolute/path/to/foobar.csv"

ver: Cómo escribir el resultado de ConvertTo-Csv a un archivo en UTF-8 sin BOM

SATO Yusuke
fuente
Buenos punteros; sugerencias /: la alternativa más simple a $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath)es Convert-Path $MyPath; si desea garantizar un CRLF final, simplemente use [System.IO.File]::WriteAllLines()incluso con una sola cadena de entrada (no es necesario Out-String).
mklement0
0

Una técnica que utilizo es redirigir la salida a un archivo ASCII usando el cmdlet Out-File .

Por ejemplo, a menudo ejecuto scripts SQL que crean otro script SQL para ejecutar en Oracle. Con la redirección simple (">"), la salida estará en UTF-16, lo cual no es reconocido por SQLPlus. Para evitar esto:

sqlplus -s / as sysdba "@create_sql_script.sql" |
Out-File -FilePath new_script.sql -Encoding ASCII -Force

El script generado se puede ejecutar a través de otra sesión de SQLPlus sin preocupaciones de Unicode:

sqlplus / as sysdba "@new_script.sql" |
tee new_script.log
Erik Anderson
fuente
44
Sí, -Encoding ASCIIevita el problema de la lista de materiales, pero obviamente solo obtienes soporte para caracteres ASCII de 7 bits . Dado que ASCII es un subconjunto de UTF-8, el archivo resultante es técnicamente también un archivo UTF-8 válido, pero todos los caracteres no ASCII en su entrada se convertirán en ?caracteres literales .
mklement0
Esta respuesta necesita más votos. La incompatibilidad sqlplus con BOM es una causa de muchos dolores de cabeza .
Amit Naidu
0

Cambie varios archivos por extensión a UTF-8 sin BOM:

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($False)
foreach($i in ls -recurse -filter "*.java") {
    $MyFile = Get-Content $i.fullname 
    [System.IO.File]::WriteAllLines($i.fullname, $MyFile, $Utf8NoBomEncoding)
}
Jaume Suñer Mut
fuente
0

Por alguna razón, las WriteAllLinesllamadas seguían produciendo una lista de materiales para mí, con el UTF8Encodingargumento BOMless y sin él. Pero lo siguiente funcionó para mí:

$bytes = gc -Encoding byte BOMthetorpedoes.txt
[IO.File]::WriteAllBytes("$(pwd)\BOMthetorpedoes.txt", $bytes[3..($bytes.length-1)])

Tuve que hacer que la ruta del archivo sea absoluta para que funcione. De lo contrario, escribió el archivo en mi escritorio. Además, supongo que esto solo funciona si sabe que su BOM es de 3 bytes. No tengo idea de cuán confiable es esperar un formato / longitud BOM determinado basado en la codificación.

Además, como está escrito, esto probablemente solo funcione si su archivo se ajusta a una matriz de PowerShell, que parece tener un límite de longitud de algún valor menor que [int32]::MaxValueen mi máquina.

xdhmoore
fuente
1
WriteAllLinessin un argumento de codificación nunca escribe una lista de materiales en , pero es concebible que su cadena comience con el carácter BOM ( U+FEFF), que al escribir efectivamente creó una lista de materiales UTF-8; por ejemplo: $s = [char] 0xfeff + 'hi'; [io.file]::WriteAllText((Convert-Path t.txt), $s)(omita el [char] 0xfeff + para ver que no se escribe BOM).
mklement0
1
En cuanto a escribir inesperadamente en una ubicación diferente: el problema es que .NET Framework generalmente tiene un directorio actual diferente que PowerShell; puede sincronizarlos primero con [Environment]::CurrentDirectory = $PWD.ProviderPath, o, como una alternativa más genérica a su "$(pwd)\..."enfoque (mejor:, "$pwd\..."incluso mejor: "$($pwd.ProviderPath)\..."o (Join-Path $pwd.ProviderPath ...)), use(Convert-Path BOMthetorpedoes.txt)
mklement0
Gracias, no me di cuenta de que podría haber una sola conversión de personaje BOM a UTF-8 BOM como esa.
xdhmoore
1
Todas las secuencias de bytes BOM (firmas Unicode) son, de hecho, la representación de bytes de la codificación respectiva del carácter único UnicodeU+FEFF abstracto .
mklement0
Ah ok Eso parece simplificar las cosas.
xdhmoore
-2

Podría usar a continuación para obtener UTF8 sin BOM

$MyFile | Out-File -Encoding ASCII
Robin Wang
fuente
44
No, convertirá la salida a la página de códigos ANSI actual (cp1251 o cp1252, por ejemplo). ¡No es UTF-8 en absoluto!
ForNeVeR
1
Gracias Robin. Es posible que esto no haya funcionado para escribir un archivo UTF-8 sin la lista de materiales, pero la opción ASCII de codificación eliminó la lista de materiales. De esa manera podría generar un archivo bat para gvim. El archivo .bat se estaba tropezando con la lista de materiales.
Greg
3
@ForNeVeR: Tienes razón en que la codificación ASCIIno es UTF-8, pero tampoco es la página de códigos ANSI actual, estás pensando Default; ASCIIverdaderamente es una codificación ASCII de 7 bits, con puntos de código> = 128 convertidos en ?instancias literales .
mklement0
1
@ForNeVeR: Probablemente estés pensando en "ANSI" o " ASCII extendido ". Intente esto para verificar que, de -Encoding ASCIIhecho, solo sea ASCII de 7 bits: 'äb' | out-file ($f = [IO.Path]::GetTempFilename()) -encoding ASCII; '?b' -eq $(Get-Content $f; Remove-Item $f)- äse ha transcrito a ?. Por el contrario, -Encoding Default("ANSI") lo preservaría correctamente.
mklement0
3
@rob Esta es la respuesta perfecta para todos los que simplemente no necesitan utf-8 o cualquier otra cosa que sea diferente a ASCII y no estén interesados ​​en comprender las codificaciones y el propósito de Unicode. Puede usarlo como utf-8 porque los caracteres utf-8 equivalentes a todos los caracteres ASCII son idénticos (significa convertir un archivo ASCII a un archivo utf-8 en un archivo idéntico (si no obtiene una lista de materiales)). Para todos los que tienen caracteres no ASCII en su texto, esta respuesta es simplemente falsa y engañosa.
TNT
-3

Este funciona para mí (use "Predeterminado" en lugar de "UTF8"):

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

El resultado es ASCII sin BOM.

Krzysztof
fuente
1
Según la documentación de Out-File que especifica la Defaultcodificación, se utilizará la página de códigos ANSI actual del sistema, que no es UTF-8, como lo solicité.
M. Dudley
Esto parece funcionar para mí, al menos para Export-CSV. Si abre el archivo resultante en un editor adecuado, la codificación del archivo es UTF-8 sin BOM, y no Western Latin ISO 9 como lo hubiera esperado con ASCII
eythort
Muchos editores abren el archivo como UTF-8 si no pueden detectar la codificación.
emptyother