Używanie PowerShell do napisania pliku w UTF-8 bez BOM


246

Out-File wydaje się wymuszać BOM podczas korzystania z UTF-8:

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

Jak mogę napisać plik w UTF-8 bez BOM za pomocą PowerShell?


23
BOM = bajt-znak porządkowy. Trzy znaki umieszczone na początku pliku (0xEF, 0xBB, 0xBF), które wyglądają jak „ï» ¿”
Signal15

40
To jest niezwykle frustrujące. Nawet moduły innych firm zostają zanieczyszczone, na przykład próba przesłania pliku przez SSH? BOM! „Tak, zepsujmy każdy plik; to brzmi jak dobry pomysł”. -Microsoft.
MichaelGG

3
Domyślne kodowanie to UTF8NoBOM, począwszy od Powershell w wersji 6.0 docs.microsoft.com/en-us/powershell/module/…
Paul Shiryaev

Porozmawiaj o zerwaniu z kompatybilnością wsteczną ...
Dragas,

Odpowiedzi:


220

Wydaje się, że używanie UTF8Encodingklasy .NET i przekazywanie $Falsedo konstruktora działa:

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

42
Ugh, mam nadzieję, że to nie jedyny sposób.
Scott Muc

114
Jedna linia [System.IO.File]::WriteAllLines($MyPath, $MyFile)wystarczy. To WriteAllLinesprzeciążenie zapisuje dokładnie UTF8 bez BOM.
Roman Kuzmin,


3
Zauważ, WriteAllLinesże wymaga $MyPathto absolutności.
sschuberth

10
@ xdhmoore WriteAllLinespobiera bieżący katalog z [System.Environment]::CurrentDirectory. Jeśli otworzysz PowerShell, a następnie zmienisz swój bieżący katalog (używając cdlub Set-Location), [System.Environment]::CurrentDirectorynie zostanie on zmieniony, a plik znajdzie się w niewłaściwym katalogu. Możesz obejść ten problem przez [System.Environment]::CurrentDirectory = (Get-Location).Path.
Shayan Toqraee,

79

Właściwa droga jak na razie jest zastosowanie rozwiązania zalecanego przez @Roman Kuzmin w komentarzach do @M. Odpowiedź Dudleya :

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

(Trochę go też skróciłem, usuwając niepotrzebne Systemwyjaśnienie przestrzeni nazw - domyślnie zostanie zastąpione automatycznie).


2
To (z jakiegokolwiek powodu) nie usunęło BOM-a dla mnie, tak jak zaakceptowana odpowiedź
Liam

@Liam, prawdopodobnie jakaś stara wersja PowerShell lub .NET?
ForNeVeR

1
Wierzę, że starsze wersje funkcji .NET WriteAllLines domyślnie zapisywały BOM. Może to być problem z wersją.
Bender the Greatest

2
Potwierdzony zapisami z BOM w Powershell 3, ale bez BOM w Powershell 4. Musiałem użyć oryginalnej odpowiedzi M. Dudleya.
chazbot7

2
Działa więc w systemie Windows 10, w którym jest domyślnie zainstalowany. :) Ponadto sugerowana poprawa:[IO.File]::WriteAllLines(($filename | Resolve-Path), $content)
Johny Skovdal

50

Pomyślałem, że to nie będzie UTF, ale właśnie znalazłem dość proste rozwiązanie, które wydaje się działać ...

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

Dla mnie daje to utf-8 bez pliku BOM niezależnie od formatu źródłowego.


8
To działało dla mnie, z wyjątkiem tego, że użyłem -encoding utf8moich wymagań.
Chim Chimz

1
Dziękuję Ci bardzo. Pracuję z logami zrzutu narzędzia - które zawierało zakładki. UTF-8 nie działał. ASCII rozwiązało problem. Dzięki.
user1529294,

44
Tak, -Encoding ASCIIunika się problemu BOM, ale oczywiście otrzymujesz tylko 7-bitowe znaki ASCII . Biorąc pod uwagę, że ASCII jest podzbiorem UTF-8, plik wynikowy jest technicznie również poprawnym plikiem UTF-8, ale wszystkie znaki inne niż ASCII w twoim danych wejściowych zostaną przekonwertowane na ?znaki dosłowne .
mklement0

