How to improve performance of 3 successive GCI -Recurse calls? - powershell

Powershell noob here.
In order to create a list of potential duplicate dirs, I have a loop that runs the following 3 GCI commands on all directories to get the total size, number of files and number of directories below the currently examins dir:
$folderSize = Get-Childitem -Path $fullPath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
$folderDirs = Get-ChildItem -Path $fullPath -Recurse -Force -ErrorAction SilentlyContinue -Directory | Measure-Object -ErrorAction SilentlyContinue
$folderFiles = Get-ChildItem -Path $fullPath -Recurse -Force -ErrorAction SilentlyContinue -File | Measure-Object -ErrorAction SilentlyContinue
The code is working fine but it seems really dumb to run 3 times a GCI with the recurse parameter on the same path. What would be a more efficient way to get those 3 informations for a given directory?

Store the results of the where first query in a variable, use the .Where({}) extension method to split them into categories based on the PSIsContainer property - at which point you can reference the automagical Count property of each (rather than invoking Measure-Object for the simple act of counting the items):
$allFileSystemItems = Get-Childitem -Path $fullPath -Recurse -Force -ErrorAction SilentlyContinue
$size = $allFileSystemItems |Measure-Object Length -Sum
# split collections into "directories" and "files"
$dirs,$files = $allFileSystemItems.Where({$_.PsIscontainer}, 'Split')
$dirCount = $dirs.Count
$fileCount = $files.Count

Related

I'm writing my first powershell script to remove old TMP/LOG files on exchange

