WhiskerBiscuit
WhiskerBiscuit

Reputation: 5157

How can I pipe multiple files into Copy-Item and keep the directory structure?

I have a very large directory located at D:\Stuff and I want to create a copy of it at D:\CopyStuff, but I only want to take files with a certain extension as well as keep the folder structure.

Getting the files I want seems simple enough:

$from = "D:\stuff"
$to = "D:\CopyStuff"   
$files = Get-ChildItem -Recurse -Path $from -Include *.config, *.txt, *.ini 

However, copying the files and keeping the structure is a bit more challenging. I could use a for-loop, but that seems against the very nature of Powershell. Here https://stackoverflow.com/a/25780745/782880, it suggests to do it this way:

Get-ChildItem -Path $sourceDir | Copy-Item -Destination $targetDir -Recurse -Container

But that copies files to D:\CopyStuff with no folders, much less my original structure. What am I doing wrong? I'm using Powershell 5.

Upvotes: 1

Views: 3533

Answers (3)

Esperento57
Esperento57

Reputation: 17472

try this :

$Source="C:\temp\test1"
$Dest="C:\temp\test2"
$EnableExt=".config", ".txt" , ".ini"

Get-ChildItem $Source -Recurse | % {

    $NewPath=$_.FullName.Replace($Source, $Dest)

    if ($_.psiscontainer)
    {
        New-Item -Path $NewPath -ItemType Directory -Force
    }
    elseif ($_.Extension -in $EnableExt)
    {
        Copy-Item $_.FullName $NewPath -Force
    }
}

Upvotes: 2

mklement0
mklement0

Reputation: 437833

Note: The solution below creates analogous target folders only for those source folders that contain files matching the -Include filter, not for all source folders.


You can get away with a single-pipeline solution by combining Get-ChildItem -Name with delay-bind script blocks:

$from = 'D:\stuff'
$to = 'D:\CopyStuff'

Get-ChildItem -Name -Recurse -LiteralPath $from -Include *.config, *.txt, *.ini |
  Copy-Item `
    -LiteralPath { Join-Path $from $_ } `
    -Destination { New-Item -Type Directory -Force (Split-Path (Join-Path $to $_)) }
  • -Name emits paths relative to the input directory as strings.

  • Delay-bind script block { Join-Path $from $_ } builds the full input file name from each relative input path.

  • Delay-bind script block { New-Item -Type Directory -Force (Split-Path (Join-Path $to $_)) } builds the full path of the target directory from the target root path and the relative input path and creates that directory on demand, using a preexisting one if present (-Force).

Upvotes: 0

Max
Max

Reputation: 771

First of all, Copy-Item can do it on its own like:

$fromFolder = "C:\Temp\Source"
$toFolder = "C:\Temp\Dest"
Copy-Item -Path $fromFolder -Destination $toFolder -Recurse -Filter *.txt

But, you may not like the result: it will make folder "Source" inside the "Dest" folder, and then copy the structure. I reckon, you need the same files/folders from inside "Source" folder to be copy to the "Dest" folder. Well, it's a bit more complex, but here it is:

$fromFolder = "C:\Temp\Source"
$toFolder = "C:\Temp\Dest"

Get-ChildItem -Path $fromFolder -Directory -Recurse | Select-Object FullName, @{N="NewPath";E={$_.FullName.Replace($fromFolder, $toFolder)}} | ForEach-Object { New-Item -Path $_.NewPath -ItemType "Directory" }
Get-ChildItem -Path $fromFolder -Include "*.txt" -Recurse | Select-Object FullName, @{N="NewPath";E={$_.FullName.Replace($fromFolder, $toFolder)}} | ForEach-Object { Copy-Item -Path $_.FullName -Destination $_.NewPath }

It copies folder structure first, then files.

NB! I do strongly recommend to use absolute paths only. Otherwise, the Replace method may give unexpected results.

Upvotes: 1

Related Questions