11

I have questions about returning an exit code value from PowerShell when run from a cmd.exe invocation. I found https://weblogs.asp.net/soever/returning-an-exit-code-from-a-powershell-script which has been helpful. But, the solution for PowerShell code is to add a function.

function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode) exit }

Generates:

ExitWithCode : The term 'ExitWithCode' 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 C:\src\t\e\exit5.ps1:6 char:1
+ ExitWithCode -exitcode 12345
+ ~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (ExitWithCode:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

But, placing the "exit" on a new line works. Is this just a language anomaly?

function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode)
exit }

Also, this page is from 2010. Is this still the current state? Is there a better/easier way now?

lit
  • 14,456
  • 10
  • 65
  • 119
  • 1
    The [currently accepted answer](https://stackoverflow.com/a/50200868/45375) solves your immediate syntax problem, but, as evidenced by [your own follow-up question](https://stackoverflow.com/q/60311778/45375), the (generally ill-advised) use of `$host.SetShouldExit()` has the serious side effect of instantly terminating interactive sessions. – mklement0 Mar 26 '20 at 22:33

4 Answers4

27

As Guenther Schmitz already explained, $host.SetShouldExit($exitcode) and exit are 2 distinct statements that must be separated either with a newline or a semicolon.

Without that separation your code should have thrown a different error, though:

Unexpected token 'exit' in expression or statement.

The error you posted looks more like you tried to use that function without defining it first.

The purpose of the function is to set a proper exit code when exiting from a script regardless of how the script was run. Normally you'd run PowerShell scripts like this:

powershell.exe -File "C:\your.ps1"

And in that case a simple exit $exitcode would be sufficient:

C:\> type test1.ps1
exit 23
C:\> powershell -File .\test1.ps1

C:\> echo %errorlevel%
23

However, another way to execute PowerShell scripts is the -Command parameter (since PowerShell scripts can be run directly from PowerShell). The difference between the -File and -Command parameters is that the latter returns only 1 or 0 (indicating whether or not the script exited with a non-zero exit code), but not the exit code itself.

C:\> powershell -Command .\test1.ps1

C:\> echo %errorlevel%
1

When omitting the parameter entirely PowerShell defaults to -Command (allowing you to easily run PowerShell statements from the commandline):

C:\> powershell .\test1.ps1

C:\> echo %errorlevel%
1

Defining an exit code via $host.SetShouldExit() ensures that the exit code is returned correctly when the script is invoked via powershell. -Command. You still should exit with the actual exit code, though, because otherwise the exit code would only be set when running the script via powershell.exe -Command, but not when running the script via powershell.exe -File:

C:\> type test2.ps1
function ExitWithCode($exitcode) {
  $host.SetShouldExit($exitcode)
  exit
}
ExitWithCode 23
C:\> powershell -File .\test2.ps1

C:\> echo %errorlevel%
0                                # ← exit without argument defaults to 0!

C:\> powershell -Command .\test2.ps1

C:\> echo %errorlevel%
23
C:\> type test3.ps1
function ExitWithCode($exitcode) {
  $host.SetShouldExit($exitcode)
  exit $exitcode
}
ExitWithCode 23
C:\> powershell -File .\test3.ps1

C:\> echo %errorlevel%
23

C:\> powershell -Command .\test3.ps1

C:\> echo %errorlevel%
23
Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
  • Many thanks for a complete answer. I gave the check to @Guenther since he responded first, your answer explains more fully. Yes, it did generate the `Unexpected token` message. I copied the wrong one. It can all be done on one line using `function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode); exit }` – lit May 06 '18 at 19:24
  • I am wondering why the use of `exit` does not also do `$host.SetShouldExit()`? What might be the reason for implementing it this way? – lit May 06 '18 at 19:25
  • 1
    Only Microsoft could answer that question. – Ansgar Wiechers May 06 '18 at 21:48
17

Guenther Schmitz's answer solves your immediate syntax problem, but it's important to note that $host.SetShouldExit() is not meant to be called by user code, as implied by Bruce Payette's answer.

Instead, it is used internally by PowerShell itself in response to an exit statement in user code.

The only conceivable reason to use it is to repurpose it for implementing a workaround around a limitation of exit-code reporting when a script is being called via the -Command
(-c) parameter of PowerShell's CLI
:

With -Command, a script's specific non-zero exit code is always translated to 1, so the specific exit code is lost - see this answer for an overview of exit-code handling in PowerShell.

$host.SetShouldExit(), despite not being intended for this purpose, happens to overcome this limitation and ensures that the script's exit code is also reported as the PowerShell process' exit code.

This workaround must not be applied when the script is being called from an interactive PowerShell session, because it will cause the session as a whole to exit instantly, which is the subject of your follow-up question.

Reliable detection of when a script is being called via -Command is nontrivial and hard to make fully reliable, however, as shown in this answer to your follow-up question.

The better approach is not to use the $host.SetShouldExit() workaround and do the following instead.

  • Invoke your script via the -File CLI parameter, in which case no workaround is needed: Just use exit $n in your script, and $n will be reported as the PowerShell process' exit code (assuming $n is an integer).

  • When calling via -Command, follow the script call with ; exit $LASTEXITCODE in the command string so as to ensure that the script's exit code is passed through.

Of course, you may not always be in control of how your script is invoked via the CLI; in that event, the workaround is worth considering.

mklement0
  • 382,024
  • 64
  • 607
  • 775
6

$host.SetShouldExit($exitcode) and exit are two commands which have to separated. either with a return (like you mentioned) or with a semicolon:

function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode); exit }
Guenther Schmitz
  • 1,955
  • 1
  • 9
  • 23
  • Is this method to exit still the professional method. – lit May 06 '18 at 15:09
  • @lit Short answer: yes. For the long answer see my actual answer. ;) – Ansgar Wiechers May 06 '18 at 17:35
  • 1
    This exits the host, not just returns an exit code from the script. Like, if you call the script from a terminal, the terminal closes instead of the script. – Michael Jun 11 '19 at 10:47
  • 1
    @lit, no it isn't the proper method, because `$host.SetShouldExit()` isn't meant to be called from user code and, as an unwanted side effect, instantly terminates an interactive session, as @Michael notes. – mklement0 Mar 20 '20 at 12:31
4

The way to return an exit code from PowerShell is to do exit $exitcode. Internally, When the runtime processes the exit keyword it will do the call to $host.SetShouldExit($exitcode) for you. But be aware that exit also exits scripts so it matters how you run your script. If want to run a script that calls exit from powershell.exe use the -File parameter rather than the -Script as in

powershell -File scriptThatCallsExit
Bruce Payette
  • 2,511
  • 10
  • 8