I'm writing a custom script to keep our Exchange servers clean. It consists of several parts.
The last part is to clean TEMP folders, and it's working with no problems.
The first part is where my problem is. I want to select all .BAK .TMP and .XML files and delete them if they are over 3 days old, and select and delete all .log files if they are over 30 days old. But no files are being selected.
$Path ="$env:SystemDrive\Program Files (x86)\GFI\MailEssentials\EmailSecurity\DebugLogs\", "$env:SystemDrive\Program Files (x86)\GFI\MailEssentials\AntiSpam\DebugLogs\", "$env:SystemDrive\inetpub\logs", "$env:windir\System32\LogFiles"
# How long do you want to keep files by default?
$Daysback = "3"
# How long do you want to keep .log files? (Recommended 30 days at least)
$DaysbackLog = "30"
$DatetoDelete = (Get-Date).AddDays(-$Daysback)
$DatetoDeleteLog = (Get-Date).AddDays(-$DaysbackLog)
Get-ChildItem $Path -Recurse -Hidden | Where-Object {($_.extension -like ".log" -and $_.LastWriteTime -lt $DatetoDeleteLog)} | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -WhatIf
Get-ChildItem $Path -Recurse -Hidden | Where-Object {($_.extension -like ".bak", "tmp", "xml" -and $_.LastWriteTime -lt $DatetoDelete)} | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -WhatIf
# The following lines clears temp folder and empty folders in the temp folder.
Get-ChildItem "$env:windir\Temp", "$env:TEMP" -recurse | Where-Object { $_.LastWriteTime -lt $DatetoDelete } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -WhatIf
Get-ChildItem "$env:windir\Temp", "$env:TEMP" -recurse | Where-Object { $_.LastWriteTime -lt $DatetoDelete } | Where {$_.PSIsContainer -and #(Get-ChildItem -LiteralPath:$_.fullname).Count -eq 0} | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -WhatIf
There are a few ways to do this, but much of it is based on personal preference and/or performance. The latter of which is not likely to be a big design factor here.
$Path = #(
"$env:SystemDrive\Program Files (x86)\GFI\MailEssentials\EmailSecurity\DebugLogs\"
"$env:SystemDrive\Program Files (x86)\GFI\MailEssentials\AntiSpam\DebugLogs\"
"$env:SystemDrive\inetpub\logs"
"$env:windir\System32\LogFiles"
)
# Extensions
$Extensions = "*.bak", "*.tmp", "*.xml"
# Temp folders to clean up
$Temps = "$env:windir\Temp", "$env:TEMP"
# How long do you want to keep files by default?
$Daysback = "3"
# How long do you want to keep .log files? (Recommended 30 days at least)
$DaysbackLog = "30"
$DatetoDelete = (Get-Date).AddDays(-$Daysback)
$DatetoDeleteLog = (Get-Date).AddDays(-$DaysbackLog)
Get-ChildItem $Path -Filter "*.log" -Recurse -Hidden |
Where-Object { $_.LastWriteTime -le $DatetoDeleteLog } |
Remove-Item -Force -ErrorAction SilentlyContinue -WhatIf
# > Move filtering left, which works because you are only looking for a single
# extension.
# > Change to -le to accommodate edge case where $_.LastWriteTime is right on
# the boundary.
$Extensions |
ForEach-Object{
Get-ChildItem $Path -Filter $_ -Recurse -Hidden
} |
Where-Object { $_.LastWriteTime -le $DatetoDelete } |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -WhatIf
# Set up extensions as an array of wild card filters.
# -Filter is much faster than -Include which may be another alternative approach
Get-ChildItem $Temps -File -Recurse |
Where-Object { $_.LastWriteTime -le $DatetoDelete } |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -WhatIf
Get-ChildItem $Temps -Directory -Recurse |
Where-Object { !$_.GetFileSystemInfos() } |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -WhatIf
I haven't tested any of the refactor. However, the approach is to simply rerun the Get-ChildItem cmdlet for each needed scenario. In my experience that's faster than trying to use the -Include parameter to grab all the extensions in 1 shot, while still be faster and easier to read than adding to a Where{} clause to filter on extension.
In the part for clearing the temp folders. I use the .Net Method .GetFileSystemInfos() on the [System.IO.DirectoryInfo] objects returned from Get-ChildItem. The method returns an array of all child objects, so if it's null we know the folder is empty. That sounds complicated, but as you can see it significantly shrinks the code and will likely perform better. I use the -File & -Directory parameters respectively to make sure to make sure I've got the right object types.
This is a little more advanced, but another way I played with to clean up the temp folders is to use a ForEach-Object loop with 2 process blocks.
$Temps |
ForEach-Object -Process {
# 1st process block get Empty directories:
Get-ChildItem -Directory -Recurse |
Where-Object{ !$_.GetFileSystemInfos() }
}, {
# 2nd process block get files older than the boundary date.
Get-ChildItem -File -Recurse |
Where-Object { $_.LastWriteTime -le $DatetoDelete }
} |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -WhatIf
Again untested, and I'm not sure how this will preform. Nevertheless, since I developed it thought I'd share.
Note: the -Process argument is necessary so that ForEach-Object assigns both block to process.
Check out ForEach-Object with Multiple Script Blocks for more information.

How do I get list of all the files on a computer using Powershell [duplicate]

