Использование PowerShell для записи файла в UTF-8 без спецификации

246

Out-File кажется, заставляет спецификацию при использовании UTF-8:

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

Как я могу написать файл в UTF-8 без спецификации, используя PowerShell?

М. Дадли
источник
23
BOM = Порядок следования байтов. Три символа, размещенные в начале файла (0xEF, 0xBB, 0xBF), которые выглядят как «ï»
Sign
40
Это невероятно расстраивает. Даже сторонние модули загрязняются, например, пытаясь загрузить файл через SSH? BOM! «Да, давайте испортим каждый файл; это звучит как хорошая идея». -Microsoft.
MichaelGG
3
Кодировка по умолчанию - UTF8NoBOM, начиная с Powershell версии 6.0. Docs.microsoft.com/en-us/powershell/module/…
Пол Ширяев,
Разговор о нарушении обратной совместимости ...
Dragas

Ответы:

220

Использование UTF8Encodingкласса .NET и передача $Falseв конструктор, кажется, работает:

$MyRawString = Get-Content -Raw $MyPath
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllLines($MyPath, $MyRawString, $Utf8NoBomEncoding)
М. Дадли
источник
42
Тьфу, я надеюсь, что это не единственный способ.
Скотт Мук
114
Одной строки [System.IO.File]::WriteAllLines($MyPath, $MyFile)достаточно. Эта WriteAllLinesперегрузка пишет именно UTF8 без спецификации.
Роман Кузьмин
6
Создан запрос функции MSDN здесь: connect.microsoft.com/PowerShell/feedbackdetail/view/1137121/…
Groostav
3
Обратите внимание, что, WriteAllLinesкажется, требуется $MyPathбыть абсолютным.
Щуберт
10
@xdhmoore WriteAllLinesполучает текущий каталог от [System.Environment]::CurrentDirectory. Если вы откроете PowerShell, а затем измените свой текущий каталог (используя cdили Set-Location), [System.Environment]::CurrentDirectoryэто не изменится, и файл окажется в неправильном каталоге. Вы можете обойти это путем [System.Environment]::CurrentDirectory = (Get-Location).Path.
Шаян Токрей
79

На данный момент правильным способом является использование решения, рекомендованного @Roman Kuzmin в комментариях к @M. Дадли ответ :

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

(Я также немного сократил это, удалив ненужное Systemуточнение пространства имен - оно будет заменено автоматически по умолчанию.)

Никогда
источник
2
Это (по какой-то причине) не удаляло спецификацию для меня, где, как это было принято в ответе
Лиам
@ Лиам, возможно какая-то старая версия PowerShell или .NET?
ForNeVeR
1
Я полагаю, что старые версии функции .NET WriteAllLines действительно записывали спецификацию по умолчанию. Так что это может быть проблема версии.
Величайший Бендер
2
Подтверждено записями с использованием спецификации в Powershell 3, но без спецификации в Powershell 4. Мне пришлось использовать оригинальный ответ М. Дадли.
chazbot7
2
Так что он работает в Windows 10, где он установлен по умолчанию. :) Также предлагается улучшение:[IO.File]::WriteAllLines(($filename | Resolve-Path), $content)
Джонни Сковдал
50

Я подумал, что это не будет UTF, но я нашел довольно простое решение, которое, кажется, работает ...

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

Для меня это приводит к UTF-8 без файла BOM независимо от исходного формата.

Lenny
источник
8
Это сработало для меня, за исключением того, что я использовал -encoding utf8для моего требования.
Чим Чимз
1
Большое спасибо. Я работаю с дампами журнала инструмента, в котором есть вкладки. UTF-8 не работал. ASCII решил проблему. Спасибо.
user1529294
44
Да, -Encoding ASCIIизбегает проблемы спецификации, но вы, очевидно, получаете только 7-битные символы ASCII . Учитывая, что ASCII является подмножеством UTF-8, результирующий файл технически также является допустимым файлом UTF-8, но все не входящие в ASCII символы в вашем вводе будут преобразованы в литеральные ?символы .
mklement0
4
@ChimChimz Я случайно проголосовал за ваш комментарий, но -encoding utf8все равно выдает UTF-8 с спецификацией. :(
TheDudeAbides
33

Примечание. Этот ответ относится к Windows PowerShell ; напротив, в кроссплатформенном выпуске PowerShell Core (v6 +) UTF-8 без спецификации является кодировкой по умолчанию для всех командлетов.
Другими словами: если вы используете PowerShell [Core] версии 6 или выше , по умолчанию вы получаете файлы без BOM UTF-8 (которые вы также можете явно запрашивать с помощью -Encoding utf8/ -Encoding utf8NoBOM, тогда как вы получаете с -BOM с кодировкой -utf8BOM).


В дополнение к простому и прагматичному ответу М. Дадлиболее краткой переформулировке ForNeVeR ):

