Как извлечь из PowerShell рекурсивный каталог и список файлов, исключая некоторые файлы и папки?
Я хочу написать сценарий PowerShell, который будет рекурсивно искать каталог, но исключать указанные файлы (например, *.log
, а также myFile.txt
), а также исключить указанные каталоги и их содержимое (например, myDir
и все файлы и папки ниже myDir
).
Я работал с Get-ChildItem
CmdLet, и Where-Object
CmdLet, но я не могу получить это точное поведение.
3 ответа
Командлет Get-ChildItem имеет -Exclude
параметр, который заманчиво использовать, но он не работает для фильтрации целых каталогов из того, что я могу сказать. Попробуйте что-то вроде этого:
функция GetFiles($path = $pwd, [string[]]$exclude) { foreach ($item в пути Get-ChildItem $) { if ($ exclude | Where {$ item-like $ _}) {continue} if (Test-Path $ item.FullName -PathType Container) { $ пункт GetFiles $item.FullName $exclude } еще { $ пункт } } }
Мне нравится ответ Кейта Хилла, за исключением того, что в нем есть ошибка, которая не позволяет ему повторяться после двух уровней. Эти команды показывают ошибку:
New-Item level1/level2/level3/level4/foobar.txt -Force -ItemType file
cd level1
GetFiles . xyz | % { $_.fullname }
С оригинальным кодом Хилла вы получите это:
...\level1\level2
...\level1\level2\level3
Вот исправленная и слегка переработанная версия:
function GetFiles($path = $pwd, [string[]]$exclude)
{
foreach ($item in Get-ChildItem $path)
{
if ($exclude | Where {$item -like $_}) { continue }
$item
if (Test-Path $item.FullName -PathType Container)
{
GetFiles $item.FullName $exclude
}
}
}
После исправления этой ошибки вы получите исправленный вывод:
...\level1\level2
...\level1\level2\level3
...\level1\level2\level3\level4
...\level1\level2\level3\level4\foobar.txt
Мне также нравится ответ Айка для краткости, хотя, как он указывает, он менее эффективен. Между прочим, причина в том, что он менее эффективен, в том, что алгоритм Хилла перестает проходить через поддерево, когда находит цель обрезки, а ajk продолжает. Но ответ Айка также страдает недостатком, который я называю ловушкой предков. Рассмотрим такой путь, который включает в себя один и тот же компонент пути (то есть subdir2) дважды:
\usr\testdir\subdir2\child\grandchild\subdir2\doc
Установите свое местоположение где-то посередине, например cd \usr\testdir\subdir2\child
, затем запустите алгоритм ajk, чтобы отфильтровать нижний subdir2
и вы не получите никакого вывода вообще, т.е. он отфильтровывает все из-за присутствия subdir2
выше на пути. Однако это ключевой случай, и его вряд ли ударить часто, поэтому я бы не стал исключать решение ajk из-за этой проблемы.
Тем не менее, я предлагаю здесь третий вариант, который не имеет ни одной из двух вышеупомянутых ошибок. Вот основной алгоритм, дополненный определением удобства для пути или путей обрезки - вам нужно только изменить $excludeList
для вашего собственного набора целей, чтобы использовать его:
$excludeList = @("stuff","bin","obj*")
Get-ChildItem -Recurse | % {
$pathParts = $_.FullName.substring($pwd.path.Length + 1).split("\");
if ( ! ($excludeList | where { $pathParts -like $_ } ) ) { $_ }
}
Мой алгоритм достаточно лаконичен, но, как и ajk, он менее эффективен, чем алгоритм Хилла (по той же причине: он не останавливает обход поддеревьев в целях сокращения). Тем не менее, мой код имеет важное преимущество перед Hill - он может работать! Поэтому можно вписаться в цепочку фильтров, чтобы создать собственную версию Get-ChildItem, в то время как рекурсивный алгоритм Хилла, не по своей вине, не может. Алгоритм ajk также может быть адаптирован к конвейерному использованию, но указание исключаемого элемента или элементов не так чисто, поскольку оно встроено в регулярное выражение, а не в простой список элементов, которые я использовал.
Я упаковал код удаления дерева в расширенную версию Get-ChildItem. Помимо моего довольно невообразимого имени - Get-EnhancedChildItem - я взволнован этим и включил его в мою библиотеку Powershell с открытым исходным кодом. Он включает в себя несколько других новых возможностей помимо обрезки деревьев. Кроме того, код предназначен для расширения: если вы хотите добавить новую возможность фильтрации, это сделать несложно. По сути, сначала вызывается Get-ChildItem, который передается по конвейеру в каждый последующий фильтр, который вы активируете с помощью параметров команды. Таким образом, как-то так...
Get-EnhancedChildItem –Recurse –Force –Svn
–Exclude *.txt –ExcludeTree doc*,man -FullName -Verbose
... преобразуется внутренне в это:
Get-ChildItem | FilterExcludeTree | FilterSvn | FilterFullName
Каждый фильтр должен соответствовать определенным правилам: принимать объекты FileInfo и DirectoryInfo в качестве входных данных, генерировать их как выходные данные и использовать stdin и stdout, чтобы его можно было вставить в конвейер. Вот тот же код, переработанный для соответствия этим правилам:
filter FilterExcludeTree()
{
$target = $_
Coalesce-Args $Path "." | % {
$canonicalPath = (Get-Item $_).FullName
if ($target.FullName.StartsWith($canonicalPath)) {
$pathParts = $target.FullName.substring($canonicalPath.Length + 1).split("\");
if ( ! ($excludeList | where { $pathParts -like $_ } ) ) { $target }
}
}
}
Единственным дополнительным компонентом здесь является функция Coalesce-Args (найденная в этом посте Китом Далби), которая просто отправляет текущий каталог по конвейеру в случае, если в вызове не указаны пути.
Поскольку этот ответ становится несколько длиннее, а не углубляюсь в подробности об этом фильтре, я отсылаю заинтересованного читателя к моей недавно опубликованной статье на Simple-Talk.com под названием Practical PowerShell: удаление файловых деревьев и расширение командлетов, где я обсуждаю Get-EnhancedChildItem на еще большей длине. Последнее, что я упомяну, это еще одна функция в моей библиотеке с открытым исходным кодом, New-FileTree, которая позволяет вам генерировать фиктивное дерево файлов для целей тестирования, чтобы вы могли использовать любой из вышеперечисленных алгоритмов. И когда вы экспериментируете с любым из них, я рекомендую % { $_.fullname }
как я сделал в самом первом фрагменте кода для более полезного вывода для изучения.
Вот еще один вариант, который менее эффективен, но более лаконичен. Вот как я обычно справляюсь с такой проблемой:
Get-ChildItem -Recurse .\targetdir -Exclude *.log |
Where-Object { $_.FullName -notmatch '\\excludedir($|\\)' }
\\excludedir($|\\)'
Выражение позволяет исключить каталог и его содержимое одновременно.
Обновление: пожалуйста, проверьте отличный ответ от msorens на наличие крайнего случая с этим подходом и гораздо более детальное решение в целом.
Немного поздно, но попробуйте этот.
function Set-Files($Path) {
if(Test-Path $Path -PathType Leaf) {
# Do any logic on file
Write-Host $Path
return
}
if(Test-Path $path -PathType Container) {
# Do any logic on folder use exclude on get-childitem
# cycle again
Get-ChildItem -Path $path | foreach { Set-Files -Path $_.FullName }
}
}
# call
Set-Files -Path 'D:\myFolder'
Недавно я изучил возможности параметризации папки для сканирования и места, где будет сохранен результат рекурсивного сканирования. В конце я также суммировал количество просканированных папок и количество файлов внутри. Поделитесь им с сообществом, если это может помочь другим разработчикам.
##Script Starts
#read folder to scan and file location to be placed
$whichFolder = Read-Host -Prompt 'Which folder to Scan?'
$whereToPlaceReport = Read-Host -Prompt 'Where to place Report'
$totalFolders = 1
$totalFiles = 0
Write-Host "Process started..."
#IMP separator ? : used as a file in window cannot contain this special character in the file name
#Get Foldernames into Variable for ForEach Loop
$DFSFolders = get-childitem -path $whichFolder | where-object {$_.Psiscontainer -eq "True"} |select-object name ,fullName
#Below Logic for Main Folder
$mainFiles = get-childitem -path "C:\Users\User\Desktop" -file
("Folder Path" + "?" + "Folder Name" + "?" + "File Name " + "?"+ "File Length" )| out-file "$whereToPlaceReport\Report.csv" -Append
#Loop through folders in main Directory
foreach($file in $mainFiles)
{
$totalFiles = $totalFiles + 1
("C:\Users\User\Desktop" + "?" + "Main Folder" + "?"+ $file.name + "?" + $file.length ) | out-file "$whereToPlaceReport\Report.csv" -Append
}
foreach ($DFSfolder in $DFSfolders)
{
#write the folder name in begining
$totalFolders = $totalFolders + 1
write-host " Reading folder C:\Users\User\Desktop\$($DFSfolder.name)"
#$DFSfolder.fullName | out-file "C:\Users\User\Desktop\PoC powershell\ok2.csv" -Append
#For Each Folder obtain objects in a specified directory, recurse then filter for .sft file type, obtain the filename, then group, sort and eventually show the file name and total incidences of it.
$files = get-childitem -path "$whichFolder\$($DFSfolder.name)" -recurse
foreach($file in $files)
{
$totalFiles = $totalFiles + 1
($DFSfolder.fullName + "?" + $DFSfolder.name + "?"+ $file.name + "?" + $file.length ) | out-file "$whereToPlaceReport\Report.csv" -Append
}
}
# If running in the console, wait for input before closing.
if ($Host.Name -eq "ConsoleHost")
{
Write-Host ""
Write-Host ""
Write-Host ""
Write-Host " **Summary**" -ForegroundColor Red
Write-Host " ------------" -ForegroundColor Red
Write-Host " Total Folders Scanned = $totalFolders " -ForegroundColor Green
Write-Host " Total Files Scanned = $totalFiles " -ForegroundColor Green
Write-Host ""
Write-Host ""
Write-Host "I have done my Job,Press any key to exit" -ForegroundColor white
$Host.UI.RawUI.FlushInputBuffer() # Make sure buffered input doesn't "press a key" and skip the ReadKey().
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp") > $null
}
##Output
##Bat Code to run above powershell command
@ECHO OFF
SET ThisScriptsDirectory=%~dp0
SET PowerShellScriptPath=%ThisScriptsDirectory%MyPowerShellScript.ps1
PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File ""%PowerShellScriptPath%""' -Verb RunAs}";
Комментируя здесь, поскольку это, по-видимому, самый популярный ответ на тему поиска файлов при исключении определенных каталогов в PowerShell.
Чтобы избежать проблем с пост-фильтрацией результатов (т.е. во избежание проблем с разрешениями и т. Д.), Мне нужно было только отфильтровать каталоги верхнего уровня, и это все, на чем основан этот пример, поэтому, хотя этот пример не фильтрует имена дочерних каталогов, он может очень легко сделать рекурсивным, чтобы поддержать это, если бы вы были так склонны.
Краткое описание того, как работает сниппет
$folder << Использует Get-Childitem для запроса файловой системы и исключения папок.
$file << Шаблон файла, который я ищу
foreach << Итерирует переменную $folder, выполняя рекурсивный поиск с помощью команды Get-Childitem
$folders = Get-ChildItem -Path C:\ -Directory -Name -Exclude Folder1,"Folder 2"
$file = "*filenametosearchfor*.extension"
foreach ($folder in $folders) {
Get-Childitem -Path "C:/$folder" -Recurse -Filter $file | ForEach-Object { Write-Output $_.FullName }
}