Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Powershell write variable to stdout as running

In my powershell script I have a variable which captures its commands output

$files= dosomething

so running dosomething alone will produce output to the stdout. My question is how do I get that same functionality out of my variable as its running? mostly as a way to see progress.

maybe best to run the command and then somehow capture that to a variable?

UPDATE 1:

here's the command I'm trying to monitor:

$remotefiles = rclone md5sum remote: --dry-run --max-age "$lastrun" --filter-from "P:\scripts\filter-file.txt"

UPDATE 2: running rclone md5sum remote: --dry-run --max-age "$lastrun" --filter-from "P:\scripts\filter-file.txt" directly produces output ...

like image 213
Tony Avatar asked Jan 25 '23 08:01

Tony


2 Answers

The simplest solution is to enclose your assignment in (...), the grouping operator, which passes the result of the assignment through; e.g:

# Enclosing an assignment in (...) passes the value that is assigned through.
PS> ($date = Get-Date)

Friday, May 21, 2021 7:43:48 PM

Note:

  • Use of (...) implies that all command output is captured first, before passing it through; that is, you won't see the passed-through output until after the command has terminated - see the bottom section for a solution.

  • An assignment by default captures only success output (from PowerShell commands) / stdout output (from external commands).

  • If you (also) want to capture error-stream / stderr output, use 2>&1 to merge the error-stream / stderr output into the success stream / stdout (see the conceptual about_Redirection topic); in the case of the external rclone program (for brevity, the actual arguments are omitted and represented with placeholder ...):

# Merge stderr into stdout and capture the combination, and also pass it through.
([string[]] $remotefiles = rclone ... 2>&1)

Note: the [string[]] type constraint ensures that the stderr lines too are captured as strings; by default, PowerShell returns them as [System.Management.Automation.ErrorRecord] instances. If you want pipeline logic, i.e. to only capture a [string] scalar if there's only one output line and use an array only for multiple lines, use
($remotefiles = rclone ... 2>&1 | % ToString) instead.


As Abraham Zinala points out, PowerShell cmdlets (including advanced functions and scripts), support the common -OutVariable parameter, which also allows capturing a cmdlet's success output in addition to outputting it to the success output stream:

# Outputs *and* saves in variable $date.
# Note that the -OutVariable argument must *not* start with "$"
PS> Get-Date -OutVariable date

Friday, May 21, 2021 7:43:48 PM

Note: Using 2>&1 does not cause the -OutVariable to also capture error-stream output, but you can capture errors separately, using the common -ErrorVariable parameter.

Caveat: Surprisingly, the -OutVariable target variable[1]always receives a [System.Collections.ArrayList] instance, which is unusual in two respects:

  • PowerShell generally uses regular array ([object[]]) instances to collect multiple output objects, such as in an assignment.

  • While difference in (read-only) access between an array list and a regular array will rarely matter, the more problematic aspect is that an array list is used even when the cmdlet outputs just ONE object, unlike in assignments; in the case at hand this means:

    • $date = Get-Date stores a [datetime] instance in variable $date
    • whereas Get-Date -OutVariable date stores a single-element array list containing a [datetime] instance in $date.
  • This unexpected behavior is the subject of GitHub issue #3154.


By contrast, Tee-Object's -Variable parameter, as shown in SADIK KUZU's helpful answer, does not exhibit this unexpected behavior; that is, it behaves the same way as an assignment.

However, given that use of Tee-Object is by definition an additional call and therefore requires an extra pipeline segment, you do pay a performance penalty (though that may not matter in practice).


Solution for a long-running (external) command whose output should be passed through as it is being produced:

# Merge stderr into stdout and output in streaming fashion,
# while capturing the (stringified) lines in variable $remoteFiles
rclone ... 2>&1 | % ToString -OutVariable remoteFiles

Note: % is the built-in alias for the ForEach-Object cmdlet; ToString calls the .ToString() method on each object, which is necessary for converting the stderr output lines to strings, because PowerShell by default wraps them in[System.Management.Automation.ErrorRecord] instances; if conversion to strings isn't desired or necessary, replace
% ToString -OutVariable remoteFiles with
Tee-Object -Variable remoteFiles.


[1] The same applies to the other common -*Variable parameters, such as -ErrorVariable.

like image 182
mklement0 Avatar answered Feb 04 '23 06:02

mklement0


Tee-Object saves command output in a file or variable and also sends it down the pipeline.

Get-Process notepad | Tee-Object -Variable proc | Select-Object processname,handles

ProcessName                              Handles
-----------                              -------
notepad                                  43
notepad                                  37
notepad                                  38
notepad                                  38

This example gets a list of the processes running on the computer, saves them to the $proc variable, and pipes them to Select-Object.

like image 32
SADIK KUZU Avatar answered Feb 04 '23 06:02

SADIK KUZU