Using script in automated build process - powershell

I have a Powershell script written, which will be used in an automated build process.
Do I need to write exit code 0 when the script will take the correct path (e.g. doesn't go into the if blocks which indicate an error condition)?
Also, what is the difference between exit (with code) and $host.SetShouldExit()?

In similar scenario I rely on exceptions (throw) and other kind of terminating errors: the PowerShell’s exit code is 1 in that case. If a scripts finishes itself (even with not terminating errors) then PowerShell’s exit code is 0, we do not have to call exit 0. If we need something but 0 and 1 then indeed we have to use exit or SetShouldExit (but see remarks below).
Let’s take a look at the scripts.
test1.ps1
'before2'
.\test2
'after2'
'before3'
.\test3
'after3'
'before4'
.\test4
'after4'
test2.ps1
'inner before 2'
exit 2
'inner after 2'
test3.ps1
'inner before 3'
$host.SetShouldExit(3)
'inner after 3'
test4.ps1
throw 'Oops'
Output of the test1.ps1:
before2
inner before 2
after2
before3
inner before 3
inner after 3
after3
before4
Oops
At ...
+ throw <<<< 'Oops'
In this test scenario the test4.ps1 kind of works and test2.ps1 and test3.ps1 kind of do not work (if to work means to fail and exit the session immediately).
The output shows that exit terminates the current script and SetShouldExit does not.
The exit code of powershell.exe .\test1 is 3 due to $host.SetShouldExit(3). I have tried to disable that line to check whether exit 2 makes the exit code to be 2. No, it does not, the exit code is 1 due to the failure in test4.
I have noticed one more difference. If I call a script from the interactive PowerShell console than $host.SetShouldExit in a script makes the console to close after the call. exit does not.
A thought. Behaviour of $host.SetShouldExit may depend on implementation of a host. For example, in one my own host exit is not supported at all (it is not allowed to close the hosting application just like that) and my implementation of SetShouldExit basically does nothing at all.

Related

How to fix 'script returned exit code 1' when running Powershell script through Jenkins?

I have a Powershell script which runs some Unity unit tests. On fail, this returns code 1, which Jenkins interprets as a fail and stops the current build. How do I avoid this behavior?
NOTE: This question is almost identical to this one, but that one uses bash, so I cannot apply it to my problem. In other words, how do I mimic the set +e behavior in Powershell? Alternatively, how do I tell Jenkins to ignore these script return codes and to continue the build anyway?
how do I mimic the set +e behavior in Powershell
You don't have to - it's the default behavior in that calls to external programs never[1] cause execution to stop prematurely, even if they report nonzero exit codes.
Alternatively, how do I tell Jenkins to ignore these script return codes and to continue the build anyway?
Ensuring that your script always terminates with exit 0 is indeed the correct way to ensure that Jenkins doesn't consider the script call failed (though you'll lose the information as to whether or not the tests failed).
[1] It can happen accidentally, due to flawed behavior up to PowerShell 7.1: If you use a 2> redirection and $ErrorActionPreference = 'Stop' happens to be effect, a script-terminating error occurs when stderr output is actually received. There is also pending RFC #277, which suggests introducing an opt-in mechanism for properly integrating external-program calls into PowerShell's error handling.

How to return non zero exit code from a Powershell module function without closing the powershell console?

