Public/Invoke-Executable.ps1

function Invoke-Executable {
    <#
    .SYNOPSIS
        Runs an executable, waits for it to finish and returns its exit code and output.

    .DESCRIPTION
        The Invoke-Executable function starts an executable with System.Diagnostics.Process, waits for it
        to exit and returns an object with the exit code. When standard output or standard error is
        redirected (the default), the text written to them is returned as well. Redirected streams are
        read while the process runs, so a process that writes a lot of output cannot block.

    .PARAMETER FilePath
        The file name or path of the executable to run, including the extension.

    .PARAMETER Arguments
        The command-line arguments passed to the executable, as a single string.

    .PARAMETER RedirectStandardOutput
        Whether standard output is captured and returned in the StandardOutput property. Default is $true.
        Must be $false when UseShellExecute is $true.

    .PARAMETER RedirectStandardError
        Whether standard error is captured and returned in the StandardError property. Default is $true.
        Must be $false when UseShellExecute is $true.

    .PARAMETER CreateNoWindow
        Whether the process is started without a new window. Default is $true.

    .PARAMETER UseShellExecute
        Whether the operating system shell starts the process. Default is $false.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        ExitCode, StandardOutput and StandardError. The output properties are $null when the stream
        was not redirected.

    .EXAMPLE
        Invoke-Executable -FilePath "setup.exe" -Arguments "/silent /norestart"

        Runs setup.exe with silent installation parameters and returns its exit code and output.

    .EXAMPLE
        $result = Invoke-Executable -FilePath "C:\Tools\mytool.exe" -Arguments "-config test.json" -CreateNoWindow $false -RedirectStandardOutput $false -RedirectStandardError $false
        if ($result.ExitCode -ne 0) { throw "mytool failed with exit code $($result.ExitCode)" }

        Runs mytool.exe in a visible window and checks the exit code.

    .NOTES
        When RedirectStandardOutput or RedirectStandardError is $true, UseShellExecute must be $false.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [parameter(Mandatory = $true, HelpMessage = "Specify the file name or path of the executable to be invoked, including the extension.")]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,

        [parameter(Mandatory = $false, HelpMessage = "Specify arguments that will be passed to the executable.")]
        [ValidateNotNull()]
        [string]$Arguments,

        [parameter(Mandatory = $false, HelpMessage = "Specify whether standard output should be redirected.")]
        [bool]$RedirectStandardOutput = $true,

        [parameter(Mandatory = $false, HelpMessage = "Specify whether standard error output should be redirected.")]
        [bool]$RedirectStandardError = $true,

        [parameter(Mandatory = $false, HelpMessage = "Specify whether to start the process without a new window.")]
        [bool]$CreateNoWindow = $true,

        [parameter(Mandatory = $false, HelpMessage = "Specify whether to use the operating system shell to start the process.")]
        [bool]$UseShellExecute = $false
    )
    $TelemetryArgs = @{
        ModuleName    = $MyInvocation.MyCommand.Module.Name
        ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
        CommandName   = $MyInvocation.MyCommand.Name
        ExecutionID   = [guid]::NewGuid().ToString()
    }
    Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer
    try {
        if ($UseShellExecute -and ($RedirectStandardOutput -or $RedirectStandardError)) {
            throw 'RedirectStandardOutput and RedirectStandardError must be $false when UseShellExecute is $true.'
        }

        $ProcessStartInfoObject = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo'
        $ProcessStartInfoObject.FileName = $FilePath
        $ProcessStartInfoObject.CreateNoWindow = $CreateNoWindow
        $ProcessStartInfoObject.UseShellExecute = $UseShellExecute
        $ProcessStartInfoObject.RedirectStandardOutput = $RedirectStandardOutput
        $ProcessStartInfoObject.RedirectStandardError = $RedirectStandardError
        if (-not [string]::IsNullOrEmpty($Arguments)) {
            $ProcessStartInfoObject.Arguments = $Arguments
        }

        $Process = New-Object -TypeName 'System.Diagnostics.Process'
        $Process.StartInfo = $ProcessStartInfoObject
        try {
            try {
                $null = $Process.Start()
            }
            catch {
                throw "$($MyInvocation.MyCommand): Failed to start '$FilePath': $($_.Exception.Message)"
            }

            # Read the redirected streams asynchronously; waiting for exit first can deadlock when a
            # process fills the output buffer.
            $StandardOutputTask = $null
            $StandardErrorTask = $null
            if ($RedirectStandardOutput) {
                $StandardOutputTask = $Process.StandardOutput.ReadToEndAsync()
            }
            if ($RedirectStandardError) {
                $StandardErrorTask = $Process.StandardError.ReadToEndAsync()
            }
            $Process.WaitForExit()

            [PSCustomObject]@{
                ExitCode       = $Process.ExitCode
                StandardOutput = $(if ($StandardOutputTask) { $StandardOutputTask.Result } else { $null })
                StandardError  = $(if ($StandardErrorTask) { $StandardErrorTask.Result } else { $null })
            }
        }
        finally {
            $Process.Dispose()
        }
        Invoke-TelemetryCollection @TelemetryArgs -Stage End
    }
    catch {
        Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
        throw
    }
}