Get-ChildItem script hangs due to large object? - powershell

Okay - I am brand new to PowerShell. I only started using it two weeks ago. I've scoured the web to create some scripts and now I'm trying something that seems a bit advanced and I'm uncertain how I should solve this.
I'm creating an audit script to determine what files are different between two backup repositories to ensure they've been properly synchronized (the synchronization scripts use robocopy and they've failed more than once without producing an error). The folders are quite extensive and upon occasion, I'm finding that the script just hangs on certain folders (always on the largest of them) and it will never complete due to this.
At first, I was using Get-ChildItem on the full source path, but that created a memory problem and the script would never complete. So, I thought I'd enumerate the child directories and perform a compare on each child directory... but depending on the folder, that goes bad as well.
Here is the script (using Powershell 2):
$serverArray=#("Server1","Server2","Server3")
for ($i=0; $i -lt 8; $i++) {
$server = $serverArray[$i]
$source="\\$server\Share\"
$destination = "D:\BackupRepository\$server"
# Copy to removable drive
$remoteDestination = "T:\BackupRepository\" + $server
$log = $server + "ShareBackup.log"
$remoteLog = "Remote_" + $server + "ShareBackup.log"
$logDestination = $localLogPath + $log
$logUNCDestination = $uncLogPath + $log
$logRemoteDestination = $localLogPath + $remoteLog
$logUNCRemoteDestination = $uncLogPath + $remoteLog
## This file is used for the process of checking
## whether or not the backup was successful
$backupReport = $localReportPath + $server + "ShareBackupReport.txt"
$remoteBackupReport = $localReportPath + "Remote_" + $server + "ShareBackupReport.txt"
## Variables for the failure emails
$failEmailSubject = "AUDIT REPORT for " + $server
$failRemoteEmailSubject = "AUDIT REPORT for " + $server
$failEmailBody = "The Audit for " + $server + " has found a file mismatch. Please consult the attached Backup Report."
$failRemoteEmailBody = "The Audit of the Remote Backups for " + $server + " has found a file mismatch. Please consult the attached Backup Report."
$sourceFolderArray = Get-ChildItem $source | ?{ $_.PSIsContainer }
$sourceFolderCount = $sourceFolderArray.Count
$mismatchCount = 0
$remoteMismatchCount = 0
for ($s1=0; $s1 -lt $sourceFolderCount; $s1++) {
$sourceFolder = $sourceFolderArray[$s1].FullName
$sourceFolderName = $sourceFolderArray[$s1].Name
$destFolder = $destination + "\" + $sourceFolderName
$remoteDestFolder = $remoteDestination + "\" + $sourceFolderName
Write-Host "Currently working on: " $sourceFolderName
$shot1 = Get-ChildItem -recurse -path $sourceFolder
$shot2 = Get-ChildItem -recurse -path $destFolder
$shot3 = Get-ChildItem -recurse -path $remoteDestFolder
$auditReportDest = "C:\BackupReports\Audits\"
$auditReportOutput = $auditReportDest + $server + "_" + $sourceFolderName + ".txt"
$auditReportRemoteOutput = $auditReportDest + $server + "_Remote_" + $sourceFolderName + ".txt"
$auditMismatchReport = $auditReportDest + "MismatchReport_" + $numericDate + ".txt"
Compare-Object $shot1 $shot2 -PassThru > $auditReportOutput
Compare-Object $shot2 $shot3 -PassTHru > $auditReportRemoteOutput
$auditCompare = Get-ChildItem $auditReportOutput
$auditRemoteCompare = Get-ChildItem $auditReportRemoteOutput
if ($auditCompare.Length -gt 0) {
$content = Get-ChildItem -Recurse $auditReportOutput
Add-Content $auditMismatchReport $content
Write-Host "Mismatch FOUND: " $sourceFolderName
$mismatchCount = $mismatchCount + 1
}
if ($auditRemoteCompare.Length -gt 0) {
$remoteContent = Get-ChilItem -Recurse $auditReportRemoteOutput
Add-Content $auditMismatchReport $remoteContent
Write-Host "Remote Mismatch FOUND: " $sourceFolderName
$remoteMismatchCount = $remoteMismatchCount + 1
}
}
send-mailmessage -from $emailFrom -to $emailTo -subject "AUDIT REPORT: Backups" -body "The full mismatch report is attached. There were $mismatchCount mismatched folders found and $remoteMismatchCount remote mismatched folders found. Please review to ensure backups are current." -Attachments "$auditMismatchReport" -priority High -dno onSuccess, onFailure -smtpServer $emailServer
}
What I've discovered when run interactively is that I'll get a "Currently working on FolderName" and if that object is "too large" (whatever that is), the script will just sit there at that point giving no indication of any error, but it will not continue (I've waited hours). Sometimes I can hit Ctrl-C interactively and rather than quitting the script, it takes the interrupt as a cancel for the current process and moves to the next item.
The rub is, I need to schedule this to happen daily to ensure the backups remain synchronized. Any help or insight is appreciated. And, yes, this is probably raw and inelegant, but right now I'm just trying to solve how I can get around the script hanging on me.

Not sure what version of PS you're using, but Get-Childitem has known problems scaling to large directories:
http://blogs.msdn.com/b/powershell/archive/2009/11/04/why-is-get-childitem-so-slow.aspx
If you're just comparing file names, you can get much better results in large directory structures using the legacy dir command. The /b (bare) switch returns just the fullname strings that can be readily used with Powershell's comparison operators.
$sourcedir = 'c:\testfiles'
$source_regex = [regex]::escape($sourcedir)
(cmd /c dir $Sourcedir /b /s) -replace "$source_regex\\(.+)$",'$1'
This uses a regular expression and the -replace operator to trim the soruce directory off of the fullnames returned by dir. The -replace operator will work with arrays, so you can do all of them in one operation without a foreach loop.

Related

Export Critical, Warning and Errors events from Windows Logs

I'm using most of the script from here.https://kb.webspy.com/s/article/windows-event-logs-and-powershell
However, I was wondering if there is a way only export Critical, Warning and Errors events. I know those events levels are 1-3
Get-WinEvent -FilterHashTable #{LogName = "System"; Level=1,2,3; StartTime=((Get-Date).AddDays(-7))} -ComputerName "server1" #| Out-GridView
I was just wondering where to add the level to this script.
# Logs to extract from server
$logArray = #("System","Security","Application")
# Grabs the server name to append to the log file extraction
$servername = $env:computername
# Provide the path with ending "\" to store the log file extraction.
$destinationpath = "C:\WindowsEventLogs\"
# Checks the last character of the destination path. If it does not end in '\' it adds one.
# '.+?\\$' +? means any character \\ is looking for the backslash $ is the end of the line charater
if ($destinationpath -notmatch '.+?\\$')
{
$destinationpath += '\'
}
# If the destination path does not exist it will create it
if (!(Test-Path -Path $destinationpath))
{
New-Item -ItemType directory -Path $destinationpath
}
# Get the current date in YearMonthDay format
$logdate = Get-Date -format yyyyMMddHHmm
# Start Process Timer
$StopWatch = [system.diagnostics.stopwatch]::startNew()
# Start Code
Clear-Host
Foreach($log in $logArray)
{
# If using Clear and backup
$destination = $destinationpath + $servername + "-" + $log + "-" + $logdate + ".evtx"
Write-Host "Extracting the $log file now."
# Extract each log file listed in $logArray from the local server.
wevtutil epl $log $destination
}
# End Code
# Stop Timer
$StopWatch.Stop()
$TotalTime = $StopWatch.Elapsed.TotalSeconds
$TotalTime = [math]::Round($totalTime, 2)
write-host "The Script took $TotalTime seconds to execute."
It seems like the code is using wevtutil to retrieve information about event logs.
wevtutil epl $log $destination
From the documentation wevtutil also accept different options and one of which is /q:<Query>.
Defines the XPath query to filter the events that are read or
exported. If this option is not specified, all events will be returned
or exported. This option is not available when /sq is true.
So you could create a Xpath query to apply filter based on event levels
wevtutil epl $log $destination /q:"*[System[(Level=1 or Level=2 or Level=3)]]"

Print pdf files on different printers depending on their content

I want to print .pdf-files on different printers - depending on their content.
How can I check whether a specific single word is present in a file?
To queue through a folder's content I've build the following so far:
Unblock-File -Path S:\test\itextsharp.dll
Add-Type -Path S:\test\itextsharp.dll
$files = Get-ChildItem S:\test\*.pdf
$adobe='C:\Program Files (x86)\Adobe\Acrobat DC\Acrobat\Acrobat.exe'
foreach ($file in $files) {
$reader = [iTextSharp.text.pdf.parser.PdfTextExtractor]
$Extract = $reader::GetTextFromPage($File.FullName,1)
if ($Extract -Contains 'Lieferschein') {
Write-Host -ForegroundColor Yellow "Lieferschein"
$printername='XX1'
$drivername='XX1'
$portname='192.168.X.41'
} else {
Write-Host -ForegroundColor Yellow "Etikett"
$printername='XX2'
$drivername='XX2'
$portname='192.168.X.42'
}
$arglist = '/S /T "' + $file.FullName + '" "' + $printername + '" "' + $drivername + " " + $portname
start-process $adobe -argumentlist $arglist -wait
Start-Sleep -Seconds 15
Remove-Item $file.FullName
}
And for now I got 2 problems with it:
1st: Add-Type -Path itextsharp.dll gives me an error.
Add-Type: One or more types in the assembly cannot be loaded. Get the LoaderExceptions property for more information. In line: 2 character: 1
I've read that it might be due to the file being blocked. There is no information about that in the properties though. And the Unblock-File comand and the start doesn't change/solve anything.
After using $error[0].exception.loaderexceptions[0] I get the information that BouncyCastle.Crypto, Version=1.8.6.0 is missing. Unfortunatelly I can't find any sources for that yet.
2nd: Will if ($Extract -Contains 'Lieferschein') work as I intend? Will it check for the phrase after the Add-Type gets loaded successfully?
Alternatively: There's also the possibility to make it depend from the content's format. One type of the files has the size of DIN A4 for example. The other one is smaller than that. If there's an easier way to check for that, you'd make me happy aswell.
Thank you in advance!
Searching for a keyword in a pdf using Powershell and iTextSharp.dll. It's a very common thing. You then just use your conditional logic to send to whatever printer you choose.
SO, something like this should do.
Add-Type -Path 'C:\path_to_dll\itextsharp.dll'
$pdfs = Get-ChildItem 'C:\path_to_pdfs' -Filter '*.pdf'
$export = 'D:\Temp\PdfExport.csv'
$results = #()
$keywords = #('Keyword1')
foreach ($pdf in $pdfs)
{
"processing - $($pdf.FullName)"
$reader = New-Object iTextSharp.text.pdf.pdfreader -ArgumentList $pdf.FullName
for ($page = 1; $page -le $reader.NumberOfPages; $page++)
{
$pageText = [iTextSharp.text.pdf.parser.PdfTextExtractor]::GetTextFromPage($reader, $page).Split([char]0x000A)
foreach ($keyword in $keywords)
{
if ($pageText -match $keyword)
{
$response = #{
keyword = $keyword
file = $pdf.FullName
page = $page
}
$results += New-Object PSObject -Property $response
}
}
}
$reader.Close()
}
"`ndone"
$results |
Export-Csv $export -NoTypeInformation
Update
As per your comment, regarding your error.
Again, iTextSharp is a legacy, and you really need to move to iText7.
Nonetheless, that is not a PowerShell code issue. It is an iTextSharp.dll missing dependency. Even with iText7, you need to ensure you have all the dependencies on your machine and properly loaded.
As noted in this SO Q&A:
How to use Itext7 in powershell V5, Exception when loading pdfWriter
1st:
After finding the correct version (1.8.6) on nuget.org the Add-Type commands work perfectly. As expected I didn't even need the unblock command as it was not marked as a blocked file in the properties. Now the script starts with:
Add-Type -Path 'c:\BouncyCastle.Crypto.dll'
Add-Type -Path 'c:\itextsharp.dll'
2nd
Regarding the check-queue: I just had to replace -contains with -match in my if clause.
if ($Extract -Contains 'Lieferschein')

How do I correctly pass parameters to ImageMagick from PowerShell?

The following code works perfectly from the command line to combine two TIFF files.
magick -quiet file1.tif file2.tif -compress JPEG filecombined.tif
However, when I try to use it in PowerShell, I am getting many errors from Magick that indicate that it is not getting the correct parameters. My PowerShell code looks something like the following.
$InputFiles = 'files1.tif file2.tif'
$DestinationFile = 'filecombined.tif'
$magick -quiet $InputFiles -compress JPEG $DestinationFile
This gives me errors stating that it cannot find the input files and the message indicates that it thinks it is one filename instead of two. In PowerShell v4, I was able to get it to work by quoting each of the names. Not sure why this helped, but the names did not have spaces. However, I had to upgrade to v5 and this method broke.
I tried using a temporary file to store the input filenames, but this just caused a different error.
$InputFiles = 'files1.tif file2.tif'
$InputFiles | Out-File list.tmp
$DestinationFile = 'filecombined.tif'
$magick -quiet '#list.tmp' -compress JPEG $DestinationFile
magick.exe: unable to open image '#z:ÿþz
Put all the parameters for Magick into an array and use the call (&) operator to execute the command.
$MagickParameters = #( '-quiet' )
$MagickParameters += 'file1.tif'
$MagickParameters += 'file2.tif'
$MagickParameters += #( '-compress', 'JPEG' )
$MagickParameters += 'filecombined.tif'
&'magick' $MagickParameters
This may not be the most efficient use of arrays, but similar methods are possible if performance is a concern.
I had a large collection of EPS images in several folders that I had to convert to PNG. I tested many Image Conversion programs, but most could not handle recursive conversion of Vector to Raster without choking (most displayed errors after processing a limited number of files. Some could not convert recursively through many subfolders). I created the following Powershell script from various sources that solved my problem and made recursive conversion of many files and folders easier. You can modify the file to perform any ImageMagick functions you need.
Have Fun.
# Recursive-Convert-EPS-to-PNG.ps1
$srcfolder = "C:\Temp"
$destfolder = "C:\Temp"
$im_convert_exe = "convert.exe"
$src_filter = "*.eps"
$dest_ext = "png"
$options = "-depth 8 -colorspace gray -threshold 40% -alpha off"
$logfile = "C:\temp\convert.log"
$fp = New-Item -ItemType file $logfile -force
$count=0
foreach ($srcitem in $(Get-ChildItem $srcfolder -include $src_filter -recurse))
{
$srcname = $srcitem.fullname
# Construct the filename and filepath for the output
$partial = $srcitem.FullName.Substring( $srcfolder.Length )
$destname = $destfolder + $partial
$destname= [System.IO.Path]::ChangeExtension( $destname , $dest_ext )
$destpath = [System.IO.Path]::GetDirectoryName( $destname )
# Create the destination path if it does not exist
if (-not (test-path $destpath))
{
New-Item $destpath -type directory | Out-Null
}
# Perform the conversion by calling an external tool
$cmdline = $im_convert_exe + " `"" + $srcname + " `"" + $options + " `"" + $destname + " `""
#echo $cmdline
invoke-expression -command $cmdline
# Get information about the output file
$destitem = Get-item $destname
# Show and record information comparing the input and output files
$info = [string]::Format( "{0} `t {1} `t {2} `t {3} `t {4} `t {5}", $count, $partial, $srcname, $destname, $srcitem.Length , $destitem.Length)
echo $info
Add-Content $fp $info
$count=$count+1
}

How to parse and delete archived event logs in Powershell

I'm trying to parse archived Security logs to track down an issue with changing permissions. This script greps through .evtx files that are +10 days old. It currently outputs what I want, but when it goes to clean up the old logs (About 50GB/daily, uncompressed, each of which are archived into their own daily folder via another script that runs at midnight) it begins complaining that the logs are in use and cannot be deleted. The process that seems to be in use when I try to delete the files through Explorer is alternately DHCP Client or Event Viewer, stopping both of these services works, but clearly I can't run without eventvwr. DHCP client is used for networking niceness but is not needed.
The only thing that touches the .evtx files is this script, they're not backed up, they're not monitored by anything else, they're not automatically parsed by the Event Log service, they're just stored on disk waiting.
The script originally deleted things as it went, but then since that failed all the deletions were moved to the end, then to the KillLogWithFire() function. Even the timer doesn't seem to help. I've also tried moving the files to a Processed subfolder, but that does't work for the same reason.
I assume that there's some way to release any handles that this script opens on any files, but attempting to .close() or .dispose() of the EventLog variable in the loop doesn't work.
$XPath = #'
*[System[Provider/#Name='Microsoft-Windows-Security-Auditing']]
and
*[System/EventID=4670]
'#
$DeletableLogs = #()
$logfile = "L:\PermChanges.txt"
$AdminUsers = ("List","of","Admin","Users")
$today = Get-Date
$marker = "
-------------
$today
-------------
"
write-output $marker >> $logfile
Function KillLogWithFire($log){
Try {
remove-item $log
}
Catch [writeerror]{
$Timer += 1
sleep $timer
write-output "Killing log $log in $timer seconds"
KillLogWithFire($log)
}
}
Function LogPermissionChange($PermChanges){
ForEach($PermChange in $PermChanges){
$Change = #{}
$Change.ChangedBy = $PermChange.properties[1].value.tostring()
#Filter out normal non-admin users
if ($AdminUsers -notcontains $Change.ChangedBy){continue}
$Change.FileChanged = $PermChange.properties[6].value.tostring()
#Ignore temporary files
if ($Change.FileChanged.EndsWith(".tmp")){continue}
elseif ($Change.FileChanged.EndsWith(".partial")){continue}
$Change.MadeOn = $PermChange.TimeCreated.tostring()
$Change.OriginalPermissions = $PermChange.properties[8].value.tostring()
$Change.NewPermissions = $PermChange.properties[9].value.tostring()
write-output "{" >> $logfile
write-output ("Changed By : "+ $Change.ChangedBy) >> $logfile
write-output ("File Changed : "+ $Change.FileChanged) >> $logfile
write-output ("Change Made : "+ $Change.MadeOn) >> $logfile
write-output ("Original Permissions :
"+ $Change.OriginalPermissions) >> $logfile
write-output ("New Permissions :
"+ $Change.NewPermissions) >> $logfile
"}
" >> $logfile
}
}
GCI -include Archive-Security*.evtx -path L:\Security\$Today.AddDays(-10) -recurse | ForEach-Object{
Try{
$PermChanges = Get-WinEvent -Path $_ -FilterXPath $XPath -ErrorAction Stop
}
Catch [Exception]{
if ($_.Exception -match "No events were found that match the specified selection criteria."){
}
else {
Throw $_
}
}
LogPermissionChange($PermChanges)
$PermChanges = $Null
$DeletableLogs += $_
}
foreach ($log in $DeletableLogs){
$Timer = 0
Try{
remove-item $log
}
Catch [IOException]{
KillLogWithFire($log)
}
}
UPDATE
Rather than editing the original code as I've been told not to do, I wanted to post the full code that's now in use as a separate answer. The Initial part, which parses the logs and is run every 30 minutes is mostly the same as above:
$XPath = #'
*[System[Provider/#Name='Microsoft-Windows-Security-Auditing']]
and
*[System/EventID=4670]
'#
$DeletableLogs = #()
$logfile = "L:\PermChanges.txt"
$DeleteList = "L:\DeletableLogs.txt"
$AdminUsers = ("List","Of","Admins")
$today = Get-Date
$marker = "
-------------
$today
-------------
"
write-output $marker >> $logfile
Function LogPermissionChange($PermChanges){
ForEach($PermChange in $PermChanges){
$Change = #{}
$Change.ChangedBy = $PermChange.properties[1].value.tostring()
#Filter out normal non-admin users
if ($AdminUsers -notcontains $Change.ChangedBy){continue}
$Change.FileChanged = $PermChange.properties[6].value.tostring()
#Ignore temporary files
if ($Change.FileChanged.EndsWith(".tmp")){continue}
elseif ($Change.FileChanged.EndsWith(".partial")){continue}
$Change.MadeOn = $PermChange.TimeCreated.tostring()
$Change.OriginalPermissions = $PermChange.properties[8].value.tostring()
$Change.NewPermissions = $PermChange.properties[9].value.tostring()
write-output "{" >> $logfile
write-output ("Changed By : "+ $Change.ChangedBy) >> $logfile
write-output ("File Changed : "+ $Change.FileChanged) >> $logfile
write-output ("Change Made : "+ $Change.MadeOn) >> $logfile
write-output ("Original Permissions :
"+ $Change.OriginalPermissions) >> $logfile
write-output ("New Permissions :
"+ $Change.NewPermissions) >> $logfile
"}
" >> $logfile
}
}
GCI -include Archive-Security*.evtx -path L:\Security\ -recurse | ForEach-Object{
Try{
$PermChanges = Get-WinEvent -Path $_ -FilterXPath $XPath -ErrorAction Stop
}
Catch [Exception]{
if ($_.Exception -match "No events were found that match the specified selection criteria."){
}
else {
Throw $_
}
}
LogPermissionChange($PermChanges)
$PermChanges = $Null
$DeletableLogs += $_
}
foreach ($log in $DeletableLogs){
write-output $log.FullName >> $DeleteList
}
The second portion does the deletion, including the helper function above graciously provided by TheMadTechnician. The code still loops as the straight delete is faster than the function, but not always successful even ages after the files have not been touched.:
# Log Cleanup script. Works around open log issues caused by PS parsing of
# saved logs in EventLogParser.ps1
$DeleteList = "L:\DeletableLogs.txt"
$DeletableLogs = get-content $DeleteList
Function Close-LockedFile{
Param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)][String[]]$Filename
)
Begin{
$HandleApp = 'C:\sysinternals\Handle.exe'
If(!(Test-Path $HandleApp)){Write-Host "Handle.exe not found at $HandleApp`nPlease download it from www.sysinternals.com and save it in the afore mentioned location.";break}
}
Process{
$HandleOut = Invoke-Expression ($HandleApp+' '+$Filename)
$Locks = $HandleOut |?{$_ -match "(.+?)\s+pid: (\d+?)\s+type: File\s+(\w+?): (.+)\s*$"}|%{
[PSCustomObject]#{
'AppName' = $Matches[1]
'PID' = $Matches[2]
'FileHandle' = $Matches[3]
'FilePath' = $Matches[4]
}
}
ForEach($Lock in $Locks){
Invoke-Expression ($HandleApp + " -p " + $Lock.PID + " -c " + $Lock.FileHandle + " -y") | Out-Null
If ( ! $LastexitCode ) { "Successfully closed " + $Lock.AppName + "'s lock on " + $Lock.FilePath}
}
}
}
Function KillLogWithFire($log){
Try {
Close-LockedFile $Log -
}
Catch [System.IO.IOException]{
$Timer += 1
sleep $timer
write-host "Killing $Log in $Timer seconds with fire."
KillLogWithFire($Log)
}
}
foreach ($log in $DeletableLogs){
Try {
remove-item $log -ErrorAction Stop
}
Catch [System.IO.IOException]{
$Timer = 0
KillLogWithFire($Log)
}
}
remove-item $DeleteList
One solution would be to get HANDLE.EXE and use it to close any open handles. Here's a function that I use roughly based off of this script. It uses handle.exe, finds what has a file locked, and then closes handles locking that file open.
Function Close-LockedFile{
Param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)][String[]]$Filename
)
Begin{
$HandleApp = 'C:\sysinternals\Handle.exe'
If(!(Test-Path $HandleApp)){Write-Host "Handle.exe not found at $HandleApp`nPlease download it from www.sysinternals.com and save it in the afore mentioned location.";break}
}
Process{
$HandleOut = Invoke-Expression ($HandleApp+' '+$Filename)
$Locks = $HandleOut |?{$_ -match "(.+?)\s+pid: (\d+?)\s+type: File\s+(\w+?): (.+)\s*$"}|%{
[PSCustomObject]#{
'AppName' = $Matches[1]
'PID' = $Matches[2]
'FileHandle' = $Matches[3]
'FilePath' = $Matches[4]
}
}
ForEach($Lock in $Locks){
Invoke-Expression ($HandleApp + " -p " + $Lock.PID + " -c " + $Lock.FileHandle + " -y") | Out-Null
If ( ! $LastexitCode ) { "Successfully closed " + $Lock.AppName + "'s lock on " + $Lock.FilePath}
}
}
}
I have handle.exe saved in C:\Sysinternals, you may want to adjust the path in the function, or save the executable there.
I was having a very similar problem and after lots of searching found this article. Whilst handle.exe worked when I first tried I did note the -c carries a warning "Closing handles can cause application or system instability"
I am also using get-winevent and it seems to (sometimes) lock the .evtx file being processed. I have written a loop to wait 5 secs an retry. Sometimes it takes up to 2 minutes or the file to be released, I have had one run overnight and it had hundreds of retries.
When I used handle the first time it worked perfectly. I then implemented it into the script and later found it to be looping an "unexplained error". I ended up having to reboot the server to get things working again so removed the handle.exe from the script and back to waiting for the file to be closed.
I can reliably release the file by stopping the script and closing down the powershell ise. As soon as the ISE is closed the file can be deleted without a problem.
Unfortunately I need this script to keep running and not be held up by the file remaining open. I am surprised that have to resort to sysinternals to release the file and that powershell does not offer an easy way to close the file.
I had the same issue as GTEM where closing the handles would cause corruption when processing hundreds of event log files. Eventually Get-WinEvent would not work properly. It would either freeze or give me the same "unexplained error".
So I opened a premier case with MS. They lead me to the actual variable I was storing the Get-WinEvent events in was what was locking the file. I guess it doesn't actually unlock the file if you are still using that variable. So to resolve this I added some code to my script after I transferred the variable to a new variable. You can see the code I added in the 3rd region listed below.
#***************************************************************************
#region *** Get the log entries.
# clear the log entry for each pass
$LogEntry = #()
# Get the vent from the log file and export it to the logentry variable and output to the screen
Get-WinEvent -Path $NewPath -FilterXPath $XPathFilter -ErrorAction SilentlyContinue | Tee-Object -Variable LogEntry
#endregion *** End get the log entries
#***************************************************************************
#***************************************************************************
#region *** This is where I copy it to the new variable for later output.
# if there are any log entries
if ($LogEntry.Count -gt 0) {
# Add the log entries to the log file
$LogEntries += $LogEntry
} # if there are any log entries
#endregion *** End were I copy to the new variable.
#***************************************************************************
#***************************************************************************
#region *** This is where I added code to allow me to remove the file lock.
# Remove the variable to release the evtx file lock
Remove-Variable -Name LogEntry
# Garbage collect to remove any additional memory tied to the file lock.
[GC]::Collect()
# sleep for 1 seconds
Sleep -Seconds 1
#endregion **** Code to remove the file lock.
#***************************************************************************
After this was done, I no longer have to use Handle.exe to close the file anymore.

How to retrieve a recursive directory and file list from PowerShell excluding some files and folders?

I want to write a PowerShell script that will recursively search a directory, but exclude specified files (for example, *.log, and myFile.txt), and also exclude specified directories, and their contents (for example, myDir and all files and folders below myDir).
I have been working with the Get-ChildItem CmdLet, and the Where-Object CmdLet, but I cannot seem to get this exact behavior.
I like Keith Hill's answer except it has a bug that prevents it from recursing past two levels. These commands manifest the bug:
New-Item level1/level2/level3/level4/foobar.txt -Force -ItemType file
cd level1
GetFiles . xyz | % { $_.fullname }
With Hill's original code you get this:
...\level1\level2
...\level1\level2\level3
Here is a corrected, and slightly refactored, version:
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
}
}
}
With that bug fix in place you get this corrected output:
...\level1\level2
...\level1\level2\level3
...\level1\level2\level3\level4
...\level1\level2\level3\level4\foobar.txt
I also like ajk's answer for conciseness though, as he points out, it is less efficient. The reason it is less efficient, by the way, is because Hill's algorithm stops traversing a subtree when it finds a prune target while ajk's continues. But ajk's answer also suffers from a flaw, one I call the ancestor trap. Consider a path such as this that includes the same path component (i.e. subdir2) twice:
\usr\testdir\subdir2\child\grandchild\subdir2\doc
Set your location somewhere in between, e.g. cd \usr\testdir\subdir2\child, then run ajk's algorithm to filter out the lower subdir2 and you will get no output at all, i.e. it filters out everything because of the presence of subdir2 higher in the path. This is a corner case, though, and not likely to be hit often, so I would not rule out ajk's solution due to this one issue.
Nonetheless, I offer here a third alternative, one that does not have either of the above two bugs. Here is the basic algorithm, complete with a convenience definition for the path or paths to prune--you need only modify $excludeList to your own set of targets to use it:
$excludeList = #("stuff","bin","obj*")
Get-ChildItem -Recurse | % {
$pathParts = $_.FullName.substring($pwd.path.Length + 1).split("\");
if ( ! ($excludeList | where { $pathParts -like $_ } ) ) { $_ }
}
My algorithm is reasonably concise but, like ajk's, it is less efficient than Hill's (for the same reason: it does not stop traversing subtrees at prune targets). However, my code has an important advantage over Hill's--it can pipeline! It is therefore amenable to fit into a filter chain to make a custom version of Get-ChildItem while Hill's recursive algorithm, through no fault of its own, cannot. ajk's algorithm can be adapted to pipeline use as well, but specifying the item or items to exclude is not as clean, being embedded in a regular expression rather than a simple list of items that I have used.
I have packaged my tree pruning code into an enhanced version of Get-ChildItem. Aside from my rather unimaginative name--Get-EnhancedChildItem--I am excited about it and have included it in my open source Powershell library. It includes several other new capabilities besides tree pruning. Furthermore, the code is designed to be extensible: if you want to add a new filtering capability, it is straightforward to do. Essentially, Get-ChildItem is called first, and pipelined into each successive filter that you activate via command parameters. Thus something like this...
Get-EnhancedChildItem –Recurse –Force –Svn
–Exclude *.txt –ExcludeTree doc*,man -FullName -Verbose
... is converted internally into this:
Get-ChildItem | FilterExcludeTree | FilterSvn | FilterFullName
Each filter must conform to certain rules: accepting FileInfo and DirectoryInfo objects as inputs, generating the same as outputs, and using stdin and stdout so it may be inserted in a pipeline. Here is the same code refactored to fit these rules:
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 }
}
}
}
The only additional piece here is the Coalesce-Args function (found in this post by Keith Dahlby), which merely sends the current directory down the pipe in the event that the invocation did not specify any paths.
Because this answer is getting somewhat lengthy, rather than go into further detail about this filter, I refer the interested reader to my recently published article on Simple-Talk.com entitled Practical PowerShell: Pruning File Trees and Extending Cmdlets where I discuss Get-EnhancedChildItem at even greater length. One last thing I will mention, though, is another function in my open source library, New-FileTree, that lets you generate a dummy file tree for testing purposes so you can exercise any of the above algorithms. And when you are experimenting with any of these, I recommend piping to % { $_.fullname } as I did in the very first code fragment for more useful output to examine.
The Get-ChildItem cmdlet has an -Exclude parameter that is tempting to use but it doesn't work for filtering out entire directories from what I can tell. Try something like this:
function GetFiles($path = $pwd, [string[]]$exclude)
{
foreach ($item in Get-ChildItem $path)
{
if ($exclude | Where {$item -like $_}) { continue }
if (Test-Path $item.FullName -PathType Container)
{
$item
GetFiles $item.FullName $exclude
}
else
{
$item
}
}
}
Here's another option, which is less efficient but more concise. It's how I generally handle this sort of problem:
Get-ChildItem -Recurse .\targetdir -Exclude *.log |
Where-Object { $_.FullName -notmatch '\\excludedir($|\\)' }
The \\excludedir($|\\)' expression allows you to exclude the directory and its contents at the same time.
Update: Please check the excellent answer from msorens for an edge case flaw with this approach, and a much more fleshed out solution overall.
Recently, I explored the possibilities to parameterize the folder to scan through and the place where the result of recursive scan will be stored. At the end, I also did summarize the number of folders scanned and number of files inside as well. Sharing it with community in case it may help other developers.
##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}";
A bit late, but try this one.
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'
Commenting here as this seems to be the most popular answer on the subject for searching for files whilst excluding certain directories in powershell.
To avoid issues with post filtering of results (i.e. avoiding permission issues etc), I only needed to filter out top level directories and that is all this example is based on, so whilst this example doesn't filter child directory names, it could very easily be made recursive to support this, if you were so inclined.
Quick breakdown of how the snippet works
$folders << Uses Get-Childitem to query the file system and perform folder exclusion
$file << The pattern of the file I am looking for
foreach << Iterates the $folders variable performing a recursive search using the Get-Childitem command
$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 }
}