4
@ChimChimz Przypadkowo głosowałem za twoim komentarzem, ale -encoding utf8nadal wysyła UTF-8 z BOM. :(
TheDudeAbides

33

Uwaga: ta odpowiedź dotyczy programu Windows PowerShell ; dla kontrastu w wieloplatformowej edycji PowerShell Core (v6 +) UTF-8 bez BOM jest domyślnym kodowaniem dla wszystkich poleceń cmdlet.
Innymi słowy: Jeśli używasz programu PowerShell [Core] w wersji 6 lub nowszej , domyślnie otrzymujesz pliki UTF-8 bez BOM (które możesz również jawnie zażądać za pomocą -Encoding utf8/ -Encoding utf8NoBOM, podczas gdy za pomocą kodowania -BOM za pomocą -utf8BOM).


Aby uzupełnić własną prostą i pragmatyczną odpowiedź M. Dudleya (i bardziej zwięzłą przeformułowanie ForNeVeR ):

Dla wygody, oto zaawansowana funkcja Out-FileUtf8NoBom, oparta na potoku alternatywa, która naśladujeOut-File , co oznacza:

  • możesz go używać tak jak Out-Filew potoku.
  • obiekty wejściowe, które nie są łańcuchami, są formatowane tak, jakby były wysyłane do konsoli, tak jak w przypadku Out-File.

Przykład:

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

Zwróć uwagę na sposób, w jaki (Get-Content $MyPath)jest zamknięty (...), co gwarantuje, że cały plik zostanie otwarty, w pełni odczytany i zamknięty przed wysłaniem wyniku przez potok. Jest to konieczne, aby móc ponownie zapisać w tym samym pliku (zaktualizować go w miejscu ).
Ogólnie jednak technika ta nie jest wskazana z dwóch powodów: (a) cały plik musi zmieścić się w pamięci i (b) jeśli polecenie zostanie przerwane, dane zostaną utracone.

Uwaga na temat wykorzystania pamięci :

  • Własna odpowiedź M. Dudleya wymaga, aby cała zawartość pliku była najpierw gromadzona w pamięci, co może być problematyczne w przypadku dużych plików.
  • Poniższa funkcja poprawia się tylko nieznacznie: wszystkie obiekty wejściowe są najpierw buforowane, ale ich reprezentacje łańcuchowe są następnie generowane i zapisywane kolejno w pliku wyjściowym.

Kod źródłowyOut-FileUtf8NoBom (dostępny również jako Gist na licencji 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()
  }

}

16

Począwszy od wersji 6 program PowerShell obsługuje UTF8NoBOMkodowanie zarówno dla zawartości zestawu, jak i pliku wyjściowego, a nawet używa go jako kodowania domyślnego.

W powyższym przykładzie powinno to wyglądać tak:

$MyFile | Out-File -Encoding UTF8NoBOM $MyPath

@ RaúlSalinas-Monteagudo, na jakiej jesteś wersji?
John Bentley,

Miły. Wersja FYI sprawdzić z$PSVersionTable.PSVersion
KCD

14

Używając Set-Contentzamiast Out-File, możesz określić kodowanie Byte, którego można użyć do zapisania tablicy bajtów w pliku. To w połączeniu z niestandardowym kodowaniem UTF8, które nie emituje BOM, daje pożądany rezultat:

# 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

Różnica w stosowaniu [IO.File]::WriteAllLines()lub podobnym polega na tym, że powinien on działać poprawnie z każdym typem elementu i ścieżki, nie tylko rzeczywistymi ścieżkami plików.


5

Ten skrypt konwertuje, do UTF-8 bez BOM, wszystkie pliki .txt w DIRECTORY1 i wysyła je do DIRECTORY2

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

Ten zawodzi bez żadnego ostrzeżenia. Jakiej wersji programu PowerShell należy użyć, aby go uruchomić?
darksoulsong

