Powershell і російські символи в консольних додатках

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



Вивчимо поведінку консольних додатків при запуску їх з командного рядка, через PowerShell і через PowerShell ISE:
Результат виконання

В 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
}


Результат виконання

У командному рядку все добре, а от у 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
}


Результат виконання

Все красиво, все працює. Хто читав мою минулу замітку, звернув увагу, що 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
}



Результат виконання

Щось пішло не так. Рішення з створенням консолі не працює. Раніше ми знаходили два вирішення проблеми кодування. Спробуємо другий:
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
}


Результат виконання

У 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
}


Результат виконання

Здається, що проблема вирішена, але продовжимо дослідження і ускладнимо наше консольний додаток, додавши в нього висновок stdError.
Результат виконання

Стає все веселіше :) У 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"


Результат виконання

Для тих що знає про існування параметра -ErrorActionerror.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
}


Результат виконання

І залишилося найскладніше — скорегувати повідомлення формується через 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"




Позбавиться від інформації, яку додає powershell до stdErr мені так і не вдалося.

Сподіваюся, що ця інформація буде корисною не тільки мені! :)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.