How to redirect errors to a variable - powershell

I have a script that pass over files and I want to redirect any errors to a variable and print them in the end of the script.
Here is example for a problem I am getting:
Get-ChildItem C:\Windows\appcompat -Recurse | ForEach-Object {
# do stuff ...
}
Sometimes there are folders that I don't have access to them and it throws exception:
I know how to ignore these errors and continue using the switch -ErrorAction, but I wanted to collects all the folders that I don't have access and print them in the end of the script.
With redirection it is possible to use 2> which will redirect the errors to a file:
Get-ChildItem C:\Windows\appcompat -Recurse 2> errors.txt | ForEach-Object {
# do stuff ...
}
Is it possible to redirect, only the errors, to a variable and then I will print them in the end of the script ?

You can use the common -ErrorVariable parameter to collect a cmdlet's errors in a variable.
Since this collecting happens in addition to errors getting sent to the error stream, as usual, you must explicitly silence the error-stream output with 2>$null if you want the collecting to be silent.
Therefore, in order to silently collect errors in, say, variable $errs, use the following:
# Shorter equivalent of `-ErrorVariable errs`: `-ev errs`
Get-ChildItem C:\Windows\appcompat -Recurse -ErrorVariable errs 2>$null | ForEach-Object {
# do stuff ...
}
Note:
The short alias name for -ErrorVariable is -ev
Be sure not to prefix the target variable name with $ - you're passing its name, not its value.
Do not use the name Errors, because $Errors is the automatic (built-in) variable in which all errors that have occurred in the session are being collected.
The target variable receives a collection of type [System.Collections.ArrayList] containing [System.Management.Automation.ErrorRecord] instances.
Note you get a collection even in the case of only a single error having occurred, which may be surprising, given that PowerShell usually unwraps single-element collections; this surprising behavior is discussed in this GitHub issue.
Unfortunately, this convenient error-collecting mechanism is not available when calling external programs (e.g., when you call git), because such calls do not support common parameters; however, there is a suggestion on GitHub to introduce an alternative syntax.

Related

Faulty PowerShell cmdlets filling up $Error automatic variable

In order to be informed when PowerShell Startup / Logon scripts running on remote computers have bugs, I tend to end scripts with the following:
If ($Error) {
(Code that sends a notification email to system administrators attaching the contents of the $Error variable for troubleshooting)
}
This is a great 'tell tale' to pick up edge cases / bugs. However, I've found some basic built-in PowerShell cmdlets dump data into $Error even on successful runs - for example, try:
$Error.Clear()
Get-NetIPConfiguration
$Error
And you'll see a load of errors in $Error that are not shown during normal output but look like:
Get-NetRoute : No matching MSFT_NetRoute objects found by CIM query for instances of the ROOT/StandardCimv2/MSFT_NetRoute class on the CIM server: SELECT * FROM
MSFT_NetRoute WHERE ((DestinationPrefix LIKE '0.0.0.0/0')) AND ((InterfaceAlias LIKE 'OpenVPN Wintun')). Verify query parameters and retry.
Get-NetConnectionProfile : No MSFT_NetConnectionProfile objects found with property 'InterfaceAlias' equal to 'Local Area Connection'. Verify the value of the property and
retry.
or
$Error.Clear()
Get-NetIPAddress
$Error
will return:
“Infinite : The term '“Infinite' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was
included, verify that the path is correct and try again.
(A nice little bug for Microsoft to solve at some point, doubtless!)
Since it's unlikely that the cmdlets will be fixed any time soon, is there a way to run these cmdlets without them clogging up $Error with their useless information?
This is not a duplicate of Powershell: How can I stop errors from being displayed in a script? since that covers errors that actually display in red on the PowerShell console during a 'failed' run of the cmdlet; this is about errors generated by some cmdlets in the background during an apparently 'successful' run of a cmdlet which for some reason only get written to the automatic $Error variable.
Nonetheless I have already tried a number of solutions suggested in that post:
Running the cmdlets with -ErrorAction Ignore
Running the cmdlets with -ErrorAction SilentlyContinue
Running the cmdlets inside try {} catch {}
Running the cmdlets inside try {} catch {} with -ErrorAction Stop
Running the cmdlets with 2>$null following them
Setting $ErrorActionPreference = "SilentlyContinue" before running the cmdlets
I may be asking the impossible, but the way these cmdlets behave does make $Error very hard to use as an actual log, just want to know if I'm missing a trick.
I would like to be able to encapsulate buggy cmdlets in such a way that 'hidden' errors do not go into the automatic $Error variable.
I agree with #zett42' comment: I think you can't really prevent cmdlets from adding to $Error.
Also knowing that these "phantom errors" might already occur with a simple (Try/Catch) statement like:
Try { 1/0 } Catch {}
Anyways, you might consider to mark the last one and remove the errors added after that/ Like:
$HashCode = if ($Error) { $Error[0].GetHashCode() }
Get-NetIPAddress
While($Error -and $Error[0].GetHashCode() -ne $HashCode) { $Error.RemoveAt(0) }
Use the common -ErrorVariable parameter in order to collect only the (non-terminating) errors directly emitted or intentionally passed through by a cmdlet (those that it internally silences or ignores will not be captured):
# $errs is a self-chosen variable; note that it must be specified WITHOUT $
Get-NetIPAddress -ErrorVariable errs
# $errs now contains any (non-terminating) errors emitted by the
# Get-NetIPAddress call, as a [System.Collections.ArrayList] instance.
# (If no errors occurred, the list is empty).
Note: To also silence errors, combine -ErrorVariable errs with -ErrorAction SilentlyContinue (-ErrorAction SilentlyContinue does not work - see below).
The automatic $Error variable is designed to provide a session-wide log of all errors.
However, (script) cmdlets that deliberately ignore errors can avoid logging unnecessary errors by using -ErrorAction Ignore in internal calls - assuming that the errors are not only to be silenced, but also needn't be inspected.
(If errors need to be inspected after having collected them with -ErrorVariable, use of -ErrorAction Ignore is not an option, because it prevents error collection.)
The CDXML-based cmdlets from the NetTCPIP module, such as Get-NetIPAddress unfortunately use -ErrorAction SilentlyContinue in cases where -ErrorAction Ignore would suffice.
Conceivably, the cmdlet-generation code predates v3 of PowerShell, when the Ignore value was introduced.