Для удобства, вот расширенная функция Out-FileUtf8NoBom, альтернатива на основе конвейера, которая имитируетOut-File , что означает:

  • Вы можете использовать его так же, как Out-Fileв конвейере.
  • входные объекты, которые не являются строками, форматируются так, как если бы вы отправляли их на консоль, как и в случае с Out-File.

Пример:

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

Обратите внимание на то, как (Get-Content $MyPath)это включено (...), что гарантирует, что весь файл будет открыт, прочитан полностью и закрыт перед отправкой результата по конвейеру. Это необходимо для возможности обратной записи в тот же файл (обновить его на месте ).
Однако, как правило, этот метод не рекомендуется по двум причинам: (а) весь файл должен уместиться в памяти и (б) если команда прервана, данные будут потеряны.

Примечание об использовании памяти :

  • Собственный ответ М. Дадли требует, чтобы все содержимое файла сначала создавалось в памяти, что может быть проблематично для больших файлов.
  • Приведенная ниже функция улучшает это лишь незначительно: все входные объекты по-прежнему буферизуются первыми, но их строковые представления затем генерируются и записываются в выходной файл один за другим.

Исходный кодOut-FileUtf8NoBom (также доступный как Mist-лицензированный Gist ):

<#
.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
источник
16

Начиная с версии 6 powershell поддерживает UTF8NoBOMкодировку как для set-content, так и out-file, и даже использует ее в качестве кодировки по умолчанию.

Так что в приведенном выше примере это должно быть просто так:

$MyFile | Out-File -Encoding UTF8NoBOM $MyPath
sc911
источник
@ RaúlSalinas-Monteagudo какая у тебя версия?
Джон Бентли
Ницца. FYI проверить версию с$PSVersionTable.PSVersion
KCD
14

При использовании Set-Contentвместо Out-Fileвы можете указать кодировку Byte, которую можно использовать для записи байтового массива в файл. Это в сочетании с пользовательской кодировкой UTF8, которая не излучает спецификацию, дает желаемый результат:

# 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

Отличие от использования [IO.File]::WriteAllLines()или аналогичного заключается в том, что он должен хорошо работать с любым типом элемента и пути, а не только с реальными путями к файлам.

Лусеро
источник
5

Этот скрипт преобразует в UTF-8 без спецификации все TXT-файлы в DIRECTORY1 и выводит их в DIRECTORY2.

foreach ($i in ls -name DIRECTORY1\*.txt)
{
    $file_content = Get-Content "DIRECTORY1\$i";
    [System.IO.File]::WriteAllLines("DIRECTORY2\$i", $file_content);
}
jamhan
источник
Этот не проходит без предупреждения. Какую версию PowerShell я должен использовать для его запуска?
darksoulsong
3
Решение WriteAllLines отлично работает с небольшими файлами. Тем не менее, мне нужно решение для больших файлов. Каждый раз, когда я пытаюсь использовать это с большим файлом, я получаю ошибку OutOfMemory.
Бермудские острова,
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" 
    }  

Источник Как удалить UTF8 Byte Order Mark (BOM) из файла с помощью PowerShell

откровенный загар
источник
2

Если вы хотите использовать [System.IO.File]::WriteAllLines(), вы должны привести второй параметр к String[](если тип $MyFileis Object[]), а также указать абсолютный путь с помощью $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)

Если вы хотите использовать [System.IO.File]::WriteAllText(), иногда вам следует | Out-String |передать второй параметр, чтобы явно добавить CRLF в конец каждой строки (особенно, когда вы используете их с 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)

Или вы можете использовать [Text.Encoding]::UTF8.GetBytes()с 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"

см .: Как записать результат ConvertTo-Csv в файл в UTF-8 без спецификации

Сато Юсуке
источник
Хорошие указатели; предложения /: более простой альтернативой $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath)является Convert-Path $MyPath; если вы хотите обеспечить конечный CRLF, просто используйте [System.IO.File]::WriteAllLines()даже с одной входной строкой (без необходимости Out-String).
mklement0
0

Я использую одну технику, чтобы перенаправить вывод в файл ASCII с помощью командлета Out-File .

Например, я часто запускаю сценарии SQL, которые создают другой сценарий SQL для выполнения в Oracle. С простым перенаправлением (">") вывод будет в UTF-16, который не распознается SQLPlus. Чтобы обойти это:

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

Сгенерированный сценарий затем может быть выполнен через другой сеанс SQLPlus без каких-либо проблем с Юникодом:

sqlplus / as sysdba "@new_script.sql" |
tee new_script.log
Эрик Андерсон
источник
4
Да, -Encoding ASCIIпозволяет избежать проблемы спецификации, но вы, очевидно, получаете поддержку только 7-битных символов ASCII . Учитывая, что ASCII является подмножеством UTF-8, результирующий файл технически также является допустимым файлом UTF-8, но все не входящие в ASCII символы в вашем вводе будут преобразованы в литеральные ?символы .
mklement0
Этот ответ требует больше голосов. Несовместимость sqlplus с BOM является причиной многих головных болей .
Амит Найду
0

