ConvertTo-Json со специальными символами с Unscape или без - результат неверный

У меня есть файл, содержащий некоторые свойства, значение которых содержит экранирующие символы, например, некоторые шаблоны Urls и Regex.

При чтении содержимого и обратном преобразовании в json с удалением или без него содержимое не является правильным. Если я преобразую обратно в json с unescaping, некоторые регулярные выражения прерываются, если я преобразую с unescing, urls и некоторые регулярные выражения прерываются.

Как я могу решить проблему?

MCVE

Вот несколько простых блоков кода, которые позволяют просто воспроизвести проблему:

содержание

$fileContent = 
@"
{
    "something":  "http://domain/?x=1&y=2",
    "pattern":  "^(?!(\\`|\\~|\\!|\\@|\\#|\\$|\\||\\\\|\\'|\\\")).*"
}
"@

С Unescape

Если я прочитал содержимое, а затем преобразовал содержимое обратно в json, используя следующую команду:

$fileContent | ConvertFrom-Json | ConvertTo-Json | %{[regex]::Unescape($_)}

Вывод (что неверно) будет:

{
    "something":  "http://domain/?x=1&y=2",
    "pattern":  "^(?!(\|\~|\!|\@|\#|\$|\||\\|\'|\")).*"
}

Без эскейпа

Если я прочитал содержимое, а затем преобразовал содержимое обратно в json, используя следующую команду:

$fileContent | ConvertFrom-Json | ConvertTo-Json 

Вывод (что неверно) будет:

{
    "something":  "http://domain/?x=1\u0026y=2",
    "pattern":  "^(?!(\\|\\~|\\!|\\@|\\#|\\$|\\||\\\\|\\\u0027|\\\")).*"
}

3 ответа

Решение

Я решил не использовать Unscapeвместо того, чтобы заменить юникод \uxxxx символы с их строковыми значениями, и теперь это работает правильно:

$fileContent = 
@"
{
    "something":  "http://domain/?x=1&y=2",
    "pattern":  "^(?!(\\`|\\~|\\!|\\@|\\#|\\$|\\||\\\\|\\'|\\\")).*"
}
"@

$fileContent | ConvertFrom-Json | ConvertTo-Json | %{
    [Regex]::Replace($_, 
        "\\u(?<Value>[a-zA-Z0-9]{4})", {
            param($m) ([char]([int]::Parse($m.Groups['Value'].Value,
                [System.Globalization.NumberStyles]::HexNumber))).ToString() } )}

Который генерирует ожидаемый результат:

{
    "something":  "http://domain/?x=1&y=\\2",
    "pattern":  "^(?!(\\|\\~|\\!|\\@|\\#|\\$|\\||\\\\|\\'|\\\")).*"
}

Если вы не хотите полагаться на Regex (из ответа @Reza Aghaei), вы можете импортировать библиотеку Newtonsoft JSON. Преимущество - свойство StringEscapeHandling по умолчанию, которое экранирует только управляющие символы. Еще одно преимущество заключается в том, чтобы избежать потенциально опасных замен строк, которые вы делали бы с помощью Regex.

Эта StringEscapeHandlingтакже является обработкой по умолчанию для PowerShell Core (версии 6 и выше), потому что с тех пор они начали использовать Newtonsoft внутри себя. Таким образом, другой альтернативой было бы использование ConvertFrom-Json и ConvertTo-Json из PowerShell Core.

Ваш код будет выглядеть примерно так, если вы импортируете библиотеку Newtonsoft JSON:

[Reflection.Assembly]::LoadFile("Newtonsoft.Json.dll")

$json = Get-Content -Raw -Path file.json -Encoding UTF8 # read file
$unescaped = [Newtonsoft.Json.Linq.JObject]::Parse($json) # similar to ConvertFrom-Json

$escapedElementValue = [Newtonsoft.Json.JsonConvert]::ToString($unescaped.apiName.Value) # similar to ConvertTo-Json
$escapedCompleteJson = [Newtonsoft.Json.JsonConvert]::SerializeObject($unescaped) # similar to ConvertTo-Json

Write-Output "Variable passed = $escapedElementValue"
Write-Output "Same JSON as Input = $escapedCompleteJson"

tl;dr

