【发布时间】:2011-12-22 20:55:33
【问题描述】:
我想编写一个 PowerShell 脚本,它将递归搜索目录,但排除指定文件(例如,*.log 和 myFile.txt),同时排除指定目录及其内容(例如,@987654325 @ 以及myDir 下的所有文件和文件夹)。
我一直在使用 Get-ChildItem CmdLet 和 Where-Object CmdLet,但我似乎无法获得这种确切的行为。
【问题讨论】:
我想编写一个 PowerShell 脚本,它将递归搜索目录,但排除指定文件(例如,*.log 和 myFile.txt),同时排除指定目录及其内容(例如,@987654325 @ 以及myDir 下的所有文件和文件夹)。
我一直在使用 Get-ChildItem CmdLet 和 Where-Object CmdLet,但我似乎无法获得这种确切的行为。
【问题讨论】:
我喜欢 Keith Hill 的回答,但它有一个错误,阻止它递归过去两个级别。这些命令显示了该错误:
New-Item level1/level2/level3/level4/foobar.txt -Force -ItemType file
cd level1
GetFiles . xyz | % { $_.fullname }
使用 Hill 的原始代码,您可以得到:
...\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 的回答,因为它简洁明了,但正如他所指出的那样,它的效率较低。顺便说一下,它效率较低的原因是因为 Hill 的算法在找到修剪目标时停止遍历子树,而 ajk 的算法仍在继续。但是 ajk 的答案也有一个缺陷,我称之为祖先陷阱。考虑这样的路径,它包含两次相同的路径组件(即 subdir2):
\usr\testdir\subdir2\child\grandchild\subdir2\doc
将您的位置设置在两者之间,例如cd \usr\testdir\subdir2\child,然后运行 ajk 的算法以过滤掉较低的 subdir2,您将完全得到 no 输出,即它会过滤掉所有内容,因为路径中存在较高的 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 的算法(出于同样的原因:它不会停止在修剪目标处遍历子树)。但是,我的代码比 Hill 的代码有一个重要的优势——它可以流水线!因此,可以将其放入过滤器链中以制作 Get-ChildItem 的自定义版本,而 Hill 的递归算法,由于其自身没有错,不能。 ajk 的算法也可以适应管道使用,但是指定要排除的一个或多个项目并不那么干净,它被嵌入在正则表达式中,而不是我使用过的简单项目列表中。
我已将我的树修剪代码打包到 Get-ChildItem 的增强版本中。除了我相当缺乏想象力的名字--Get-EnhancedChildItem--我对此感到很兴奋,并将其包含在我的open source Powershell library 中。除了树修剪之外,它还包括其他几个新功能。此外,代码被设计为可扩展的:如果您想添加新的过滤功能,这很简单。本质上,首先调用 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 函数(在 Keith Dahlby 的 this post 中找到),如果调用未指定任何路径,它只会将当前目录发送到管道中。
因为这个答案有点冗长,所以我没有详细介绍这个过滤器,所以我建议感兴趣的读者阅读我最近在 Simple-Talk.com 上发表的题为Practical PowerShell: Pruning File Trees and Extending Cmdlets 的文章,我在其中更深入地讨论了 Get-EnhancedChildItem长度。不过,我要提到的最后一件事是我的开源库New-FileTree 中的另一个函数,它可以让您生成一个虚拟文件树用于测试目的,以便您可以使用上述任何算法。当您尝试其中任何一个时,我建议像在第一个代码片段中所做的那样通过管道连接到% { $_.fullname },以便检查更有用的输出。
【讨论】:
$excludeList | where { $pathParts -like $_ } 所以$_ 采用排除列表中每个成员的值。现在让我们将原始行抽象地重写为if (not_on_exclusion_list) { $_ }。即如果满足条件,则输出当前管道的成员,即Get-ChildItem返回的当前项。
$item.FullName 而不仅仅是 $item 作为 Test-Path 的第一个参数,这应该是让它为您工作所需要的全部内容。
Get-ChildItem cmdlet 有一个-Exclude 参数,很想使用它,但它不能用于过滤掉我所知道的整个目录。试试这样的:
【讨论】:
Test-Path $item -PathType Container。我不得不改用$item.PSIsContainer。 PS:我正在使用带有 -Recurse 开关的 Get-ChildItem(以防它有任何影响)。
这是另一种选择,效率较低但更简洁。这就是我通常处理此类问题的方式:
Get-ChildItem -Recurse .\targetdir -Exclude *.log |
Where-Object { $_.FullName -notmatch '\\excludedir($|\\)' }
\\excludedir($|\\)' 表达式允许您同时排除目录及其内容。
更新:请查看 msorens 的优秀答案,了解这种方法的边缘情况缺陷,以及更充实的整体解决方案。
【讨论】:
\\excludedir($|\\)这个表达式的作用吗?谢谢。
\excludedir 的任何文件或文件夹。 ($|\\) 部分表示模式匹配完整路径名的结尾或尾部反斜杠。所以它将匹配\dir1\dir2\excludedir 或dir1\excludedir\dir2。我强烈建议您查看@msorens 的答案。除了总体上是一个很好的答案之外,他还指出了我的方法中的一个缺点。
最近,我探索了参数化要扫描的文件夹以及将存储递归扫描结果的位置的可能性。最后,我也总结了扫描的文件夹数量和里面的文件数量。与社区分享,以防它对其他开发者有所帮助。
##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}";
【讨论】:
有点晚了,试试这个吧。
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'
【讨论】:
在此处发表评论,因为这似乎是关于搜索文件同时在 powershell 中排除某些目录的主题的最流行答案。
为了避免结果后过滤的问题(即避免权限问题等),我只需要过滤掉顶级目录,这就是本示例所基于的全部内容,因此虽然本示例不过滤子目录名称,如果您愿意,可以很容易地递归地支持这一点。
快速分解 sn-p 的工作原理
$folders
$file
foreach
$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 }
}
【讨论】: