如何使用PowerShell的copy-item命令并保留目录结构

58

我有一个目录结构,看起来像这样:

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

我想要复制C:\tmp\下f1文件夹的内容,以得到如下结果

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

我尝试了这个:

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

但它复制的内容没有正确地复制结构。

16个回答

48

在PowerShell 3.0及更高版本中,可以简单地使用以下方法完成此操作:

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

参考: Get-ChildItem


2
-container 标志是用来做什么的? - Cullub
5
关于-container标志,请参见什么是Powershell的Copy-Item的-container参数的含义? - browly
30
请注意,在本示例中,$targetDir 必须已经存在。在我的测试中,我发现如果 $targetDir 不存在,文件将被复制,但目录结构将被展平。 - bwerks
4
bwerks是正确的。不知道为什么这个回答会得到任何投票。 - WhiskerBiscuit
8
无效,我刚刚尝试了一下,结果就像bwerks的评论所说的那样,结构被压扁了。 - Maroine Abdellah
@bwerks做得很好,这甚至没有回答所问的问题。 - Jim

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
    }

注意:

  • -filter“*”不起作用,它只是用来说明您想要复制特定文件(例如所有*.config文件)时应该如何操作。
  • 仍然需要使用-Force参数调用New-Item命令才能实际创建文件夹结构。