Измените несколько файлов по расширению на UTF-8 без спецификации:

$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

По какой-то причине, WriteAllLinesзвонки все еще производили BOM для меня, с UTF8Encodingаргументом BOMless и без него. Но у меня сработало следующее:

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

Я должен был сделать путь к файлу абсолютным, чтобы он работал. В противном случае он записал файл на мой рабочий стол. Кроме того, я полагаю, это работает, только если вы знаете, что ваша спецификация составляет 3 байта. Я понятия не имею, насколько надежно ожидать заданный формат / длину спецификации на основе кодирования.

Кроме того, как написано, это, вероятно, работает, только если ваш файл помещается в массив powershell, который, кажется, имеет ограничение длины на некоторое значение ниже, чем [int32]::MaxValueна моем компьютере.

xdhmoore
источник
1
WriteAllLinesбез аргумента кодирования никогда не записывается сама спецификация , но вполне возможно, что ваша строка начиналась с символа BOM ( U+FEFF), который при записи эффективно создавал спецификацию UTF-8; например: $s = [char] 0xfeff + 'hi'; [io.file]::WriteAllText((Convert-Path t.txt), $s)(опускаем , [char] 0xfeff + чтобы увидеть , что нет BOM не написано).
mklement0
1
Что касается неожиданной записи в другое место: проблема в том, что .NET Framework обычно имеет другой текущий каталог, чем PowerShell; вы можете либо синхронизировать их сначала с [Environment]::CurrentDirectory = $PWD.ProviderPath, либо, в качестве более общей альтернативы вашему "$(pwd)\..."подходу (лучше:, "$pwd\..."даже лучше: "$($pwd.ProviderPath)\..."или (Join-Path $pwd.ProviderPath ...)), использовать(Convert-Path BOMthetorpedoes.txt)
mklement0
Спасибо, я не осознавал, что может быть какой-либо символ BOM для преобразования UTF-8 BOM.
xdhmoore
1
Все последовательности байтов спецификации (подписи Unicode) фактически являются байтовым представлением соответствующей кодировки абстрактного одиночного символа UnicodeU+FEFF .
mklement0
Ах хорошо. Это, кажется, делает вещи проще.
xdhmoore
-2

Можно использовать ниже, чтобы получить UTF8 без спецификации

$MyFile | Out-File -Encoding ASCII
Робин Ван
источник
4
Нет, он преобразует выходные данные в текущую кодовую страницу ANSI (например, cp1251 или cp1252). Это совсем не UTF-8!
ForNeVeR
1
Спасибо, Робин. Возможно, это не сработало для записи файла UTF-8 без спецификации, но опция -Encoding ASCII удалила спецификацию. Таким образом, я мог сгенерировать файл bat для gvim. Файл .bat срабатывает в спецификации.
Грег
3
@ForNeVeR: Вы правы, что кодировка ASCIIне UTF-8, но это не текущая кодовая страница ANSI - вы думаете Default; ASCIIдействительно является 7-битной кодировкой ASCII, с кодовыми точками> = 128, преобразованными в литеральные ?экземпляры.
mklement0
1
@ForNeVeR: Вы, вероятно, думаете о «ANSI» или « расширенном ASCII». Попробуйте, чтобы убедиться, что -Encoding ASCIIэто действительно только 7-битный ASCII: 'äb' | out-file ($f = [IO.Path]::GetTempFilename()) -encoding ASCII; '?b' -eq $(Get-Content $f; Remove-Item $f)- äбыл транслитерирован в ?. Напротив, -Encoding Default(«ANSI») правильно его сохранит.
mklement0
3
@rob Это идеальный ответ для всех, кому просто не нужен utf-8 или что-то еще, что отличается от ASCII и не заинтересовано в понимании кодировок и цели юникода. Вы можете использовать его как utf-8, потому что эквивалентные символы utf-8 для всех символов ASCII идентичны (означает, что преобразование ASCII-файла в файл utf-8 приводит к идентичному файлу (если он не получает спецификацию)). Для всех, у кого в тексте нет символов ASCII, этот ответ является просто ложным и вводящим в заблуждение.
TNT
-3

Это работает для меня (используйте «По умолчанию» вместо «UTF8»):

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

Результат ASCII без спецификации.

Кшиштоф
источник
1
В документации Out-File, указывающей Defaultкодировку, будет использоваться текущая кодовая страница ANSI системы, которая не является UTF-8, как мне требовалось.
М. Дадли
Мне кажется, это работает, по крайней мере, для Export-CSV. Если вы откроете полученный файл в соответствующем редакторе, кодировка файла будет UTF-8 без спецификации, а не Western Latin ISO 9, как я и ожидал с ASCII
eythort
Многие редакторы открывают файл как UTF-8, если не могут определить кодировку.
пусто,