The term '>>' is not recognized as the name of a cmdlet, function, script file, or operable program

I want to run a powershell script and save/redirect the result to another file.
My script is:
# Define time for report (default is 10 day)
$startDate = (get-date).AddDays(-10)
# Store successful logon events from security logs with the specified dates and workstation/IP in an array
$slogonevents = Get-Eventlog -LogName Security -after $startDate | where {$_.eventID -eq 4624 }
# Crawl through events; print all logon history with type, date/time, status, account name, computer and IP address if user logged on remotely
foreach ($e in $slogonevents){
# Logon Successful Events
# Local (Logon Type 2)
if (($e.EventID -eq 4624 ) -and ($e.ReplacementStrings[8] -eq 2)){
write-host "Type: Local Logon`tDate: "$e.TimeGenerated "`tStatus: Success`tUser: "$e.ReplacementStrings[5] "`tWorkstation: "$e.ReplacementStrings[11]
}
# Remote (Logon Type 10)
if (($e.EventID -eq 4624 ) -and ($e.ReplacementStrings[8] -eq 10)){
write-host "Type: Remote Logon`tDate: "$e.TimeGenerated "`tStatus: Success`tUser: "$e.ReplacementStrings[5] "`tWorkstation: "$e.ReplacementStrings[11] "`tIP Address: "$e.ReplacementStrings[18]
}
} >> D:\test.txt
but I get errors like that
>> : The term '>>' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and
try again.
At D:\Cyber_security\Python\Untitled1.ps1:26 char:3
+ } >> D:\test.txt
+ ~~
+ CategoryInfo : ObjectNotFound: (>>:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
why this is happening?
To address an incidental problem up front: even if you fix the redirection problem (see below), your foreach loop won't produce success-stream output, resulting in an empty file. You're using Write-Host, which is is typically the wrong tool to use, unless the intent is to write to the display only (though in PowerShell 5 and above you can capture Write-host output if you redirect it to the success output stream, e.g. with *>&1). Instead, use Write-Output (e.g. Write-Output "foo") or, preferably, implicit output (just "foo"). See also: the bottom section of this answer.
foreach is a language statement, and as such you cannot directly apply a redirection (> or >>) to it - see bottom section for an explantion.
You need to wrap it in a (by definition pipeline-based) command or expression first, for which there are two options:
Streaming option (preferred): Wrap the statement in a script block ({ ... }) and call it via &, the call operator (or, if you want the statement to run directly in the caller's scope as opposed to a child scope, as created by &, use ., the dot-sourcing operator)
& { foreach ($i in 1..2) { $i } } > test.txt
Collect-all-output-first option: Use $(...), the subexpression operator:
$(foreach ($i in 1..2) { $i }) > test.txt
Alternatively, use the ForEach-Object cmdlet, which is a command (as all named units of execution are in PowerShell), which also results in streaming processing (perhaps confusingly, a built-in alias for ForEach-Object is also named foreach, with the syntactical context deciding whether the cmdlet or the language statement is being referenced):
1..2 | ForEach-Object { $_ } > test.txt
As for what you tried:
The > (>>) operator is, in effect, an alias of the Out-File cmdlet (Out-File -Append), and therefor requires a pipeline to function.
However, language statements cannot directly be used in a pipeline, and by themselves are always self-contained statements, meaning that whatever comes after isn't considered part of the same statement.
This becomes more obvious when you replace >> with Out-File -Append:
# !! FAILS, because `| Out-File -Append test.txt` is considered
# !! a separate statement, resulting in the following error:
# !! "An empty pipe element is not allowed."
foreach ($i in 1..2) { $i } | Out-File -Append test.txt
The error message An empty pipe element is not allowed. implies that | was considered the start of a new statement.
The same happened with >>, albeit with the more obscure error message shown in your question, but you can easily reproduce it by executing >> test.txt in isolation.
Note: Unlike POSIX-compatible shells such as Bash, PowerShell does not allow you to place a redirection anywhere within a statement, and fails if it starts a statement; e.g., Get-Date >> test.txt' works fine and even Get-Date >>test.txt -Format g, but >> test.txt 'hi' does not.
Design musings:
Given that an expression can serve as the - first only - segment of a pipeline (e.g., 1..2 | Out-File -Append test.txt), it isn't obvious why a language statement cannot be used that way too.
The reason is a fundamental limitation in PowerShell's grammar:
A pipeline by itself is a statement,
but it cannot (directly) contain statements.
Hence the need to nest statements inside pipelines using the techniques shown above (& { ... } / $(..)).
Another unfortunate manifestation of this design is when you attempt to use language statements with && and ||, the PowerShell (Core) 7+ pipeline-chain operators:
Since exit and throw are language statements too, the following idiom - which would work in POSIX-combatible shells - does not work:
# Exit the script with an exit code of 1 if the Get-ChildItem call
# reports an error.
# !! FAILS, because `exit`, as a language statement, cannot be
# !! used directly in a pipeline.
Get-ChildItem NoSuchDir -ErrorAction SilentlyContinue || exit 1
Again, nesting of the statement is required, such as $(...):
# OK, due to $(...)
Get-ChildItem NoSuchDir -ErrorAction SilentlyContinue || $(exit 1)
Perhaps needless to say:
This requirement is obscure and easy to forget...
... and it is exacerbated by the fact that placing e.g. exit 1 after && or || does not cause a syntax (parse) error and only fails at runtime, and only when the condition is met.
That is, you may not notice the problem until the LHS command actually reports an error.
Additionally, the error message you get when it does fail can be confusing: The term 'exit' is not recognized as a name of a cmdlet, function, script file, or executable program. This is because exit in this context is then interpreted as the name of a command (such as a function or external program) rather than as a language statement.

PowerShell PSDefaultParamterValues

I came across this private error variable example recently.
#hiding errors:
$ErrorActionPreference = 'SilentlyContinue'
#telling all cmdlets to use a private variable for error logging:
$PSDefaultParameterValues.Add('*:ErrorVariable', '+myErrors')
#initializing the variable:
$myErrors = $null
#do stuff:
Stop-Service -Name Spooler
dir c:\gibtsnichtabcs
#check errors at end USING PRIVATE VARIABLE:
$myErrors
I just want to understand the $PSDefaultParameterValues.Add line above as to how prepending + to the myErrors variable name makes it cumulative. Thanks.
The $PSDefaultParameterValues line causes PowerShell to pass argument -ErrorVariable +myErrors to all cmdlets or advanced functions.
The documentation for the -ErrorVariable common parameter says:
By default, new error messages overwrite error messages that are
already stored in the variable. To append the error message to the
variable content, type a plus sign (+) before the variable name.
So the "cumulative" behaviour is implemented in PowerShell's handling of the common parameters.

How to save powershell hyper-v errors to file

I am making a script to turn virtual machines on and off in hyper-v.
Sometimes the Stop-VM command fails and I need to save the bug or reflect it in some way in a log file
I tried putting the command in a trycath but it didn't work.
Command:
Stop-VM $VMapagar
Sometimes the command gives me this error and does not turn off the machine
Stop-VM: Could not stop.
I would like to be able to reflect the failure in some way in a log.txt
Thanks!
Use Try..Catch to trap the error by telling PS to treat it as a terminating error, then process it as you require:
# Rest of your script
Try {
# Run your command, but tell PS to stop if it find an error
# You can explore the effects of the other possible values for -ErrorAction in PS documentation.
Stop-VM $VMpagar -ErrorAction Stop
# If it's got this far, then there can't have been an error so write a success message to console
Write-Host "OK"
}
Catch {
# This code will process if there was an error in the "Try" block
# By default, within the "Catch" block, the "$_" variable contains the error message
Write-Host "Error: $_"
# Write the error to a log file - "`n" tells PS to write a newline before the subsequent text
Add-Content -Path 'c:\temp\log.txt' -Value "`n$_"
# You could stop the script here using "Throw" or "Exit" commands if you want the whole script to stop on ANY error
}
# Your script will continue from this point if you haven't stopped it
Scepticalist's helpful answer shows how to capture a terminating error, by using the common -ErrorAction (-ea) parameter with value 'Stop' in order to promote non-terminating errors (the most common kind) to terminating ones, which allows them to be trapped with a try/ catch / finally statement.
Note that this approach limits you to capturing the first non-terminating error (whereas a single cmdlet call may emit multiple ones), because it - thanks to -ErrorAction Stop - then instantly terminates the statement and transfers control the catch block (where the automatic $_ variable reflects the triggering error in the form of an [ErrorRecord] instance).
Also note that execution continues after a catch block by default - unless you explicitly use throw to re-throw the terminating error (or use a statement such as exit to exit the script).
To capture - potentially multiple - non-terminating errors you have two options:
Redirect them directly to a file, using the redirection operator > with the number of the error stream, 2:
Stop-Vm $vms 2>errs.txt
This sends any errors quietly to file errs.txt; that is, you won't see them in the console. If no errors occur, an empty file is created.
Note: This technique is the only option for directly redirecting an external program's errors (stderr output); however, using redirection 2>&1 you can capture success output (stdout) and errors (stderr) combined, and split them by their source stream later - see the bottom section of this answer.
Use the common -ErrorVariable (-ev) parameter to collect any non-terminating errors in a variable - note that the target variable must be specified without the $:
Stop-Vm $vms -ErrorVariable errs
By default, the errors are still output as well and therefore print to the console (host) by default, but you can add -ErrorAction SilentlyContinue to prevent that. Caveat: Do not use -ErrorAction Ignore, as that will categorically suppress errors and prevent their collection.
You can then inspect the $errs array (list), which is empty if no errors occurred and otherwise contains one or more [ErrorRecord] instances, and send the collected errors to a file on demand; e.g.:
if ($errs) { $errs > errs.txt }
See also:
This answer for information about PowerShell's two fundamental error types.
GitHub docs issue #1583 for a comprehensive overview of PowerShell's surprisingly complex error handling.

How does one test if a program/executable is present on a system in powershell?

I have a script that requires openssl be installed on the system. I want to do a check to see if it is installed. I've considered using test-path but because this script will be on many computers, there's no way of knowing where the user installed openssl or if it is in the system path.
Is there a way to do something like test-command openssl (I know that doesn't exist) and get an error level or the like to return in powershell?
Many thanks!
Use the Get-Command cmdlet to explicitly test if executables can be called by their name only (implying that they're in one of the directories listed in $env:PATH) and for command discovery in general:
$found = [bool] (Get-Command -ErrorAction Ignore -Type Application openssl)
Write-Verbose -vb "openssl.exe found? $found"
Casting to [bool] evaluates to $true only if Get-Command returns output, which it only does if the executable is found.
-Type Application ensures that only external programs are considered; by default, Get-Command finds commands of all types, including *.ps1 scripts, cmdlets, functions, and aliases.
The Get-Command by itself would also allow you to find out the executable's full path, via the output object's .Source property.
Alternatively, if all that matters is whether the executable is available, you can simply try to execute your actual command and handle an error indicating that the executable is not available via try / catch:
try {
openssl ... # your *actual* command, not a test
} catch [System.Management.Automation.CommandNotFoundException] {
# Handle the error, reflected in $_
Write-Warning $_
# ...
}
Note that you don't strictly need to catch [System.Management.Automation.CommandNotFoundException] specifically, if you're confident that the only error that can occur is due to a missing executable, but note that you could then get potential false positives from errors that occur during evaluation of expression-based arguments, such as attempting to pass (1 / 0) as an argument.
Also note that attempting to test the availability of an executable by invoking it without arguments inside a try / catch statement is not a viable approach, because the executable may enter an interactive shell (as is the case with openssl) or produce undesired side effects.
You can use try/catch blocks to do something like the following:
$OpenSSLMissing = $false
try {openssl} catch {$OpenSSLMissing = $true}
if ($OpenSSLMissing)
{
# Do something
}
else
{
# Do something else
}