开发者

How to use PowerShell copy-item and keep structure

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\开发者_StackOverflowfolderB\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.


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


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.


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


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


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.


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/


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>>


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


$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.


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.


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"


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
}


$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
}


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


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)

How to use PowerShell copy-item and keep structure

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