Búsqueda de sitios web

Importación CSV personalizada con PowerShell


Siempre estoy buscando oportunidades para usar PowerShell de una manera que agregue valor a mi trabajo. Y ojalá el tuyo. Esta es una de las razones por las que vale la pena dedicar tiempo y esfuerzo a aprender PowerShell. Se puede utilizar de muchas maneras más allá de los comandos listos para usar. Una vez que comprenda el lenguaje de PowerShell y adopte el paradigma de los objetos en proceso, PowerShell ofrece posibilidades ilimitadas. En mi caso, quería hacer más con el comando nativo Import-Csv. Quería mantener la funcionalidad original, pero quería hacer más. Así es como lo hice. Incluso si no necesita el resultado final, espero que preste atención a las técnicas y conceptos de secuencias de comandos de PowerShell. Estos son elementos que puedes aplicar a tu propio trabajo.

Comando de copia

Comencé usando la función Copiar-Comando del módulo PSScriptTools. Sabía que quería agregar algunos parámetros y funcionalidades. Originalmente pensé en crear una función de proxy y reemplazar Import-CSV con mi versión personalizada. Pero finalmente decidió crear una función "envoltorio". Este tipo de función comparte la mayoría, si no todos, los parámetros de un comando de destino que se le pasan. La función Copiar-Comando copia los parámetros del comando original a su nuevo comando.

Ajuste de parámetros

Una vez que tuve los nuevos parámetros, comencé a modificarlos. Primero, me deshice del parámetro LiteralPath y lo combiné con el parámetro Path. En esencia, trato cada camino como un camino literal. También agregué validación de parámetros personalizados.

[Parameter(
    Position = 0,
    Mandatory,
    ValueFromPipeline,
    ValueFromPipelineByPropertyName,
    HelpMessage = "The path to the CSV file. Every path is treated as a literal path."
    )]
[ValidateNotNullOrEmpty()]
#Validate file exists
[ValidateScript({
    If ((Test-Path $_) -AND ((Get-Item $_).PSProvider.Name -eq 'FileSystem')) {
        $True
    }
    else {
        Write-Warning "Failed to verify $($_.ToUpper()) or it is not a file system object."
        Throw "Failed to validate the path parameter."
        $False
    }
    })]
[Alias("PSPath")]
[string[]]$Path,

El script de validación verifica la ruta y que sea un objeto del sistema de archivos.

También sabía que había dos funciones opcionales que quería agregar al importar datos CSV. Primero, quería capturar el archivo fuente. Quería que fuera otra propiedad en la salida personalizada.

[Parameter(HelpMessage = "Add a custom property to reflect the import source file.")][switch]$IncludeSource,

También quería tener la posibilidad de definir un nombre de tipo. Cuando importa datos desde un archivo CSV, PowerShell escribe un objeto personalizado genérico en la canalización. Pero si le doy un nombre de tipo y uso archivos de formato personalizado (lo cual sabes que hago), puedo mejorar la experiencia de importación. Llegaremos a eso en un momento.

Sistema.Colecciones.Lista.Genérica[]

Ambas acciones requerirán que modifique los datos después de haberlos importado con el comando nativo Import-Csv. Recuerde, estoy escribiendo una función contenedora que ofrece la misma funcionalidad más mis adiciones. Como necesito modificar objetos después de la importación y antes de escribirlos en la canalización, necesito un lugar temporal para guardarlos. Técnicamente, una matriz funcionaría. Podría inicializar una matriz vacía y luego agregar los elementos importados a la matriz. Sin embargo, técnicamente, cuando agrega un elemento a una matriz, PowerShell destruye y recrea la matriz. Para conjuntos de datos pequeños, esto no es tan crítico. Pero me gusta practicar la eficiencia cuando puedo, por eso he estado usando listas genéricas.

Puede definir un nuevo objeto de lista como este:

$in = [System.Collections.Generic.List[object]]::New()

Puede especificar qué tipo de objeto está en la lista, como una cadena o, en mi caso, el "objeto" genérico. En mi función, estoy aprovechando la declaración Usando. Antes de la función tengo esta línea de código:

Using Namespace System.Collections.Generic

En mi función, esto simplifica el código necesario para crear el objeto de lista.

$in = [List[object]]::New()

Puede agregar un solo objeto a la lista con el método Add(), o varios a la vez con AddRange(). No he encontrado mucha diferencia entre los dos, así que para simplificar, agregaré cada elemento importado a la lista.

Microsoft.PowerShell.Utility\Import-Csv @PSBoundParameters | ForEach-Object { $in.Add($_) }

Observe que estoy agregando PSBoundParameters al comando nativo. Estoy usando el nombre de comando completo. Esto me permite llamar a mi función Import-Csv, que tendría prioridad en el símbolo del sistema. Por cierto, no lo soy. Mi función se llama Import-CsvCustom, pero quería incluirla en caso de que decida cambiar el nombre y demostrar un concepto de scripting.

Personalizando objetos

Con todos los datos importados en la lista, ahora puedo modificar los objetos. Si el usuario especificó incluir la ruta de origen, agregaré una propiedad de nota a los objetos de la lista.

if ($IncludeSource) {
    Write-Verbose "[PROCESS] Adding CSVSource property"
    $in | Add-Member -MemberType NoteProperty -Name CSVSource -Value $cPath -Force
}