My powershell module has a function and I want it to return a non zero exit code. However, being a module function it is loaded into the context of the powershell console when I run Import-Module. So, when the function executes exit 1 - poof, goes the console window!
At least, this is how I explain it closing the powershell window when it exits.
So, how can a ps module function exit with a non zero exit code without killing the console where the module was imported?
P.S.
I did notice several questions on SO about this subject, but none seems to examine this particular case.
EDIT 1
I would like to provide some context. I have a PS module with a lot of functions. Some of them are used as is in Azure DevOps yaml build scripts. The latter knows to recognize non zero exit code and abort the pipeline, so it is not necessary to throw from a function to abort the flow.
However, if I want to call that function from the console, e.g. to test something quickly and it exits with non zero code, the whole console window is closed. This is extremely annoying.
Sometimes there is a workaround. So, instead of this code:
dotnet build ...
$ExitCode = $LastExitCode
DoSomethingInAnyCase()
if ($ExitCode)
{
exit $ExitCode
}
We can have the following version:
try
{
dotnet build ...
}
finally
{
DoSomethingInAnyCase()
}
Both versions would correctly return the right exit code, but because the second one does not have the explicit exit statement, it does not close the console.
You'll have to set $global:LASTEXITCODE in order to set the exit code, but note that PowerShell functions aren't really meant to set exit codes, only scripts, and the latter only for reporting exit codes to the outside world, via the PowerShell process' own exit code, when PowerShell is called via its (CLI powershell.exe for Windows PowerShell, pwsh for PowerShell Core) from a build tool, scheduled task, or another shell, for instance.
Also note that setting $global:LASTEXITCODE directly:
does not make $?, the automatic success-status variable, reflect $false in the caller's context, the way that exit <nonzero-value> does from a script and the way that calling an external program that reports a nonzero exit code does.
is not enough to make the PowerShell process as a whole report this exit code.
In short: All this gains you is that the caller can inspect $LASTEXITCODE after your function was called, as you would after calling an external program.
Generally, exit codes are an inter-process concept and do not fit well into PowerShell's in-process world.
For more information about exit codes in PowerShell, see this post.
PowerShell's analog to exit codes is $?, the automatic, Boolean success-status variable ($true or $false), which reflects a PowerShell command's success immediately afterwards.
(If that command is an external-program call, an exit code of 0 sets $? to $true, and any nonzero one sets it to $false).
As it turns out, setting that was what you really meant to ask.
As of PowerShell Core 7.0.0-preview.5, you cannot set $? directly.
For now, these are the only ways to cause $? to reflect $false in the caller's scope, and, conversely, you cannot always ensure that it is $true:
From a script: Exit the script with exit <nonzero-integer>
From a cmdlet or function (script as well):
Throw a script-terminating error (Throw) or a statement-terminating error ($PSCmdlet.ThrowTerminatingError()) - the latter being only available in advanced functions and scripts.
Write an error to PowerShell's error stream with $PSCmdlet.WriteError() - the latter being only available in advanced functions and scripts.
Note that this unexpectedly currently does not apply to the Write-Error cmdlet - see this GitHub issue
Note that both techniques invariably involve emitting an error.
Since it sounds like that's precisely what you're trying to avoid, you'll have to wait until the ability to set $? directly is implemented.
The decision to implement this ability has been made, but it's unclear when it will be implemented.
Workaround: Run cmd.exe and set whatever exit code you want before exiting the function. Example:
function Test-Function {
$cmd = Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::System)) "cmd.exe"
& $cmd /c exit 3
# $LASTEXITCODE will be set to 3
}

PowerShell and process exit codes

This self-answered question tries to address two distinct aspects of dealing with process exit codes in PowerShell:
In PowerShell code, how can you query the exit code set by an external process (a call to an external program), and how do such exit codes integrate with PowerShell's error handling?
When someone else calls PowerShell via its CLI, pwsh (PowerShell Core) / powershell.exe (Windows PowerShell), what determines the PowerShell process' exit code that communicates success vs. failure to the calling process (which could be a build / CI / automation-server task, a scheduled task, or a different shell, for instance).
Current as of PowerShell [Core] 7.2.1
PowerShell-internal use of exit codes:
PowerShell-internally, where native PowerShell commands generally run in-process, exit codes from child processes that run external programs play a very limited role:
Native PowerShell commands generally don't set exit codes and don't act on them.
PowerShell has an abstract counterpart to exit codes: $?, the automatic, Boolean success-status variable:
It reflects whether the most recently executed command had any errors, but in practice it is rarely used, not least because - up to version 6.x - something as seemingly inconsequential as enclosing a command in (...) resets $? to $true - see GitHub issue #3359 - and because using Write-Error in user functions doesn't set $? to $false - see GitHub issue #3629; however, eventually providing the ability for user code to set $? explicitly has been green-lit for a future version.
While $? also reflects (immediately afterwards) whether an external program reported an exit code of 0 (signaling success, making $? report $true) or a nonzero exit code (typically signaling failure, making $? $false), it is the automatic $LASTEXICODE variable that contains the specific exit code as an integer, and that value is retained until another external program, if any, is called in the same session.
Caveat: Due to cmd.exe's quirks, a batch file's exit code isn't reliably reported, but you can work around that with cmd /c <batch-file> ... `& exit - see this answer; GitHub issue #15143 additionally suggests building this workaround into PowerShell itself.
Also, up to v7.1, $? can report false negatives if the external program reports exit code 0 while also producing stderr output and there is also a PowerShell redirection involving 2> or *> - see this answer and GitHub issue #3996; as of PowerShell PowerShell Core 7.2.0-preview.4; the corrected behavior is a available as experimental feature PSNotApplyErrorActionToStderr.
Finally, it's best to treat $LASTEXITCODE as read-only and let only PowerShell itself set it. (Technically, the variable is writeable and lives in the global scope, so if you do want to modify manually, after all, be sure to assign to $global:LASTEXITCODE, so as not accidentally create a transient local copy that has no effect.)
Unlike terminating errors or non-terminating errors reported by PowerShell-native commands, nonzero exit codes from external programs can not be automatically acted upon by the $ErrorActionPreference preference variable; that is, you cannot use that variable to silence stderr output from external programs nor can you, more importantly, choose to abort a script via value 'Stop' when an external program reports a nonzero exit code.
Better integration of external programs into PowerShell's error handling is being proposed in RFC #277.
How to control what PowerShell reports as its exit code when it is called from the outside:
Setting an exit code that at least communicates success (0) vs. failure (nonzero, typically) is an important mechanism for letting outside callers know whether your PowerShell code succeeded overall or not, such as when being called from a scheduled task or from an automation server such as Jenkins via the PowerShell CLI (command-line interface) - pwsh for PowerShell [Core] vs. powershell.exe for Windows PowerShell.
The CLI offers two ways to execute PowerShell code, and you can use exit <n> to set an exit code, where <n> is the desired exit code:
-File <script> [args...] expects the path of a script file (*.ps1) to execute, optionally followed by arguments.
Executing exit <n> directly inside such a script file (not inside another script that you call from that script) makes the PowerShell process report its exit code as <n>.
If a given script file exits implicitly or with just exit (without an exit-code argument), exit code 0 is reported.
-Command <powershell-code> expects a string containing one or more PowerShell commands.
To be safe, use exit <n> as a direct part of that command string - typically, as the last statement.
If your code is called from tools that check success by exit code, make sure that all code paths explicitly use exit <n> to terminate.
Caveat: If the PowerShell process terminates due to an unhandled script-terminating error - irrespective of whether the CLI was invoked with -File or -Command - the exit code is always 1.
A script-terminating (fatal) error is either generated from PowerShell code with the throw statement or by escalating a less a severe native PowerShell error with -ErrorAction Stop or $ErrorActionPreference = 'Stop', or by pressing Ctrl-C to forcefully terminate a script.
If exit code 1 isn't specific enough (it usually is, because typically only success vs. failure needs to be communicated), you can wrap your code in a try / catch statement and use exit <n> from the catch block.
The exact rules for how PowerShell sets its process exit code are complex; find a summary below.
How PowerShell sets its process exit code:
If an unhandled script-terminating error occurs, the exit code is always 1.
With -File, executing a script file (*.ps1):
If the script directly executes exit <n>, <n> becomes the exit code (such statements in nested calls are not effective).
Otherwise, it is 0, even if non-terminating or statement-terminating errors occurred during script execution.
With -Command, executing a command string containing one or more statements:
If an exit <n> statement is executed directly as one of the statements passed in the command string (typically, the last statement), <n> becomes the exit code.
Otherwise, it is the success status of the last statement executed, as implied by $?, that determines the exit code:
If $? is:
$true -> exit code 0
$false -> exit code 1 - even in the case where the last executed statement was an external program that reported a different nonzero exit code.
Given that the last statement in your command string may not be the one whose success vs. failure you want to signal, use exit <n> explicitly to reliably control the exit code, which also allows you to report specific nonzero exit codes.
For instance, to faithfully relay the exit code reported by an external program, append ; exit $LASTEXITCODE to the string you pass to -Command.
Inconsistencies and pitfalls as of PowerShell 7.0:
Arguably, -Command (-c) should report the specific exit code of the last statement - provided it has one - instead of the abstract 0 vs. 1. For instance, pwsh -c 'findstr'; $LASTEXITCODE should report 2, findstr.exe's specific exit code, instead of the abstract 1 - see GitHub issue #13501.
Exit-code reporting with *.ps1 files / the -File CLI parameter:
It is only an explicit exit <n> statement that meaningfully sets an exit code; instead, it should again be the last statement executed in the script that determines the exit code (which, of course, could be an exit statement), as is the case in POSIX-compatible shells and with -Command, albeit in the suboptimal manner discussed.
When you call a *.ps1 script via -File or as the last statement via -Command, PowerShell's exit code in the absence of the script exiting via an exit statement is always 0 (except in the exceptional Ctrl-C / throw cases, where it becomes 1).
By contrast, when called in-session, again in the absence of exit, $LASTEXICODE reflects the exit code of whatever external program (or other *.ps1 if it set an exit code) was executed last - whether executed inside the script or even before.
In other words:
With -File, unlike with -Command, the exit code is categorically set to 0 in the absence of an exit statement (barring abnormal termination).
In-session, the exit code (as reflected in $LASTEXITCODE) is not set at all for the script as a whole in the absence of an exit statement.
See GitHub issue #11712.

