Return $?=$false and $LASTEXITCODE > 0 from a function?

by scottbass at 2013-03-11 22:28:11

Hi,

Test scripts:

Child.ps1:

1…3 | % {Write-Host "Line: $"}
exit 0


Parent.ps1:

# call script
1…2 | % {
Write-Host "Invocation #: $
"
.\Child.ps1
Write-Host '$?: ',$?
Write-Host '$LASTEXITCODE: ',$LASTEXITCODE
}

# define function
function TestExitCode {
[CmdletBinding(SupportsShouldProcess=$true)]
param()

1…3 | % {Write-Host "Line: $"}
exit 0
}

# call function
8…9 | % {
Write-Host "Invocation #: $
"
TestExitCode
Write-Host '$?: ',$?
Write-Host '$LASTEXITCODE: ',$LASTEXITCODE
}


Ok, don’t use exit in a function…bad idea, it halts the parent script.

Change exit 0 to exit 1 in Child.ps1 and rerun.

So, how can I return a user-defined error condition back to the parent script, from within a function? In my function, I want to do the following:

Success: return file system objects to the pipeline, set $?=$true, set $LASTEXITCODE=0.
Failure: return $null to the pipeline, set $?=$false, set $LASTEXITCODE > 0.

I think I can set $LASTEXITCODE from the function by defining $global:LASTEXITCODE=<whatever>. But is there any way to set $? from a function, and to control returned items (pipeline) vs. error condition?

Thanks,
Scott
by DonJ at 2013-03-13 08:05:01
Well… you’re meant to use Throw to toss an exception and your parent would use Try/Catch around the function call to trap the error. Think more "programming" and less "batch file."
by MasterOfTheHat at 2013-03-13 08:59:03
DonJ makes a good point, but IF you still want to set a return code, you can use exit($returncode) where $returncode is your int code. Then you can check $LASEXITCODE in the calling script to see what the return code was.

$? will be True if the previous command completed successfully and False if it didn’t. That means that it will return False for any return code other than 0, BUT you HAVE to check it in the statement immediately following the script call. For example:

returncode_called.ps1
exit(4)
returncode_caller.ps1
.\returncode_called.ps1
"?: $?"
"lec: $LASTEXITCODE"

returns:
?: False
lec: 4

BUT
.\returncode_called.ps1
"lec: $LASTEXITCODE"
"?: $?"

returns
lec: 4
?: True

That’s because the "lec: $LASTEXITCODE" line executed successfully, thus changing the value of $?.
by scottbass at 2013-03-13 20:49:51
Slight variation:

returncode_called.ps1

function returncode_called {
exit(4)
}

exit(4)


returncode_caller.ps1 (version1)

1…3 | % {
$
.\returncode_called.ps1
"?: $?"
"lec: $LASTEXITCODE"
}


returncode_caller.ps1 (version2)

1…3 | % {
$

returncode_called
"?: $?"
"lec: $LASTEXITCODE"
}


The first version iterates all 3 times, the second version halts on iteration 1.

What I want is a function to generate a user-defined error condition, preferably without halting the parent script. If throw from the child function, and try/catch in the parent script, is the best practice approach, then so be it. But the end user of the function (i.e. the parent script) will need to ensure that the error condition, which I preferred was not fatal, needs to be caught or trapped. Is there a way to "throw" a non-terminating error (I assume that’s Write-Error???)

IOW, I need a way to indicate to the parent that something went wrong in the function (archiving files), so don’t execute what follows (deleting the original files), but also don’t halt the rest of the looping within the parent script.
by MasterOfTheHat at 2013-03-14 07:42:35
If I’m telling you something you already know, sorry about that… Just trying give as much help as I can.

The difference in your 2 examples is the scope that the "exit(4)" command is in when it’s called. (Lots of good info in the about_Scopes help topic .) The exit command is always going to kill the current scope.

