Búsqueda de sitios web

Hay un archivo en mi depósito de PowerShell


Si hay una tarea que nunca he dejado de hacer es buscar archivos. Constantemente creo nuevas formas de organizar archivos y mostrarlos en un formato significativo. Naturalmente, PowerShell es una gran herramienta para esta tarea. Get-ChildItem es obviamente el punto de partida adecuado. El cmdlet funciona bien para obtener solo archivos de una carpeta y puedo realizar un filtrado inicial básico por nombre y extensión con comodines. Pero mi última tarea fue organizar archivos por fecha y, debido a la forma en que funciona Get-ChildItem, tendré que recurrir al filtrado tardío con Where-Object. No hay nada malo en eso. Pero si esta es una tarea que probablemente repetiré, entonces hay una función de PowerShell en la mesa de dibujo. Mi objetivo es crear una función que muestre archivos agrupados en grupos de antigüedad, como 1 semana o 6 meses. Aunque lo que más me preocupa es la edad basada en el momento de la última escritura, es posible que yo (o usted) necesitemos basar el envejecimiento en el momento de la creación. Codifiquemos.

Construyendo una lista

Mi función recibirá información de canalización de Get-Childitem. Sin embargo, debido a que voy a ordenar y agrupar archivos, no puedo hacerlo hasta que mi función haya procesado todos los archivos de Get-ChildItem. Necesito almacenarlos temporalmente a medida que pasan por el oleoducto. Podría usar un objeto de matriz y agregarle cada objeto canalizado. Mi código podría verse así.

Begin {
    $files = @()
}
Process {
    $files += $inputobject
}
End {
    $files | Sort-Object -property LastWriteTime ...
}

Técnicamente no hay nada malo en este enfoque y lo he usado durante años. Pero últimamente he recurrido a una lista genérica. Específicamente un objeto [System.Collections.Generic.list[]]. La razón es que cuando agrega a una matriz, cada vez que agrega un elemento, la matriz se borra y se reconstruye. Para un número reducido de artículos, eso no supone ningún problema. Pero podría estar procesando miles de archivos. Habrá una ligera compensación en el rendimiento por la facilidad de uso. Agregué casi 9000 archivos a una matriz en poco más de 3 segundos. Usar la lista genérica tomó menos de un segundo.

En el bloque Inicio de mi función, podría definir el objeto de colección de esta manera:

$list = [system.Collections.Generic.list[system.io.fileinfo]]::new()

Pero voy a utilizar una técnica de secuencias de comandos para simplificar esa sintaxis. En el archivo .ps1 que define mi función, antes de la función voy a insertar una declaración Usando.

Using namespace System.Collections.Generic

Ahora, en el bloque Inicio, mi definición es mucho más simple:

 $list = [List[System.IO.FileInfo]]::new()

Generalmente le dice a PowerShell qué tipo de objeto va a su lista. En mi caso, sé que serán objetos System.IO.FileInfo. Si no está seguro o tendrá una combinación de objetos, simplemente puede usar "objeto".

El objeto de lista tiene varios métodos en los que no entraré aquí. En el bloque Proceso, agregaré cada archivo a la lista.

 $list.Add($FilePath)

Cuando llegue al bloque Fin, $list contendrá todos los archivos canalizados a mi función. Ahora puedo hacer mi magia con el cubo.

Grupos de edad

Tuve que decidir qué tipo de cubos envejecidos quería. Lo decidí por año, mes y número de días. Controlaré esto con un parámetro.

[Parameter(HelpMessage = "How do you want the files organized? The default is Month.")]
[ValidateNotNullOrEmpty()]
[ValidateSet("Month","Year","Days")]
[string]$GroupBy = "Month"

También necesitaba controlar en qué basaba mis decisiones al envejecer.

[Parameter(HelpMessage = "Specify if grouping by the file's CreationTime or LastWriteTime property. The default is LastWriteTime.")]
[ValidateSet("CreationTime","LastWritetime")]
[ValidateNotNullOrEmpty()]
[string]$Property = "LastWriteTime",

Para cada archivo de mi lista, necesito agregar una propiedad que indique su grupo de edad. Sé que voy a agregar una propiedad al objeto de archivo.

$file | Add-Member -MemberType NoteProperty -Name AgeGroup -Value $value

El valor se calculará en función del grupo de agrupación. Si quiero agrupar por año, para cada archivo ejecutaré este código:

