suppose I have the following code, when the error happens, I'd like to see the error that the error first happened at function b, and then happened at function a. But in fact it only tells me the error happen at function a, since function a could be called many times, I don't know which outer function calling function a caused the problem
cls
function a{
Remove-Item "not-exist-item"
}
function b{
a
}
b
Remove-Item : Cannot find path 'C:\Program Files (x86)\Microsoft SQL Server\100\Tools\Binn\not-exis
t-item' because it does not exist.
At C:\Users\Daniel.Wu\AppData\Local\Temp\2\a.ps1:***3 char:14***
+ Remove-Item <<<< "not-exist-item"
+ CategoryInfo : ObjectNotFound: (C:\Program File...\not-exist-item:String) [Remove-Item], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand
If you are on PowerShell v2.0, use Get-PSCallStack. If you're still on v1, use a function like this:
function Get-CallStack {
trap { continue }
1..100 | foreach {
$var = Get-Variable -scope $_ MyInvocation
$var.Value.PositionMessage -replace "`n"
}
}
One option is to set
$ErrorActionPreference = 'inquire'
and then invoke the problematic script. On error you are prompted for choices, choose Suspend to enter into the nested prompt mode. Then type
Get-PSCallStack
or even
Get-PSCallStack | fl
to get the current call stack information.
Does get-help about_debuggers provide any illumination?
Related
I used a method with and without a class and the Write-Error seems to produce different outputs. In case of class, it doesn't specify the function and the line number is always 1,1
function oper1() {
Try {
[string] $cmd = ".\some_exe_which_does_not_exist.exe"
iex $cmd
}
Catch {
Write-Error $_.Exception.Message
}
}
oper1
Output for above:
oper1 : The term '.\some_exe_which_does_not_exist.exe' 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
F:\debug\encryption_concat_tests\Untitled1.ps1:11 char:1
+ oper1
+ ~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,oper1
When I enclosed the same function in a class, I got this:
class Operator {
[void] oper1() {
Try {
[string] $cmd = ".\some_exe_which_does_not_exist.exe"
iex $cmd
}
Catch {
Write-Error $_.Exception.Message
}
}
}
[Operator] $operator = New-Object Operator
$operator.oper1()
The term '.\some_exe_which_does_not_exist.exe' 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 line:1 char:1
+ F:\debug\encryption_concat_tests\Untitled1.ps1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException
What could be the reason for this behaviour for methods inside classes?
As an aside: Invoke-Expression (iex) should generally be avoided; definitely don't use it to invoke an external program - just invoke it directly, as shown below.
In PowerShell class methods:
Do not use Write-Error, as classes are not designed to emit non-terminating errors.
The only reason you're seeing any output at all is a bug as of PowerShell Core 7.0.0-rc.3 with methods whose return type happens to be [void] - see GitHub issue #5331.
Instead, communicate errors solely by throwing them with the Throw statement or by not catching terminating errors (which include exceptions from .NET methods and cmdlet calls with -ErrorAction Stop).
Note: Throw and -ErrorAction Stop (or $ErrorActionPreference = 'Stop') create script-terminating (thread-terminating) errors, whereas exceptions thrown by a .NET method (not caught and re-thrown in the class method) only create statement-terminating errors; that is, while the class-method body is terminated right away, execution continues in the caller by default; the latter also applies to the call operator (&) not finding an executable, errors in expressions such as 1 / 0, and cmdlet calls that emit statement-terminating errors (the most severe error type they can report) without their being promoted to script-terminating ones with -ErrorAction Stop; see this GitHub docs issue for a comprehensive overview of PowerShell's complex error handling.
See this answer for more information about error handling and stream-output behavior in class methods in particular.
Here's a corrected version of your code.
class Operator {
[void] oper1() {
Try {
# Try to invoke a non-existent executable.
& ".\some_exe_which_does_not_exist.exe"
}
Catch {
# Re-throw the error.
# Alternatively, don't use try / catch, but the error
# then only aborts the method call, not the entire script.
Throw
}
}
}
$operator = [Operator]::new()
$operator.oper1()
I also have this problem! But I really wanted to figure out how to output information and return the desired type of information from the function!
I solved the problem using Write-Warning
[bool] hidden AddUserToGroup([Microsoft.ActiveDirectory.Management.ADGroup]$group, [Microsoft.ActiveDirectory.Management.ADUser]$user)
{
try
{
Add-ADGroupMember -Server $this.server -Identity $group -Members $user;
return $true;
}
catch [System.SystemException]
{
Write-Warning $_.Exception.Message;
return $false;
}
}
Try-catch appears to not reliably trap all errors. Try-catch on get-ChildItem does not report all access errors that are reported outside of try-catch.
Edit: It is not true that try-catch is being unreliable, and there is a sound reason why it reported only one error. See my comment to the accepted answer to see my misunderstanding behind this question.
In Windows 10 Pro 64, running PowerShell 5.1, this script:
$sScriptName = "ErrorTest.ps1"
write-host ("I will get the file structure of ""C:\Windows\System32"", but this yields two access-denied errors:")
$aoFileSystem = #(get-ChildItem "C:\Windows\System32" -recurse -force)
write-host ("I will now do it again and trap for those errors. But I only get one of them:")
try {$aoFileSystem = #(get-ChildItem $sPath -recurse -force -ErrorAction Stop)}
catch [System.UnauthorizedAccessException]
{$sErrorMessage = $_.ToString()
write-host ("System.UnauthorizedAccessException: " + $sErrorMessage.substring(20, $sErrorMessage.length - 32))}
running in the ISE as administrator, gets this output:
PS C:\WINDOWS\system32> D:\<path>\ErrorTest.ps1
I will get the file structure of "C:\Windows\System32", but this yields two access-denied errors:
get-ChildItem : Access to the path 'C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5' is denied.
At D:\<path>\ErrorTest.ps1:5 char:19
+ ... aoFileSystem = #(get-ChildItem "C:\Windows\System32" -recurse -force)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (C:\Windows\Syst...che\Content.IE5:String) [Get-ChildItem], UnauthorizedAccessException
+ FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand
get-ChildItem : Access to the path 'C:\Windows\System32\LogFiles\WMI\RtBackup' is denied.
At D:\<path>\ErrorTest.ps1:5 char:19
+ ... aoFileSystem = #(get-ChildItem "C:\Windows\System32" -recurse -force)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (C:\Windows\Syst...es\WMI\RtBackup:String) [Get-ChildItem], UnauthorizedAccessException
+ FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand
I will now do it again and trap for those errors. But I only get one of them:
System.UnauthorizedAccessException: C:\WINDOWS\system32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5
PS C:\WINDOWS\system32>
I want to log the access errors, but when I try to trap for them, I only get the first one. What do I need to do to get the second one to show up when I trap for them?
Edit: I got a message telling me that I need to edit my question to explain how it's different from Can PowerShell trap errors in GetChildItem and continue looping?. So I'll repeat here what I said in response to the comment below from Scepticalist. That question is for get-ChildItem in a ForEach loop. My problem does not involve such a loop. Still, I tried something from the accepted answer there, using -ErrorAction SilentlyContinue, but that hides the errors without trapping them. However, in the solution I found and am about to post as an answer, I do use -ErrorAction SilentlyContinue in combination with -ErrorVariable.
The errors emitted by Get-ChildItem "C:\Windows\System32" -recurse -force are non-terminating errors, and, given that such errors by definition don't terminate a command, a single command may emit multiple errors of this type.
All non-terminating errors emitted by a cmdlet can be captured in a designated variable whose name you pass to the -ErrorVariable common parameter.
Additionally, all errors (both non-terminating and terminating ones) are by default recorded in the session-global automatic $Error collection variable.
try / catch only acts on terminating errors, so by default it has no effect on commands that emit non-terminating errors only, such as Get-ChildItem "C:\Windows\System32" -recurse -force
You can instruct the command to turn the first non-terminating error encountered into a terminating one by adding -ErrorAction Stop (using the -ErrorAction common parameter), in which case an enclosing try / catch does catch the error, but note that execution then stops after the first error encountered either way.
Caveat: There are two types of terminating errors (and the fact that there are may be a historical accident); while both types can be caught with try / catch, their default behavior differs:
Statement-terminating errors, which only terminate the statement at hand and, after emitting an error message, by default continue script execution; typically, these errors are emitted by (compiled) cmdlets if they encounter errors that aren't limited to the input at hand and doesn't allow them to continue processing further input.
Script-terminating errors (runspace-terminating errors), which abort processing altogether by default. PowerShell code that uses the Throw statement generates such errors, as does passing -ErrorAction Stop to a command that emits non-terminating errors.
Caveat: While -ErrorAction Stop has no effect on statement-terminating errors, the seemingly equivalent preference-variable setting, $ErrorActionPreference = 'Stop', unexpectedly promotes statement-terminating errors to script-terminating ones.
Further reading:
For a comprehensive overview of PowerShell's - bewilderingly complex - error handling, see this GitHub docs issue.
As for when to report a terminating vs. a non-terminating error when authoring commands, see this answer.
I did some research on the ErrorAction parameter, and doing that, discovered the ErrorVariable parameter. (Both are common parameters, so they don't show up in the documentation of get-ChildItem.) And from there, I was able to figure out how to correctly trap for the errors.
The new script below shows four ways of doing this. Methods 2 and 3 work okay. Method 4 doesn't work because it incorrectly attempts to use the parameter InputObject in ForEach-object. Method 1, the original try-catch method, doesn't work and I still don't know why. This is troubling because it's important that error trapping works as expected. If I did not know to expect two errors, I would not have known that try-catch was not giving me the right output.
I am not accepting this (my own) answer because, although the script below shows two methods to correctly trap for these errors, it would still be better if someone can explain why try-catch does not work for this.
New script:
$sScriptName = "ErrorTest.ps1"
$sPath = "C:\Windows\System32"
write-host ("I will get the file structure of """ + $sPath + """, but this yields two access-denied errors:")
$aoFileSystem = #(get-ChildItem $sPath -recurse -force)
write-host ("I will now do it again and trap for those errors.")
write-host ("`r`nMethod 1: Original method using try-catch (incorrect results; only finds one of the two errors; why?):")
try {$aoFileSystem = #(get-ChildItem $sPath -recurse -force -ErrorAction Stop)}
catch [System.UnauthorizedAccessException]
{$sErrorMessage = $_.ToString()
write-host ("System.UnauthorizedAccessException: " + $sErrorMessage.substring(20, $sErrorMessage.length - 32))}
write-host ("`r`nGet array for Methods 2 to 4.")
$aoFileSystem = #(get-ChildItem $sPath -recurse -force -ErrorAction SilentlyContinue -ErrorVariable aoChildItemError)
write-host ("`r`nMethod 2: Output by piping to ForEach-object (correct results):")
$aoChildItemError |
ForEach-object `
{$oErrorRecord = $_
write-host ($oErrorRecord.CategoryInfo.reason + ": """ + $oErrorRecord.TargetObject + """")}
write-host ("`r`nMethod 3: Output by for loop (correct results):")
for ($nCount = 0; $nCount -lt $aoChildItemError.count; $nCount++)
{$oErrorRecord = $aoChildItemError[$nCount]
write-host ($oErrorRecord.CategoryInfo.reason + ": """ + $oErrorRecord.TargetObject + """")}
write-host ("`r`nMethod 4: Output by ForEach-object loop without pipeline (incorrect results because it incorrectly attempts to use the parameter ""InputObject"" in ""ForEach-object""):")
ForEach-object -InputObject $aoChildItemError `
{$oErrorRecord = $_
write-host ($oErrorRecord.CategoryInfo.reason + ": """ + $oErrorRecord.TargetObject + """")}
New output:
PS C:\WINDOWS\system32> D:\_\z-temp\ErrorTest.ps1
I will get the file structure of "C:\Windows\System32", but this yields two access-denied errors:
get-ChildItem : Access to the path 'C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5' is denied.
At D:\_\z-temp\ErrorTest.ps1:6 char:19
+ $aoFileSystem = #(get-ChildItem $sPath -recurse -force)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (C:\Windows\Syst...che\Content.IE5:String) [Get-ChildItem], UnauthorizedAccessException
+ FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand
get-ChildItem : Access to the path 'C:\Windows\System32\LogFiles\WMI\RtBackup' is denied.
At D:\_\z-temp\ErrorTest.ps1:6 char:19
+ $aoFileSystem = #(get-ChildItem $sPath -recurse -force)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (C:\Windows\Syst...es\WMI\RtBackup:String) [Get-ChildItem], UnauthorizedAccessException
+ FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand
I will now do it again and trap for those errors.
Method 1: Original method using try-catch (incorrect results; only finds one of the two errors; why?):
System.UnauthorizedAccessException: C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5
Get array for Methods 2 to 4.
Method 2: Output by piping to ForEach-object (correct results):
UnauthorizedAccessException: "C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5"
UnauthorizedAccessException: "C:\Windows\System32\LogFiles\WMI\RtBackup"
Method 3: Output by for loop (correct results):
UnauthorizedAccessException: "C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5"
UnauthorizedAccessException: "C:\Windows\System32\LogFiles\WMI\RtBackup"
Method 4: Output by ForEach-object loop without pipeline (incorrect results because it incorrectly attempts to use the parameter "InputObject" in "ForEach-object"):
UnauthorizedAccessException UnauthorizedAccessException : " C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5 C:\Window
s\System32\LogFiles\WMI\RtBackup "
PS C:\WINDOWS\system32>
Quite simple! You specified the parameter -ErrorAction Stop in your second directory access statement. Switch it to -ErrorAction Continue and you get both error messages.
When a command returns an error, I get like an error message, plus what looks like a full stack of the error:
C:\> dir foo
dir : Cannot find path 'C:\foo' because it does not exist.
At line:1 char:1
+ dir foo
+ ~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\foo:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
Is there a way to only see the error (that's the only thing usefull for me) and not display the full stack ?
Like:
C:\> dir foo
dir : Cannot find path 'C:\foo' because it does not exist.
You need to catch the error if you want to control how or what is displayed:
try {
dir foo -ErrorAction Stop
} catch {
Write-Host $_
}
Sometimes you'll need to add -ErrorAction Stop (or $ErrorActionPreference = 'Stop') to ensure that all errors are terminating (so they can be caught).
All powershell errors are captured in the auto variable $error. The item at index zero ($error[0]) is the most recent, index 1 next to most recent, etc. Each object in the array is a System.Management.Automation.ErrorRecord. An ErrorRecord contains a number of properties. You can use the select-object cmdlet to see a subset of properties:
$error[0]|select * -excludeproperty *stacktrace
If you want to be able to view the error record at an arbitrary while you're developing a script I'd suggest a function in your profile maybe like:
function show-myError {
param( $errorIndex = 0)
# You can also add anything else you want to exclude
$error[0]|select * -exclude *stacktrace
}
sal err show-myError
On the other hand if you're getting an error at a specific place in a specific script you can use catch/try as suggested in earlier answer. Or if you don't have a specific place, but do have a specific script, then I suggest trap, which you can place at the top of a function or top of a PS1 file. In the catch block or trap block you can output $error[0] using select with the -exclude parameter.
Why calling .\MyScript.ps1 -Uninstall from Powershell gives an error
+ Super-Function $Args
+ ~~~~~
+ CategoryInfo : InvalidData : (:) [Super-Function], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Super-Function
While calling "Super-Function" from the script itself with Super-Function -Uninstall , replacing $Args with the switch works ?
Why copy pasting the function on Powershell and then going for Super-Function -Uninstall works too ?
Here's the content of MyScript.ps1
function Super-Function([parameter(Mandatory=$False)][ValidateScript({Test-Path _$})][String]$var1 = ".\MyFile.ext",
[parameter(Mandatory=$False)][ValidateScript({Test-Path _$})][String]$var2 = "HKLM:\SOFTWARE\Wow6432Node\Publisher\SoftwareName",
[parameter(Mandatory=$False)][ValidateScript({([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")})][Switch]$Uninstall,
[parameter(Mandatory=$False)][ValidateScript({([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")})][Switch]$Install
)
{
}
Super-Function $Args
You have a few issues there that I see. Your ValidateScript for each argument have an issue. First, might just be a typo, you have the characters backwards for current pipe item. Should be $_ instead of _$. Next I find it befuddling that you test the presence of the admin role against a couple of boolean switches. Lets just move that inside the function (If what you had works that is fine. Just does make much sense)
Lastly, and most importantly, what you are trying to do with $args is called splatting. Use #args which will splat the hashtable of arguments, passed in from the script, against the function.
function Super-Function{
param(
[parameter(Mandatory=$False)][ValidateScript({Test-Path $_})][String]$var1 = ".\MyFile.ext",
[parameter(Mandatory=$False)][ValidateScript({Test-Path $_})][String]$var2 = "HKLM:\SOFTWARE\Wow6432Node\Publisher\SoftwareName",
[parameter(Mandatory=$False)][Switch]$Uninstall,
[parameter(Mandatory=$False)][Switch]$Install
)
# Use this to verify what has been assinged to your parameters. Will not show default values.
#$PSBoundParameters
If(([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
"Sure"
} Else {
Throw "Nope"
}
}
Super-Function #args
I wish to pass "Write-Host mm" as script block to function "f", and I hope "f" will execute it 10 times, so I tried:
function f([ScriptBlock]$s)
{
1..10|$s
}
f(Write-Host mm)
Unfortunately, powershell gives error:
At C:\Users\engineer\Documents\Untitled1.ps1:3 char:11
+ 1..10|$s
+ ~~
Expressions are only allowed as the first element of a pipeline.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ExpressionsMustBeFirstInPipeline
How to correct my script?
Thanks for Jason's first answer, but seems doesn't work:
Thanks, but seems this doesn't work, I've got PS 4.0 and
function f([ScriptBlock]$s)
{
1..10 | & $s
}
f { Write-Host mm }
Execute this script and it prints out:
Thanks, but seems this doesn't work, I've got PS 4.0 and
d:\ > function f([ScriptBlock]$s)
{
1..10 | & $s
}
f { Write-Host mm }
mm
This is strange! A script prints out itself! I've got PS4.0 and running ISE.
Why is that?
You are trying to execute your scriptblock 10 times, but instead you try to pipe in an array from 1 to 10. You should pipe that array to foreach-object instead.
function f([ScriptBlock]$s)
{
1..10 | % {& $s}
# % is an alias for Foreach-Object
}
f { Write-Host mm }
You need to use the invocation operator &. Also, script blocks are enclosed in curly braces, so your example would be:
function f([ScriptBlock]$s)
{
1..10 | & $s
}
f { Write-Host mm }
Note that you don't call PowerShell functions like you would a C# method - you don't use parentheses around the arguments, you call PowerShell functions like commands, with spaces between arguments.