3
Rozwiązanie WriteAllLines doskonale sprawdza się w przypadku małych plików. Potrzebuję jednak rozwiązania dla większych plików. Za każdym razem, gdy próbuję użyć tego z większym plikiem, pojawia się błąd 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" 
    }  

Źródło Jak usunąć UTF8 Byte Order Mark (BOM) z pliku za pomocą PowerShell


2

Jeśli chcesz użyć [System.IO.File]::WriteAllLines(), powinieneś rzucić drugi parametr na String[](jeśli typ $MyFilejest Object[]), a także określić ścieżkę bezwzględną za pomocą $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath):

$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)

Jeśli chcesz użyć [System.IO.File]::WriteAllText(), czasami powinieneś | Out-String |przesłać drugi parametr, aby dodać CRLF na końcu każdej linii w sposób wyraźny (szczególnie, gdy używasz ich z 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)

Lub możesz użyć [Text.Encoding]::UTF8.GetBytes()z 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"

patrz: Jak zapisać wynik ConvertTo-Csv do pliku w UTF-8 bez BOM


Dobre wskazówki; sugestie /: prostszą alternatywą $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath)jest Convert-Path $MyPath; jeśli chcesz zapewnić końcowe CRLF, po prostu użyj [System.IO.File]::WriteAllLines()nawet z jednym ciągiem wejściowym (nie ma takiej potrzeby Out-String).
mklement0

0

Jedną z technik, których używam, jest przekierowanie danych wyjściowych do pliku ASCII za pomocą polecenia cmdlet Out-File .

Na przykład często uruchamiam skrypty SQL, które tworzą inny skrypt SQL do wykonania w Oracle. Dzięki prostemu przekierowaniu („>”) dane wyjściowe będą w formacie UTF-16, który nie jest rozpoznawany przez SQLPlus. Aby obejść ten problem:

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

Wygenerowany skrypt może być następnie wykonany przez inną sesję SQLPlus bez żadnych obaw związanych z Unicode:

sqlplus / as sysdba "@new_script.sql" |
tee new_script.log

4
Tak, -Encoding ASCIIpozwala uniknąć problemu BOM, ale oczywiście otrzymujesz wsparcie tylko dla 7-bitowych znaków ASCII . Biorąc pod uwagę, że ASCII jest podzbiorem UTF-8, plik wynikowy jest technicznie również poprawnym plikiem UTF-8, ale wszystkie znaki inne niż ASCII w twoim danych wejściowych zostaną przekonwertowane na ?znaki dosłowne .
mklement0

Ta odpowiedź wymaga więcej głosów. Niezgodność sqlplus z BOM jest przyczyną wielu bólów głowy .
Amit Naidu

0

Zmień wiele plików przez rozszerzenie na UTF-8 bez 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)
}

0

Z jakiegokolwiek powodu WriteAllLineswywołania nadal generowały dla mnie BOM, z UTF8Encodingargumentem Bez BOM i bez niego. Ale następujące działały dla mnie:

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

Musiałem ustawić bezwzględną ścieżkę pliku, aby działała. W przeciwnym razie plik zostanie zapisany na moim pulpicie. Przypuszczam, że to działa tylko wtedy, gdy wiesz, że twój BOM ma 3 bajty. Nie mam pojęcia, jak wiarygodne jest oczekiwanie określonego formatu / długości BOM na podstawie kodowania.

Ponadto, jak napisano, prawdopodobnie działa to tylko wtedy, gdy plik mieści się w tablicy PowerShell, która wydaje się mieć limit długości o wartości niższej niż [int32]::MaxValuena moim komputerze.


1
WriteAllLinesbez argumentu kodującego nigdy nie zapisuje samej BOM , ale możliwe jest, że Twój łańcuch zaczynał się od znaku BOM ( U+FEFF), który po napisaniu skutecznie stworzył BOM UTF-8; np .: $s = [char] 0xfeff + 'hi'; [io.file]::WriteAllText((Convert-Path t.txt), $s)(pomiń, [char] 0xfeff + aby zobaczyć, że BOM nie jest zapisany).
mklement0

