Here's another task that I seem to be constantly fiddling with using PowerShell. What files did I work on yesterday? Or what files were modified in the last 48 hours? Obviously, Get-ChildItem is going to be the primary command. It is simple enough to get files based on an extension from a given folder path, even recursively. But to filter on age, I have to use Where-Object and compare, in this scenario the LastWriteTime property, to a DateTime value. By the way, I am in the PowerShell Working Group focused on cmdlets and there was a request to add DateTime filtering to Get-ChildItem. Unfortunately, the .NET Framework doesn't provide an early filtering mechanism for created or modified dates. Any filtering would have to be done after Get-ChildItem returned all the files, which is no different than using Where-Object. The end result would be a lot of work on Get-ChildItem with no performance gain so this feature won't be implemented. But that's ok. I have a function for that!
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Get-LastModifiedFile
This function should work in both Windows PowerShell 5.1 and PowerShell 7.x. Here's the full file.
#requires -version 5.1
Function Get-LastModifiedFile {
[cmdletbinding()]
[alias("glm")]
[OutputType("System.IO.FileInfo")]
Param(
[Parameter(Position = 0, HelpMessage = "Specify a file filter like *.ps1.")]
[ValidateNotNullOrEmpty()]
[string]$Filter = "*",
[Parameter(Position = 1, HelpMessage = "Specify the folder to search.")]
[ValidateScript({
#this will write a custom error message if validation fails
If ((Test-Path -path $_ -PathType Container) -AND ((Get-Item -path $_).psprovider.name -eq 'Filesystem')) {
return $True
}
else {
Throw "The specified Path value $_ is not a valid folder or not a file system location."
return $False
}
})]
[string]$Path = ".",
[Parameter(HelpMessage = "Specify the search interval based on the last write time.")]
[ValidateSet("Hours", "Minutes", "Days", "Months", "Years")]
[string]$Interval = "Hours",
[Parameter(HelpMessage = "Specify the number of intervals.")]
[alias("ic")]
[ValidateScript({$_ -ge 1})]
[int32]$IntervalCount = 24,
[Parameter(HelpMessage = "Recurse from the specified path.")]
[switch]$Recurse
)
Write-Verbose "Starting $($myinvocation.mycommand)"
$msg ="Searching {0} for {1} files modified in the last {2} {3}." -f (Convert-Path $Path),$filter,$IntervalCount,$Interval
Write-Verbose $msg
switch ($Interval) {
"minutes" { $last = (Get-Date).AddMinutes(-$IntervalCount) }
"hours" { $last = (Get-Date).AddHours(-$IntervalCount) }
"days" { $last = (Get-Date).AddDays(-$IntervalCount) }
"months" { $last = (Get-Date).AddMonths(-$IntervalCount) }
"years" { $last = (Get-Date).AddYears(-$IntervalCount) }
}
Write-Verbose "Cutoff date is $Last"
#remove bound parameters that don't belong to Get-ChildItem
"IntervalCount", "Interval" | ForEach-Object {
if ($PSBoundParameters.ContainsKey($_)) {
[void]$PSBoundParameters.Remove($_)
}
}
#add -File to PSBoundParameters
$PSBoundParameters.Add("file", $True)
if ($recurse) {
Write-Verbose "Recursing..."
}
else {
Write-Verbose "Searching..."
}
#get the files and filter on the LastWriteTime using the Where() method for
#better performance
(Get-ChildItem @PSBoundParameters).Where({$_.LastWriteTime -ge $last})
Write-Verbose "Ending $($myinvocation.mycommand)"
}
I've added comments and help messages throughout the code so you should be able to understand what I'm doing. But I want to point out a few things.
First, I am using a ValidateScript() attribute on the Path parameter.
If ((Test-Path -path $_ -PathType Container) -AND ((Get-Item -path $_).psprovider.name -eq 'Filesystem')) {
return $True
}
else {
Throw "The specified Path value $_ is not a valid folder or not a file system location."
return $False
}
I am testing that the $Path value is a container and that it is a FileSystem object. I could have simply used the IF condition, but I wanted to have more control over the error message if validation failed. The code should be pretty clear. Here's what it produces.
This is from PowerShell 7. Windows PowerShell also displays the custom error message plus a bit more of the exception.
Splatting PSBoundParameters
Another feature in my function is the re-use of PSBoundparameters. Because almost all of the parameters are the same as Get-ChildItem, I can splat $PSBoundParameters. However, I need to remove invalid parameters IntervalCount and Interval.
"IntervalCount", "Interval" | ForEach-Object {
if ($PSBoundParameters.ContainsKey($_)) {
[void]$PSBoundParameters.Remove($_)
}
}
I also want to add the Get-ChildItem File parameter.
$PSBoundParameters.Add("file", $True)
Technically, PowerShell lets you splat AND specify parameters but I wanted to keep my code clean. The parameters that have default values like Filter and Path won't be detected as PSBoundParameters unless I specify a different value. But that's ok because the default values are the same as Get-ChildItem.
Switch the Interval
My function uses a ValidateSet() attribute for the Interval and I am using a default value of Hours. When setting a default, make sure it belongs to your validation set. This ensures that I have a known value that I can use in a Switch statement.
switch ($Interval) {
"minutes" { $last = (Get-Date).AddMinutes(-$IntervalCount) }
"hours" { $last = (Get-Date).AddHours(-$IntervalCount) }
"days" { $last = (Get-Date).AddDays(-$IntervalCount) }
"months" { $last = (Get-Date).AddMonths(-$IntervalCount) }
"years" { $last = (Get-Date).AddYears(-$IntervalCount) }
}
Because I have multiple possible values, this is a cleaner approach than using a more complicated If/ElseIf/ElseIf/Else statement. The Switch statement produces a DateTime value that serves as my cutoff date. I should also point out that the dash before $IntervalCount is not indicating that it is a parameter. It is the minus sign. I am adding a negative value from $IntervalCount.
Using the Where() Method
I use the $Last value in my final filtering.
(Get-ChildItem @PSBoundParameters).Where({$_.LastWriteTime -ge $last})
This code may look a bit different to you. I am using the Where() method and not the Where-Object cmdlet. I could have used this:
Get-ChildItem @PSBoundParameters | Where-Object {$_.LastWriteTime -ge $last}
And get the same result. Where() is a built-in method on collections or arrays. That's why the Get-ChildItem expression is wrapped in parentheses. The syntax in the Where() method is the same as Where-Object. The difference is performance. Usually, the method is faster than using the cmdlet. In my testing, we're talking milliseconds difference, but faster is faster so I'll go with it.
The function output is the filtered Get-ChildItem objects to the pipeline so I can use it any way I need.
These are the file types I have worked on in the last 6 months. Those 153 files with no extensions are from an unhidden .git folder which I'll have to fix.
Summary
This is why I love PowerShell. With a little effort, I created a command-line tool I can quickly run at the console to find what I need. I even defined an alias, glm, to make it even easier to use at a command prompt. I hope you picked up a tip or two that you can use in your own PowerShell scripting. To learn more about parameter validation options, read the About_Functions_Advanced_Parameters help topic.