I need to run my PowerShell module's PowerShell function from a Bash shell. The PowerShell function is supposed to return an exit code for the Bash shell to evaluate.
However, when I write:
function f
{
[CmdletBinding()]
param
( [Parameter()][string]$Test
)
process { exit 1 }
}
… in Test.ps1
, using this in a PowerShell module (Test.psm1
) like this:
. .\Test.ps1
… and have it run in PowerShell like this:
Import-Module .\Test.psm1
f ''
… the PowerShell console is getting closed. This is not what I want to happen.
How can I return an exit code from PowerShell module's function without terminating the current PowerShell session?
Let me provide some background information, to complement your own answer:
Exit codes are a system-level concept relating to processes: a process can signal the success status of its invocation to the calling process by way of an integer value, with - by widely observed convention - 0
signaling success, and any nonzero value a failure condition.[1]
exit
, optionally with a given exit code, is only used in two scenarios in PowerShell:
When executed outside a script file: to exit the entire PowerShell process.
To exit a script file. Whether that exit code is relayed as the exit code of the entire PowerShell process depends on how PowerShell was invoked (see below).
Whether or not exit
is called from inside a function is incidental to its effect; that is, if the function happens to be called from outside a script file, the PowerShell process is exited; from inside a script file, that script alone is exited.
In PowerShell's own error handling, exit codes play only a very limited role. The closest analog to an exit code in PowerShell is the automatic $?
variable, which is a Boolean value ($true
or $false
) that abstractly indicates whether any error was reported by the most recently executed statement:[2]
For calls to PowerShell-native commands except script files, the occurrence of a (statement-)terminating error or anything having been written to PowerShell's error stream (i.e. a non-terminating error) is what causes $?
to reflect $false
. While the latter is consistently true for binary cmdlets, in functions it only applies to advanced functions that explicitly use $PSCmdlet.ThrowTerminatingError()
or $PSCmdlet.WriteError()
- see GitHub issue #3629.
For calls to external programs and script files, it is a nonzero exit code (as reflected in the automatic $LASTEXITCODE
variable)[3] that sets $?
to $false
.
To communicate an exit code from PowerShell code to a calling process - such as in CI/CD scenarios - you must therefore ensure that calls to the PowerShell CLI (powershell.exe
for Windows PowerShell, pwsh
for PowerShell (Core) 7) relay the desired exit code as the CLI's own process exit code, which is achieved in one of two ways:
If all your code runs in a script file (.ps1
), use the -File
(-f
) CLI parameter and ensure that your script file uses an exit
call with the desired exit code, which PowerShell then automatically passes through as its own exit code.
E.g., if your foo.ps1
script exits with exit 5
, the following CLI call will report 5
as its exit code:
pwsh -File foo.ps1
On Unix-like platforms, this mechanism is implicitly used if you create an executable shell script with a shebang line.
If you must pass PowerShell commands, i.e. a piece of PowerShell code to the CLI, use the latter's -Command
(-c
) parameter, in which case PowerShell's own exit code is determined as follows:
$?
, is mapped to exit code 0
or 1
: if $?
is $true
, the exit code is 0
, 1
otherwise; however, by using an explicit exit
statement as the - by definition - last one, you can control the specific exit code.In case a(n unhandled) script-terminating (fatal) error occurs, such as via throw
, the exit code is 1
in both invocation scenarios.
Focusing on your specific scenario:
A function generally shouldn't be concerned with signaling a process exit code - except, perhaps, if it is known to execute as part of a script file and is designed to act on behalf of that file as whole.
Generally, functions should signal failure to their (of necessity in-session) caller in PowerShell terms, which - if the function call happens to be the last statement executed in a -Command
(-c
) CLI invocation - translates to a process exit code of 1
.
Unfortunately, there is no way to directly and abstractly signal failure from a PowerShell function: $?
is set solely based on whether $PSCmdlet.ThrowTerminatingError()
or $PSCmdlet.WriteError()
was explicitly called, which (a) is only an option from advanced functions, as noted, and (b) invariably cause error-stream output, which the caller would need to silence to prevent it from surfacing.
$?
directly for the caller in the future has been green-lit, but is yet to be implemented - see GitHub issue #10917However, you can use a throw
statement from inside a function to both stop execution of the function instantly and to signal a severe error to the caller - without directly exiting an interactive session, as would happen if you used exit
and called the function from outside a script (as shown in your question).
In a CLI call, if the code passed to -Command
doesn't handle the resulting script-terminating error, the CLI reports 1
as the exit code, as noted; however, this will cause an error message to surface as well.
To (optionally) avoid an error message and report a specific exit code, you could pass the desired exit code as an argument to throw
, let the in-session caller catch it via try
/ catch
, and report it as the CLI's exit code via an exit
call in the catch
block:
# Reports exit code 5.
pwsh -c 'function foo { throw 5 }; try { foo } catch { exit $_.TargetObject }'
[1] The permitted range of integer values depends on the host platform: windows permits [int]
values (signed 32-bit integers), which on Unix-like platforms you're limited to [byte]
values (unsigned 8-bit integers). In cross-platform code, it is therefore best to limit exit codes to between 0
and 255
, inclusively.
[2] $?
is implicitly also used with &&
and ||
, the pipeline-chaining operators.
[3] Strictly speaking, a script-file's execution is considered successful if it terminates with exit 0
(which is the same as just exit
) or exits implicitly (even if errors occurred inside the script); only if exit
is actually used is $LASTEXITCODE
set; thus, $LASTEXITCODE
may reflect an incidental value afterwards, such as from an earlier, unrelated external-program call, which is problematic - see GitHub issue #11712. Fortunately, however, $?
is set correctly in all cases, and script-file execution therefore also works with &&
and ||
.