Как использовать 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.
Контейнерный переключатель поддерживает структуру папок. Наслаждаться.
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, проблема в том, что целевая папка (клиент *) не существует, поэтому файлы помещаются в корень целевой папки. Большинство других ответов не рассматривают этот сценарий конкретно и, следовательно, не отвечают на заданный вопрос. Для достижения вашего решения потребуется два шага:
- Выберите файлы, которые хотите скопировать
- Скопируйте выбранные файлы в папку назначения, убедившись, что папка назначения существует.
$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") и "*\" (для структуры папок)