Search code examples
powershellconsoleoutput

In Powershell Core is it possible to get native progress output to console host window when running from within a script block?


I would like to get progress-output from native programs like restic backup or yt-dlp in powershell, even though I'm running the program from within a block like this. Is it possible for the inner script block to output to the console like when executing it directly? I have control over the executeWithPidLock implementation.

executeWithPidLock {
  .\restic.exe -r temp-repo backup 'C:\Tools\AlwaysMouseWheel'
}

I know I can run with new window like this and get console progress output, which is not that bad, but not really what I want, as I don't want window popping up when running from Task scheduler:

measure-command {
    $process = Start-Process -FilePath "./restic" -ArgumentList @("-r", "temp-repo","backup",'C:\Tools\') -PassThru -Wait
    $process.WaitForExit()
}

EDIT (to add some extra needed info, based on first answer):

An option is to manually pipe to out-host, as correctly suggested by mklement0.

Measure-Command {  yt-dlp "https://www.youtube.com/watch?v=NS5NKykuQno" | out-host}

However this will not behave like directly in console for progress information. For 'restic' it completely skips the progress information. For 'yt-dlp' it prints each %-line, where the direct execution deletes each old line, like this.

[download]  33.3% of  242.55MiB at    6.74MiB/s ETA 00:23
[download]  33.4% of  242.55MiB at    2.00MiB/s ETA 01:20
[download]  33.4% of  242.55MiB at    2.70MiB/s ETA 00:59
[download]  33.4% of  242.55MiB at    3.75MiB/s ETA 00:43

If it is not possible in Powershell 7.3.9 to execute a native command and get unmolested progress info, that is a valid answer (though a link to some doc would be greatly appreciated).


Solution

  • Some general advice:

    • To synchronously execute console applications or batch files in the current console window, call them directly (c:\path\to\some.exe ... or & $exePath ...). This also allows you to capture their output directly and to later query their exit code via $LASTEXITCODE. Do not use Start-Process (or the System.Diagnostics.Process API it is based on) - see this answer. GitHub docs issue #6239 provides guidance on when use of Start-Process is and isn't appropriate.

    Heeding this advice also solves your specific problem:

    • PowerShell streams the output from external (console) applications line by line to the pipeline, so you can see or process lines as they're being emitted, even from inside a script block ({ ... }).

    • A script block passed to Measure-Command is a special case, as the latter explicitly suppresses output from the commands in the script block.

    You can work around this - for display-only output - with Out-Host, as the following example shows, which also demonstrates the streaming nature of the output:

    Measure-Command {
      cmd /c 'echo 1 & timeout 2 >NUL & echo err >&2 & echo 2' *>&1 | Out-Host
    }
    

    1 will print right away, even though the command hasn't finished executing yet.

    Note the use of redirection *>&1 to ensure that stderr output is output too:

    • Redirected stderr lines print in red and - in Windows PowerShell - the first stderr line prints as if it were a PowerShell error (no longer a problem in PowerShell (Core) 7+).

      • Separately, there is a bug that affects PowerShell (Core) 7.3.x up to at least 7.3.10: in regular console windows (conhost.exe) (as opposed to in Windows Terminal), the ANSI/VT escape-code based coloring is rendered incorrectly in a pipeline involving a native (external program) - see GitHub issue #18771, which was inappropriately closed.
    • To avoid that, insert a ForEach-Object ToString pipeline segment before Out-Host.

    If you invoked the script block directly (typically via &, the call operator), you wouldn't need Out-Host and the external program's output would be streamed to PowerShell's success output stream

    -- EDIT (by OP)

    So if you control your function yourself, you can do this:

    function executeBlockWithCall([ScriptBlock]$Block) {
        Write-Host "Do stuff before..."
        # Invoke-Command $Block # this also works to get progress- output to main console
        & $Block
        Write-Host "...and after"
    }
    executeBlockWithCall { yt-dlp "https://www.youtube.com/watch?v=NS5NKykuQno" }
    

    and the progress output will stream unmolested to your console window.