Проблема не не влияет на PowerShell (ядро) 6+ (установки по требованию, кросс-платформенное PowerShell издание), которая использует другую реализацию и командлетов, основанный на Newtonsoft.JSON (чей прямое использование показано в ответе r3verse в ), начиная с Powershell 7.2. Здесь ваша примерная команда туда и обратно работает, как ожидалось.

Только в Windows PowerShell влияет (в комплекте-с-Windows PowerShell издание которой последняя и окончательная версия 5.1). Но обратите внимание, что представление JSON - хоть и неожиданное, но технически правильное .

Простой, но надежное решение сосредоточено только на неэкранированном эти управляющий последовательностью Unicode , которые неожиданно создают , а именно - для & ' < > - при исключении ложных срабатываний:

      # The following sample JSON with undesired Unicode escape sequences for `& < > '`, was
# created with Windows PowerShell's ConvertTo-Json as follows:
#   ConvertTo-Json "Ten o'clock at <night> & later. \u0027 \\u0027"
# Note that \u0027 and \\u0027 are NOT Unicode escape sequences and must not be
# interpreted as such.
# The *desired* JSON representation - without the unexpected escaping - would be:
#   "Ten o'clock at <night> & later. \\u0027 \\\\u0027"
$json = '"Ten o\u0027clock at \u003Cnight\u003e \u0026 later. \\u0027 \\\\u0027"'

[regex]::replace(
  $json, 
  '(?<=(?:^|[^\\])(?:\\\\)*)\\u(00(?:26|27|3c|3e))', 
  { param($match) [char] [int] ('0x' + $match.Groups[1].Value) },
  'IgnoreCase'
)

Вышеупомянутое выводит желаемое представление JSON без ненужного экранирования:

      "Ten o'clock at <night> & later. \\u0027 \\\\u0027"

Справочная информация :

в Windows PowerShell неожиданно представляет следующие символы диапазона ASCII их escape-последовательностями Unicode в строках JSON:

  • & (Управляющая последовательность Unicode: \u0026)
  • ' ( \u0027)
  • < а также > ( \u003c а также \u003e)

Для этого нет веских причин (эти символы требуют экранирования только в тексте HTML / XML).

Однако любой совместимый парсер JSON, включая ConvertFrom-Json - преобразует эти escape-последовательности обратно в символы, которые они представляют.

Другими словами: хотя текст JSON, созданный оболочкой Windows PowerShell, является неожиданным и может затруднять читаемость , он технически правильный и - хотя и не идентичен - эквивалентен исходному представлению с точки зрения данных, которые он представляет.


Устранение проблемы читабельности :

В стороне: в то время как [regex]::Unescape(), цель которого состоит в том, чтобы неэкранировать только регулярные выражения , также преобразует escape-последовательности Unicode в символы, которые они представляют, он принципиально не подходит для выборочного отмены экранирования последовательностей Unicode строк JSON , учитывая, что все другие escape-последовательности должны быть сохранены, чтобы строка JSON оставалась синтаксически действительной .

Хотя ваш ответ в целом работает хорошо, у него есть ограничения (помимо легко решаемой проблемы, которая a-zA-Z должно быть a-fA-Fчтобы ограничить сопоставление теми буквами, которые являются действительными шестнадцатеричными. цифры):

  • Это не исключает ложных срабатываний , таких как \\u0027 или \\\\u0027 ( \\ убегает, так что u0027 part становится дословной строкой и не может рассматриваться как escape-последовательность).

  • Он преобразует все escape-последовательности Unicode , что создает две проблемы:

    • Escape-последовательности, представляющие символы, требующие экранирования, также будут преобразованы в дословные представления символов, что нарушит представления JSON с помощью \u005c, например, учитывая, что символ, который он представляет, требует экранирования.

    • Для символов Unicode, отличных от BMP, которые должны быть представлены как пары управляющих последовательностей Unicode (так называемые суррогатные пары), ваше решение будет ошибочно пытаться отключить экранирование каждой половины пары отдельно.

Для надежного решения, которое преодолевает эти ограничения, см. Этот ответ (суррогатные пары остаются как escape-последовательности Unicode, escape-последовательности Unicode, символы которых требуют экранирования, преобразуются в \на основе (в стиле C) экранирования, например \n, если возможно).

Однако, если единственное требование - отменить экранирование тех escape-последовательностей Unicode, которые Windows PowerShell ConvertTo-Json неожиданно создает, решения наверху достаточно.

Другие вопросы по тегам