The main difference between the three commands is the use of the -Wait
argument and parentheses
To build on Mathias R. Jessen's helpful comment:
It is primarily the use of Start-Process
's -Wait
switch that is required:
Without -Wait
, Start-Process
runs asynchronously.
Without -PassThru
, Start-Process
produces no output.
While -PassThru
makes Start-Process
output a System.Diagnostics.Process
instance representing the newly launched process, unless -Wait
is also present that instance's .ExitCode
property has no value yet, because the launched process typically hasn't exited yet.
Additionally, parentheses ((...)
) are required too, because Start-Process
emits the System.Diagnostics.Process
instance representing the newly launched process to the pipeline (as received by ForEach-Object
) right away, and then waits for the process to exit. By using (...)
, the grouping operator, you're forcing a wait for Start-Process
itself to exit, at which point the Process
' instance .ExitCode
property is available, thanks to -Wait
.
In general, wrapping a command in (...)
forces collecting its output in full, up front - which includes waiting for it to exit - before the results are passed through the pipeline (as opposed to the streaming (one-by-on output) behavior that is the default, which happens while the command is still running).
Therefore, the following works - but see the bottom section for a simpler alternative:
# Note: With (...), you could also pipe the output to ForEach-Object, as in
# your question, but given that there's by definition only *one*
# output object, that is unnecessary.
(
Start-Process -PassThru -Wait -FilePath 'msiexec' -ArgumentList '/i C:\Users\Downloads\Everything-1.4.1.1015.x64.msi -quiet'
).ExitCode
It follows from the above that using two separate statements would work too (given that any statement runs to completion before executing the next):
$process = Start-Process -PassThru -Wait -FilePath 'msiexec' -ArgumentList '/i C:\Users\Downloads\Everything-1.4.1.1015.x64.msi -quiet'
$process.ExitCode
msiexec.exe
is unusual in that:
it is a GUI(-subsystem) executable (as opposed to a console(-subsystem) executable), which therefore - even when invoked directly - runs asynchronously.
yet it reports a meaningful process exit code that the caller may be interested in, requiring the caller to wait for its exit (termination) in order to determine this exit code.
As an aside: For invoking console applications, Start-Process
is not the right tool in general, except in unusual scenarios - see this answer.
An simpler alternative to using msiexec
with Start-Process -PassThru -Wait
is to use direct invocation via cmd /c
, which ensures both (a) synchronous invocation and (b) that PowerShell reflects msiexec
's exit code in its automatic $LASTEXITCODE
variable:
cmd /c 'msiexec /i C:\Users\Downloads\Everything-1.4.1.1015.x64.msi -quiet'
$LASTEXITCODE # output misexec's exit code
Note: If the msiexec
command line needs to include PowerShell variable values, pass an expandable (double-quoted) string ("..."
) to cmd /c
instead and - as with verbatim (single-quoted) string ('...'
) strings - use embedded double quoting around embedded arguments, as necessary.