Last active
December 9, 2023 22:32
-
-
Save mklement0/0fc086da1af9a72a94cbdb4a59d55230 to your computer and use it in GitHub Desktop.
PowerShell function that looks up information about Windows errors, including HRESULT values, by number or name.
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
<# | |
Prerequisites: Windows PowerShell v5.1 or PowerShell Core (v6+) | |
License: MIT | |
Author: Michael Klement <mklement0@gmail.com> | |
DOWNLOAD and INSTANT DEFINITION OF THE FUNCTION: | |
irm https://gist.github.com/mklement0/0fc086da1af9a72a94cbdb4a59d55230/raw/Get-WinError.ps1 | iex | |
The above directly defines the function below in your session and offers guidance for making it available in future | |
sessions too. To silence the guidance information, append 4>$null | |
CAVEAT: If you run this command *from a script*, you'll get a spurious warning about dot-sourcing, which you can ignore | |
and suppress by appending 3>$null. However, it's best to avoid calling this command from scripts, because later versions | |
of this Gist aren't guaranteed to be backward-compatible; howevever, you can modify the command to lock in a | |
*specific revision* of this Gist, which is guaranteed not to change: see the instructions at | |
https://gist.github.com/mklement0/880624fd665073bb439dfff5d71da886?permalink_comment_id=4296669#gistcomment-4296669 | |
DOWNLOAD ONLY: | |
irm https://gist.github.com/mklement0/0fc086da1af9a72a94cbdb4a59d55230/raw | Set-Content Get-WinError.ps1 -Encoding utf8 | |
The above downloads to the specified file, which you then need to dot-source to make the function available | |
in the current session (again, use 4>$null to silence the guidance information): | |
. ./Get-WinError.ps1 | |
To learn what the function does: | |
* see the next comment block | |
* or, once downloaded and defined, invoke the function with -? or pass its name to Get-Help. | |
To define an ALIAS for the function, (also) add something like the following to your $PROFILE: | |
Set-Alias gwe Get-WinError | |
#> | |
function Get-WinError { | |
<# | |
.SYNOPSIS | |
Looks up information about Windows errors. | |
.DESCRIPTION | |
Looks up error numbers used by Windows APIs, including HRESULT | |
error / status codes. | |
Alternatively, search by (part of) an error's symbolic name. | |
On Windows, results are output directly by default, via the Err.exe | |
Microsoft CLI that is downloaded on demand. | |
Use -Online to use the default web browers to show results, courtesy of | |
https://hresult.info; implied on Unix-like platforms. | |
CAVEATS: | |
* Only HRESULT error values produce meaningful results with -Online; | |
with simple numbers as such as 0x2, the true result will be buried | |
among many false positives. | |
The HRESULT equivalent of system error 0x2 (ERROR_FILE_NOT_FOUND) is | |
0x80070002. | |
* https://hresult.info recognizes FEWER errors than Err.exe | |
.EXAMPLE | |
Get-WinError -1073740791, 2, 0x80070002 | |
Looks up the given error numbers, both decimal and hex. forms | |
are supported. | |
.EXAMPLE | |
-1073740791, 0x80070002 | Get-WinError -Online | |
Looks up the given error numbers and opens a result page in the | |
default web browswer for each. | |
NOTE: With -Online, only HRESULT values produce meaningful results. | |
.EXAMPLE | |
Get-WinError BUFFER_OVER | |
Looks for errors whose symbolic name contains substring "BUFFER_OVER". | |
Also works with -Online. | |
#> | |
[CmdletBinding(PositionalBinding = $false)] | |
param( | |
[Parameter(ValueFromPipeline, Position = 0)] | |
[string[]] $ErrorNumberOrName, | |
[switch] $Online, | |
[switch] $Force # download Err.exe without asking for confirmation, if necessary. | |
) | |
begin { | |
Set-StrictMode -Version 1 | |
If ($env:OS -eq 'Windows_NT') { | |
if (-not $Online) { | |
# Make sure that `Err.exe` is in $env:PATH or in a custom location. | |
# Otherwise, download on demand. | |
$exeFileName = 'Err.exe' | |
$customDir = "$HOME\bin" | |
$customExePath = "$customDir\$exeFileName" | |
$downloadURL = 'https://download.microsoft.com/download/4/3/2/432140e8-fb6c-4145-8192-25242838c542/Err_6.4.5/Err_6.4.5.exe' | |
if (-not ($errExe = (Get-Command -ErrorAction Ignore $exeFileName).Path) -and -not ($errExe = (Get-Command -ErrorAction Ignore $customExePath).Path)) { | |
# Err.exe must be downloaded. | |
if ($Force) { | |
$ProgressPreference = 'SilentlyContinue' | |
} | |
else { | |
# Prompt user for permission to download. | |
$confirmed = 0 -eq $host.ui.PromptForChoice("Download Required", "OK to download required utility '$exeFileName' to '$customDir', from '$downloadUrl'?", ('&Yes', '&No'), 1) | |
if (-not $confirmed) { | |
throw "Required download aborted. Use -Online for online lookups, which don't require a download." | |
} | |
} | |
Write-Verbose "Required CLI 'Err.exe' not found; downloading on demand from '$downloadUrl', to '$customDir'." | |
$null = New-Item -Type Directory -ErrorAction Stop -Force $customDir # make sure the custom dir. exists. | |
Invoke-WebRequest -ErrorAction Stop -OutFile $customExePath $downloadURL | |
$errExe = $customExePath | |
} | |
Write-Verbose "Using CLI '$errExe'" | |
} | |
} | |
else { | |
# Unix-like platforms: only online lookups supported. | |
if (-not $Online) { | |
$Online = $true | |
Write-Warning "Non-Windows platform: Assuming -Online; CLI lookups not supported." | |
} | |
} | |
} | |
process { | |
foreach ($e in $ErrorNumberOrName) { | |
if ($Online) { | |
# Note: Supports decimal and hex numbers, and symbolic name substrings. | |
# !! Use "www.", as only then are query strings properly handled (as of 25 Oct 2022). | |
Start-Process "https://www.hresult.info/Search?q=$e" | |
} | |
else { | |
# If the argument cannot be parsed as an [int], assume it is | |
# a symbolic name (substring), which requires prefixing with ":" for Err.exe | |
# (While prefix "=" is also supported for matching *in full*, we don't bother surfacing | |
# this functionality as well, given that it's unlikely that full symbolic names are substrings of others) | |
if (-not ($e -as [int])) { | |
$e = ':' + $e | |
} | |
# Sample XML to parse; there may be *multiple* <err> elements: | |
# <?xml version="1.0" standalone="no"?> | |
# <ErrV1> | |
# <err n='0xc0000409' name='STATUS_STACK_BUFFER_OVERRUN' src='ntstatus.h'>The system detected an overrun of a stack-based buffer in this application. This overrun could potentially allow a malicious user to gain control of this application.</err> | |
# </ErrV1> | |
# If no info was found, there is *no* <err> element. | |
$results = ([xml] (& $errExe /:xml $e)).SelectNodes('//err') | |
if ($results.Count -eq 0) { | |
# No results found. | |
$PSCmdlet.WriteError( | |
[System.Management.Automation.ErrorRecord]::new( | |
"No information about error '$e' found.", | |
'UnknownErrorNumber', | |
'InvalidArgument', | |
$e | |
) | |
) | |
} | |
else { | |
# Transform the XML results to [pscustomobject] instances. | |
foreach ($el in $results) { | |
# Note: | |
[pscustomobject] @{ | |
HexNumber = '0x{0:X}' -f [int] $el.n | |
Name = $el.Name | |
Source = $el.src | |
Message = $el.InnerText -replace '\r?\n', ' ' # some messages contain newlines | |
} | |
} | |
} | |
} | |
} | |
} | |
} # function Get-WinError | |
# -------------------------------- | |
# GENERIC INSTALLATION HELPER CODE | |
# -------------------------------- | |
# Provides guidance for making the function persistently available when | |
# this script is either directly invoked from the originating Gist or | |
# dot-sourced after download. | |
# IMPORTANT: | |
# * DO NOT USE `exit` in the code below, because it would exit | |
# the calling shell when Invoke-Expression is used to directly | |
# execute this script's content from GitHub. | |
# * Because the typical invocation is DOT-SOURCED (via Invoke-Expression), | |
# do not define variables or alter the session state via Set-StrictMode, ... | |
# *except in child scopes*, via & { ... } | |
if ($MyInvocation.Line -eq '') { | |
# Most likely, this code is being executed via Invoke-Expression directly | |
# from gist.github.com | |
# To simulate for testing with a local script, use the following: | |
# Note: Be sure to use a path and to use "/" as the separator. | |
# iex (Get-Content -Raw ./script.ps1) | |
# Derive the function name from the invocation command, via the enclosing | |
# script name presumed to be contained in the URL. | |
# NOTE: Unfortunately, when invoked via Invoke-Expression, $MyInvocation.MyCommand.ScriptBlock | |
# with the actual script content is NOT available, so we cannot extract | |
# the function name this way. | |
& { | |
param($invocationCmdLine) | |
# Try to extract the function name from the URL. | |
$funcName = $invocationCmdLine -replace '^.+/(.+?)(?:\.ps1).*$', '$1' | |
if ($funcName -eq $invocationCmdLine) { | |
# Function name could not be extracted, just provide a generic message. | |
# Note: Hypothetically, we could try to extract the Gist ID from the URL | |
# and use the REST API to determine the first filename. | |
Write-Verbose -Verbose "Function is now defined in this session." | |
} | |
else { | |
# Indicate that the function is now defined and also show how to | |
# add it to the $PROFILE or convert it to a script file. | |
Write-Verbose -Verbose @" | |
Function `"$funcName`" is now defined in this session. | |
* If you want to add this function to your `$PROFILE, run the following: | |
"``nfunction $funcName {``n`${function:$funcName}``n}" | Add-Content `$PROFILE | |
* If you want to convert this function into a script file that you can invoke | |
directly, run: | |
"`${function:$funcName}" | Set-Content $funcName.ps1 -Encoding $('utf8' + ('', 'bom')[[bool] (Get-Variable -ErrorAction Ignore IsCoreCLR -ValueOnly)]) | |
"@ | |
} | |
} $MyInvocation.MyCommand.Definition # Pass the original invocation command line to the script block. | |
} | |
else { | |
# Invocation presumably as a local file after manual download, | |
# either dot-sourced (as it should be) or mistakenly directly. | |
& { | |
param($originalInvocation) | |
# Parse this file to reliably extract the name of the embedded function, | |
# irrespective of the name of the script file. | |
$ast = $originalInvocation.MyCommand.ScriptBlock.Ast | |
$funcName = $ast.Find( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false).Name | |
if ($originalInvocation.InvocationName -eq '.') { | |
# Being dot-sourced as a file. | |
# Provide a hint that the function is now loaded and provide | |
# guidance for how to add it to the $PROFILE. | |
Write-Verbose -Verbose @" | |
Function `"$funcName`" is now defined in this session. | |
If you want to add this function to your `$PROFILE, run the following: | |
"``nfunction $funcName {``n`${function:$funcName}``n}" | Add-Content `$PROFILE | |
"@ | |
} | |
else { | |
# Mistakenly directly invoked. | |
# Issue a warning that the function definition didn't take effect and | |
# provide guidance for reinvocation and adding to the $PROFILE. | |
Write-Warning @" | |
This script contains a definition for function "$funcName", but this definition | |
only takes effect if you dot-source this script. | |
To define this function for the current session, run: | |
. "$($originalInvocation.MyCommand.Path)" | |
"@ | |
} | |
} $MyInvocation # Pass the original invocation info to the helper script block. | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice one!