1
Co do nieoczekiwanego pisania w innej lokalizacji: problem polega na tym, że struktura .NET zazwyczaj ma inny katalog bieżący niż PowerShell; możesz albo zsynchronizować je najpierw [Environment]::CurrentDirectory = $PWD.ProviderPath, albo jako bardziej ogólną alternatywę dla swojego "$(pwd)\..."podejścia (lepiej "$pwd\...""$($pwd.ProviderPath)\..."(Join-Path $pwd.ProviderPath ...)(Convert-Path BOMthetorpedoes.txt)
:,

Dzięki, nie zdawałem sobie sprawy, że może istnieć jeden znak BOM w takiej konwersji BOM UTF-8.
xdhmoore

1
Wszystkie sekwencje bajtów BOM (podpisy Unicode) są w rzeczywistości reprezentacją bajtów odpowiedniego kodowania abstrakcyjnego pojedynczego znaku UnicodeU+FEFF .
mklement0

Ach ok. To wydaje się upraszczać.
xdhmoore

-2

Można użyć poniżej, aby uzyskać UTF8 bez BOM

$MyFile | Out-File -Encoding ASCII

4
Nie, przekonwertuje dane wyjściowe na bieżącą stronę kodową ANSI (na przykład cp1251 lub cp1252). To wcale nie jest UTF-8!
ForNeVeR

1
Dzięki Robin. Może to nie zadziałało w przypadku zapisu pliku UTF-8 bez BOM, ale opcja -Encoding ASCII usunęła BOM. W ten sposób mogłem wygenerować plik bat dla gvim. Plik .bat pojawiał się na BOM.
Greg,

3
@ForNeVeR: Masz rację, że kodowanie ASCIInie jest UTF-8, ale nie jest to również bieżąca strona kodowa ANSI - myślisz o tym Default; ASCIInaprawdę jest 7-bitowym kodowaniem ASCII, przy czym punkty kodowe> = 128 są konwertowane na dosłowne ?instancje.
mklement0

1
@ForNeVeR: Prawdopodobnie myślisz o „ANSI” lub „ rozszerzonym ASCII”. Spróbuj tego, aby sprawdzić, czy -Encoding ASCIIrzeczywiście jest to tylko 7-bitowy ASCII: 'äb' | out-file ($f = [IO.Path]::GetTempFilename()) -encoding ASCII; '?b' -eq $(Get-Content $f; Remove-Item $f)- äzostał transliterowany na a ?. Natomiast -Encoding Default(„ANSI”) poprawnie to zachowałby.
mklement0

3
@rob To idealna odpowiedź dla wszystkich, którzy po prostu nie potrzebują utf-8 lub czegokolwiek innego niż ASCII i nie są zainteresowani zrozumieniem kodowania i celu Unicode. Możesz użyć go jako utf-8, ponieważ równoważne znaki utf-8 dla wszystkich znaków ASCII są identyczne (oznacza to, że konwersja pliku ASCII na plik utf-8 daje identyczny plik (jeśli nie otrzyma BOM)). Dla wszystkich, którzy mają w tekście znaki spoza ASCII, ta odpowiedź jest po prostu fałszywa i myląca.
TNT

-3

Ten działa dla mnie (użyj „Domyślne” zamiast „UTF8”):

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

Wynikiem jest ASCII bez BOM.


1
Zgodnie z dokumentacją wyjściową określającą Defaultkodowanie będzie używać bieżącej strony kodowej ANSI systemu, która nie jest UTF-8, jak wymagałem.
M. Dudley

Wydaje mi się, że to działa, przynajmniej w przypadku Export-CSV. Jeśli otworzysz plik wynikowy w odpowiednim edytorze, kodowanie pliku to UTF-8 bez BOM, a nie Western Latin ISO 9, jak bym się spodziewał z ASCII
eythort

Wiele edytorów otwiera plik jako UTF-8, jeśli nie mogą wykryć kodowania.
emptyother 22.07.17
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.