Skip to content

Instantly share code, notes, and snippets.

@mklement0
Last active December 9, 2023 22:32
Show Gist options
  • Save mklement0/0fc086da1af9a72a94cbdb4a59d55230 to your computer and use it in GitHub Desktop.
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.
<#
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.
}
@eabase
Copy link

eabase commented Dec 9, 2023

Nice one!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment