3

I have this powershell scrip where I want to copy files from our production server to our test server, and only have it copy files which are newer or changed. The script itself has many more functions but this copying is one of those. I'm using the following XCopy command for that:

  ... some other scriptingcode
  try {
      xCopy $source $destination /f /e /r /k /y /d
  }
  catch {
      Write-Host "issue: $PSItem" -ForegroundColor Yellow -BackgroundColor Red
  }
  ... some more other scriptingcode

When I run the XCopy command directly from the powershell prompt it shows it's progress, the files being copied, while it's running, but when I run it from the script itself, it doesn't output anything. And just adding -Verbose at the end just gives me a script error.

I've tried the following:

  $output = $(xCopy $source $destination /f /e /r /k /y /d) -join "`r\`n"
  Write-Host $output

But that only gives me the output after it's already been copied, and isn't a problem when no, 1 or 2 files are copied, but not really helpfull when you have a 100 or 1.000 files to copy. FYI, the -join "'r'n" is because the output is an array when more than one line is output, so I had to join the lines into one string which could be shown by Write-Host.

I have tried to google for a solution, but I guess my google skills aren't good enough to get me any results to this problem. So, is there a way to have XCopy output while it's doing it, just like directly from the powershell commandline?

SuperDre
  • 157
  • 10
  • 1
    Normally `xcopy` shouldn't behave differently when called from the console or from a script. Do you redirect/pipe output of your script? In this case output might be delayed as many native executables buffer output when redirected. – zett42 Oct 25 '21 at 17:02
  • 1
    As an aside: `try` / `catch` has no effect on calls to external programs such as `xcopy.exe`. Check `$LASTEXITCODE` for containing a nonzero value to infer failure. – mklement0 Oct 25 '21 at 18:00

3 Answers3

4

You shouldn't see a problem with your script, because by default PowerShell passes stdout and stderr output from external programs such as xcopy.exe directly through to the host (console), without interfering with the output or its timing - irrespective of whether you execute the program interactively or from a script.

