网站搜索

使用 PowerShell 查找修改的文件


这是我似乎经常使用 PowerShell 处理的另一项任务。我昨天处理了哪些文件?或者过去 48 小时内修改了哪些文件?显然,Get-ChildItem 将成为主要命令。从给定文件夹路径获取基于扩展名的文件非常简单,甚至可以递归获取。但要按年龄进行筛选,我必须使用Where-Object 并将LastWriteTime 属性(在本例中)与DateTime 值进行比较。顺便说一句,我所在的 PowerShell 工作组专注于 cmdlet,并且有人请求将日期时间过滤添加到 Get-ChildItem。不幸的是,.NET Framework 不提供创建或修改日期的早期过滤机制。任何过滤都必须在 Get-ChildItem 返回所有文件后完成,这与使用Where-Object 没有什么不同。最终结果将是在 Get-ChildItem 上进行大量工作而没有性能提升,因此不会实现此功能。但没关系。我有一个功能!

获取最后修改的文件

此函数应该适用于 Windows PowerShell 5.1 和 PowerShell 7.x。这是完整的文件。

#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)"
}

我在整个代码中添加了注释和帮助消息,因此您应该能够理解我在做什么。但我想指出几点。

首先,我在 Path 参数上使用 ValidateScript() 属性。

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
}

我正在测试 $Path 值是一个容器并且它是一个 FileSystem 对象。我本来可以简单地使用 IF 条件,但我想在验证失败时更好地控制错误消息。代码应该很清楚。这是它产生的结果。

这是来自 PowerShell 7。Windows PowerShell 还显示自定义错误消息以及更多异常信息。

绘制 PSBoundParameters

我的函数中的另一个功能是重用 PSBound 参数。因为几乎所有参数都与 Get-ChildItem 相同,所以我可以 splat $PSBoundParameters。但是,我需要删除无效参数 IntervalCount 和 Interval。

"IntervalCount", "Interval" | ForEach-Object {
    if ($PSBoundParameters.ContainsKey($_)) {
        [void]$PSBoundParameters.Remove($_)
    }
}

我还想添加 Get-ChildItem File 参数。

$PSBoundParameters.Add("file", $True)

从技术上讲,PowerShell 允许您进行操作并指定参数,但我想保持代码干净。除非我指定不同的值,否则具有默认值(例如 Filter 和 Path)的参数不会被检测为 PSBoundParameters。但这没关系,因为默认值与 Get-ChildItem 相同。

切换间隔

我的函数使用 ValidateSet() 属性作为间隔,并且我使用默认值“小时”。设置默认值时,请确保它属于您的验证集。这确保了我有一个可以在 Switch 语句中使用的已知值。

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) }
}

因为我有多个可能的值,所以这是比使用更复杂的 If/ElseIf/ElseIf/Else 语句更干净的方法。 Switch 语句生成一个 DateTime 值作为我的截止日期。我还应该指出, $IntervalCount 之前的破折号并不表明它是一个参数。它是减号。我从 $IntervalCount 添加一个负值。

使用Where()方法

我在最终过滤中使用 $Last 值。

(Get-ChildItem @PSBoundParameters).Where({$_.LastWriteTime -ge $last})

这段代码对您来说可能有点不同。我使用的是Where() 方法,而不是Where-Object cmdlet。我本来可以用这个:

Get-ChildItem @PSBoundParameters | Where-Object {$_.LastWriteTime -ge $last}

并得到相同的结果。 Where() 是集合或数组的内置方法。这就是 Get-ChildItem 表达式用括号括起来的原因。 Where() 方法中的语法与Where-Object 相同。区别在于性能。通常,该方法比使用 cmdlet 更快。在我的测试中,我们谈论的是毫秒差异,但越快越好,所以我会同意它。

函数输出是经过过滤的 Get-ChildItem 对象到管道,因此我可以以任何需要的方式使用它。

这些是我在过去 6 个月中处理过的文件类型。这 153 个没有扩展名的文件来自一个未隐藏的 .git 文件夹,我必须修复它。

概括

这就是我喜欢 PowerShell 的原因。经过一点努力,我创建了一个命令行工具,我可以在控制台上快速运行以找到我需要的东西。我什至定义了一个别名 glm,以使其在命令提示符下使用起来更加容易。我希望您能学到一两个可以在您自己的 PowerShell 脚本中使用的技巧。要了解有关参数验证选项的更多信息,请阅读 About_Functions_Advanced_Parameters 帮助主题。

相关文章