Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active February 22, 2023 07:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jaykul/28daba801f09070128fc6701b7564c26 to your computer and use it in GitHub Desktop.
Save Jaykul/28daba801f09070128fc6701b7564c26 to your computer and use it in GitHub Desktop.
Work around binaries that don't respect the console code page
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