Detecting PowerShell script is executed with error

I have a PowerShell script:
...
Any-Command -ErrorCode Stop
...
Then I call this script from a bat script:
...
powershell myscript.ps1
...
Now I would like to know in the bat script if the called PowerShell script is stopped on error and not reached the end of the script. How to do it?
One way to do it would be to add a top level trap statement to your script, something like:
trap {
exit 1; # or whatever error code
}
Note: trap may be considered old fashioned (although I like it top-level), the other option would be a try-finally around your whole script.
So when Any-Command exits with an exception, the trap statement is executed and the script exits with error code 1. If you don't add this, Powershell will exit with error code 0 (after all Powershell ran just fine, although the script didn't).
You can then detect the error code in a bat script using something like:
powershell -noprofile -noninteractive -file <<your script>>
IF ERRORLEVEL 1 (echo ERROR) ELSE (echo OK)
pause
This will detect any error code of 1 or higher.
You can also use %ERRORLEVEL% but be wary that this may be overridden as an environment variable.
Note: if you're running your script in a nested runspace, you could consider using $host.SetShouldExit(1) to ensure the root host exits as well.
Just adding to the answer given by Marcus
I noticed that the trap code even hides the actual error. In my case I needed the actual error occurred. Just adding the modified code for the completion.
trap
{
write-error $("Error: " + $_.Exception.Message);
exit 1; # or whatever error code
}
More info at http://huddledmasses.org/trap-exception-in-powershell/

Can I make the PowerShell $error object behave consistently in both script and interactive mode?

