Powershell and Cyrillic in the console (updated)

In the development process is very often necessary to run a powershell script from a console application. What could be easier?

the
#test.ps1
& $PSScriptRoot\ConsoleApp.exe



Examine the behavior of console applications when running them from the command line, using PowerShell and using PowerShell ISE:

execution Result


In PowerShell ISE, there is a problem with encoding, as the ISE expects the output to the encoding 1251. Use Google and find two problem solving: c use [Console]::OutputEncoding and through the powershell pipeline. Let's use the first solution:

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
}


execution Result


At the command prompt, all is well, but in the ISE error. Exception setting "OutputEncoding": "The handle is invalid.".. Again, take the hands of Google and the first result, we find a solution, and you have to run the console application to create a console. Well, let's try.

test3.ps1
$ErrorActionPreference = "Stop"

function RunConsole($scriptBlock)
{
# Popular solution to the "troubleshoot" error: 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
}


execution Result


Everything is beautiful, everything works. Those who have read my last note, I noticed that WinRM brings us a lot of sharp impressions. Try to run the test via WinRM. To run use this script:

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
}



execution Result


Something went wrong. The solution with the creation of the console is not working. We have previously found two solutions to the problem encoding. Let's try the second:

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
}


execution Result


In the ISE and using WinRM solution works, but via the command line and shell no.
It is necessary to combine these two methods and the problem will be solved!

test5.ps1
$ErrorActionPreference = "Stop"
#$VerbosePreference = "Continue"

function RunConsole($scriptBlock)
{
if([Environment]::UserInteractive)
{
# Popular solution to the "troubleshoot" error: 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
}


execution Result


It seems that the problem is solved, but will continue to study and complicate our console application and add the output to stdError.

execution Result


It gets better :) In the ISE, the script execution was interrupted in the middle, and using WinRM not only that, I interrupted, another message from stdErr can't be read. The first step will solve the problem with a stop run from the script application to do this before you run the application change the value of the global $ErrorActionPreference variable.

test7.ps1
$ErrorActionPreference = "Stop"
#$VerbosePreference = "Continue"

function RunConsole($scriptBlock)
{
if([Environment]::UserInteractive)
{
# Popular solution to the "troubleshoot" error: 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"


execution Result


For those that know about the-ErrorAction parameter
error.cmd
the
echo error message 1 > &2

errorActionTest.ps1
the
#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"

What will be the result of the execution of such a script?

The second step will modify the script remotely using WinRM, so he didn't fall:

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
}


execution Result


And it remains the most difficult — to adjust the generated message through stdErr and not to change its position in the log. In the process of solving this task, colleagues have offered to create their own console using the win api function AllocConsole.

test8.ps1
$ErrorActionPreference = "Stop"
#$VerbosePreference = "continue"

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

&cmd /c ver | Out-Null
$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 

$prevErrAction = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try
{
&$scriptBlock
}
finally
{
$ErrorActionPreference = $prevErrAction
[Console]::OutputEncoding = $encoding
}
}

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




Get rid of information that adds powershell stdErr to me and failed.

I hope that this information will be useful not only to me! :)

update 1
In some usage scenarios, created additional console, which has been the result of the script execution. In the script test8.ps1 bug fixes.

update 2
As many commentators of the article there is confusion between the concepts character set (char set) and encoding (encoding) would like to once again note that the article solves the problem of mismatch encoding of the console and the calling application.

As you can see from the script test8.ps1, the encoding is specified in the static property [Console]::OutputEncoding, and no one bothers to specify in it one of the unicode encodings of the family:
the
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("utf-8")

But for the script to work in standard windows console (aka cmd.exe) you need to change the console font from standard "Rasters Fonts" to Consolas or "Lucida Console". If these scripts we used on their own workstations or servers, such a change would be acceptable, but since we have to distribute our solutions to customers, to intervene in the system settings of the servers we have no right. It is for this reason that the script uses cp866 as the charset which is configured by default to console.
Article based on information from habrahabr.ru

Comments

Popular posts from this blog

Active/Passive PostgreSQL Cluster, using Pacemaker, Corosync

Automatic deployment ElasticBeanstalk using Bitbucket Pipelines