I'm new to Powershell and I've tried finding a solution to my problem online but I can't seem to find one. Basically I need to write something that will let powershell look through all drives and directories to find the following:
total number of files (exclude folders), largest file size, average file size, total file size
Here's what I've written so far:
$source = "get-psdrive"
#attempt at looking through all drives/directories which didn't work
foreach($a in $source) {
#counts files in given directory
Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object |
ForEach-Object { $_.Count }
#select largest file in given directory
Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue -Force |
Sort-Object Length -Descending |
Select-Object -First 1
#get average file size in a given directory
Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object -Property Length -Average
#sum of file sizes in given directory
(Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object -Sum Length).Sum
}
The issue is that it only looks in the C drive. I don't know if there's a way to look through my computer instead of specific drives but I couldn't seem to find a way. Any help is appreciated.
With "..." you define a string, so in your example you try to loop over a string.
Try it like this:
$Drives = Get-PSDrive -PSProvider 'FileSystem'
foreach($Drive in $drives) {
#counts files in given directory
Get-ChildItem -Path $Drive.Root -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object |
ForEach-Object { $_.Count }
#select largest file in given directory
Get-ChildItem -Path $Drive.Root -Recurse -File -ErrorAction SilentlyContinue -Force |
Sort-Object Length -Descending |
Select-Object -First 1
#get average file size in a given directory
Get-ChildItem -Path $Drive.Root -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object -Property Length -Average
#sum of file sizes in given directory
(Get-ChildItem -Path $Drive.Root -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object -Sum Length).Sum
}
Note! I didn't chang your Get-ChildItem code (except the path where it will search). I only wrote the foreach loop.
Could make it a bit faster by feeding the results into a variable, instead of running GCI it 4 times each loop. Something like this:
#specify preferred measures
$average_size = "MB"
$individual_size = "GB"
$total_size = "TB"
Get-PSDrive -PSProvider FileSystem |ForEach-Object {
"`n$($_.Root)" |Write-Host -ForegroundColor Cyan
$details=$null
$details = Get-ChildItem -Path $_.Root -Recurse -File -ErrorAction SilentlyContinue -Force
"`nFile Count: $('{0:N0}' -f $details.count)"
"Average size: "+"$([math]::round(($details |
Measure-Object -Property Length -Average |select -Property average).average /"1$($average_size)", 2))"+" $($average_size)"
"Total size: "+"$([math]::round(($details |
Measure-Object -Property Length -sum |select -Property sum).sum /"1$($total_size)", 2))"+" $($total_size)"
$details |
Sort-Object Length -Descending |
Select-Object -First 5 -Property #{L='Path';E ={$_.fullname}},#{L="Size $($individual_size)";E={[math]::round(($_.length) /"1$($individual_size)", 3)}} |ft
}
Couldn't stretch it to assign sizes dynamically, but where do you draw the line...

Search for *.dll but only output those of a specific version using Powershell

I have this script in Powershell:
$100DLLPath = (Get-ChildItem -Path c:\ -Recurse -erroraction 'silentlycontinue' -Include *100.dll)
$VersionInfo100 = (Get-ChildItem $100DLLPath).VersionInfo
Write-Host $VersionInfoATL100 | FT
Now this will display the properties of any *.100.dll. What I want to do is search for those same DLLs but only display those that meet a certain fileversion.
To compare versions, it is best to use them not as strings, but as System.Version objects.
Something like this:
# the version you want to comare against
[version]$version = '2000.1.2.3' # Major.Minor.Build.Revision
Get-ChildItem -Path C:\ -Filter '*100.dll' -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.VersionInfo.FileVersionRaw -ge $version } | # or any other comparison operator
ForEach-Object { $_.VersionInfo } | Format-Table -AutoSize
Instead of FileVersionRaw, you may also want to do this against the ProductVersionRaw property. Both are [version] objects.
$100DLLPath = (Get-ChildItem -Path c:\ -Recurse -erroraction 'silentlycontinue' -Include *100.dll)
foreach ($dll in $100DLLPath) {
if ($dll.VersionInfo -match '1\.2\.3\.4') {
## emit file info
$dll.VersionInfo
}
}

With PowerShell's Get-ChildItem, how to I list matching files AND count them at the same time

