Конвертировать источники в UTF-8 без спецификации - PullRequest
0 голосов
/ 05 февраля 2019

Я пытаюсь преобразовать все мои исходные файлы из целевой папки в кодировку UTF-8 (без спецификации).Я использую следующий скрипт 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 no-BOM, все национальные символы преобразуются в неизвестные символы (например, если я снова запускаю сценарий).Как изменить скрипт для решения проблемы?

Ответы [ 2 ]

0 голосов
/ 05 февраля 2019

Как указывает Ансгар Вихерс в комментарии, проблема заключается в том, что Windows PowerShell при отсутствии спецификации по умолчанию интерпретирует файлы как "ANSI" -кодируется , т. е. кодировка, подразумеваемая языковым стандартом устаревшей системы (кодовая страница ANSI), как отражено .NET Framework (но не .NET Core ) в [System.Text.Encoding]::Default.

Учитывая, что, исходя из ваших последующих комментариев, файлы без спецификации среди ваших входных файлов представляют собой смесь в кодировке Windows-1251 и UTF-8файлы , вы должны проверить их содержимое , чтобы определить их конкретную кодировку:

  • Считайте каждый файл с помощью -Encoding Utf8 и проверьте,результирующая строка содержит Unicode REPLACEMENT CHARACTER (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)    
}

A Более быстрая альтернатива заключается в использовании [IO.File]::ReadAllText() с UTF-8 объект кодирования, который генерирует исключение при обнаружении недопустимых байтов as-UTF-8 (синтаксис PSv5 +):

$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 Ядро по умолчанию (без спецификации) UTF-8, поэтому просто пропустите -Encoding не работает для чтения файлов в кодировке ANSI.

  • Аналогично, [System.Text.Encoding]::Default неизменно reпорты UTF-8 в .NET Core.

Поэтому необходимо вручную определить кодовую страницу 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 (уже) без спецификации, вы должны явно поручить 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)    
}

Примечание. Файлы UTF-8 без спецификации не требуют перезаписи в вашем сценарии, но это не мешает работе и упрощает код; альтернативой будет проверка , если первые 3 байта каждого файла являются спецификацией UTF-8 и пропустить такой файл:
$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), решение все еще работает , пока этифайлы имеют спецификацию , поскольку PowerShell (тихо) отдает предпочтение спецификации над кодировкой, указанной в -Encoding.

Обратите внимание, что использование -Raw / WriteAllText() длячтение / запись файлов в целом (одна строка) не только немного ускоряет обработку, но и обеспечивает сохранение следующих характеристик каждого входного файла :

  • определенный стиль новой строки (CRLF (Windows) и LF-only (Unix))
  • независимо от того, имеет ли последняя строка завершающий символ новой строки.

В отличие от этого, нетиспользование -Raw (построчное чтение) и использование .WriteAllLines() делает не сохранение этих характеристик: выnvariably получить соответствующие новой платформе строки (в Windows PowerShell, всегда CRLF), и вы всегда получаете в конце новой строки.


Обратите внимание, что мультиплатформенный Powershell Core edition заметно по умолчанию использует UTF-8 при чтении файла без спецификации и также по умолчанию создает файлы UTF-8 без спецификации - создание файла 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-Content и Set-Content относительно медленны , поэтому использование типов .NET для чтения и перезаписи файлов будет работать лучше.

Как и выше, никакое кодирование не должно быть явно указано в следующем решении (даже в Windows PowerShell ), потому что .NET с самого начала рекомендовал использовать UTF-8 без спецификации для UTF-8 (все еще распознавая спецификацию UTF-8 , если присутствует):

$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)
  )   
}
0 голосов
/ 05 февраля 2019

Правильно проверьте наличие 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...