Как использовать powershell copy-item и сохранить структуру

У меня есть структура каталогов, которая выглядит следующим образом:

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

Я хочу скопировать содержимое папок f1 в c:\tmp\, чтобы получить это

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

Я попробовал это:

copy-item -recur -path: "*/f1/" -destination: c:\tmp\

Но он копирует содержимое без правильного копирования структуры.

16 ответов

В PowerShell версии 3.0 и новее это просто делается следующим образом:

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

Ссылка: 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 
    } 

Замечания:

  • -Фильтр "*" ничего не делает. Это просто для иллюстрации, если вы хотите скопировать некоторые конкретные файлы. Например, все файлы *.config.
  • Еще нужно вызвать New-Item с -Force, чтобы фактически создать структуру папок.

Здесь есть другой ответ с тем же решением, хотя он не очень ясен, поэтому я добавлю его, так как мне потребовалось некоторое время, чтобы найти этот ответ.

Я покопался и нашел множество решений этой проблемы, причем все они были не просто прямой командой copy-item, а каким-то изменением. Да, некоторые из этих вопросов предшествуют PS 3.0, чтобы ответы не были неправильными, но с помощью powershell 3.0 я наконец смог добиться этого, используя ключ -Container для copy-item.

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, does not create the folder1 container
#Copy-Item D:\tmp\test_copy\* D:\tmp\test_copy2 -Recurse -Container

#if the destination does not exists this created the matching folder structure and file with no errors
Copy-Item D:\tmp\test_copy\* D:\tmp\test_copy2\ -Recurse -Container

надеюсь, это поможет кому-то

Использование xcopy или же robocopyоба из которых были разработаны именно для этой цели. Предполагая, что ваши пути являются только путями файловой системы, конечно.

Если вы хотите правильно копировать структуру папок с помощью 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", как вы упомянули.

Поскольку я потратил время на поиск более простого способа, который не требует конвейерной обработки или встроенных сценариев:

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

Также можно поставить -Filter аргумент, если требуется условие для копии.

Работает с PowerShell 5.1.18362.752.

Источник: https://devblogs.microsoft.com/scripting/powertip-use-powershell-to-copy-items-and-retain-folder-structure/

Контейнерный переключатель поддерживает структуру папок. Наслаждаться.

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 -fo
rce}}
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>>

Вот небольшая вариация основного вопроса, когда исходный файл является относительным и необходимо скопировать только подмножество файлов со структурой папок. Этот сценарий может произойти, если вы запустите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.

Мне нужно было сделать то же самое, поэтому я нашел эту команду:

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

И в вашем случае:

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

А если вам нужно исключить некоторые элементы, создайте текстовый файл со списком исключений. Например:

Создайте текстовый файл "exclude.txt" на диске C:\ и добавьте в него:

.svn
.git

И ваша команда копирования теперь будет выглядеть так:

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

Ниже работал на меня

@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"
    )

Для каждой строки (файла) в списке создайте в целевой папке один и тот же путь, указанный в прочитанной строке (%%~pa - это путь элемента, на который ссылается %%a). Затем скопируйте прочитанный файл в целевую папку

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

Как правило, Copy-Item сделает это за вас, пока целевая папка уже существует. Документация не соответствует тому , что я проверены путем тестирования. Завершающая косая черта в пути назначения не решает этого. При копировании иерархии папок необходимо использовать -Recurse. По умолчанию для параметра Container установлено значение true, вы можете не использовать его.

В вашем вопросе вы фильтруете список файлов для копирования на основе именованной подпапки. Хотя это работает для свойства Path объекта Copy-Item, проблема в том, что целевая папка (клиент *) не существует, поэтому файлы помещаются в корень целевой папки. Большинство других ответов не рассматривают этот сценарий конкретно и, следовательно, не отвечают на заданный вопрос. Для достижения вашего решения потребуется два шага:

  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
}

Я создал следующую функцию, которая будет копировать все файлы из каталога, сохраняя структуру папок. Затем вы можете указать начальный индекс, с которого должна начинаться структура папок.

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

Используйте его следующим образом:

      Copy-FileKeepPath -FileToCopy 'C:\folderA\folderB\folderC\client1\f1\' -des "C:\tmp" -filter * -startIndex "C:\folderA\folderB\folderC\".Length
$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
}

Этот код работает, особенно если вы используете Get-ChildItem в сочетании с методом filter/where-object.

Однако в этом коде есть одна небольшая ошибка: с помощью оператора "IF" и следующего кода "mkdir" будет создана папка в $targetDir... после этого команда "copy-item" создаст ту же папку внутри папка, только что созданная командой "mkdir".


Вот пример того, как у меня это работало с функцией "Где-объект". Вы можете просто опустить оператор IF.

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

Убедитесь, что пути не обозначены знаком "\" в конце.

Забудьте Copy-Item, он никогда не делает того, чего вы ожидаете, используйте xcopy из выражения invoke-expression

Invoke-Expression "xcopy $sourceDir $targetDir /E"

У меня было аналогичное требование, когда я хотел делиться только файлами библиотеки (разные платформы и типы сборки). Я собирался написать свой сценарий PowerShell, но понял, что это можно сделать менее чем за минуту с помощью инструмента SyncBackFree. Это сработало, как и ожидалось.

1> Создать профиль

2> Выберите исходную и целевую папку

3> Нажмите "Выбрать подкаталоги и файлы.

4> Нажмите слева "Изменить фильтр".

5> Добавьте расширение файла ("*.lib") и "*\" (для структуры папок)

Другие вопросы по тегам