Search code examples
powershellcopy-item

Copy-Item does not copy to correct target directory


I'm trying to copy a directory structure with a PowerShell script. I wish to exclude a certain directory.

My directory structure looks like this:

C:\
└───murks
    ├───murks_source
    │   │   test1.txt
    │   │   test2.txt
    │   │
    │   ├───sub
    │   │       subtest1.txt
    │   │       subtest2.txt
    │   │       subtest3.txt
    │   │
    │   └───sub2
    │           sub2test.txt
    │
    └───murks_target

Now, when I run the script, it doesn't create the subdirectory "sub2" with the subordinate files. Instead, it copies all files (also those of the "sub2" subdirectory) directly into the murks_target_directory. I don't understand this behaviour, since the "Select -ExpandProperty FullName"-part looks good so far.

Any help or hints are highly appreciated. Thanks in advance!

My script looks like this so far:

$rootFolderPath = 'C:\murks\murks_source'
$excludeDirectories = ("sub");

function Exclude-Directories
{
    process
    {
        $allowThrough = $true
        foreach ($directoryToExclude in $excludeDirectories)
        {
            $directoryText = "*\" + $directoryToExclude
            $childText = "*\" + $directoryToExclude + "\*"
            if (($_.FullName -Like $directoryText -And $_.PsIsContainer) `
                -Or $_.FullName -Like $childText)
            {
                $allowThrough = $false
                break
            }
        }

        if ($allowThrough)
        {
            return $_
        }
    }
}

Get-ChildItem $rootFolderPath -Recurse | Exclude-Directories | Select -ExpandProperty FullName | Copy-Item -Destination C:\murks\murks_target -Force

Solution

  • The below custom function should do what you want:

    function Copy-Path {
        [CmdletBinding()]
        param(
            [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
            [ValidateScript({Test-Path -Path $_ -PathType Container})]
            [string]$Source,
    
            [Parameter(Position = 1)]
            [string]$Destination,
    
            [string[]]$ExcludeFolders = $null,
            [switch]$IncludeEmptyFolders
        )
        $Source      = $Source.TrimEnd("\")
        $Destination = $Destination.TrimEnd("\")
    
        Get-ChildItem -Path $Source -Recurse | ForEach-Object {
            if ($_.PSIsContainer) {
                # it's a folder
                if ($ExcludeFolders.Count) {
                    if ($ExcludeFolders -notcontains $_.Name -and $IncludeEmptyFolders) {
                        # create the destination folder, even if it is empty
                        $target = Join-Path -Path $Destination -ChildPath $_.FullName.Substring($Source.Length)
                        if (!(Test-Path $target -PathType Container)) {
                            Write-Verbose "Create folder $target"
                            New-Item -ItemType Directory -Path $target | Out-Null
                        }
                    }
                }
            }
            else {
                # it's a file
                $copy = $true
                if ($ExcludeFolders.Count) {
                    # get all subdirectories in the current file path as array
                    $subs = $_.DirectoryName.Replace($Source,"").Trim("\").Split("\")
                    # check each sub folder name against the $ExcludeFolders array
                    foreach ($folderName in $subs) {
                        if ($ExcludeFolders -contains $folderName) { $copy = $false; break }
                    }
                }
    
                if ($copy) {
                    # create the destination folder if it does not exist yet
                    $target = Join-Path -Path $Destination -ChildPath $_.DirectoryName.Substring($Source.Length)
                    if (!(Test-Path $target -PathType Container)) {
                        Write-Verbose "Create folder $target"
                        New-Item -ItemType Directory -Path $target | Out-Null
                    }
                    Write-Verbose "Copy file $($_.FullName) to $target"
                    $_ | Copy-Item -Destination $target -Force
                }
            }
        }
    }
    

    Once in place, use it like this:

    Copy-Path -Source 'C:\murks\murks_source' -Destination 'C:\murks\murks_target' -ExcludeFolders 'sub' -Verbose
    

    Where parameter -ExcludeFolders can be an array of foldernames to skip.
    Adding switch parameter IncludeEmptyFolders will also create empty folders in the destination. If left out, empty folders will not be copied.

    Using your example file structure, the result is

    C:\murks\murks_target
    |   test1.txt
    |   test2.txt
    |
    \---sub2
            sub2test.txt