PowerShell中的Copy-Item排除列表似乎无法正常工作

80

我有以下 PowerShell 脚本段落:

$source = 'd:\t1\*'
$dest = 'd:\t2'
$exclude = @('*.pdb','*.config')
Copy-Item $source $dest -Recurse -Force -Exclude $exclude

以下代码能够将t1文件夹下的所有文件和文件夹复制到t2,但是它只会排除"root"/"first-level"文件夹中的排除列表,而不会排除子文件夹中的排除列表。

如何使其在所有文件夹中排除排除列表?

12个回答

107
我认为最好的方法是使用Get-ChildItem并将其输入Copy-Item命令中进行管道连接。
我发现这个方法可行:
$source = 'd:\t1'
$dest = 'd:\t2'
$exclude = @('*.pdb','*.config')
Get-ChildItem $source -Recurse -Exclude $exclude | Copy-Item -Destination {Join-Path $dest $_.FullName.Substring($source.length)}
基本上,这里发生的事情是逐个检查有效文件,然后将它们复制到新路径。结尾处的 'Join-Path' 语句是为了在复制文件时保留目录。该部分将目标目录与源路径后面的目录连接起来。
我从这里得到了灵感,然后对其进行了一些修改,以使其对此示例起作用。
希望它能够正常运行!

1
那正是我要发布的解决方案。:) 我发现对于 Select-String、Copy-Item 和其他一些命令,-Include 和 -Exclude 不起作用。但对于 Get-ChildItem (dir) 命令来说,它确实可以正常工作。 - JasonMArcher
如果我没记错的话,如果你查看那些 cmdlets 的帮助文件,它们会说明这些参数不能按预期工作。选择就是航运。 - James Pogran
3
请注意,如果$source是路径对象,则应编写“$source.path.length”以获取其长度。 - AZ.
1
这个解决方案依赖于使用没有通配符和尾部斜杠的路径。我认为唯一的解决方案是在Microsoft connect上声明Copy-Item的行为为错误。问题在于没有使用-name选项的gci命令不会告诉你相对路径,而使用-name选项会得到相对路径,但你不知道相对于哪个源目录。 - bernd_k
1
使用copy-item - exclude属性仍然存在问题吗?我的意思是上面的答案虽然可行,但现在已经是2016年了。微软是否修复或添加了copy-item的新功能? - LP13
2
根据2016年3月16日微软的说法,这个问题已经被修复,并将在下一个公共版本的PowerShell中发布;https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/11088483-6-years-old-bug-with-powershell-copy-item - Signal15

15

我也遇到了这个问题,花了20分钟尝试了这里提供的解决方案,但仍然存在问题。
所以我选择使用robocopy - 好吧,它不是powershell,但应该在支持powershell的任何地方都可以使用。

它直接起效了:

robocopy $source $dest /S /XF <file patterns to exclude> /XD <directory patterns to exclude>

例如

robocopy $source $dest /S /XF *.csproj /XD obj Properties Controllers Models

此外,它还具有许多功能,例如可恢复复制。 文档在这里


1
“robocopy”也是我的最爱,但是要注意如果你的脚本依赖于退出代码。请参阅https://blogs.technet.microsoft.com/deploymentguys/2008/06/16/robocopy-exit-codes/。 - SBF
像魔术一样运作 - rupweb

7

由于注释格式的代码不好,我会将其发布为答案,但这只是对@landyman的答案的补充。 建议的脚本有一个缺点-它将创建双重嵌套的文件夹。例如,对于“d:\ t1 \ sub1”,它将创建一个空目录“d:\ t2 \ sub1 \ sub1”。这是因为复制目录的Copy-Item期望在-Destination属性中包含父目录名称,而不是目录名称本身。 这是我找到的解决方法:

Get-ChildItem -Path $from -Recurse -Exclude $exclude | Copy-Item -Force -Destination {
  if ($_.GetType() -eq [System.IO.FileInfo]) {
    Join-Path $to $_.FullName.Substring($from.length)
  } else {
    Join-Path $to $_.Parent.FullName.Substring($from.length)
  }
}

1
谢谢!我正好遇到了这个问题。 - ferventcoder

6
请注意,语法规范要求一个字符串数组,即String[]。
SYNTAX
    Copy-Item [[-Destination] <String>] [-Confirm] [-Container] [-Credential <PSCredential>] [-Exclude <String[]>] [-Filter <String>] [-Force] [-FromSession <PSSession>] [-Include 
    <String[]>] -LiteralPath <String[]> [-PassThru] [-Recurse] [-ToSession <PSSession>] [-UseTransaction] [-WhatIf] [<CommonParameters>]

