Powershell и русские символы в консольных приложениях
В процессе разработки очень часто возникает необходимость запустить из powershell скрипта консольное приложение. Что может быть проще?
Изучим поведение консольных приложений при запуске их из командной строки, через PowerShell и через PowerShell ISE:
В PowerShell ISE возникла проблема с кодировкой, так как ISE ожидает вывод в кодировке 1251. Воспользуемся гуглом и найдем два решения проблемы: c использованием [Console]:: OutputEncoding и через powershell pipeline. Воспользуемся первым решением:
В командной строке все хорошо, а вот в ISE ошибка. Exception setting «OutputEncoding»: «The handle is invalid.». Снова берем в руки гугл, и в первом же результате находим решение — надо запустить какое-нибудь консольное приложение для создания консоли. Ну что-же. попробуем.
Все красиво, все работает. Кто читал мою прошлую заметку, обратил внимание, что WinRM приносит нам много острых впечатлений. Попробуем запустить тест через WinRM. Для запуска воспользуемся вот таким скриптом:
Что-то пошло не так. Решение с созданием консоли не работает. Ранее мы находили два решения проблемы кодировки. Попробуем второй:
В ISE и через WinRM решение работает, а вот через командную строку и shell — нет.
Надо объединить эти два способа и проблема будет решена!
Кажется, что проблема решена, но продолжим исследование и усложним наше консольное приложение, добавив в него вывод в stdError.
Становится все веселее:) В ISE исполнение скрипта прервалось на середине, а через WinRM мало того, что прервалось, так еще сообщение из stdErr прочитать невозможно. Первым шагом решим проблему с остановкой запускаемого из скрипта приложения, для этого перед запуском приложения изменим значение глобальной переменной $ErrorActionPreference.
Вторым шагом доработаем скрипт удаленного запуска через WinRM, чтобы он не падал
И осталось самое сложное — скорректировать сообщение формируемое через stdErr и при этом не изменить его положение в логе. В процессе решения этой задачи коллеги предложили самостоятельно создать консоль, воспользовавшись win api функцией AllocConsole.
Избавится от информации, которую добавляет powershell к stdErr мне так и не удалось.
#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"
Результат выполнения
Для тех что знает о существовании параметра -ErrorAction
error.cmd
errorActionTest.ps1
Какой будет результат выполнения такого скрипта?
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 мне так и не удалось.
Надеюсь, что эта информация окажется полезной не только мне! :)
Комментарии (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
Результат выполнения
chcp 65001test-2.ps1$ErrorActionPreference = "Stop" chcp 65001 & $PSScriptRoot\ConsoleApp1.exe
Результат выполнения
4 февраля 2017 в 00:49
0↑
↓
Русские символы? Кажется, вы хотели сказать «кириллица».