In example 1, when you open Powershell, the global scope is created. When returncode_caller.ps1 is executed, a script scope is created for that script. At this point, the scope for returncode_caller.ps1 is a child scope of the global scope, so everything available to the global scope is now available to the returncode_caller.ps1 scope but not vice-versa. When that script calls ".\returncode_called.ps1", another script scope is created for the returncode_called.ps1 script. So now the scope for returncode_called.ps1 is a child scope of the returncode_caller.ps1 scope, which is a child of the global scope, and everything that was available to the returncode_caller.ps1 scope is available to the returncode_called.ps1 scope. When the "exit(4)" command is executed, (line 5 in returncode_called.ps1), the scope for returncode_called.ps1 is killed and control returns to the parent scope, (returncode_caller.ps1).

powershell process scope (global scope)
|
-> returncode_caller.ps1 scope (script scope; child of global scope; parent of returncode_called.ps1 scope)
|
-> returncode_called.ps1 scope (script scope; child of returncode_called.ps1 scope) <– exit(4) executed here

In example 2, I’m assuming you dot-sourced returncode_called.ps1, (". .\returncode_called.ps1"), in the powershell console before executing the returncode_caller.ps1 script. Otherwise, you couldn’t be calling the function that way. That means that the returncode_called function is now part of the global scope and can be used by that scope or any of its child scopes. When returncode_caller.ps1 executes, a new script scope is created that is a child of the global scope. So now, when "returncode_called", (line 3 in "returncode_caller.ps1 (version 2)"), is called, it executes in the script scope for returncode_caller because it was available in the global scope, which is the parent of returncode_caller. That means that the "exit(4)" command on line 2 of returncode_called.ps1 is executed, which kills the returncode_caller.ps1 script.

powershell process scope (global scope) <– returncode_called function available here after the dot-sourcing
|
-> returncode_caller.ps1 scope (script scope; child of global scope) <– exit(4) executed here


All that just to explain why version 1 iterates through all 3 and version 2 only hits the first one! Now to tackle the main question…

Using return codes is, like DonJ said, the old-school, batch file way of scripting things. A lot of applications still use them, (I still see return codes in SCCM 2012 client installs, for example), but it is discouraged for managed code. The "right" way of handling errors is by throwing and handling exceptions. There are a lot of good write-ups on the web about it. Google around for "powershell exception handling", "powershell try catch", "erroraction", "errorvariable", etc.

It sounds like what you want to do is (1) call a child script from a main script, (2) define a condition in the child script that causes it to fail, (3) see that failure in the parent script, and (4) continue on executing the parent script, presumably while reporting the failure. Sound about right?

The basic steps for that are going to include calling the child script within a try/catch block in the parent, throwing an exception in the child, and handling the exception in the parent. Consider these 2 scripts:

returncode2_caller.ps1
$exceptions = @()
try
{
.\returncode2_called.ps1 $true
}
catch
{
$Error[0]
}

try
{
.\returncode2_called.ps1 $false
}
catch
{
$Error[0]
}

"Write this stuff after the exception is caught"


returncode2_called.ps1
Param($fail)
if($fail)
{
throw "This script failed"
}
else
{
"This script worked"
}


Very basic example, but it works… Here’s the output:
PS F:\Storage\Scripts\Windows\Junk> .\returncode2_caller.ps1
This script failed
At F:\Storage\Scripts\Windows\Junk\returncode2_called.ps1:4 char:2
+ throw "This script failed"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (This script failed:String) , RuntimeException
+ FullyQualifiedErrorId : This script failed

This script failed
At F:\Storage\Scripts\Windows\Junk\returncode2_called.ps1:4 char:2
+ throw "This script failed"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (This script failed:String) , RuntimeException
+ FullyQualifiedErrorId : This script failed

Write this stuff after the exception is caught
PS F:\Storage\Scripts\Windows\Junk> .\returncode2_caller.ps1
This script failed
At F:\Storage\Scripts\Windows\Junk\returncode2_called.ps1:4 char:2
+ throw "This script failed"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (This script failed:String) , RuntimeException
+ FullyQualifiedErrorId : This script failed

This script worked
Write this stuff after the exception is caught
by scottbass at 2013-03-17 23:48:51
[quote]It sounds like what you want to do is (1) call a child script from a main script, (2) define a condition in the child script that causes it to fail, (3) see that failure in the parent script, and (4) continue on executing the parent script, presumably while reporting the failure. Sound about right?[/quote]
Hi Master,

Thanks for the detailed explanation. I really appreciate the time you took to write it up.