如果在数组生成时不明确类型,就会得到一个Object[],在许多情况下被忽略,导致出现“错误行为”的外观,因为缺乏类型安全性。由于PowerShell可以处理脚本块,除了类型特定变量的评估(以便可以确定有效字符串)之外,还会留下潜在的注入模式攻击的漏洞,这可能会影响执行策略较松的任何系统。因此,以下代码是不可靠的:
PS > $omissions = @("*.iso","*.pdf","*.zip","*.msi")
PS > $omissions.GetType()

Note the result....
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

而这个可以运作……例如:

PS > $omissions = [string[]]@("*.iso","*.pdf","*.zip","*.msi")
**or**
PS > [string[]]$omissions = ("*.iso,*.pdf,*.zip,*.msi").split(',')
PS > $omissions.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String[]                                 System.Array

请注意,即使只有一个“单一”元素,仍需要进行相同的转换,以创建一个1元素数组。
如果您在家中尝试此操作,请确保使用Replace-Variable“省略”清除$omissions的存在,然后再进行上述示例中的转换。
至于我测试过并且可靠工作的管道...
--------------------------------------------------------------------------------------- cd $sourcelocation

ls | ?{$_ -ne $null} | ?{$_.BaseName -notmatch "^\.$"} | %{$_.Name} | cp -Destination $targetDir -Exclude $omissions -recurse -ErrorAction silentlycontinue 
---------------------------------------------------------------------------------------

上面的代码列出了当前目录中源文件的目录列表,过滤掉潜在的问题项,将文件转换为基本名称,并强制cp(copy-item alias)通过“名称”在“当前目录”中重新访问文件 - 从而重新获取文件对象并复制它。这将创建空目录,包括可能包含被排除文件的目录(当然不包括这些被排除的文件)。还要注意,“ls”(get-childitem)不会递归 - 这留给了cp。最后 - 如果您遇到问题并需要调试,请删除-ErrorAction silentlycontinue开关和参数,否则会隐藏许多可能中断脚本的烦恼。
对于那些与“\”包含相关的评论,请记住,您是通过解释器(即PowerShell)在.NET子层上工作的,在c#中,例如,包含单个“\”(或多个字符串中的单个“\”)导致编译器要求您通过使用“\\”来转义反斜杠,或者在字符串前面加上@,如@“\”; 剩下的选项是将字符串封装在单引号中,如'\'。所有这些都是由于ASCII插值造成的字符组合,如“\n”等。
后面的主题更大,所以我就把它留给你去考虑吧。

5
exclude参数不能与dirs一起使用。Bo的一个变体脚本可以解决这个问题:
$source = 'c:\tmp\foo'
$dest = 'c:\temp\foo'
$exclude = '\.bak'
Get-ChildItem $source -Recurse  | where {$_.FullName -notmatch $exclude} | 
    Copy-Item -Destination {Join-Path $dest $_.FullName.Substring($source.length)}

1
你的“where”子句实际上将在正则表达式“.bak”上执行“notmatch”,即“[任何字符]bak”。你的正则表达式需要是'.bak',或者如果您想要直接字符串匹配,可以使用'-notlike'。 - Ryan Fisher

3
我在寻找一种方法,能够复制从某个特定日期/时间戳开始修改的文件,以便进行归档。这样我就可以保存我所处理的精确文件(假设我知道何时开始)。虽然我知道这正是SCM的用途,但有时我只想在不检查它的情况下快照我的工作。
使用 landyman 的提示和其他资料,我发现以下方法可行:
$source = 'c:\tmp\foo'
$dest = 'c:\temp\foo'
$exclude = @('*.pdb', '*.config')
Get-ChildItem $source -Recurse -Exclude $exclude |  
    where-object {$_.lastwritetime -gt "8/24/2011 10:26 pm"} | 
    Copy-Item -Destination {Join-Path $dest $_.FullName.Substring($source.length)}

1
$sourcePath="I:\MSSQL\Backup\Full"
$excludedFiles=@("MASTER", "DBA", "MODEL", "MSDB")
$sourceFiles=(ls $sourcePath -recurse -file) | where-object { $_.directory.name -notin $excludedFiles }

这是我所做的,我需要将一堆备份文件复制到网络上的另一个位置以供客户取回。我们不希望他们拥有上述系统数据库备份。

