Выход UTF-8 от PowerShell

Я пытаюсь использовать Process.Start с перенаправленным вводом-выводом для вызова PowerShell.exe со строкой, а для возврата обратно - все в UTF-8. Но я, похоже, не могу выполнить эту работу.

Что я пробовал:

  • Передача команды для запуска через параметр -Command
  • Запись PowerShell script в виде файла на диск с кодировкой UTF-8
  • Запись PowerShell script в виде файла на диск с UTF-8 с спецификацией
  • Запись PowerShell script в виде файла на диск с UTF-16
  • Настройка Console.OutputEncoding как в моем консольном приложении, так и в PowerShell script
  • Настройка $OutputEncoding в PowerShell
  • Настройка Process.StartInfo.StandardOutputEncoding
  • Выполнение всего с Encoding.Unicode вместо Encoding.UTF8

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

Вот мой код:

static void Main(string[] args)
{
    DumpBytes("Héllo");

    ExecuteCommand("PowerShell.exe", "-Command \"$OutputEncoding = [System.Text.Encoding]::UTF8 ; Write-Output 'Héllo';\"",
        Environment.CurrentDirectory, DumpBytes, DumpBytes);

    Console.ReadLine();
}

static void DumpBytes(string text)
{
    Console.Write(text + " " + string.Join(",", Encoding.UTF8.GetBytes(text).Select(b => b.ToString("X"))));
    Console.WriteLine();
}

static int ExecuteCommand(string executable, string arguments, string workingDirectory, Action<string> output, Action<string> error)
{
    try
    {
        using (var process = new Process())
        {
            process.StartInfo.FileName = executable;
            process.StartInfo.Arguments = arguments;
            process.StartInfo.WorkingDirectory = workingDirectory;
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.CreateNoWindow = true;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.RedirectStandardError = true;
            process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
            process.StartInfo.StandardErrorEncoding = Encoding.UTF8;

            using (var outputWaitHandle = new AutoResetEvent(false))
            using (var errorWaitHandle = new AutoResetEvent(false))
            {
                process.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        output(e.Data);
                    }
                };

                process.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        error(e.Data);
                    }
                };

                process.Start();

                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                process.WaitForExit();
                outputWaitHandle.WaitOne();
                errorWaitHandle.WaitOne();

                return process.ExitCode;
            }
        }
    }
    catch (Exception ex)
    {
        throw new Exception(string.Format("Error when attempting to execute {0}: {1}", executable, ex.Message),
            ex);
    }
}

Update

Я обнаружил, что если я сделаю это script:

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Host "Héllo!"
[Console]::WriteLine("Héllo")

Затем вызовите его через:

ExecuteCommand("PowerShell.exe", "-File C:\\Users\\Paul\\Desktop\\Foo.ps1",
  Environment.CurrentDirectory, DumpBytes, DumpBytes);

Первая строка повреждена, но вторая не соответствует:

H?llo! 48,EF,BF,BD,6C,6C,6F,21
Héllo 48,C3,A9,6C,6C,6F

Это говорит о том, что мой код перенаправления работает нормально; когда я использую Console.WriteLine в PowerShell, я получаю UTF-8, как я ожидаю.

Это означает, что команды PowerShell Write-Output и Write-Host должны делать что-то другое с выходом, а не просто вызывать Console.WriteLine.

Обновление 2

Я даже попробовал следующее, чтобы заставить кодовую страницу консоли PowerShell использовать UTF-8, но Write-Host и Write-Output продолжают создавать разорванные результаты, когда работает [Console]::WriteLine.

$sig = @'
[DllImport("kernel32.dll")]
public static extern bool SetConsoleCP(uint wCodePageID);

[DllImport("kernel32.dll")]
public static extern bool SetConsoleOutputCP(uint wCodePageID);
'@

$type = Add-Type -MemberDefinition $sig -Name Win32Utils -Namespace Foo -PassThru

$type::SetConsoleCP(65001)
$type::SetConsoleOutputCP(65001)

Write-Host "Héllo!"

& chcp    # Tells us 65001 (UTF-8) is being used

Решение

Ответ Ли был правильным. Как говорит Ли, я пытался все заставить PowerShell производить UTF-8, но это кажется невозможным. Вместо этого нам просто нужно прочитать поток, используя ту же кодировку PowerShell (стандартная OEM-кодировка). Нет необходимости сообщать Process.StartInfo для чтения с другой кодировкой, так как он уже читает по умолчанию.

Обновить, снова

Собственно, это не так. Я думаю, что Process.Start использует всю текущую кодировку; когда я запускал его под консольным приложением, он использовал OEM-кодировку и мог читать результат. Но при работе под Windows Service это не так. Поэтому я должен был принудительно его явно.

Вы можете получить кодовую страницу, используемую консолью, по ссылке @andyb, размещенной:

http://blogs.msdn.com/b/ddietric/archive/2010/11/08/decoding-standard-output-and-standard-error-when-redirecting-to-a-gui-application.aspx

Мне нужно было использовать подписи здесь: http://www.pinvoke.net/default.aspx/kernel32.getcpinfoex

Затем назначьте его:

CPINFOEX info;
if (GetCPInfoEx(CP_OEMCP, 0, out info))
{
    var oemEncoding = Encoding.GetEncoding(info.CodePage);
    process.StartInfo.StandardOutputEncoding = oemEncoding;
}

Ответы

Ответ 1

Это ошибка в .NET. Когда PowerShell запускается, он кэширует выходной дескриптор (Console.Out). Свойство Encoding этого текстового сценария не получает значения свойства StandardOutputEncoding.

Когда вы меняете его из PowerShell, свойство Encoding кэшированного обработчика вывода возвращает кешированное значение, поэтому вывод по-прежнему кодируется кодировкой по умолчанию.

В качестве обходного пути я предлагаю не менять кодировку. Он будет возвращен вам как строка Unicode, после чего вы сможете самостоятельно управлять кодировкой.

Пример кэширования:

102 [C:\Users\leeholm]
>> $r1 = [Console]::Out

103 [C:\Users\leeholm]
>> $r1

Encoding                                          FormatProvider
--------                                          --------------
System.Text.SBCSCodePageEncoding                  en-US



104 [C:\Users\leeholm]
>> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8

105 [C:\Users\leeholm]
>> $r1

Encoding                                          FormatProvider
--------                                          --------------
System.Text.SBCSCodePageEncoding                  en-US

Ответ 2

Не специалист по кодированию, но после прочтения этих...

... кажется довольно очевидным, что переменная $OutputEncoding влияет только на передачу данных в собственные приложения.

При отправке в файл с помощью PowerShell кодирование может управляться параметром -encoding в командлете out-file, например.

write-output "hello" | out-file "enctest.txt" -encoding utf8

Ничего другого, что вы можете сделать на фронте PowerShell тогда, но следующий пост может вам помочь:.

Ответ 3

Задайте [Console]::OuputEncoding как кодировку, которую вы хотите, и распечатайте с помощью [Console]::WriteLine.

Если метод вывода powershell имеет проблему, тогда не используйте его. Он чувствует себя немного плохо, но работает как шарм:)