Similarly, if you send stdout (success) output through the pipeline , PowerShell streams it, i.e. sends each output line as it is being received (conceivably, a given program may itself buffer lines before emitting them when not printing to the console, but that doesn't seem to be the case for xcopy.exe).

The simplest way to print an external program's output to the console in streaming fashion while also capturing its output is to use the Tee-Object cmdlet:

# Prints the output to the console as it is being received
# while also capturing it in variable $output
xcopy $source $destination /f /e /r /k /y /d | Tee-Object -Variable output

Afterwards, $output contains the stdout output as an array of lines (or a single string, if there happened to be just one output line).

To also capture stderr output, append redirection 2>&1 to the external-program call; stderr lines are captured as System.Management.Automation.ErrorRecord instances, as if they were PowerShell errors; to convert all array elements to strings afterwards, use $output = [string[]] $output.

Character-encoding note: When PowerShell is involved in handling external program output (in the pipeline, capturing in a variable, saving to a file), the output is invariably decoded into .NET strings first, based on the encoding stored in [Console]::OutputEncoding, which may situationally have to be modified to match the specific encoding used by a given external program - see this answer for more information.

If you want to perform transformations or apply additional formatting to the lines being received from the external program, consider Cpt.Whale's helpful solution based on ForEach-Object.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 2
    I did not know `Tee-Object` lets you specify a variable, I thought it only split to files. TIL! – codewario Oct 25 '21 at 18:03
  • I tried the `Tee-Object` as provided, but it didn't show anything directly to the screen, only when I had a breakpoint right after it and I stopped the script when it hit the breakpoint. But if I remove the breakpoint, nothing is shown. I do run the scripts directly from Windows PowerShell ISE (x86), so maybe there lies some problem with the not showing of the output directly. – SuperDre Oct 25 '21 at 18:13
  • Glad to hear it, @BendertheGreatest. Another option would be (though it doesn't offer much of an advantage, except in PS Core if you want to capture _stderr_ lines merged with `2>&1` directly as _strings_): `xcopy $source $destination /f /e /r /k /y /d | Out-String -Stream -OutVariable output` – mklement0 Oct 25 '21 at 18:24
  • @SuperDre, there must be something unusual about your setup - I don't see the issue, neither in the 32-bit nor in the 64-bit version of the ISE. As an aside: The ISE is obsolescent, and I suggest migrating to Visual Studio Code (see next comment). You can try to append `| Out-Host` to see if it helps in your situation, but I have no explanation for your symptom: `xcopy $source $destination /f /e /r /k /y /d | Tee-Object -Variable output | Out-Host` – mklement0 Oct 25 '21 at 18:38
  • 2
    As an aside: The PowerShell ISE is [no longer actively developed](https://docs.microsoft.com/en-us/powershell/scripting/components/ise/introducing-the-windows-powershell-ise#support) and [there are reasons not to use it](https://stackoverflow.com/a/57134096/45375) (bottom section), notably not being able to run PowerShell (Core) 6+. The actively developed, cross-platform editor that offers the best PowerShell development experience is [Visual Studio Code](https://code.visualstudio.com/) with its [PowerShell extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.PowerShell). – mklement0 Oct 25 '21 at 18:38
  • @mklement0 thanx for suggesting VSC, never occured to me to use it for this as I normally don't use VSC, still mostly use VS2019 or even VS6 (because if VB6). – SuperDre Oct 28 '21 at 14:07
3

For xcopy, you can both output to the host and capture the output with a foreach:

try {
  $output = xcopy $src $dst /f /e /r /k /y /d | 
    Foreach {
      # write line to console
      $_ | Write-Host -ForegroundColor Yellow -BackgroundColor Red
    
      # add line to $output
      $_
    }
}

Note that xcopy is not a powershell command, so it's not able to do some things like use the default -verbose or -ErrorAction flags. It also does not throw terminating errors, so Try/Catch will not work unless you set your $ErrorActionPreference correctly.

Cpt.Whale
  • 4,784
  • 1
  • 10
  • 16
  • Damn you this is a really clever use of ForEach-Object :D – codewario Oct 25 '21 at 17:23
  • One thing to watch out for is if this approach is used with PowerShell objects instead of strings/output from external commands; `Write-Host` will `ToString()` proper objects first whereas `Out-Host` does not. `Out-Host` may be desired if colored output isn't necessary, or else additional processing will need to be done to get a suitable output when non-string objects are in play. – codewario Oct 25 '21 at 17:41
  • Also, `ErrorAction` AFAIK *never* triggers on non-zero exit codes. You would have to check that yourself and either `throw`/`Write-Error` accordingly (in this case the try is meaningless as OP can just handle the failure case in the current scope). – codewario Oct 25 '21 at 17:44
  • As Bender says, that's really a cleaver way of fixing my problem. This is exactly what I needed, although at the moment Bender the Greatest is also an excellent solution as I don't need the output for further analysis. For now I'll use Bender's solution, but I'll keep your solution commented out at the same spot. The `Try/Catch` was something that was left over from my try with `Copy-Item`. – SuperDre Oct 25 '21 at 17:55
  • @BendertheGreatest :P - I only did a quick test with `xcopy`, but setting the error preference does let you run the catch block on an error. Or did you mean just the `-ErrorAction` flag? I think I could have picked a less confusing example, but it was only meant to help show `xcopy` isn't going to behave like a posh cmdlet. – Cpt.Whale Oct 25 '21 at 18:56
  • I... have to raise an eyebrow on that one. At least with Windows PowerShell I've never had a script inherently throw an error when `$ErrorActionPreference = 'Stop'` is set and something exits with a non-zero exit code. In fact I had to address an issue with one of my setup scripts a few weeks ago surrounding this, where a command was not completing and its success was not checked. Unless this is something new in PS Core I haven't tried yet, and there is a mechanism for setting what the appropriate exit codes are I don't see this being a particularly useful feature if it does work this way now. – codewario Oct 25 '21 at 19:06
  • I just tried this in PS Core: `try{ ping w -n 1 } catch { 'i failed' }` the catch block did not run. I set `$ErrorActionPreference = 'Stop'` before running this. Note that the exit code of ping here is `-1`. – codewario Oct 25 '21 at 19:10
1

This is a side-effect of using the sub-expression operator $() here (note you would get the same effect using the group-expression operator () as well). The reason this happens is because $() and () function like parentheses in mathematics, and order of operations (technically order of precedence in computing) dictates that inner expressions be worked out before outer expressions. So anything inside the parentheses must complete first before it can be processed by the outer code.

In your case here, the "outer code" is what displays information to the console, hence, why the inner command has to complete before it gets displayed.


If you don't need to evaluate $output later on, the simplest and most performant approach is to pipe your command output directly to Out-Host instead (you probably also want to redirect error output as well):

# 2>&1 will redirect the error stream (external command STDERR gets written here)
# to the success stream (external command STDOUT gets written here)
# The success stream is what gets passed down the pipeline
xCopy $source $destination /f /e /r /k /y /d 2>&1 | Out-Host

However, if you need to assign $output to a variable at the same time for further processing, see @Cpt.Whale's solution involving the clever use of ForEach-Object. There are some edge caveats to their approach but does allow you to display the output and either stage the output in another variable for later processing or process the lines of output as they are read in. @mklement0's answer also shows how to use Tee-Object to simultaneously output to a variable and the success stream.

codewario
  • 19,553
  • 20
  • 90
  • 159
  • thanx, the Out-Host is also an excellent solution as at the moment I don't need the output for later use. Both your and @Cpt.Whale's solution would not have been something I would have thought about, and the `Out-Host` also never showed up in my google results, haha.. – SuperDre Oct 25 '21 at 17:57
  • 2
    Most of the time you *don't* want to use `Out-Host` as there are certain implications of using it also. For starters, unlike `Write-Host` which writes to the *information stream*, `Out-Host` outputs directly to the console making any redirection impossible short of using `Start-Transcript` for logging. But in this case you should be fine. – codewario Oct 25 '21 at 18:02