1
Get-ChildItem与Join-Path对我来说大部分都能工作,但我意识到它正在复制根目录到其他根目录中,这是不好的。
例如:
- c:\SomeFolder - c:\SomeFolder\CopyInHere - c:\SomeFolder\CopyInHere\Thing.txt - c:\SomeFolder\CopyInHere\SubFolder - c:\SomeFolder\CopyInHere\SubFolder\Thin2.txt - 源目录:c:\SomeFolder\CopyInHere - 目标目录:d:\PutItInHere 目标: 将c:\SomeFolder\CopyInHere内的所有子项复制到d:\PutItInHere的根目录中,但不包括c:\SomeFolder\CopyInHere本身。 - 例如,将CopyInHere的所有子项作为PutItInHere的子项。
上面的示例基本实现了这一目标,但会创建一个名为SubFolder的文件夹,并在其中创建一个名为SubFolder的文件夹。
那是因为Join-Path计算出SubFolder子项的目标路径为d:\PutItInHere\SubFolder,所以SubFolder被创建在名为SubFolder的文件夹中。
我通过使用Get-ChildItems来获取项目集合,然后使用循环来遍历解决了这个问题。
Param(
[Parameter(Mandatory=$True,Position=1)][string]$sourceDirectory,
[Parameter(Mandatory=$True,Position=2)][string]$destinationDirectory
)
$sourceDI = [System.IO.DirectoryInfo]$sourceDirectory
$destinationDI = [System.IO.DirectoryInfo]$destinationDirectory
$itemsToCopy = Get-ChildItem $sourceDirectory -Recurse -Exclude @('*.cs', 'Views\Mimicry\*')
foreach ($item in $itemsToCopy){        
    $subPath = $item.FullName.Substring($sourceDI.FullName.Length)
$destination = Join-Path $destinationDirectory $subPath
if ($item -is [System.IO.DirectoryInfo]){
    $itemDI = [System.IO.DirectoryInfo]$item
    if ($itemDI.Parent.FullName.TrimEnd("\") -eq $sourceDI.FullName.TrimEnd("\")){      
        $destination = $destinationDI.FullName  
    }
}
$itemOutput = New-Object PSObject 
$itemOutput | Add-Member -Type NoteProperty -Name Source -Value $item.FullName
$itemOutput | Add-Member -Type NoteProperty -Name Destination -Value $destination
$itemOutput | Format-List
Copy-Item -Path $item.FullName -Destination $destination -Force
}

简而言之,它使用当前项目的完整名称进行目标计算。然而,它会检查其是否为DirectoryInfo对象。如果是,则检查其父文件夹是否为源目录,这意味着正在迭代的当前文件夹是源目录的直接子级,因此我们不应将其名称附加到目标目录中,因为我们希望该文件夹在目标目录中创建,而不是在目标目录的子文件夹中创建。随后,其他每个文件夹都能正常工作。

0

我遇到了类似的问题,需要稍微扩展一下。我想要一个适用于像这样的源的解决方案

$source = "D:\scripts\*.sql"

我找到了这个解决方案:

function Copy-ToCreateFolder
{
    param(
        [string]$src,
        [string]$dest,
        $exclude,
        [switch]$Recurse
    )

    # The problem with Copy-Item -Rec -Exclude is that -exclude effects only top-level files
    # Copy-Item $src $dest    -Exclude $exclude       -EA silentlycontinue -Recurse:$recurse
    # https://dev59.com/gHRB5IYBdhLWcg3wETxJ

    if (Test-Path($src))
    {
        # Nonstandard: I create destination directories on the fly
        [void](New-Item $dest -itemtype directory -EA silentlycontinue )
        Get-ChildItem -Path $src -Force -exclude $exclude | % {

            if ($_.psIsContainer)
            {
                if ($Recurse) # Non-standard: I don't want to copy empty directories
                {
                    $sub = $_
                    $p = Split-path $sub
                    $currentfolder = Split-Path $sub -leaf
                    #Get-ChildItem $_ -rec -name  -exclude $exclude -Force | % {  "{0}    {1}" -f $p, "$currentfolder\$_" }
                    [void](New-item $dest\$currentfolder -type directory -ea silentlycontinue)
                    Get-ChildItem $_ -Recurse:$Recurse -name  -exclude $exclude -Force | % {  Copy-item $sub\$_ $dest\$currentfolder\$_ }
                }
            }
            else
            {

                #"{0}    {1}" -f (split-path $_.fullname), (split-path $_.fullname -leaf)
                Copy-Item $_ $dest
            }
        }
    }
}

如果您在嵌套文件夹中,最后一个复制项目可能无法正常工作。 - Blake Niemyjski

0

使用正则表达式进行排除的一种将项目从一个文件夹复制到另一个文件夹的方法:

$source = '.\source'
$destination = '.\destination'
$exclude = '.*\.pdf$|.*\.mp4$|\\folder1(\\|$)|\\folder2(\\|$)'

$itemsToCopy = Get-ChildItem $source -Recurse |
    Where-Object FullName -notmatch $exclude | Select-Object -Expand FullName

$sourceFullNameLength = (Get-Item $source).FullName.Length

foreach ($item in $itemsToCopy) {
    $relativeName = $item.Substring($sourceFullNameLength + 1)
    Copy-Item -Path $item -Destination "$destination\$relativeName"
}

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