I'm writing a PowerShell script that uses $error to detect and respond to errors. My problem is that I get different behavior for the $error object depending on how I run the script. If I run it interactively (specifically, from PowerShell ISE), then errors get added to the collection, but if I run the same script from the command line, the same errors occur, but are not added to the collection.
Here's the script (pared down to illustrate the issue):
# export_exception_test.ps1
# show me how many errors before we start
"Count = " + $error.Count
try {
# treat non-terminating errors as terminating errors.
$ErrorActionPreference = "Stop"
# echo the setting to output
$ErrorActionPreference
# use sqlcmd to save the output of stored procedures to text files.
# The first and third call will fail because the output folder does not exist.
# The second call will succeed.
'first call to sqlcmd'
sqlcmd -S myserver\myinstance -E -s `"`t`" -Q "EXEC mydatabase.dbo.FI_Codes" -b -o "B:\Exports_new\FI_Codes.txt"
'second call to sqlcmd'
sqlcmd -S myserver\myinstance -E -s `"`t`" -Q "EXEC mydatabase.dbo.FI_Codes" -b -o "B:\Exports_newt\FI_Codes.txt"
'third call to sqlcmd'
sqlcmd -S myserver\myinstance -E -s `"`t`" -Q "EXEC mydatabase.dbo.FI_Codes" -b -o "B:\Exports_new\FI_Codes.txt"
# and a whole bunch more of these...
# The error count should be two more than when we started.
"Count = " + $error.Count
# And this should be the most recent error message
"Message = " + $error[0].Message
}
catch [Exception]
{
'exception was caught!!!'
"Count in catch clause = " + $error.Count
$_.Exception.Message
# the ultimate goal is to return a non-successful return code when the exports fail
exit 1
}
finally {
# set this back to what it was
$ErrorActionPreference = "Continue"
# primitive trace output
'finally.'
}
When I run this from PowerShell ISE, it behaves as expected. SQLCMD raises a non-terminating error, which is caught and handled. Here's the output:
PS U:\> C:\Users\etmatt\Documents\PowerShellScripts\export_exception_test.ps1
Count = 0
Stop
first call to sqlcmd
exception was caught!!!
Count in catch clause = 1
Sqlcmd: Error: Error occurred while opening or operating on file B:\Exports_new\FI_Codes.txt (Reason: The system cannot find the path specified).
finally.
But when I run it from the command line, as I would if I were to set up a scheduled task, nothing gets added to $error, and no exceptions are raised. Here's the output:
C:\Users\etmatt>powershell.exe -file "C:\Users\etmatt\Documents\PowerShellScripts\export_exception_test.ps1"
Count = 0
Stop
first call to sqlcmd
Sqlcmd: Error: Error occurred while opening or operating on file B:\Exports_new\FI_Codes.txt (Reason: The system cannot find the path specified).
second call to sqlcmd
third call to sqlcmd
Sqlcmd: Error: Error occurred while opening or operating on file B:\Exports_new\FI_Codes.txt (Reason: The system cannot find the path specified).
Count = 0
Message =
finally.
C:\Users\etmatt>
I also get the same results if I run the script file from the powershell command line rather than cmd.exe (which makes sense). For example:
PS C:\> ."C:\Users\etmatt\Documents\PowerShellScripts\export_exception_test.ps1"
I suspect this has something to do with the execution context or maybe something about parser modes that I'm still not quite grokking, or maybe even my profile (although so far I've been running everything from the same PC under the same account). I've seen a lot of examples online that use this basic approach of try/catch with $ErrorActionPreference = "Stop", so it seems like I should be able to make this work.
So my question is essentially: can I make this script work like I think it should? And if not, what am I misunderstanding? How do I catch these sorts of errors? If it helps, I don't need super detailed exception handling for this one, I just need to know when something goes wrong so that my task monitor can alert me. A non-zero return code from powershell.exe would be sufficient.
I'm looking into the use $?, and $LASTEXITCODE, but I'm betting $? will have the same issue as $error, and both only apply to the last statement executed, which is less than ideal since my real script is quite a bit longer than this example.
EDIT:
OK, I've since learned that Windows executables (such as sqlcmd), never add anything to the $error collection, even if they return a non-zero exit code. $error is only used by cmdlets, if I understand correctly. I got my script working through the repeated use of $LASTEXITCODE, although I could have used $? as well, because it recognizes Windows executable exit codes and gets set to false for non-zero values.
To sum up, the behavior of the script when run from the command line is the correct, expected behavior. Still don't know why I got different results from the PowerShell ISE, though.
For me I am calling batch command using cmd /C <command> and I have the similar problem. For non zero return code $? is set correctly but there is no $error object (how would powershell know? so this is ok) and the code behaves similarly in ISE as well as command line execution.
My problem is that in such cases when $ErrorActionPreference = "Stop" it is not throwing exception. Probably because it doesn't have $error object to throw. So now I need to check $? after every call to other script or program which is not neat. They should come up with good solution for such situation (probably by throwing in-built exception saying call to external script/program failed)