3

Ansgar Wiechers' answer works well whenever starting a new PowerShell process. https://stackoverflow.com/a/50202663/447901 This works in both cmd.exe and powershell.exe.

C:>type .\exit1.ps1
function ExitWithCode($exitcode) {
  $host.SetShouldExit($exitcode)
  exit $exitcode
}
ExitWithCode 23

In a cmd.exe interactive shell.

C:>powershell -NoProfile -Command .\exit1.ps1
C:>echo %ERRORLEVEL%
23
C:>powershell -NoProfile -File .\exit1.ps1
C:>echo %ERRORLEVEL%
23

In a PowerShell interactive shell.

PS C:>powershell -NoProfile -Command .\exit1.ps1
PS C:>$LASTEXITCODE
23
PS C:>powershell -NoProfile -File .\exit1.ps1
PS C:>$LASTEXITCODE
23

HOWEVER... Running the .ps1 script inside an existing interactive PowerShell host will exit the host completely.

PS C:>.\exit1.ps1
    <<<poof! gone! outahere!>>>

How can I prevent it from exiting the host shell?

mklement0
  • 382,024
  • 64
  • 607
  • 775
lit
  • 14,456
  • 10
  • 65
  • 119
  • 6
    Patient: Doctor, it hurts when I do "this". Doctor: Then don't *DO* "this"!! You are asking "How can I get the light to not turn off when I turn it off?". :) The `exit` command will exit the current command parser. If the host program you are running is at the top level, the `exit` command will close that parser, leaving nothing else left open. I believe the answer is if you don't want it to exit, stop telling it to exit? – Matthew Feb 20 '20 at 01:46
  • Customer: I brought this car back because it cannot make a right turn. Sales: Then don't make any right turns!! – lit Feb 20 '20 at 12:37

2 Answers2

3

Do not use $host.SetShouldExit(): it is not meant to be called by user code. Instead, it is used internally by PowerShell in response to an exit statement in user code.

Simply use exit 23 directly in your exit1.ps1 script, which will do what you want:

  • When run inside a PowerShell session, the script will set exit code 23 without exiting the PowerShell process as a whole; use $LASTEXITCODE to query it afterwards.

      .\exit.ps1; $LASTEXITCODE # -> 23
    
  • When run via the PowerShell CLI:

  • with -File, the exit code set by the script automatically becomes the PowerShell process' exit code, which the caller can examine; when called from cmd.exe, %ERRORLEVEL% reflects that exit code.

         powershell -File .\exit.ps1
         :: This outputs 23
         echo %ERRORLEVEL%
    
  • with -Command, additional work is needed, because PowerShell then simply maps any nonzero exit code to 1, which causes the specific exit code to be lost; to compensate for that, simply execute exit $LASTEXITCODE as the last statement:

         powershell -Command '.\exit.ps1; exit $LASTEXITCODE'
         :: This outputs 23
         echo %ERRORLEVEL%
    

For more information about how PowerShell sets exit codes, see this answer.


If:

  • you do not control how your script is invoked via the CLI, yet must ensure that the correct exit code is reported even when the script is invoked via -Command,

  • and you're willing to assume the risk of using $host.SetShouldExit(), even though it isn't designed for direct use,

you can try the following:

function ExitWithCode($exitcode) {
  if ([Environment]::CommandLine -match ( # Called via the CLI? (-File or -Command)
    ' .*?\b' + 
    [regex]::Escape([IO.Path]::GetFileNameWithoutExtension($PSCommandPath)) +
    '(?:\.ps1\b| |$)')
  ) {
    # CAVEAT: While this sets the exit code as desired even with -Command,
    #         the process terminates instantly.
    $host.SetShouldExit($exitcode)
  }
  else {
    # Exit normally, which in interactive session exits the script only.
    exit $exitcode
  }
}

ExitWithCode 23

The function looks for the file name of the executing script on the process command line to detect whether the enclosing script is being invoked directly via the CLI, via the automatic $PSCommandPath variable, which contains the script's full path.

If so, the $host.SetShouldExit() call is applied to ensure that the exit code is set as intended even in the case of invocation via -Command.
Note that this amounts to a repurposing of the effectively internal .SetShouldExit() method.
Surprisingly, this repurposing works even if additional commands come after the script call inside the -Command string, but note that this invariably means that the success status of the truly last command - if it isn't the script call - is then effectively ignored.

This approach isn't foolproof[1],but probably works well enough in practice.


[1]

  • There could be false positives, given that only the file name is looked for, without extension (because -Command allows omitting the .ps1 extension of scripts being called).
  • There could be false negatives, if the script is being called via another script or via an alias.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks!. Here are few things I've learnt from trial and error: 1. `PowerShell_ISE.exe` 10.0.19041.1 will always return exit code 1 or 0 - no matter if I use `-File` or `-Command`, while `VS Code` 1.60.2 works well. 2. if I write both `return X` and `exit X` - the return will mess-up the exit code. – itsho Oct 04 '21 at 14:39
  • 1
    @itsho, I don't see that behavior in the ISE with `-File`, and I generally wouldn't expect the host environment to make a difference. Note that `return` is for returning _data_, not _exit codes_. For more information on exit codes, see [this answer](https://stackoverflow.com/a/57468523/45375). For information on `return` vs. `exit`, see [this answer](https://stackoverflow.com/a/67642758/45375). – mklement0 Oct 04 '21 at 15:01
1

How can I prevent it from exiting the host shell?

You can check if the currently running PowerShell process is a child of another PowerShell parent process, and only call $host.SetShouldExit() when that condition is true. For example:

function ExitWithCode($exitcode) {
   # Only exit this host process if it's a child of another PowerShell parent process...
   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$PID" | Select-Object -Property ParentProcessId).ParentProcessId
   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentPID" | Select-Object -Property Name).Name
   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }

   exit $exitcode
}
ExitWithCode 23

Hope this helps.

leeharvey1
  • 1,316
  • 9
  • 14
  • This is looking good. It still does not work for cmd.exe when using `-Command`, but that is probably not a good use anyway. I am guessing that it will need to be `if ('powershell.exe', 'pwsh.exe' -contains $parentProcName)` for future versions. – lit Feb 20 '20 at 22:13