Búsqueda de sitios web

Encontrar archivos modificados con PowerShell


Aquí hay otra tarea con la que parece que estoy constantemente jugando usando PowerShell. ¿En qué archivos trabajé ayer? ¿O qué archivos se modificaron en las últimas 48 horas? Obviamente, Get-ChildItem será el comando principal. Es bastante sencillo obtener archivos basados en una extensión de una ruta de carpeta determinada, incluso de forma recursiva. Pero para filtrar por edad, tengo que usar Where-Object y comparar, en este escenario, la propiedad LastWriteTime, con un valor DateTime. Por cierto, estoy en el grupo de trabajo de PowerShell centrado en cmdlets y hubo una solicitud para agregar el filtrado DateTime a Get-ChildItem. Lamentablemente, .NET Framework no proporciona un mecanismo de filtrado temprano para las fechas de creación o modificación. Cualquier filtrado debería realizarse después de que Get-ChildItem devolviera todos los archivos, lo cual no es diferente a usar Where-Object. El resultado final sería mucho trabajo en Get-ChildItem sin ganancia de rendimiento, por lo que esta característica no se implementará. Pero eso esta bien. ¡Tengo una función para eso!

Obtener el último archivo modificado

Esta función debería funcionar tanto en Windows PowerShell 5.1 como en PowerShell 7.x. Aquí está el archivo completo.

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

Agregué comentarios y mensajes de ayuda a lo largo del código para que puedas entender lo que estoy haciendo. Pero quiero señalar algunas cosas.

Primero, estoy usando un atributo ValidateScript() en el parámetro Ruta.

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
}

Estoy probando que el valor $Path es un contenedor y que es un objeto FileSystem. Podría haber usado simplemente la condición IF, pero quería tener más control sobre el mensaje de error si fallaba la validación. El código debería ser bastante claro. Esto es lo que produce.

Esto es de PowerShell 7. Windows PowerShell también muestra el mensaje de error personalizado y un poco más de excepción.

Salpicando PSBoundParameters

Otra característica de mi función es la reutilización de PSBoundparameters. Debido a que casi todos los parámetros son los mismos que Get-ChildItem, puedo agregar $PSBoundParameters. Sin embargo, necesito eliminar los parámetros no válidos IntervalCount e Interval.

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

También quiero agregar el parámetro Get-ChildItem File.

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

Técnicamente, PowerShell te permite distribuir Y especificar parámetros, pero quería mantener mi código limpio. Los parámetros que tienen valores predeterminados como Filtro y Ruta no se detectarán como PSBoundParameters a menos que especifique un valor diferente. Pero está bien porque los valores predeterminados son los mismos que los de Get-ChildItem.

Cambiar el intervalo

Mi función usa un atributo ValidateSet() para el Intervalo y estoy usando un valor predeterminado de Horas. Al establecer un valor predeterminado, asegúrese de que pertenezca a su conjunto de validación. Esto asegura que tengo un valor conocido que puedo usar en una declaración 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) }
}

Debido a que tengo múltiples valores posibles, este es un enfoque más limpio que usar una declaración If/ElseIf/ElseIf/Else más complicada. La declaración Switch produce un valor DateTime que sirve como mi fecha límite. También debo señalar que el guión antes de $IntervalCount no indica que sea un parámetro. Es el signo menos. Estoy agregando un valor negativo de $IntervalCount.

Usando el método Where()

Utilizo el valor $Last en mi filtrado final.

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

Este código puede parecerle un poco diferente. Estoy usando el método Where() y no el cmdlet Where-Object. Podría haber usado esto:

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

Y obtener el mismo resultado. Where() es un método integrado en colecciones o matrices. Es por eso que la expresión Get-ChildItem está entre paréntesis. La sintaxis del método Where() es la misma que la del objeto Where. La diferencia es el rendimiento. Normalmente, el método es más rápido que utilizar el cmdlet. En mis pruebas, estamos hablando de una diferencia de milisegundos, pero más rápido es más rápido, así que lo seguiré.

La salida de la función son los objetos Get-ChildItem filtrados en la canalización, por lo que puedo usarlos de la forma que necesite.

Estos son los tipos de archivos en los que he trabajado en los últimos 6 meses. Esos 153 archivos sin extensiones provienen de una carpeta .git no oculta que tendré que arreglar.

Resumen

Por eso me encanta PowerShell. Con un poco de esfuerzo, creé una herramienta de línea de comandos que puedo ejecutar rápidamente en la consola para encontrar lo que necesito. Incluso definí un alias, glm, para que sea aún más fácil de usar en el símbolo del sistema. Espero que hayas aprendido uno o dos consejos que puedas usar en tus propias secuencias de comandos de PowerShell. Para obtener más información sobre las opciones de validación de parámetros, lea el tema de ayuda About_Functions_Advanced_Parameters.

Artículos relacionados