These questions are related to viewtopic.php?f=2&t=1455. So, Select-Files, Backup-Files, and Remove-Files are functions sourced in via the global profile, and are available to all users. They are meant to work as both standalone as well as via the pipeline.

So, I can do (for example):

Select-Files C:\Some\Path -recurse -age 30 to retrieve all files older than 30 days, or
Select-Files C:\Some\Path -recurse -age 30 | Archive-Files to archive files older than 30 days, or
Archive-Files C:\Some\Path -recurse -age 30 to also archive files order than 30 days, or
Select-Files C:\Some\Path -recurse -age 30 | Archive-Files | Remove-Files to archive and remove files older than 30 days, or
Remove-Files C:\Some\Path -recurse -age 30 to remove files older than 30 days.

Sometimes there can be problems with 7-zip, esp. with locked files. In that scenario, I 1) want to let the parent script know that the archive failed, 2) don’t return anything to the pipeline, and 3) don’t have the parent script halt. In this scenario, the parent script is Archive-Logs.ps1, also in viewtopic.php?f=2&t=1455.

Based on what you and Don said, for #3, best practice is to use try/catch in the parent script, and to throw an error from the archive-files function.

I’m curious, what is Get-ChildItem doing in this scenario?
1…3 | % {$; gci foo; $?}
AFAIK it can’t be throwing an error, since all iterations execute:
1…3 | % {$
; gci foo; $?; if ($_ -eq 2) {throw}}
Is it doing the equivalent of Write-Error?

