58

I have a directory structure that looks like this:

C:\folderA\folderB\folderC\client1\f1\files
C:\folderA\folderB\folderC\client1\f2\files
C:\folderA\folderB\folderC\client2\f1\files
C:\folderA\folderB\folderC\client2\f2\files
C:\folderA\folderB\folderC\client3\f1\files
C:\folderA\folderB\folderC\client4\f2\files

I want to copy the content of the f1 folders in C:\tmp\ to get this

C:\tmp\client1\f1\files
C:\tmp\client2\f1\files
C:\tmp\client3\f1\files

I tried this:

Copy-Item -recur -path: "*/f1/" -destination: C:\tmp\

But it copies the contents without copying the structure correctly.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sylvain
  • 19,099
  • 23
  • 96
  • 145

16 Answers16

48

In PowerShell version 3.0 and newer this is simply done this way:

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

Reference: Get-ChildItem

Laurel
  • 5,965
  • 14
  • 31
  • 57
Thomas M
  • 585
  • 4
  • 2
  • 2
    What does the `-container` flag do? – Cullub Jul 12 '17 at 14:39
  • 5
    @Cullub Regarding the `-container` flag, see [What is the meaning of Powershell's Copy-Item's -container argument?](https://stackoverflow.com/questions/129088/what-is-the-meaning-of-powershells-copy-items-container-argument) – browly Aug 07 '17 at 21:53
  • 30
    Note that in this example, `$targetDir` must already exist. In my testing I found that when `$targetDir` does not exist, the files will be copied but the directory structure will be flattened. – bwerks Oct 29 '17 at 19:16
  • 4
    bwerks is correct. No idea why this received any votes. – WhiskerBiscuit Jan 05 '19 at 03:07
  • 8
    this is not working, i just tryed and the structure is just flattened as said in bwerks comment – Maroine Abdellah Aug 01 '19 at 07:39
  • @bwerks nailed it, this doesn't even answer the question asked. – Jim Feb 09 '21 at 18:02
30

PowerShell:

$sourceDir = 'c:\folderA\folderB\folderC\client1\f1'
$targetDir = ' c:\tmp\'

Get-ChildItem $sourceDir -filter "*" -recurse | `
    foreach{
        $targetFile = $targetDir + $_.FullName.SubString($sourceDir.Length);
        New-Item -ItemType File -Path $targetFile -Force;
        Copy-Item $_.FullName -destination $targetFile
    }

Note:

  • The -filter "*" does not do anything. It is just here to illustrate if you want to copy some specific files. E.g. all *.config files.
  • Still have to call a New-Item with -Force to actually create the folder structure.
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sentient
  • 2,185
  • 2
  • 19
  • 20
  • I don't understand, is this powershell? Or a .bat script? Sorry I am new to NT – Gabriel Fair May 14 '13 at 01:13
  • 2
    FWIW, this fails if you put a trailing directory separator on `$sourceDir`. – Tim Martin Jun 04 '13 at 10:41
  • Works for PS 2.0 and PS 4.0 ? Mabye any differences in cmdlets. – Kiquenet Apr 21 '14 at 12:01
  • 1
    The code above tries to create files of every folder. A fast fix would be to exclude the folders like so: `foreach{ if( $_.Attributes -neq 'Directory' ){ $targetFile =... } }` – LosManos Jun 04 '14 at 14:20
  • @Sentient It keeps the folder structure - I was a bit unclear. My code excludes making _files out of folders_; it still iterates the files and folders are created as necessary. Come to think of it - empty folders are not created then. – LosManos Jun 10 '14 at 08:19
  • IMHO this should be the answer – DanielV Mar 02 '20 at 11:04
  • I'm running PS version 5.1 and I need to change the parameter `-filter` to `-Include` to get it to work. Otherwise this is the only answer that works perfectly. – GMSL Mar 09 '21 at 10:52
  • I think this should be the accepted answer but can be improved on (semicolons?!). Like @LosManos I added logic so New-Item wasn't creating directories that already existed. Hope it helps someone. `if (!(Test-Path $CurrentTargetDir)) { New-Item -ItemType 'Directory' -Path $CurrentTargetDir -Force Copy-Item -Path $SourceFile -Destination $TargetFile -Force } else { Copy-Item -Path $SourceFile -Destination $TargetFile -Force }` – klabarge Jul 12 '22 at 14:15
18

I have been digging around and found a lot of solutions to this issue, all being some alteration, not just a straight copy-item command. Granted, some of these questions predate PowerShell 3.0 so the answers are not wrong, but using PowerShell 3.0 I was finally able to accomplish this using the -Container switch for Copy-Item:

Copy-Item $from $to -Recurse -Container

This was the test I ran, no errors and the destination folder represented the same folder structure:

New-Item -ItemType dir -Name test_copy
New-Item -ItemType dir -Name test_copy\folder1
New-Item -ItemType file -Name test_copy\folder1\test.txt

# NOTE: with no \ at the end of the destination, the file
#       is created in the root of the destination, and
#       it does not create the folder1 container
#Copy-Item D:\tmp\test_copy\* D:\tmp\test_copy2 -Recurse -Container

# If the destination does not exist, this created the
# matching folder structure and file with no errors
Copy-Item D:\tmp\test_copy\* D:\tmp\test_copy2\ -Recurse -Container
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
workabyte
  • 3,496
  • 2
  • 27
  • 35
  • I noticed that if you want to copy one directory to another directory, then you need to end both paths with a trailing '\'. Otherwise it copies the files to the root of that target directory. Strange home MS does things. – b01 Jul 20 '18 at 20:34
  • you mean the content of one dir to the content of another? – workabyte Jul 20 '18 at 20:36
  • 1
    Lifesaver! Thanks – dim Apr 02 '20 at 05:54
  • Perfect answer! – Babu James Nov 23 '20 at 21:14
  • 4
    I tried this and if the destination container does not exist, this will only transfer the files without folders which contradicts what the documentation says for the [Copy-Item](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/copy-item?view=powershell-5.1#example-3--copy-directory-and-contents-to-a-new-directory). The only solution I have found is to create the target folder before performing the Copy-Item cmdlet. – Jim Feb 09 '21 at 17:57
  • I too found that if the destination folder structure did not exist, it would only create the first subdirectory that didn't already exist and put all the files in there. If ran agian, it would create the next level and again dump all the files in there. Very strange. But create the destination directory structure first and it works well, like this: Get-ChildItem -Path $(srcDirectory) -Directory -Recurse | ForEach-Object { New-Item -ItemType Directory -Path $(dstDirectory) -Name $_ } Copy-Item -Path $(srcDirectory)/* -Destination $(dstDirectory) -Recurse -Container -Force – Kris Jun 02 '21 at 05:27
13

Use xcopy or robocopy, both of which have been designed for exactly that purpose. Assuming your paths are only filesystem paths, of course.

Joey
  • 344,408
  • 85
  • 689
  • 683
  • 5
    I know but I'd like to do that in powershell because that's a part of a bigger script and I'm going to pipe the output of copy (using -PassThru) to other commands. – Sylvain Mar 25 '11 at 15:40
  • you can use `cmd /c xcopy` to use xcopy from powershell – Zachary Yates Apr 13 '14 at 01:46
  • @ZacharyYates: You can use `xcopy` to use xcopy from PowerShell. It's not a `cmd` built-in, so there is no need to use `cmd /c` here. – Joey Apr 13 '14 at 08:30
  • few answers below speak to a -container switch, worth taking a look at. copy-item -container will do just that without all the extra work native to powershell. – workabyte Aug 20 '14 at 17:40
  • @ZacharyYates: unfortunately, xcopy fails where the full path of a file exceeds 256 chars and stops (i.e. no subsequent files are copied). – Mark Roworth Apr 25 '20 at 12:37
7

If you want to copy a folder structure correctly with PowerShell, do it like so:

$sourceDir = 'C:\source_directory'
$targetDir = 'C:\target_directory'

Get-ChildItem $sourceDir -Recurse | % {
   $dest = $targetDir + $_.FullName.SubString($sourceDir.Length)

   If (!($dest.Contains('.')) -and !(Test-Path $dest))
   {
        mkdir $dest
   }

   Copy-Item $_.FullName -Destination $dest -Force
}

This accounts for creating directories and just copying the files. Of course you'll need to modify the Contains() call above if your folders contain periods or add a filter if you want to search for "f1" as you mentioned.

Adam Caviness
  • 3,424
  • 33
  • 37
  • This seems the most useful PS answer as it takes care of creating folders that do not exist, which is a common requirement and many other answers are ignoring. To be more explicit on the destination folder you can use: $destFile = $processingPath + $_.FullName.SubString($importPath.Length) $destFolder = Split-Path $destFile if (!(Test-Path $destFolder)) { mkdir $destFolder } – rexall May 17 '21 at 15:52
4

The Container switch (to Copy-Item) maintain the folder structure. Enjoy.

testing>> tree

Folder PATH listing
Volume serial number is 12D3-1A3F
C:.
├───client1
│   ├───f1
│   │   └───files
│   └───f2
│       └───files
├───client2
│   ├───f1
│   │   └───files
│   └───f2
│       └───files
├───client3
│   └───f1
│       └───files
└───client4
    └───f2
        └───files

testing>> ls client* | % {$subdir = (Join-Path $_.fullname f1); $dest = (Join-Path temp ($_
.name +"\f1")); if(test-path ($subdir)){ Copy-Item $subdir $dest -recurse -container -force}}

testing>> tree

Folder PATH listing
Volume serial number is 12D3-1A3F
C:.
├───client1
│   ├───f1
│   │   └───files
│   └───f2
│       └───files
├───client2
│   ├───f1
│   │   └───files
│   └───f2
│       └───files
├───client3
│   └───f1
│       └───files
├───client4
│   └───f2
│       └───files
└───temp
    ├───client1
    │   └───f1
    │       └───files
    ├───client2
    │   └───f1
    │       └───files
    └───client3
        └───f1
            └───files

testing>>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
mjsr
  • 7,410
  • 18
  • 57
  • 83
  • 4
    though your answer is correct its not clear, may want to add a very simple example call at the top to help clarify. I read the answer but it seemed convoluted so i moved on looking for another solution (coming back i see that its the same solution i found but not as clear as it could be). – workabyte Aug 20 '14 at 15:41
4

Since I spent time finding a more straightforward way that wouldn't involve piping or inline scripting:

Copy-Item -Path $sourceDir -Destination $destinationDir -Recurse -Container -Verbose

One can also supply a -Filter argument if a condition for the copy is demanded.

Works with PowerShell 5.1.18362.752.

Source: https://devblogs.microsoft.com/scripting/powertip-use-powershell-to-copy-items-and-retain-folder-structure/

Rafael Costa
  • 248
  • 3
  • 13
2

I needed to do the same thing, so I found this command:

XCopy souce_path destination_path /E /C /I /F /R /Y

And in your case:

XCopy c:\folderA\folderB\folderC c:\tmp /E /C /I /F /R /Y

And if you need to exclude some items, create text file with a list of exclusions. E.g.:

Create text file 'exclude.txt' on drive C:\ and add this in it:

.svn
.git

And your copy command will look like this now:

XCopy c:\folderA\folderB\folderC c:\tmp /EXCLUDE:c:\exclude.txt /E /C /I /F /R /Y
mikhail-t
  • 4,103
  • 7
  • 36
  • 56
  • This was what I was looking for thanks. I also used the /w and /S to copy sub directories and to skip empty folders – Gabriel Fair May 16 '13 at 05:30
  • While this works its kind of clunky that you need to name the specific folders/files to include. Providing a pattern match of some kind would make it better. – Jim Feb 09 '21 at 18:45
1

Here is a slight variation of the main question where the source file is relative and only a subset of files need to be copied with folder structure. This scenario can happen if you run git diff on a repo and have a subset of changes. E.g.

src/folder1/folder2/change1.txt      
src/folder3/change2.txt   
->   
c:\temp

Code

# omitting foreach($file in $files)
$file = 'src/folder1/folder2/change1.txt'
$tempPath = 'c:\temp'

# Build destination path
$destination = Join-Path $tempPath -ChildPath (Split-Path $file)

# Copy and ensure destination exists
Copy-Item -Path $file -Destination (New-Item -Path $destination -Type Directory -Force)

Results in
c:\temp\src\folder1\folder2\change1.txt
c:\temp\src\folder3\change2.txt

The main techniques in script are evaluating folder structure and ensuring it is created with New-Item command

pgk
  • 2,025
  • 1
  • 14
  • 15
0

The below worked for me

@echo off
    setlocal enableextensions disabledelayedexpansion

    set "target=e:\backup"

    for /f "usebackq delims=" %%a in ("TextFile.txt") do (
        md "%target%%%~pa" 2>nul
        copy /y "%%a" "%target%%%~pa"
    )

For each line (file) inside the list, create, under the target folder, the same path indicated in the read line (%%~pa is the path of the element referenced by %%a). Then, copy the read file to the target folder.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
gladiator
  • 1,249
  • 8
  • 23
0
$sourceDir = 'C:\source_directory'
$targetDir = 'C:\target_directory'

Get-ChildItem $sourceDir -Recurse | % {
   $dest = $targetDir + $_.FullName.SubString($sourceDir.Length)

   If (!($dest.Contains('.')) -and !(Test-Path $dest))
   {
        mkdir $dest
   }

   Copy-Item $_.FullName -Destination $dest -Force
}

This code works, especially if you use Get-ChildItem in connection with a filter/where-object method.

However, there is one minor error with this code: By the "IF" statement and the following code "mkdir" the folder on the $targetDir will be created...afterwards the command "copy-item" creates the same folder within the folder just created by "mkdir" command.


Here is an example of how it worked for me with a "Where-Object" function. You can simply omit the IF statement.

$Sourcefolder= "C:\temp1"
$Targetfolder= "C:\temp2"


$query = Get-ChildItem $Sourcefolder -Recurse | Where-Object {$_.LastWriteTime -gt [datetime]::Now.AddDays(-1)}
$query | % {
    $dest = $Targetfolder + $_.FullName.SubString($Sourcefolder.Length)
    Copy-Item $_.FullName -Destination $dest -Force
}

Make sure that the paths are not indicated with an "\" at the end.

0

Forget Copy-Item, it never does what you expect, Invoke-Expression invokes a commandline command and allows for using powershell variables. I use the good-old xcopy command with /E so directories are copied (also empty ones) and /Y to suppress prompting and confirm to overwrite.

$sourceDir = "c:\source\*"
$targetDir = "c:\destination\"
Invoke-Expression "xcopy $sourceDir $targetDir /E /Y"
klaas-jan
  • 17
  • 3
  • Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes – Tyler2P Dec 02 '20 at 19:34
  • This is the solution I chose and it worked. If you don't want to copy empty folders, replace `/E` with `/S` – fall Jan 26 '21 at 08:08
  • The author's original question also requires to filter parent folders that contain specifically named child folders, which this does not address. – Jim Feb 09 '21 at 18:43
0

Generally, Copy-Item will do this for you as long as the target folder already exists. The documentation does not match what I have validated through testing. A trailing slash in the destination path does not resolve this. When copying a folder hierarchy, you must use -Recurse. The Container parameter is defaulted to true, you can leave that off.

In your question you are filtering the list of files to copy based on a named subfolder. While this works for the Path property of Copy-Item, the problem is that the target folder ( client* ) do not exist, which is why the files are placed in the root of the target folder. The majority of the other answers do not address this scenario specifically and thus do not answer the question asked. To achieve your solution this will take two steps:

  1. Select the files you want to copy
  2. Copy the selected files to the destination folder while ensuring the destination folder exists
$source = 'C:\FolderA\FolderB\FolderC'
$dest = 'C:\tmp'
# Only need the full name
$files = Get-ChildItem -Path $source -Recurse -File | Where-Object { $_.FullName -match '^.+\\client\d\\f1\\.+\..+$' } | ForEach-Object { $_.FullName }

# Iterate through the list copying files
foreach( $file in $files ) {
    $destFile = $file.Replace( $source, $dest )
    $destFolder = Split-Path -Path $destFile -Parent

    # Make sure the destination folder for the file exists
    if ( -not ( Test-Path -Path $destFolder ) ) {
        New-Item -Path ( Split-Path -Path $destFolder -Parent ) -Name ( Split-Path -Path $destFolder -Leaf ) -ItemType Directory -Force
    }
    
    Copy-Item -Path $file -Destination $destFolder
}
Jim
  • 692
  • 7
  • 15
0
$source ="c:\"
$destination="c:\tmp"
sl $source
md $destination
ls "." -rec -Filter *.zip | %{
$subfolder=($_.FullName)
$pathtrim=($subfolder -split [regex]::escape([system.io.path])::directoryseperatorchar)[-3] # update -2,-3 here to match output, we need 'client1\f1\files' here 
echo $pathtrim
$finaldest=Join-Path -Path $destination -ChildPath $pathtrim
cp $source $finaldest -Verbose
}
Himan
  • 125
  • 3
  • 12
0

I have created the following function that will copy all files from a directory keep the folder structure. You can then specify the start index where the folder structure should start.

    function Copy-FileKeepPath {

    param (
        $filter,$FileToCopy,$des,$startIndex
    )
    Get-ChildItem -Path $FileToCopy -Filter $filter -Recurse -File | ForEach-Object {
        $fileName = $_.FullName
        #Remove the first part to ignore from the path.
        $newdes=Join-Path -Path $des -ChildPath $fileName.Substring($startIndex)
        $folder=Split-Path -Path $newdes -Parent
        $err=0
    
        #check if folder exists"
        $void=Get-Item $folder -ErrorVariable err  -ErrorAction SilentlyContinue
        if($err.Count -ne 0){
          #create when it doesn't
          $void=New-Item -Path $folder -ItemType Directory -Force -Verbose
        }

        $void=Copy-Item -Path $fileName -destination $newdes -Recurse -Container -Force -Verbose
    }
}

Use it as follows:

Copy-FileKeepPath -FileToCopy 'C:\folderA\folderB\folderC\client1\f1\' -des "C:\tmp" -filter * -startIndex "C:\folderA\folderB\folderC\".Length
Darrel K.
  • 1,611
  • 18
  • 28
-3

I had a similar requirement where I wanted to share only library files (different platform and build types). I was about to write my PowerShell script, that I realized it can be done less than a minute using the "SyncBackFree" tool. It worked as expected.

1> Create Profile

2> Select Source and Destination Folder

3> Click "Choose sub-directories and files

4> Click left side "Change Filter"

5> Add file extension ("*.lib") and "*\" (for folder structure)

enter image description here

userom
  • 403
  • 5
  • 8