📜 ⬆️ ⬇️

Powershell and Cyrillic in console applications (updated)

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

#test.ps1 & $PSScriptRoot\ConsoleApp.exe 



Let us study the behavior of console applications when they are launched from the command line, via PowerShell, and through PowerShell ISE:

Execution result


In PowerShell ISE, there was a problem with the encoding, since ISE expects output in the 1251 encoding. We use Google and find two solutions to the problem: using [Console] :: OutputEncoding and through the powershell pipeline. We 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


The command line is fine, but in ISE error. Exception setting "OutputEncoding": "The handle is invalid." . Once again, we’ll take Google into our hands, and in the very first result we find a solution - we need to launch some console application to create the console. Well, let's try.

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 } 


Execution result


Everything is beautiful, everything works. Who read my last note, noticed that WinRM brings us many sharp impressions. Let's try to run the test through 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 does not work. Earlier we found two solutions to the encoding problem. Let's try the second one:

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 ISE and through WinRM, the solution works, but not via the command line and the shell.
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) { #   "" : 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 has been solved, but we will continue the study and complicate our console application by adding output to stdError.

Execution result


It becomes more fun :) In ISE, the execution of the script was interrupted in the middle, and through WinRM, not only was it interrupted, it is also impossible to read the message from stdErr. The first step is to solve the problem with stopping the application launched from the script, to do this, before starting the application, change the value of the global variable $ 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" 


Execution result


For those that know about the existence of the -ErrorAction parameter
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" 

What will be the result of such a script?

The second step is to modify the remote launch script via WinRM so that it does not crash:

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 the only thing left is to correct the message generated by stdErr and not to change its position in the log. In the process of solving this task, colleagues suggested creating a console on their own, using the win api AllocConsole function.

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 [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" 



I would not get rid of the information that powershell adds to stdErr.

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

update 1
In some usage scenarios, an additional console was created, into which the result of executing the scripts was issued. The test8.ps1 script has been fixed.

update 2
Since many commentators of the article have a confusion between the concepts of character set (char set) and encoding, I would like to once again note that the article solves the problem of precisely the inconsistency between console encodings and the application being called.

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

But, for the script to work in the standard windows console (aka cmd.exe), you need to change the console font from the standard “Rasters Fonts” to Consolas or “Lucida Console”. If we used these scripts only on our own workstations or servers, such a change would be acceptable, but since we have to distribute our solutions to customers, we have no right to interfere with the system settings of the servers. It is for this reason that cp866 is used in scripts as the default encoding for the console.

Source: https://habr.com/ru/post/321076/


All Articles