Last active
February 22, 2023 07:37
-
-
Save Jaykul/28daba801f09070128fc6701b7564c26 to your computer and use it in GitHub Desktop.
Work around binaries that don't respect the console code page
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class EncodingAttribute : System.Management.Automation.ArgumentTransformationAttribute { | |
[object] Transform([System.Management.Automation.EngineIntrinsics]$engineIntrinsics, [object]$inputData) { | |
return [System.Text.Encoding]::GetEncoding("$inputData") | |
} | |
} | |
function Invoke-WithEncoding { | |
<# | |
.SYNOPSIS | |
Temporarily switch output encoding | |
.DESCRIPTION | |
This function is used to wrap commands that ignore console encoding | |
and write UTF-16 or UTF-8 output to the console. | |
By default, assumes the command is UTF-8 encoded. | |
Additionally, assumes these commands may write verbose output to stderr, and provides some parsing of those into errors/verbose streams. | |
.EXAMPLE | |
Invoke-WithEncoding { git rev-parse --git-dir } | |
.EXAMPLE | |
Invoke-WithEncoding { wsl --list --online } -Encoding [Text.Encoding]::Unicode | |
#> | |
[CmdletBinding()] | |
param( | |
# The ScriptBlock command to run | |
[Parameter(Mandatory, Position = 0)] | |
[ScriptBlock]$Command, | |
# The encoding to use. Defaults to UTF-8 (which is what git uses). | |
# You may also want UTF-16 (Windows calls this "Unicode", and it's what WSL.exe uses). | |
[Encoding()][ArgumentCompleter({ | |
param([string]$commandName, [string]$parameterName, [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [System.Collections.IDictionary]$fakeBoundParameters) | |
return [System.Text.Encoding]::GetEncodings().Name -Match "^$wordToComplete" | |
})] | |
[Text.Encoding]$Encoding = [Text.Encoding]::UTF8, | |
# StdErr output that isn't "error" or "fatal" gets ignored unless there's a non-zero ExitCode | |
[switch]$Quiet | |
) | |
# 1. The main purpose of this script is to capture and put back the current encoding | |
$currentEncoding = [Console]::OutputEncoding | |
# 2. We also clear errors if there's not a non-zero exit code, otherwise, | |
# executables (like git) that write to stderr may generate error records | |
$errorCount = $global:Error.Count | |
# 3. We also buffer the output to allow the bad behavior of using stderr for verbose output | |
$fatal = @(); | |
$stream = "" | |
$buffer = @() | |
try { | |
try { | |
[Console]::OutputEncoding = $Encoding | |
} catch [System.IO.IOException] { | |
# This only happens on some specific versions of PowerShell Core prior to 2020 | |
Write-Warning "Unable to set OutputEncoding. $_" | |
} | |
$ErrorActionPreference, $oldErrorAction = 'Continue', $ErrorActionPreference | |
$Output = & $Command 2>&1 | |
$ErrorActionPreference = $oldErrorAction | |
# Clear out stderr output that was added to the $Error collection | |
if ($global:Error.Count -gt $errorCount) { | |
$newCount = $global:Error.Count - $errorCount | |
# $script:StdErr.InsertRange(0, $global:Error.GetRange(0, $newCount)) | |
# if ($script:StdErr.Count -gt 256) { | |
# $script:StdErr.RemoveRange(256, ($script:StdErr.Count - 256)) | |
# } | |
$global:Error.RemoveRange(0, $newCount) | |
} | |
# Even if we said -Quiet, if there was a real error we need to see all the output | |
if ($LASTEXITCODE -ne 0 -or -not $quiet) { | |
$VerbosePreference = "Continue" | |
} | |
# Now split the output. The empty error record causes an extra iteration to flush the final output | |
switch (@($Output) + ([System.Management.Automation.ErrorRecord]::new([Exception]::new(""), "NotAnError", "NotSpecified", $null))) { | |
{ $_ -is [System.Management.Automation.ErrorRecord] } { | |
# Handle "fatal" (Terminating), "error" (non-terminating), "warning", and verbose output to stderr. | |
# We output them as they came in, but we save "fatal" for the end because it ends the command | |
$null = $_.Exception.Message -match "^((?<stream>fatal|error|warning):)?\s*(?<message>.*)$" | |
$message = $Matches["message"] | |
$level = if ($Matches["stream"]) { $Matches["stream"] } else { "Verbose" } | |
Write-Debug "$([char]27)[38;2;255;0;0m$([char]27)[48;2;255;255;255m $level $([char]27)[38;2;255;255;255m$([char]27)[49m $message" | |
# If this is the same stream as the last one, then append the output (we'll output it all at once) | |
if ($stream -eq $level -and $message.Length) { | |
# Don't optimize this to += please | |
$buffer = @($buffer) + $message | |
} else { | |
# Otherwise, if we've captured output, write that, and start a new buffer | |
if ($buffer) { | |
if ($stream -eq "fatal") { | |
# Buffer fatal errors until the end | |
$fatal = @($fatal) + $buffer | |
} else { | |
. "Write-$stream" ($buffer.ForEach("Trim") -Join "`n") -ErrorAction $ErrorActionPreference -Verbose:$($VerbosePreference -notin "Ignore", "SilentlyContinue") | |
} | |
} | |
$buffer = @($message) | |
} | |
$stream = $level | |
} | |
default { $_ } # Normal output just passes through | |
} | |
if ($fatal -or $LASTEXITCODE -ne 0) { | |
# Differentiate the ErrorId by the Exit Code | |
$ErrorId = if ($LASTEXITCODE) { | |
"ExitCode:$LASTEXITCODE" | |
} else { | |
"FatalError" | |
} | |
# If we didn't find a fatal message, use the exit code as the message: | |
if (!$fatal) { | |
$fatal = @("Command failed with exit code $LASTEXITCODE") | |
} | |
$PSCmdlet.ThrowTerminatingError( | |
[System.Management.Automation.ErrorRecord]::new( | |
[InvalidOperationException]::new($fatal -join "`n"), | |
$ErrorId, | |
"InvalidOperation", | |
[PSCustomObject]@{ | |
"command" = $Command | |
"pwd" = $pwd | |
})) | |
} | |
} finally { | |
try { | |
[Console]::OutputEncoding = $currentEncoding | |
} catch [System.IO.IOException] { | |
# PSAvoidUsingEmptyCatchBlock | |
Write-Verbose "Unable to put back OutputEncoding`n$_" | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment