Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Copy to Multiple Destinations with PowerShell

Posted on February 22, 2022February 22, 2022

In honor of today, 2/22/2022, I thought I'd share a PowerShell function that allows you to copy files to multiple destinations. If you look at help for Copy-Item, which you should, you'll see that the Destination parameter does not take an array. That's ok. I can fix that. However, I have a disclaimer that you should consider this a proof-of-concept and not production-ready code.

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!

The first step was to create a copy of the Copy-Item cmdlet. I used the Copy-Command function from the PSScriptTools module, which you can install from the PowerShell Gallery. I used the function to create a "wrapper" function based on Copy-Item. Copy-Command copies the original parameters. I edited the Destination parameter to accept an array of locations.

[Parameter(
    Mandatory,
    Position = 1,
    ValueFromPipelineByPropertyName,
    HelpMessage = "Specify a file system destination."
)]
[ValidateScript({Test-Path $_})]
[string[]]$Destination,

Because the parameters of my new function are identical to Copy-Item, I can use PSBoundParameters to build a Copy-Item command. However, I need to remove the Destination parameter since Copy-Item isn't written to support an array.

  [void]($PSBoundParameters.Remove("Destination"))

This line goes in the Process scriptblock because I defined Destination to take pipeline input by value.

The concept behind my new function is quite simple. Run a Copy-Item command foreach destination. But, if I'm copying many files, I want this to run quickly and more or less in parallel. That's when I realized I could use Start-ThreadJob. If you don't have the ThreadJob module installed, you can get it from the PowerShell Gallery. Unlike Start-Job, which spins up a new runspace, Start-Threadjob creates a job in a separate thread within the local process. This tends to be faster.

The tricky part was figuring out how to pass parameters. I could have splatted parameter values.

start-threadjob {param($splat,$location) Copy-item @splat -destination $location} -arguments @($psboundparameters,$location)

And I will keep this concept in mind for future projects. Instead, I decided to recreate the parameters from PSBoundParameters. If I can create a command string, I can turn that into a scriptblock, which I can then pass to Start-ThreadJob.

$PSBoundParameters.GetEnumerator() | ForEach-Object -Begin { $p = "Copy-Item" } -Process {
    if ($_.value -eq $True) {
        $p += " -$($_.key)"
    }
    else {
        $p += " -$($_.key) $($_.value)"
    }
} -End {
    $p += " -destination $location"
}

In the ForEach-Object Begin parameter, I'm initializing a string with the Copy-Item command. Then for each enumerated PSBoundParameter, I reconstruct the parameter name and value. The "gotcha" is handling switches like -Recurse or -WhatIf. So if the value is equal to True, I'll assume the key is a switch parameter. Usually, I abstain from explicit boolean comparisons in an If statement, but in this case, I am checking the value of $_.value and not merely if it exists. When I'm finished, $p will look like "Copy-Item -Verbose -WhatIf -Path C:\Scripts\WMIDiag.exe -destination d:\temp". I can turn that into a scriptblock and run it as a ThreadJob.

$sb = [scriptblock]::Create($p)
#WhatIf code ommitted here
$j = Start-ThreadJob $sb

The approach I am taking, depending on how you are running the copy command, could create a thread job for every file to be copied. There is a bit of overhead, so this may not perform well for many small files, but I'd take it when copying large files.

If I trust that I'll never make a mistake, I could stop. But, I might want to use -Passthru from Copy-Item. Or need to see if there are any errors. This means I will have to get the job results. In my functions' Begin block, I'll initialize a generic list.

$jobs = [System.Collections.Generic.List[object]]::new()

I'll add each job to the list created in the Process script block.

 $jobs.add($j)

In the End scriptblock, assuming there are jobs, I'll wait for the last of them to complete, get the results, and remove the job.

If ($jobs) {
    Write-Verbose "$((Get-Date).TimeOfDay) [END    ] Processing $($jobs.count) thread jobs"
    $jobs | Wait-Job | Receive-Job
    $jobs | Remove-Job
}

Now, I can copy files to 2 (or more) directories with one command!

copy files to multiple locations.

Want to try?

#requires -version 5.1
#requires -module ThreadJob

Function Copy-ItemToMultiDestination {
    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'Medium'
    )]
    [alias("cp2")]
    Param(
        [Parameter(
            Mandatory,
            Position = 0,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            HelpMessage = "Specify a file system source path."
        )]
        [ValidateScript({ Test-Path $_ })]
        [string[]]$Path,
        [Parameter(
            Mandatory,
            Position = 1,
            ValueFromPipelineByPropertyName,
            HelpMessage = "Specify a file system destination."
        )]
        [ValidateScript({ Test-Path $_ })]
        [string[]]$Destination,
        [switch]$Container,
        [switch]$Force,
        [string]$Filter,
        [string[]]$Include,
        [string[]]$Exclude,
        [switch]$Recurse,
        [switch]$PassThru,
        [Parameter(ValueFromPipelineByPropertyName)]
        [pscredential]
        [System.Management.Automation.CredentialAttribute()]$Credential
    )

    Begin {

        Write-Verbose "$((Get-Date).TimeOfDay) [BEGIN  ] Starting $($MyInvocation.Mycommand)"
        #initialize a list for thread jobs
        $jobs = [System.Collections.Generic.List[object]]::new()

    } #begin

    Process {
        [void]($PSBoundParameters.Remove("Destination"))

        foreach ($location in $Destination) {
            Write-Verbose "$((Get-Date).TimeOfDay) [PROCESS] Creating copy job"
            $PSBoundParameters.GetEnumerator() | ForEach-Object -Begin { $p = "Copy-Item" } -Process {
                if ($_.value -eq $True) {
                    $p += " -$($_.key)"
                }
                else {
                    $p += " -$($_.key) $($_.value)"
                }
            } -End {
                $p += " -destination $location"
            }

            Write-Verbose "$((Get-Date).TimeOfDay) [PROCESS] $p"
            $sb = [scriptblock]::Create($p)

            #Start-ThreadJob doesn't support -whatif
            if ($PSCmdlet.ShouldProcess($p)) {
                $j = Start-ThreadJob $sb
                #add each job to the list
                $jobs.add($j)
            }
        } #foreach location

    } #process

    End {
        If ($jobs) {
            Write-Verbose "$((Get-Date).TimeOfDay) [END    ] Processing $($jobs.count) thread jobs"
            $jobs | Wait-Job | Receive-Job
            $jobs | Remove-Job
        }

        Write-Verbose "$((Get-Date).TimeOfDay) [END    ] Ending $($MyInvocation.Mycommand)"

    } #end

} #end function Copy-ItemToMultiDestination

Remember, this is a proof of concept. Although my mind is already whirling on other commands I could "update" using some of these techniques. Have fun.


Behind the PowerShell Pipeline

Share this:

  • Share on X (Opens in new window) X
  • Share on Facebook (Opens in new window) Facebook
  • Share on Mastodon (Opens in new window) Mastodon
  • Share on LinkedIn (Opens in new window) LinkedIn
  • Share on Reddit (Opens in new window) Reddit
  • Print (Opens in new window) Print
  • Email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

2 thoughts on “Copy to Multiple Destinations with PowerShell”

  1. Pingback: Copy to Multiple Destinations with PowerShell - The Lonely Administrator - Syndicated Blogs - IDERA Community
  2. Pingback: PowerShell SnippetRace 16/17-2022 | PowerShell Usergroup Austria

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2026 The Lonely Administrator | Powered by SuperbThemes!
%d