How do I show AND count matching files using a single Get-ChildItem command in PowerShell? Currently I am using two Get-ChildItem commands, the first to count, the second to display the files - works fine but it is not very effective when scanning an entire disk ...
Command to count the matches:
$count = Get-ChildItem -Path $searchLocation -Filter $filename -Recurse -ErrorAction SilentlyContinue | Measure-Object | %{$_.Count}
Command to display the files:
Get-ChildItem -Path $searchLocation -Filter $filename -Recurse -ErrorAction SilentlyContinue | %{$_.FullName}
As Get-ChildItem is returning an array, its size is stored in .Length member and explicit measurement is not needed. Thus, store the file names in the same collection and then print length for number of entries and iterate the collection for file names. Swapping the variable name as $files to reflect this like so,
$files = Get-ChildItem -Path $searchLocation -Filter $filename `
-Recurse -ErrorAction SilentlyContinue
# ` can used to divide command into multiple lines (and work-around for markup stupidness)
# prints the number of items
$files.Length
# prints the full names
$files | %{$_.FullName}
An alternative method is to add a file number to each file as it processes.
$i = 1
$Files = Get-ChildItem -Path $searchLocation -Filter $filename -Recurse -ErrorAction SilentlyContinue
Foreach($Item in $Files) {
$Item | Add-Member -MemberType NoteProperty -Name FileNo -Value $i
$i++
}
$Files | Select-Object FileNo, Fullname
You can then see the order the files were processed, get the last file number by doing $File[-1].FileNo. And it will maintain all the additional file metadata suck as CreationTime, DirectoryName, VersionInfo etc.
Simply like this :
$AllFile=Get-ChildItem $searchLocation -File -Filter $filename -Recurse | select FullName
$AllFile.Count
$AllFile.FullName
Or you can ad a rank into your loop like this :
$Rang=0
Get-ChildItem "c:\temp" -File -Filter "*.txt" -Recurse | %{
$Rang++
Add-Member -InputObject $_ -Name "Rang" -MemberType NoteProperty -Value $rang
$_
} | select Rang, FullName

Powershell, look through all drives

I'm new to Powershell and I've tried finding a solution to my problem online but I can't seem to find one. Basically I need to write something that will let powershell look through all drives and directories to find the following:
total number of files (exclude folders), largest file size, average file size, total file size
Here's what I've written so far:
$source = "get-psdrive"
#attempt at looking through all drives/directories which didn't work
foreach($a in $source) {
#counts files in given directory
Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object |
ForEach-Object { $_.Count }
#select largest file in given directory
Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue -Force |
Sort-Object Length -Descending |
Select-Object -First 1
#get average file size in a given directory
Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object -Property Length -Average
#sum of file sizes in given directory
(Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object -Sum Length).Sum
}
The issue is that it only looks in the C drive. I don't know if there's a way to look through my computer instead of specific drives but I couldn't seem to find a way. Any help is appreciated.
With "..." you define a string, so in your example you try to loop over a string.
Try it like this:
$Drives = Get-PSDrive -PSProvider 'FileSystem'
foreach($Drive in $drives) {
#counts files in given directory
Get-ChildItem -Path $Drive.Root -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object |
ForEach-Object { $_.Count }
#select largest file in given directory
Get-ChildItem -Path $Drive.Root -Recurse -File -ErrorAction SilentlyContinue -Force |
Sort-Object Length -Descending |
Select-Object -First 1
#get average file size in a given directory
Get-ChildItem -Path $Drive.Root -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object -Property Length -Average
#sum of file sizes in given directory
(Get-ChildItem -Path $Drive.Root -Recurse -File -ErrorAction SilentlyContinue -Force |
Measure-Object -Sum Length).Sum
}
Note! I didn't chang your Get-ChildItem code (except the path where it will search). I only wrote the foreach loop.
Could make it a bit faster by feeding the results into a variable, instead of running GCI it 4 times each loop. Something like this:
#specify preferred measures
$average_size = "MB"
$individual_size = "GB"
$total_size = "TB"
Get-PSDrive -PSProvider FileSystem |ForEach-Object {
"`n$($_.Root)" |Write-Host -ForegroundColor Cyan
$details=$null
$details = Get-ChildItem -Path $_.Root -Recurse -File -ErrorAction SilentlyContinue -Force
"`nFile Count: $('{0:N0}' -f $details.count)"
"Average size: "+"$([math]::round(($details |
Measure-Object -Property Length -Average |select -Property average).average /"1$($average_size)", 2))"+" $($average_size)"
"Total size: "+"$([math]::round(($details |
Measure-Object -Property Length -sum |select -Property sum).sum /"1$($total_size)", 2))"+" $($total_size)"
$details |
Sort-Object Length -Descending |
Select-Object -First 5 -Property #{L='Path';E ={$_.fullname}},#{L="Size $($individual_size)";E={[math]::round(($_.length) /"1$($individual_size)", 3)}} |ft
}
Couldn't stretch it to assign sizes dynamically, but where do you draw the line...