Estoy jugando con algunas ideas sobre cómo hacer de esta una propiedad oculta y permitirme especificar el nombre de la propiedad. Es poco probable que tenga una columna llamada CSVSource en alguno de mis archivos CSV. Pero por regla general me gusta tener flexibilidad.

Como mencioné, cuando importas un archivo CSV, obtienes objetos personalizados genéricos.

Y como puedes ver en la imagen anterior, obtengo una lista de todos los objetos y propiedades. Pero, ¿qué pasa si tuviera un archivo de formato personalizado que cargué en mi sesión?

Update-FormatData C:\scripts\bedrock.format.ps1xml

Ahora puedo importar los datos y asignar un nombre de tipo.

Importar-CsvPersonalizado

¿Quieres probarlo por ti mismo?

#requires -version 5.1

<#
This is a copy of:

CommandType Name       Version Source
----------- ----       ------- ------
Cmdlet      Import-Csv 3.1.0.0 Microsoft.PowerShell.Utility

Created: 17 May 2021
Author : Jeff Hicks 

Learn more about PowerShell: https://jdhitsolutions.com/blog/essential-powershell-resources/

#>

<#
I am using a namespace to make defining a List[] object easier later
in the script.
#>
Using Namespace System.Collections.Generic

Function Import-CSVCustom {

    #TODO - Add comment-based help
    [CmdletBinding(DefaultParameterSetName = 'Delimiter')]
    Param(
        [Parameter(ParameterSetName = 'Delimiter', Position = 1)]
        [ValidateNotNull()]
        [char]$Delimiter,

        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            HelpMessage = "The path to the CSV file. Every path is treated as a literal path."
            )]
        [ValidateNotNullOrEmpty()]
        #Validate file exists
        [ValidateScript({
            If ((Test-Path $_) -AND ((Get-Item $_).PSProvider.Name -eq 'FileSystem')) {
                $True
            }
            else {
                Write-Warning "Failed to verify $($_.ToUpper()) or it is not a file system object."
                Throw "Failed to validate the path parameter."
                $False
            }
            })]
        [Alias("PSPath")]
        [string[]]$Path,

        [Parameter(ParameterSetName = 'UseCulture', Mandatory)]
        [ValidateNotNull()]
        [switch]$UseCulture,

        [ValidateNotNullOrEmpty()]
        [string[]]$Header,

        [ValidateSet('Unicode', 'UTF7', 'UTF8', 'ASCII', 'UTF32', 'BigEndianUnicode', 'Default', 'OEM')]
        [string]$Encoding,

        [Parameter(HelpMessage = "Add a custom property to reflect the import source file.")]
        [switch]$IncludeSource,

        [Parameter(HelpMessage = "Insert an optional custom type name.")]
        [ValidateNotNullOrEmpty()]
        [string]$PSTypeName
    )

    Begin {
        Write-Verbose "[BEGIN  ] Starting $($MyInvocation.Mycommand)"
        Write-Verbose "[BEGIN  ] Using parameter set $($PSCmdlet.ParameterSetName)"
        Write-Verbose ($PSBoundParameters | Out-String)

        #remove parameters that don't belong to the native Import-Csv command
        if ($PSBoundParameters.ContainsKey("IncludeSource")) {
            [void]$PSBoundParameters.Remove("IncludeSource")
        }
        if ($PSBoundParameters.ContainsKey("PSTypeName")) {
            [void]$PSBoundParameters.Remove("PSTypeName")
        }
    } #begin

    Process {
        <#
        Initialize a generic list to hold each imported object so it can be
        processed for CSVSource and/or typename
        #>
        $in = [List[object]]::New()

        #convert the path value to a complete filesystem path
        $cPath = Convert-Path -Path $Path
        #update the value of the PSBoundparameter
        $PSBoundParameters["Path"] = $cPath

        Write-Verbose "[PROCESS] Importing from $cPath"

        <#
        Add each imported item to the collection.

        It is theoretically possible to have a CSV file of 1 object, so
        instead of testing to determine whether to use Add() or AddRange(),
        I'll simply Add each item.

        I'm using the fully qualified cmdlet name in case I want this function
        to become my Import-Csv command.
        #>
        Microsoft.PowerShell.Utility\Import-Csv @PSBoundParameters | ForEach-Object { $in.Add($_) }

        Write-Verbose "[PROCESS] Post-processing $($in.count) objects"

        if ($IncludeSource) {
            Write-Verbose "[PROCESS] Adding CSVSource property"
            $in | Add-Member -MemberType NoteProperty -Name CSVSource -Value $cPath -Force
        }
        if ($PSTypeName) {
            Write-Verbose "[PROCESS] Adding PSTypename $PSTypeName"
            $($in).foreach({ $_.psobject.typenames.insert(0, $PSTypeName)})
        }
        #write the results to the pipeline
        $in
    } #process

    End {
        Write-Verbose "[END    ] Ending $($MyInvocation.Mycommand)"
    } #end

} #end Import-CsvCustom

Guarde el código en un archivo que pueda ubicar como fuente en su sesión de PowerShell. Luego importe archivos CSV y pruébelo. La función debería funcionar en Windows PowerShell y PowerShell 7.x. La función incluye resultados detallados y, con suerte, mucha documentación interna para ayudarle a comprender lo que estoy haciendo.

Espero que esto le resulte útil, o al menos informativo. Y tal vez usted también comience a pensar en formas de utilizar PowerShell para hacer más.

Artículos relacionados