Search code examples
powershell

Start-Process, Invoke-Command or?


Using the program got your back or GYB. I run the following command

Start-Process -FilePath 'C:\Gyb\gyb.exe' -ArgumentList @("--email <Email Address>", "--action backup", "--local-folder $GYBfolder", "--service-account", "--batch-size 4") -Wait

The issue is that when the process is done my script does not complete.

$GYBfolder = $GYBfolder.Replace('"', "")
$output = [PSCustomObject]@{
    Name   = $SourceGYB
    Folder = $GYBfolder
}

$filename = "C:\reports\" + $SourceGYB.Split("@")[0] + "_Backup.csv"

$output | Export-Csv $filename -NoTypeInformation | Format-Table text-align=left -AutoSize
Return $filename

For some reason the script stops right before the return. I am curious to know if I should be using a different command to run GYB? Any thoughts on why the script does not process the return?


Solution

  • There's great information in the comments, but let me attempt a systematic overview:

    • To synchronously execute external console applications and capture their output, call them directly (C:\Gyb\gyb.exe ... or & 'C:\Gyb\gyb.exe' ...), do not use Start-Process - see this answer.

      • Only if gyb.exe were a GUI application would you need Start-Process -Wait in order to execute it synchronously.

        • A simple, but non-obvious shortcut is to pipe the invocation to another command, such as Out-Null, which also forces PowerShell to wait (e.g. gyb.exe | Out-Null) - see below.
      • When Start-Process is appropriate, the most robust way to pass all arguments is as a single string encoding all arguments, with appropriate embedded "..." quoting, as needed; this is unfortunate, but required as a workaround for a long-standing bug: see this answer.

    • Invoke-Command's primary purpose is to invoke commands remotely; while it can be used locally, there's rarely a good reason to do so, as &, the call operator is both more concise and more efficient - see this answer.

    • When you use an array to pass arguments to an external application, each element must contain just one argument, where parameter names and their values are considered distinct arguments; e.g., you must use @(--'action', 'backup', ...) rather than
      @('--action backup', ...)

    Therefore, use the following to run your command synchronously:

    • If gyb.exe is a console application:
    # Note: Enclosing @(...) is optional
    $argList = '--email',  $emailAddress, '--action', 'backup', '--local-folder', $GYBfolder, '--service-account', '--batch-size',  4
    
    # Note: Stdout and stderr output will print to the current console, unless captured.
    & 'C:\Gyb\gyb.exe' $argList
    
    • If gyb.exe is a GUI application, which necessitates use of Start-Process -Wait (a here-string is used, because it makes embedded quoting easier):
    # Note: A GUI application typically has no stdout or stderr output, and
    #       Start-Process never returns the application's *output*, though
    #       you can ask to have a *process object* returned with -PassThru.
    Start-Process -Wait 'C:\Gyb\gyb.exe' @"
    --email $emailAddress --action backup --local-folder "$GYBfolder" --service-account --batch-size 4
    @"
    

    The shortcut mentioned above - piping to another command in order to force waiting for a GUI application to exit - despite being obscure, has two advantages:

    • Normal argument-passing syntax can be used.
    • The automatic $LASTEXITCODE variable is set to the external program's process exit code, which does not happen with Start-Process. While GUI applications rarely report meaningful exit codes, some do, notably msiexec.
    # Pipe to | Out-Null to force waiting (argument list shortened).
    # $LASTEXITCODE will reflect gyb.exe's exit code.
    # Note: In the rare event that the target GUI application explicitly
    #       attaches to the caller's console and produces output there,
    #       pipe to `Write-Output` instead, and possibly apply 2>&1 to 
    #       the application call so as to also capture std*err* output.
    & 'C:\Gyb\gyb.exe' --email $emailAddress --action backup | Out-Null
    

    Note: If the above unexpectedly does not run synchronously, the implication is that gyb.exe itself launches another, asynchronous operation. There is no generic solution for that, and an application-specific one would require you to know the internals of the application and would be nontrivial.


    A note re argument passing with direct / &-based invocation:

    • Passing an array as-is to an external program essentially performs splatting implicitly, without the need to use @argList[1]. That is, it passes each array element as its own argument.

    • By contrast, if you were to pass $argList to a PowerShell command, it would be passed as a single, array-valued argument, so @argList would indeed be necessary in order to pass the elements as separate, positional arguments. However, the more typical form of splatting used with PowerShell commands is to use a hashtable, which allows named arguments to be passed (parameter name-value pairs; e.g., to pass a value to a PowerShell command's
      -LiteralPath parameter:
      $argHash = @{ LiteralPath = $somePath; ... }; Set-Content @argHash


    [1] $argList and @argList are largely identical in this context, but, strangely, @argList, honors use of --%, the stop-parsing symbol operator, even though it only makes sense in a literally specified argument list.