Powershell и русские символы в консольных приложениях

В процессе разработки очень часто возникает необходимость запустить из powershell скрипта консольное приложение. Что может быть проще?
#test.ps1
& $PSScriptRoot\ConsoleApp.exe

f9b8f5bd78ba4bb88940ce728b191103.png

Изучим поведение консольных приложений при запуске их из командной строки, через PowerShell и через PowerShell ISE:
Результат выполнения
0d5852b0dd4249a29d32012047e6340e.png

В PowerShell ISE возникла проблема с кодировкой, так как ISE ожидает вывод в кодировке 1251. Воспользуемся гуглом и найдем два решения проблемы: c использованием [Console]:: OutputEncoding и через powershell pipeline. Воспользуемся первым решением:
test2.ps1
$ErrorActionPreference = "Stop"

function RunConsole($scriptBlock)
{
    $encoding = [Console]::OutputEncoding 
    [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
    try
    {
        &$scriptBlock
    }
    finally
    {
        [Console]::OutputEncoding = $encoding
    }
}

RunConsole {
    & $PSScriptRoot\ConsoleApp1.exe
}


Результат выполнения
5fc4c158556743d8b90212e53ee14dc2.png

В командной строке все хорошо, а вот в ISE ошибка. Exception setting «OutputEncoding»: «The handle is invalid.». Снова берем в руки гугл, и в первом же результате находим решение — надо запустить какое-нибудь консольное приложение для создания консоли. Ну что-же. попробуем.
test3.ps1
$ErrorActionPreference = "Stop"

function RunConsole($scriptBlock)
{
    # Популярное решение "устранения" ошибки: Exception setting "OutputEncoding": "The handle is invalid."
    & cmd /c ver | Out-Null

    $encoding = [Console]::OutputEncoding 
    [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
    try
    {
        &$scriptBlock
    }
    finally
    {
        [Console]::OutputEncoding = $encoding
    }
}

RunConsole {
    & $PSScriptRoot\ConsoleApp1.exe
}


Результат выполнения
9f3ae8aee5154589a7f487a942e65a2b.png

Все красиво, все работает. Кто читал мою прошлую заметку, обратил внимание, что WinRM приносит нам много острых впечатлений. Попробуем запустить тест через WinRM. Для запуска воспользуемся вот таким скриптом:
remote1.ps1
param($script)

$ErrorActionPreference = "Stop"

$s = New-PSSession "."
try
{
    $path = "$PSScriptRoot\$script"
    Invoke-Command -Session $s -ScriptBlock { &$using:path }
}
finally
{
    Remove-PSSession -Session $s
}



Результат выполнения
1a46c20921c8473a9de9bb4480c48607.png

Что-то пошло не так. Решение с созданием консоли не работает. Ранее мы находили два решения проблемы кодировки. Попробуем второй:
test4.ps1
$ErrorActionPreference = "Stop"
#$VerbosePreference = "Continue"

function RunConsole($scriptBlock)
{
    function ConvertTo-Encoding ([string]$From, [string]$To)
    {
        Begin
        {
            $encFrom = [System.Text.Encoding]::GetEncoding($from)
            $encTo = [System.Text.Encoding]::GetEncoding($to)
        }
        Process
        {
            $bytes = $encTo.GetBytes($_)
            $bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
            $encTo.GetString($bytes)
        }
    }

    Write-Verbose "RunConsole: Pipline mode"
    &$scriptBlock | ConvertTo-Encoding cp866 windows-1251 
}

RunConsole {
    & $PSScriptRoot\ConsoleApp1.exe
}


Результат выполнения
bb87a5386a3a43ac9968ab0ab1854f5e.png

В ISE и через WinRM решение работает, а вот через командную строку и shell — нет.
Надо объединить эти два способа и проблема будет решена!
test5.ps1
$ErrorActionPreference = "Stop"
#$VerbosePreference = "Continue"

function RunConsole($scriptBlock)
{
    if([Environment]::UserInteractive)
    {
        # Популярное решение "устранения" ошибки: Exception setting "OutputEncoding": "The handle is invalid."
        & cmd /c ver | Out-Null

        $encoding = [Console]::OutputEncoding 
        [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")

        try
        {
            Write-Verbose "RunConsole: Console.OutputEncoding mode"
            &$scriptBlock
            return
        }
        finally
        {
            [Console]::OutputEncoding = $encoding
        }
    }

    function ConvertTo-Encoding ([string]$From, [string]$To)
    {
        Begin
        {
            $encFrom = [System.Text.Encoding]::GetEncoding($from)
            $encTo = [System.Text.Encoding]::GetEncoding($to)
        }
        Process
        {
            $bytes = $encTo.GetBytes($_)
            $bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
            $encTo.GetString($bytes)
        }
    }

    Write-Verbose "RunConsole: Pipline mode"
    &$scriptBlock | ConvertTo-Encoding cp866 windows-1251 
}

RunConsole {
    & $PSScriptRoot\ConsoleApp1.exe
}


Результат выполнения
15a4d9218b5346ad98c9b4ac84dd24c8.png

Кажется, что проблема решена, но продолжим исследование и усложним наше консольное приложение, добавив в него вывод в stdError.
Результат выполнения
c815ca99e22e45548b45571eec510387.png

Становится все веселее:) В ISE исполнение скрипта прервалось на середине, а через WinRM мало того, что прервалось, так еще сообщение из stdErr прочитать невозможно. Первым шагом решим проблему с остановкой запускаемого из скрипта приложения, для этого перед запуском приложения изменим значение глобальной переменной $ErrorActionPreference.
test7.ps1
$ErrorActionPreference = "Stop"
#$VerbosePreference = "Continue"

function RunConsole($scriptBlock)
{
    if([Environment]::UserInteractive)
    {
        # Популярное решение "устранения" ошибки: Exception setting "OutputEncoding": "The handle is invalid."
        & cmd /c ver | Out-Null

        $encoding = [Console]::OutputEncoding 
        [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")

        try
        {
            Write-Verbose "RunConsole: Console.OutputEncoding mode"
            $prevErrAction = $ErrorActionPreference
            $ErrorActionPreference = "Continue"
            try
            {
                &$scriptBlock
                return
            }
            finally
            {
                $ErrorActionPreference = $prevErrAction
            }
        }
        finally
        {
            [Console]::OutputEncoding = $encoding
        }
    }

    function ConvertTo-Encoding ([string]$From, [string]$To)
    {
        Begin
        {
            $encFrom = [System.Text.Encoding]::GetEncoding($from)
            $encTo = [System.Text.Encoding]::GetEncoding($to)
        }
        Process
        {
            $bytes = $encTo.GetBytes($_)
            $bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
            $encTo.GetString($bytes)
        }
    }

    Write-Verbose "RunConsole: Pipline mode"
    $prevErrAction = $ErrorActionPreference
    $ErrorActionPreference = "Continue"
    try
    {
        &$scriptBlock | ConvertTo-Encoding cp866 windows-1251 
        return
    }
    finally
    {
        $ErrorActionPreference = $prevErrAction
    }
}

RunConsole {
    & $PSScriptRoot\ConsoleApp2.exe
}
Write-Host "ExitCode = $LASTEXITCODE"


Результат выполнения
e01bea6ccf394a1cabf2b67b1a1ebaea.png

Для тех что знает о существовании параметра -ErrorAction
error.cmd
echo error message 1>&2

errorActionTest.ps1
#error.cmd
#echo error message 1>&2

#errorActionTest.ps1
$ErrorActionPreference = "Stop"
Write-Host "before"
Invoke-Expression -ErrorAction SilentlyContinue -Command $PSScriptRoot\error.cmd
Write-Host "after"

Какой будет результат выполнения такого скрипта?

Вторым шагом доработаем скрипт удаленного запуска через WinRM, чтобы он не падал
remote2.ps1
param($script)

$ErrorActionPreference = "Stop"

$s = New-PSSession "."
try
{
    $path = "$PSScriptRoot\$script"

    $err = @()
    $r = Invoke-Command -Session $s -ErrorAction Continue -ErrorVariable err -ScriptBlock `
    {
        $ErrorActionPreference = "Stop"
        & $using:path | Out-Host
        return $true
    } 

    if($r -ne $true)
    {
        Write-Error "The remote script was completed with an error"
    }

    if($err.length -ne 0)
    {
        Write-Warning "Error occurred on remote host"
    }
}
finally
{
    Remove-PSSession -Session $s
}


Результат выполнения
3b0edaeaef964c9e90be77cce49e3c68.png

И осталось самое сложное — скорректировать сообщение формируемое через stdErr и при этом не изменить его положение в логе. В процессе решения этой задачи коллеги предложили самостоятельно создать консоль, воспользовавшись win api функцией AllocConsole.
test8.ps1
$ErrorActionPreference = "Stop"
#$VerbosePreference = "continue"

$consoleAllocated = [Environment]::UserInteractive
function AllocConsole()
{
    if($Global:consoleAllocated)
    {
        return
    }

    $a = @' 
[DllImport("kernel32", SetLastError = true)] 
public static extern bool AllocConsole(); 
'@

    $params = New-Object CodeDom.Compiler.CompilerParameters 
    $params.MainClass = "methods" 
    $params.GenerateInMemory = $true 
    $params.CompilerOptions = "/unsafe" 
 
    $r = Add-Type -MemberDefinition $a -Name methods -Namespace kernel32 -PassThru -CompilerParameters $params

    Write-Verbose "Allocating console"
    [kernel32.methods]::AllocConsole() | Out-Null
    Write-Verbose "Console allocated"
    $Global:consoleAllocated = $true
}

function RunConsole($scriptBlock)
{
    AllocConsole

    $encoding = [Console]::OutputEncoding 
    [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
    $prevErrAction = $ErrorActionPreference
    $ErrorActionPreference = "Continue"
    try
    {
        & $scriptBlock
    }
    finally
    {
        $ErrorActionPreference = $prevErrAction
        [Console]::OutputEncoding = $encoding
    }
}

RunConsole {
    & $PSScriptRoot\ConsoleApp2.exe
}
Write-Host "ExitCode = $LASTEXITCODE"



57f8e279ab7140658af997c867b88c15.png
Избавится от информации, которую добавляет powershell к stdErr мне так и не удалось.

Надеюсь, что эта информация окажется полезной не только мне! :)

Комментарии (3)

  • 3 февраля 2017 в 21:39

    +1

    Я извиняюсь, а разве chcp unicode либо chcp 1251 не помогут?
    • 3 февраля 2017 в 21:51

      +1

      Не поможет.

      chcp 1251

      test-1.ps1
      $ErrorActionPreference = "Stop"
      chcp 1251
      & $PSScriptRoot\ConsoleApp1.exe
      


      Результат выполнения
      86c409853b4147fbb99af836bfbd105d.png

      chcp 65001
      test-2.ps1
      $ErrorActionPreference = "Stop"
      chcp 65001
      & $PSScriptRoot\ConsoleApp1.exe
      


      Результат выполнения
      78b85e41c216463db9d8c82f89aa9608.png
  • 4 февраля 2017 в 00:49

    0

    Русские символы? Кажется, вы хотели сказать «кириллица».

© Habrahabr.ru