$value = $file.$property.year
$sort = "AgeGroup"

La variable $sort se utilizará al final de la función para ayudar a formatear la salida. Para agrupar meses, necesitaba el mes y el año. También necesitaba convertir el valor en un objeto DateTime para que se ordenara correctamente. $Property será CreationTime o LastWriteTime.

$value = "$($file.$property.month)/$($file.$property.year)"
$sort = {$_.AgeGroup -as [datetime]}

Valores personalizados de propiedad personalizada

Agrupar según el número de días fue el paso más complicado. Decidí definir un conjunto de nombres de depósitos como OneWeek y SixMonth. Sin embargo, esto significaba que la clasificación se realizaría según el valor de la cadena, lo que no sería exacto. Mi solución fue definir una enumeración en mi archivo de función.

Enum FileAge {
    YearPlus
    Year
    NineMonth
    SixMonth
    ThreeMonth
    OneMonth
    OneWeek
    OneDay
}

¿Por qué? Porque en una enumeración, cada valor en realidad tiene un valor numérico que comienza en 0. Aunque estoy usando cadenas para indicar el depósito de antigüedad, cuando ordeno, estará en el valor numérico implícito. Con esta enumeración, puedo asignar un valor basado en el total de días.

$age = New-TimeSpan -start $file.$property -end $now
$value = switch ($age.totaldays -as [int]) {
    {$_ -ge 365} {[FileAge]::YearPlus ; break}
    {$_ -gt 270 -AND $_ -lt 365 }{[FileAge]::Year;break}
    {$_ -gt 180 -AND $_ -le 270 }{[FileAge]::NineMonth;break}
    {$_ -gt 90 -AND $_ -le 180} {[FileAge]::SixMonth;break}
    {$_ -gt 30 -AND $_ -le 90} {[FileAge]::ThreeMonth;break}
    {$_ -gt 7 -AND $_ -le 30} {[FileAge]::OneMonth;break}
    {$_ -gt 1 -And $_ -le 7} {[FileAge]::OneWeek;break}
    {$_ -le 1} {[FileAge]::OneDay;break}
}
$sort = "AgeGroup"

Podría haber creado un objeto personalizado, incluso usando una clase de PowerShell. Pero estoy feliz de aprovechar el tipo System.IO.FileInfo. Sin embargo, el formato predeterminado no sabe nada sobre mi propiedad personalizada. Sé que querré crear mi propio formato, por lo que necesito insertar un nuevo nombre de tipo.

 $file.psobject.TypeNames.insert(0,"FileAgingInfo")

Al planificar con anticipación, sé que usaré la agrupación en mi formato personalizado, lo que significa que los objetos deben ordenarse. Normalmente, dejaría de ordenar una función, pero en este caso la necesito.

$list | Sort-Object -Property $sort,DirectoryName,$property,Name

Primero estoy ordenando mi variable $sort, luego el nombre del directorio, luego la propiedad CreationTime o LastWriteTime y finalmente el nombre del archivo.

Aquí está la función completa:

#requires -version 5.1

#declaring a namespace here makes the code simpler later on
Using namespace System.Collections.Generic

