Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Finding Modified Files with PowerShell

Posted on October 14, 2021October 14, 2021

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!

Manage and Report Active Directory, Exchange and Microsoft 365 with
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.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d