I would like to call external commands from PowerShell Core such as Git, Python or other proprietary executables, but I want the call to throw when the exit code is non-zero while respecting PowerShell's ErrorAction (set via $ErrorActionPreference or ErrorAction argument). This normally is not the case, e.g. the following doesn't throw, but I would like it to:
cmd /c "exit 1" git foo What is the best / recommended way of doing that considering at least the following criteria:
- [exception support]: generates an exception when the command fails or returns a non-zero exit code
- [consistency]: consistent syntax accross various use cases (see use cases below)
- [simplicity]: call in a most straightforward way, i.e. one line with as few extra characters as possible compared to the original direct command
- [output]: output of command can either be printed to console in real-time while the command is running or captured into a variable
- [spaces support]: support for spaces in path of command or in arguments
- [variables support]: support for commands and arguments being stored in variables
- [universality]: support for arbitrary arguments of the command
- [interactivity support]: interactive commands remain interactive (e.g. console menus controlled via arrow keys)
- [security]: safe to use in cloud production environments (as Invoke-Expression is often criticized for this)
- [no dependency]: no external dependencies / modules
- [ps core]: use in PowerShell Core
- [kiss-dry]: respects common software development principles such as: KISS, DRY
- [explicit argument] (OPTIONAL): explicit argument names when using functions (as this is required by our style guide, an exception for this might be possible though)
I reviewed many other posts already, but neither of these did tick all my criteria. I hope I didn't oversee anything. My focus is on usability, so here are some use cases, I would like to have supported with optimal user experience.
Use cases
An ideal function for me would let me do arbitrary calls as for example:
$ErrorActionPreference = 'Stop' # USE CASE: simple command Invoke-Call -Command "git version" # USE CASE: unknown command should fail with an exception Invoke-Call -Command "not-a-command" # USE CASE: failing commands should fail with an exception Invoke-Call -Command "git foo" # USE CASE: store output in variable $output = Invoke-Call -Command "git version" Write-Host $output # USE CASE: command with spaces Invoke-Call -Command "'C:\Program Files\Git\cmd\git.exe' version" # USE CASE: command with real-time output and non-zero exit code should show all output in real-time, then fail with an exception Invoke-Call -Command "ping -n 2 localhost & exit 1" # USE CASE: complex command with argument including spaces Invoke-Call -Command "cmd /c 'ping -n 2 localhost & exit 1'" # USE CASE: arbitrary command in a variable including arguments $commandWithArguments = "'C:\Program Files\Git\cmd\git.exe' version --build-options" Invoke-Call -Command $commandWithArguments # USE CASE: explicit error action should override $ErrorActionPreference Invoke-Call -Command "not-a-command" -ErrorAction Ignore Attempt using Invoke-Expression
function Invoke-ExpressionWithErrorHandling { param ( [Parameter(Mandatory)] [string] $Command ) Invoke-Expression -Command $Command if ($LASTEXITCODE -ne 0) { Write-Error "Expression exited with exit code $LASTEXITCODE" } } Use case testing:
PS D:\> $ErrorActionPreference = 'Stop' PS D:\> PS D:\> # USE CASE: simple command PS D:\> Invoke-ExpressionWithErrorHandling -Command "git version" git version 2.46.0.windows.1 PS D:\> PS D:\> # USE CASE: unknown command should fail with an exception PS D:\> Invoke-ExpressionWithErrorHandling -Command "not-a-command" Invoke-Expression: Line | 8 | Invoke-Expression -Command $Command | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | The term 'not-a-command' is not recognized as a name of a cmdlet, function, script file, or executable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. PS D:\> PS D:\> # USE CASE: failing commands should fail with an exception PS D:\> Invoke-ExpressionWithErrorHandling -Command "git foo" git: 'foo' is not a git command. See 'git --help'. The most similar commands are flow hook Invoke-ExpressionWithErrorHandling: Expression exited with exit code 1 PS D:\> PS D:\> # USE CASE: store output in variable PS D:\> $output = Invoke-ExpressionWithErrorHandling -Command "git version" PS D:\> Write-Host $output git version 2.46.0.windows.1 PS D:\> PS D:\> # USE CASE: command with spaces PS D:\> Invoke-ExpressionWithErrorHandling -Command "`"C:\Program Files\Git\cmd\git.exe`" version" Invoke-Expression: Line | 8 | Invoke-Expression -Command $Command | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | Unexpected token 'version' in expression or statement. As you can see this fails when passing commands with full paths containing spaces. I could not find any variant of quotes to make this work.
Missed criteria:
- [spaces support]
More elaboration required for criteria:
- [security]