Search code examples
powershellpipelinecopy-itemforeach-object

How to pipe directly to Copy-Item instead of within a ForEach-Object


Because the -Exclude parameter of Get-ChildItem is not filtering on subfolders when using the -Recurse flag, see among other unable-to-exclude-directory-using-get-childitem-exclude-parameter-in- powershell

But the -Exclude parameter can be used to filter out folders on the root level

I wrote my own recursive function:

function Get-ChildItem-Recurse() {
    [cmdletbinding()]
    Param(
      [parameter(ValueFromPipelineByPropertyName = $true)]
      [alias('FullName')]
      [string[]] $Path,
      [string] $Filter,
      [string[]] $Exclude,
      [string[]] $Include,
      [switch] $Recurse = $true,
      [switch] $File = $false
    )

    Process {
      ForEach ( $P in $Path ) {
        Get-ChildItem -Path $P -Filter $Filter -Include $Include -Exclude $Exclude | ForEach-Object {
        if ( -not ( $File -and $_.PSIsContainer ) ) {
          $_
        }
        if ( $Recurse -and $_.PSIsContainer ) {
          $_ | Get-ChildItem-Recurse -Filter $Filter -Exclude $Exclude -Include $Include -Recurse:$Recurse
        }
      }
    }
  }
}

When I pipe the result to a ForEach-Object to Copy the result to a different destination, all works fine, and the items except those that match the exclude parameter are copied

$source = 'D:\Temp\'
$destination = 'D:\Temp_Copy\'

Get-ChildItem-Recurse -Path $source -Exclude @( '*NotThis*', '*NotThat*' ) | ForEach-Object {
  $_ | Copy-Item -Destination ( "$($destination)$($_.FullName.Substring($source.Length))" ) -Force 
}

When I pipe it directly to the Copy-Item commandlet, I get a null-valued error, because .Substring() is called on $_.FullName that is apparently null

Get-ChildItem-Recurse -Path $source -Exclude @( '*NotThis*', '*NotThat*' ) |
  Copy-Item -Destination ( "$($destination)$($_.FullName.Substring($source.Length))" ) -Force

Because the native commandlet Get-ChildItem does allow me to pipe its result to Copy-Item, I like my own custom function also to be able to do that. But I can't figure out why it's not working.


Solution

  • Use a scriptblock to dynamically bind a piped input value to the parameter:

    Get-ChildItem ... |Copy-Item -Destination { "$($destination)$($_.FullName.Substring($source.Length))" }
    

    The following answer by mklement0 has extensive details on this kind of dynamic binding (retroactively named "delay-bind scriptblocks", or colloquially "pipeline-bound scriptblocks"):
    For PowerShell cmdlets, can I always pass a script block to a string parameter?