And why doesn’t this execute all iterations?
$erroractionpreference=continue
try {1…3 | % {$; gci foo; $?; if ($ -eq 2) {throw}}} catch {"ERROR]
Sorry for being dense, I’ll hit the docs now…

Thanks,
Scott
by MasterOfTheHat at 2013-03-18 10:18:59
[quote]I’m curious, what is Get-ChildItem doing in this scenario?
1…3 | % {$; gci foo; $?}[/quote]
I assume that your question here is about the 3 "gci : Cannot find path ‘C:\Windows\foo’ because it does not exist." exceptions that are displayed when you execute gci with an invalid path?
That exception is thrown in Get-ChildItem, (the same as the "throw <Exception>" command), and is displayed to the console because the default value of $ErrorActionPreference is "Continue". If you had set $ErrorActionPreference to "SilentlyContinue", (or you had just used "gci foo -erroraction silentlycontinue" instead of "gci foo"), you would have the same output without the exceptions.
PS C:\Windows> 1..3 | % {$
; gci foo; $?}
1
gci : Cannot find path ‘C:\Windows\foo’ because it does not exist.
At line:1 char:15
+ 1..3 | % {$; gci foo; $?}
+ ~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Windows\foo:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

False
2
gci : Cannot find path ‘C:\Windows\foo’ because it does not exist.
At line:1 char:15
+ 1..3 | % {$
; gci foo; $?}
+ ~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Windows\foo:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

False
3
gci : Cannot find path ‘C:\Windows\foo’ because it does not exist.
At line:1 char:15
+ 1..3 | % {$; gci foo; $?}
+ ~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Windows\foo:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

False
PS C:\Windows> 1..3 | % {$
; gci foo -erroraction silentlycontinue; $?}
1
False
2
False
3
False


Take a look at this Scripting Guy blog post for a pretty good explanation of -ErrorAction and $ErrorActionPreference.

[quote]AFAIK it can’t be throwing an error, since all iterations execute:
1…3 | % {$; gci foo; $?; if ($ -eq 2) {throw}}[/quote]
Actually, if you look closer at the output, it only executes for 1 and 2:
PS C:\Windows> 1..3 | % {$; gci foo; $?; if ($ -eq 2) {throw}}
1
gci : Cannot find path ‘C:\Windows\foo’ because it does not exist.
At line:1 char:15
+ 1..3 | % {$; gci foo; $?; if ($ -eq 2) {throw}}
+ ~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Windows\foo:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

False
2
gci : Cannot find path ‘C:\Windows\foo’ because it does not exist.
At line:1 char:15
+ 1..3 | % {$; gci foo; $?; if ($ -eq 2) {throw}}
+ ~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Windows\foo:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

False
ScriptHalted
At line:1 char:43
+ 1..3 | % {$; gci foo; $?; if ($ -eq 2) {throw}}
+ ~~~~~
+ CategoryInfo : OperationStopped: (:slight_smile: , RuntimeException
+ FullyQualifiedErrorId : ScriptHalted

The last exception is where the script stopped because it hit your "throw" command.

[quote]Is it doing the equivalent of Write-Error?[/quote]
Negative. They’re similar, but the difference is that "throw" causes a terminating error and "Write-Error" causes a non-terminating error. In the output of the following scripts, notice that the "Write this stuff after the throw line" line is never executed because the "throw" line is above it. However, the "Write this stuff after the write-error line" line IS executed after the "Write-Error" line.
returncode3_caller.ps1:
try
{
.\returncode3_called.ps1 $true
}
catch
{
$Error[0]
}

try
{
.\returncode3_called.ps1 $false
}
catch
{
$Error[0]
}

returncode3_called.ps1:
Param($fail)
if($fail)
{
throw "This script failed"
"Write this stuff after the throw line"
}
else
{
Write-Error "This script just threw an error"
"Write this stuff after the write-error line"
}

Output:
> .\returncode3_caller.ps1
This script failed
At F:\Storage\Scripts\Windows\Junk\returncode3_called.ps1:4 char:2
+ throw "This script failed"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (This script failed:String) , RuntimeException
+ FullyQualifiedErrorId : This script failed

F:\Storage\Scripts\Windows\Junk\returncode3_called.ps1 : This script just threw an error
At F:\Storage\Scripts\Windows\Junk\returncode3_caller.ps1:12 char:2
+ .\returncode3_called.ps1 $false
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:slight_smile: [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,returncode3_called.ps1

Write this stuff after the write-error line


[quote]
And why doesn’t this execute all iterations?
$erroractionpreference=continue
try {1…3 | % {$; gci foo; $?; if ($ -eq 2) {throw}}} catch {"ERROR][/quote]
And this one only executes twice for the same reason your second example only executes twice. As soon as you "throw" the exception, the script halts. (In this case, since no exception was specified, the "exception" is "ScriptHalted".)

Clear as mud?

And you’re not being dense! We’re all here to learn and get better at what we do! In my experience, exception handling is not an easy concept in any language. Either that, or it’s the last thing we’re taught or the last thing we try to learn when we’re learning a new language. It’s all good!
by scottbass at 2013-03-18 15:44:01
Hi Master,

Thanks again…

Sorry, I didn’t make my questions clear:

[quote]I’m curious, what is Get-ChildItem doing in this scenario?
1…3 | % {$; gci foo; $?}
[/quote]
What I meant is: gci is clearly generating an error, but it’s a non-terminating error, since all iterations execute. AFAIK, you generate a non-terminating error via Write-Error. So, is gci doing the equivalent of Write-Error in this scenario? And how can I do the same in my scripts?

[quote]AFAIK it can’t be throwing an error, since all iterations execute:
1…3 | % {$
; gci foo; $?; if ($_ -eq 2) {throw}}[/quote]
Similar comments to the above. So, it appears that gci isn’t throwing a terminating error since, when I explicitly throw a terminating error on iteration #2, the scriptblock terminates when throw is called.

[quote]And why doesn’t this execute all iterations?
$erroractionpreference=continue
try {1…3 | % {$; gci foo; $?; if ($ -eq 2) {throw}}} catch {"ERROR][/quote]
I thought $ErrorActionPreference=Continue meant "display the error, but continue the script". Furthermore, I’m also using try/catch. So:

1) Why doesn’t $ErrorActionPreference=Continue allow all iterations to execute?
2) Why doesn’t try/catch catch the terminating error, and allow all iterations to execute?
3) In this scenario, how do I throw a terminating error, catch it, and allow all iterations to execute?

Thanks,
Scott
by scottbass at 2013-03-18 15:49:32
[quote]3) In this scenario, how do I throw a terminating error, catch it, and allow all iterations to execute?[/quote]
I’ve worked out #3: move the try/catch block:
1…3 | % {try {$; gci foo; $?; if ($ -eq 2) {throw}} catch {"ERROR]
or
1…3 | % {$; gci foo; $?; if ($ -eq 2) {try {throw} catch {"ERROR]