#define an enumeration that will be used in a new custom property
Enum FileAge {
    YearPlus
    Year
    NineMonth
    SixMonth
    ThreeMonth
    OneMonth
    OneWeek
    OneDay
}
Function Get-FileAgeGroup {
    [cmdletbinding()]
    [alias("gfa")]
    [OutputType("FileAgingInfo")]
    Param(
        [Parameter(Mandatory,Position=0,ValueFromPipeline,HelpMessage = "The path to a file. If you pipe from a Get-ChildItem command, be sure to use the -File parameter.")]
        [ValidateNotNullOrEmpty()]
        [System.IO.FileInfo]$FilePath,
        [Parameter(HelpMessage = "Specify if grouping by the file's CreationTime or LastWriteTime property. The default is LastWriteTime.")]
        [ValidateSet("CreationTime","LastWritetime")]
        [ValidateNotNullOrEmpty()]
        [string]$Property = "LastWriteTime",
        [Parameter(HelpMessage = "How do you want the files organized? The default is Month.")]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("Month","Year","Days")]
        [string]$GroupBy = "Month"
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Grouping by $GroupBy on the $Property property"

        #initialize a list to contain all piped in files
        #this code is shorter because of the Using statement at the beginning of this file
        $list = [List[System.IO.FileInfo]]::new()

        #get a static value for now
        $now = Get-Date
    } #begin

    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Adding $FilePath"
        #add each file to the list
        $list.Add($FilePath)
    } #process

    End {
        #now process all the files and add aging properties
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Sorting $($list.count) files"

        #add custom properties based on the age grouping
        foreach ($file in $list) {
            switch ($GroupBy) {
                "Month" {
                    $value = "$($file.$property.month)/$($file.$property.year)"
                    $sort = {$_.AgeGroup -as [datetime]}
                }
                "Year" {
                    $value = $file.$property.year
                    $sort = "AgeGroup"
                }
                "Days" {
                    $age = New-TimeSpan -start $file.$property -end $now
                    $value = switch ($age.totaldays -as [int]) {
                        {$_ -ge 365} {[FileAge]::YearPlus ; break}
                        {$_ -gt 270 -AND $_ -lt 365 }{[FileAge]::Year;break}
                        {$_ -gt 180 -AND $_ -le 270 }{[FileAge]::NineMonth;break}
                        {$_ -gt 90 -AND $_ -le 180} {[FileAge]::SixMonth;break}
                        {$_ -gt 30 -AND $_ -le 90} {[FileAge]::ThreeMonth;break}
                        {$_ -gt 7 -AND $_ -le 30} {[FileAge]::OneMonth;break}
                        {$_ -gt 1 -And $_ -le 7} {[FileAge]::OneWeek;break}
                        {$_ -le 1} {[FileAge]::OneDay;break}
                    }
                    $sort = "AgeGroup"
                }
            } #switch

            #add a custom property to each file object
            $file | Add-Member -MemberType NoteProperty -Name AgeGroup -Value $value

            #insert a custom type name which will be used by the custom format file
            $file.psobject.TypeNames.insert(0,"FileAgingInfo")
        } #foreach file

        #write the results to the pipeline. Sorting results so that the default
        #formatting will be displayed properly
        $list | Sort-Object -Property $sort,DirectoryName,$property,Name

        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
    } #end

} #close Get-FileAgeGroup

Salida formateada

El objetivo de este trabajo es facilitarme la visualización de archivos agrupados por edad de esta manera:

Es por eso que necesitaba un nombre de tipo personalizado y una salida preordenada. Utilicé New-PSFormatXML para crear un archivo ps1xml de formato personalizado.

<!--
Format type data generated 09/17/2021 11:54:13 by PROSPERO\Jeff

This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.

https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
  <ViewDefinitions>
    <View>
      <!--Created 09/17/2021 11:54:13 by PROSPERO\Jeff-->
      <Name>default</Name>
      <ViewSelectedBy>
        <TypeName>FileAgingInfo</TypeName>
      </ViewSelectedBy>
      <GroupBy>
        <!--
            You can also use a scriptblock to define a custom property name.
            You must have a Label tag.
            <ScriptBlock>$_.machinename.toUpper()</ScriptBlock>
            <Label>Computername</Label>

            Use <Label> to set the displayed value.
-->
        <PropertyName>AgeGroup</PropertyName>
        <Label>AgeGroup</Label>
      </GroupBy>
      <TableControl>
        <!--Delete the AutoSize node if you want to use the defined widths.-->
        <AutoSize />
        <TableHeaders>
          <TableColumnHeader>
            <Label>Directory</Label>
            <Width>16</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Created</Label>
            <Width>24</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Modified</Label>
            <Width>23</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Length</Label>
            <Width>11</Width>
            <Alignment>right</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Name</Label>
            <Width>10</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <!--
            By default the entries use property names, but you can replace them with scriptblocks.
            <ScriptBlock>$_.foo /1mb -as [int]</ScriptBlock>
-->
              <TableColumnItem>
                <PropertyName>DirectoryName</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>CreationTime</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>LastWriteTime</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Length</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Name</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

Al final de mi archivo ps1, importo el archivo de formato a mi sesión de PowerShell.

Update-FormatData $psscriptroot\FileAge.format.ps1xml

Por ahora solo tengo una vista.

Debido a que estoy usando la enumeración, mis archivos están ordenados correctamente y obtengo resultados significativos. Puede que necesite modificar un poco el formato del archivo.

O puedo ajustar mi expresión de PowerShell.

Como ocurre con muchas de mis publicaciones, el resultado final no es tan importante como las técnicas que utilicé para llegar allí. Comentarios y preguntas son bienvenidos.

Artículos relacionados