我不明白,这是PowerShell吗?还是.bat脚本?抱歉,我对NT还很陌生。 - Gabriel Fair
2
顺便提一下,如果在 $sourceDir 后面添加尾部目录分隔符,则此操作将失败。 - Tim Martin
1
上面的代码试图创建每个文件夹的文件。一个快速的解决方法是排除这些文件夹,如下所示:foreach{ if( $_.Attributes -neq 'Directory' ){ $targetFile =... } } - LosManos
我正在运行PS 5.1版本,我需要将参数-filter更改为-Include才能使其正常工作。否则,这是唯一完全正常工作的答案。 - GMSL
我认为这应该是被接受的答案,但可以改进(分号?!)。像@LosManos一样,我添加了逻辑,以便New-Item不会创建已经存在的目录。希望对某人有所帮助。`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
显示剩余3条评论

18

我一直在寻找解决这个问题的方法,所有的解决方案都需要进行某些修改,而不仅仅是直接复制项目的命令。尽管如此,其中一些问题是在 PowerShell 3.0 之前提出的,因此这些答案并不是错误的,但是使用 PowerShell 3.0,我最终能够通过使用 -Container 开关来完成此操作:

Copy-Item $from $to -Recurse -Container
这是我运行的测试,没有错误,并且目标文件夹表示了相同的文件夹结构:
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

我注意到如果你想将一个目录复制到另一个目录,那么你需要在两个路径末尾加上反斜杠''。否则它会将文件复制到目标目录的根目录下。微软的做法有点奇怪。 - b01
你的意思是将一个目录的内容复制到另一个目录吗? - workabyte
1
救命稻草!谢谢。 - dim
完美的答案! - Babu James
4
如果目标容器不存在,我尝试使用这个方法,它只会传输文件而不包含文件夹,这与Copy-Item文档中所说的相矛盾。唯一的解决办法是在执行Copy-Item cmdlet之前创建目标文件夹。 - Jim
我也发现,如果目标文件夹结构不存在,它只会创建第一个子目录,然后把所有文件放在那里。如果再次运行,它会创建下一级并再次将所有文件转储到那里。非常奇怪。但是首先创建目标目录结构,就可以很好地工作,像这样: 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

13
使用 xcopyrobocopy,它们都是专门设计用于此目的的。当然,假设您的路径仅为文件系统路径。

5
我明白,但我想在PowerShell中完成这个操作,因为它是一个更大脚本的一部分,我将使用复制命令(使用-PassThru参数)的输出来管道传输到其他命令。 - Sylvain
你可以使用 cmd /c xcopy 命令在 Powershell 中使用 xcopy。 - Zachary Yates
@ZacharyYatesпјҡдҪ еҸҜд»ҘеңЁPowerShellдёӯдҪҝз”Ёxcopyе‘Ҫд»ӨгҖӮе®ғдёҚжҳҜcmdзҡ„еҶ…зҪ®е‘Ҫд»ӨпјҢеӣ жӯӨиҝҷйҮҢдёҚйңҖиҰҒдҪҝз”Ёcmd /cгҖӮ - Joey
@joey 不知道这个 - 太棒了! - Zachary Yates
一些下面的答案提到了一个-container开关,值得一看。copy-item -container会执行此操作,而不需要PowerShell本地所有额外工作。 - workabyte
@ZacharyYates:不幸的是,当文件的完整路径超过256个字符时,xcopy会失败并停止(即不会复制后续文件)。 - Mark Roworth

7
如果您想使用PowerShell正确地复制文件夹结构,请按如下方式操作:
$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
}

这涉及到创建目录并仅复制文件。当然,如果您的文件夹包含句点,您需要修改上面的Contains()调用,或者如果您想搜索“f1”,则添加过滤器。

这似乎是最有用的PS答案,因为它可以处理不存在的文件夹,这是一个常见的需求,许多其他答案都忽略了。要更明确地指定目标文件夹,您可以使用:$destFile = $processingPath + $_.FullName.SubString($importPath.Length) $destFolder = Split-Path $destFile如果目标文件夹不存在,则执行以下操作: if (!(Test-Path $destFolder)) { mkdir $destFolder } - rexall

5

1
这似乎是之前由@workabyte提供的相同解决方案。 - derekbaker783
这比其他解决方案更清晰。 - undefined

4
< p > 容器开关(用于Copy-Item)维护文件夹结构。享受它吧。

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

5
虽然您的回答是正确的,但表述不够清晰。建议在顶部添加一个非常简单的示例来帮助澄清。我读了这个答案,但它似乎很混乱,所以我继续寻找另一个解决方案(后来回来看到它是我找到的同一个解决方案,但并不像它本可以那样清晰明了)。 - workabyte

2

我需要做同样的事情,所以我找到了这个命令:

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

在您的情况下:

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

如果您需要排除某些项目,请创建一个包含排除列表的文本文件。例如:

在 C 盘中创建文本文件“exclude.txt”,并将以下内容添加到其中:

.svn
.git

现在你的复制命令应该是这样的:

XCopy c:\folderA\folderB\folderC c:\tmp /EXCLUDE:c:\exclude.txt /E /C /I /F /R /Y

这正是我所需要的,谢谢。我还使用了/w和/S来复制子目录并跳过空文件夹。 - Gabriel Fair
虽然这个方法可以运行,但需要指定特定的文件夹/文件才能包含在内,有些笨重。提供某种模式匹配会使它更好。 - Jim

2

这里是主要问题的一个小变化,源文件是相对的,只需要复制一部分文件并保留文件夹结构。如果您在存储库上运行git diff并且有一部分更改,则可能会出现此情况。例如:

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

代码

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

结果为
c:\temp\src\folder1\folder2\change1.txt
c:\temp\src\folder3\change2.txt

脚本中的主要技术是评估文件夹结构并确保使用New-Item命令创建它。


0
通常情况下,只要目标文件夹已经存在,Copy-Item 命令就可以为您完成此操作。但是,文档中的内容与我通过测试验证的结果不符。在目标路径中添加尾随斜杠并不能解决这个问题。当复制文件夹层次结构时,必须使用 -Recurse 参数。Container 参数默认为 true,因此您可以省略它。
在您的问题中,您正在根据命名子文件夹筛选要复制的文件列表。虽然这对于 Copy-Item 的 Path 属性有效,但问题在于目标文件夹(client*)不存在,这就是为什么文件被放置在目标文件夹的根目录中的原因。大多数其他答案没有明确解决这种情况,因此也没有回答所提出的问题。要实现您的解决方案,需要分两步进行:
  1. 选择要复制的文件
  2. 将所选文件复制到目标文件夹,并确保目标文件夹存在
$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
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接