3

I'm trying to emulate classes using PS modules as described in this answer. However, it doesn't seem possible to exit from within such an "instance method":

$sb =
{
    function bar
    {
        "bar"
    }

    function quux
    {
        exit
    }

    Export-ModuleMember -Function bar,quux
}
$foo = New-Module $sb -AsCustomObject
$foo.bar()
$foo.quux()

results in

bar
Exception calling "quux" with "0" argument(s): "System error."
At line:17 char:1
+ $foo.quux()
+ ~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ScriptMethodFlowControlException
  1. Why does this happen in the first place and why is the exception so cryptic?
  2. What is the canonical way of doing this correctly? I do want to exit and stop all further execution, not resume control flow. I also need to be able to return an exit code.
Community
  • 1
  • 1
Adrian Frühwirth
  • 42,970
  • 10
  • 60
  • 71

2 Answers2

1

You could throw an exception and use trap to catch it and exit the script:

trap{
    exit
}

$sb =
{
    function bar
    {
        "bar"
    }

    function quux
    {
        throw "Stopping execution"
    }

    Export-ModuleMember -Function bar,quux
}
$foo = New-Module $sb -AsCustomObject
$foo.bar()
$foo.quux()
Martin Brandl
  • 56,134
  • 13
  • 133
  • 172
  • No, I want to stop all execution immediately. – Adrian Frühwirth Sep 26 '16 at 11:59
  • This obviously throws an exception which doesn't allow me to exit "gracefully" (plus it produces output - the exception - which is a no-no), neither does it allow me to exit with a specific exit code. It doesn't really answer why my minimal example throws the exception it does in the first place either. Thanks for trying to help but this just doesn't solve my problem :( – Adrian Frühwirth Sep 26 '16 at 12:16
  • I didn't read your question carefully enough. However I may found a solution for your. – Martin Brandl Sep 26 '16 at 12:30
  • I don't know why but this just resumes control flow after calling `$foo.quux()` for me. – Adrian Frühwirth Sep 26 '16 at 12:37
  • 1
    You probably have to set `$ErrorActionPreference = 'stop'` – Martin Brandl Sep 26 '16 at 12:45
  • @AdrianFrühwirth, I know this is years later, but `trap{ exit }` should indeed cause overall execution to terminate, in response to the _statement_-terminating error `$foo.quux()` generates; it is only `$foo.quux()` _by itself_ that causes execution to continue by default. – mklement0 Mar 31 '20 at 16:22
0

Why does this happen in the first place and why is the exception so cryptic?

The behavior you're seeing appears to be a bug (still present as of PowerShell 7.0; see this GitHub issue):

Calling exit from a script method attached to a [pscustomobject] instance - whether the script method is indirectly attached via New-Module -AsCustomObject or directly via
Add-Member -MemberType ScriptMethod - unexpectedly fails with the cryptic error you saw: instead of exiting, the error is emitted as a statement-terminating error (see below) and execution continues by default.

Methods defined on a PowerShell 5+ class are not affected.

What is the canonical way of doing this correctly? I do want to exit and stop all further execution, not resume control flow. I also need to be able to return an exit code.

Typically, exit is called from the top-level scope of a script file, not from inside a function (whether or not inside a module).

Note that exit either immediately exits the enclosing script, or, if the function is called directly in an interactive session, exists the whole session.

If your quux() method is called from within a script, and you indeed want to exit the enclosing script instantly, the only direct workaround in PowerShell 4- is not to try to emulate a PowerShell class and to define and call the quux function directly.

However, an indirect workaround is to add a minor tweak to the throw approach from Martin Brandl's helpful answer:


The solution is complicated by PowerShell's inconsistent use of terminating errors, which fall into two categories:

  • statement-terminating errors, which (by default) terminate the executing statement only, and then continue execution.

  • script-terminating errors (fatal errors), which terminate execution overall (a script and all its callers, and, in the case of a call to PowerShell's CLI, the PowerShell process as a whole).

This surprising distinction and PowerShell's surprisingly complex error handling in general are described in this GitHub docs issue.

A script method attached to a [pscustomobject] instance, as used in your dynamic-module approach, is only capable of generating a statement-terminating error, not a script-terminating one.

Again, PowerShell 5+ class methods behave differently: they create script-terminating errors.

To achieve overall termination while also reporting an exit code[1], you additionally need to do the following in the caller's scope:

  • catch / trap the statement-terminating error.
  • issue an exit $n statement in response (which in the context of the script scope will succeed), where $n is the desired exit code.

You have two options:

  • Add a trap statement to the caller's scope, which fires for both statement- and script-terminating errors and calls exit $n, where $n is the desired exit code.

    • To do so, the only tweak to Martin's answer is to replace trap { exit } with, say,
      trap { exit 1 } to report exit code 1.

    • Communicating the specific exit code from the throwing function takes extra work - see below.

    • Note that trap is rarely used, but it enables a straightforward solution in your case.

  • Alternatively, use a try / catch statement around the method call or a group of statements that could throw an error, and call exit in the catch handler:
    try { $foo.quux() } catch { exit 1 }


Workaround that sets a specific exit code:

# Create a custom object with methods,
# via a dynamic module.
$foo = New-Module -AsCustomObject {
    function bar
    {
        "bar"
    }
    function quux
    {
      # Throw a statement-terminating error with the desired exit code,
      # to be handled by the `trap` statement in the caller's scope.      
      throw 5
    }
}

# Set up the trap statement for any terminating error.
# Note: Normally, you'd place the `trap` statement at the
#       beginning of the script.
trap {
  # Get the exit code from the most recent error or default to 1.
  # Note: $_ is a *wrapper* error record arund the original error record
  #       created with `throw`; however, the wrapper's .ToString() representation
  #       is the (string representation of) the original object thrown.
  if (-not ($exitCode = $_.ToString() -as [int])) { $exitCode = 1 }
  exit $exitCode
}

$foo.bar()
$foo.quux() # This triggers the trap.

# Getting here means that the script will exit with exit code 0
# (assuming no other terminating error occurs in the remaining code).

[1] If a script automatically terminates due to a script-terminating error, as a class-based solution would generate, its exit code is invariably reported as 1 (and the error message prints), but only if the script was called via PowerShell's CLI (powershell -file ... or powershell -commmand ...); by contrast, inside an interactive PowerShell session $LASTEXITCODE is not set in this case (but the error message prints).
Therefore, even if a PS 5+ solution is an option, you may still want to trap the script-terminating error and explicitly translate it into an exit call with a specific exit code, as shown next.

mklement0
  • 382,024
  • 64
  • 607
  • 775