linuxpowershell-corepowershell-v6.0

How do I create function that runs a command?


So what I'm trying to do is recreate a similar function as I would use in Bash, but in Powershell:

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "FAILED: $*"; }

The part that I'm most interested at the moment is the try() function here. Essentially what it does is allow me to wrap a command with this function and let it manage the exit code. The effect is something like this:

try doSomething -args

If doSomething exits with a non-zero, it will output the command to stderr and stop the script from executing.

I realize that Powershell has an error action that can be used to abort scripts, but it seems that it only applies to commandlets. I need something that I can use on anything throughout the script. I'm also looking to avoid tons of verbose try/catch logic crudding up the script, hence the desire for something elegant like try/yell/die. This way, I can write the handling in this function alone and just use it to call anything that I want handled.

I found $MyInvocation and figured that this might be the way in, but I can't seem to find a way to actually execute it from within the function. For example:

function run() {
    $MyInvocation # ?? what do??
}

run doSomething -args

I think I can figure out the rest on my own, I just don't quite know how to write this wrapper function. Any ideas?

Update

So I did something kind of cheesy and I substringed the command and did an Invoke-Expression on what was left and it seems to work. It feels super hacky, so I'm still open to ideas:

function attempt() {
    $thisCommand = $MyInvocation.Line.Trim()
    Write-Output $thisCommand
    Invoke-Expression $thisCommand.Substring(8)
    if($LASTEXITCODE -ne 0) { 
        throw "Command failed $thisCommand" 
        exit 111
    }
}

attempt doSomething -args

Solution

  • If you want a function to run an arbitrary command and throw an error if that command fails you could do something like this:

    function Test-Command {
        try {
            $cmd, $params = $args
            $params = @($params)
            $global:LastExitCode = 0
            $output = & $cmd @params 2>&1
            if ($global:LastExitCode -ne 0) {
                throw $output
            }
            $output
        } catch {
            throw $_
        }
    }
    

    Breakdown:

    $cmd, $params = $args takes the automatic variable $args (an array of the arguments passed to the function) and assigns its first element to the variable $cmd and the rest to the variable $params.

    $params = @($params) ensures that $params contains an array (required for the next step), even if the variable was empty or held just a single value.

    & $cmd @params invokes the command using the call operator & while splatting the parameters. Do NOT use Invoke-Expression.

    The redirection operator 2>&1 merges error output with normal output, so that both output streams are captured in the variable $output.

    If $cmd is a PowerShell cmdlet errors will throw an exception, which is caught by the try statement. The rest of the code in the try block will then be skipped. Note, however, that not all errors thrown by PowerShell cmdlets are automatically terminating errors (see for instance "An Introduction to Error Handling in PowerShell" on the Scripting Guy blog). To turn non-terminating errors into terminating ones you need to set $ErrorActionPreference = 'Stop' (and reset it to its original value after you're done).

    If $cmd is an external command errors will not throw exceptions, but the automatic variable $LastExitCode is updated with the exit code of the command. A command returning a non-zero exit code will trigger the if condition and cause a custom exception to be thrown, using the command output as the exception message. That exception is then also caught by the try statement. The rest of the code in the try block is skipped again.

    $global:LastExitCode = 0 resets the variable $LastExitCode before each run. That is necessary because only external commands return an exit code, whereas PowerShell cmdlets do not. Since $LastExitCode preserves the exit code of the external command last run in the current session, not resetting the variable would fudge the status detection of a PowerShell cmdlet run after an external command.

    The last line in the try block, which echoes the captured command output, is only reached if the command neither throws an exception nor returns a non-zero exit code.

    Any caught exception is handled in the catch block, which simply passes the exception to the caller of the function. Instead of throwing you could also output the error and exit, of course.