¿Cómo creo un tipo personalizado en PowerShell para que lo utilicen mis scripts?

88

Me gustaría poder definir y usar un tipo personalizado en algunos de mis scripts de PowerShell. Por ejemplo, supongamos que necesitaba un objeto que tuviera la siguiente estructura:

Contact
{
    string First
    string Last
    string Phone
}

¿Cómo haría para crear esto para poder usarlo en una función como la siguiente?

function PrintContact
{
    param( [Contact]$contact )
    "Customer Name is " + $contact.First + " " + $contact.Last
    "Customer Phone is " + $contact.Phone 
}

¿Es posible algo como esto, o incluso se recomienda en PowerShell?

Scott Saad
fuente

Respuestas:

133

Antes de PowerShell 3

El sistema de tipo extensible de PowerShell no le permitió originalmente crear tipos concretos que pueda probar de la forma en que lo hizo en su parámetro. Si no necesita esa prueba, está bien con cualquiera de los otros métodos mencionados anteriormente.

Si desea un tipo real al que pueda enviar o verificar el tipo, como en su script de ejemplo ... no se puede hacer sin escribirlo en C # o VB.net y compilarlo. En PowerShell 2, puede usar el comando "Agregar tipo" para hacerlo de manera bastante simple:

add-type @"
public struct contact {
   public string First;
   public string Last;
   public string Phone;
}
"@

Nota histórica : en PowerShell 1 fue aún más difícil. Tenía que usar CodeDom manualmente, hay una función muy antigua de script new-struct en PoshCode.org que ayudará. Tu ejemplo se convierte en:

New-Struct Contact @{
    First=[string];
    Last=[string];
    Phone=[string];
}

Usar Add-Typeo New-Structte permitirá probar la clase en tu param([Contact]$contact)y crear nuevas usando $contact = new-object Contacty así sucesivamente ...

En PowerShell 3

Si no necesitas una clase "real" a la que puedas enviar contenido, no tienes que usar la forma de Agregar miembro que Steven y otros han demostrado anteriormente.

Desde PowerShell 2, puede usar el parámetro -Property para New-Object:

$Contact = New-Object PSObject -Property @{ First=""; Last=""; Phone="" }

Y en PowerShell 3, tenemos la capacidad de usar el PSCustomObjectacelerador para agregar un TypeName:

[PSCustomObject]@{
    PSTypeName = "Contact"
    First = $First
    Last = $Last
    Phone = $Phone
}

Todavía está obteniendo un único objeto, por lo que debe crear una New-Contactfunción para asegurarse de que todos los objetos salgan igual, pero ahora puede verificar fácilmente que un parámetro "es" uno de esos tipos decorando un parámetro con el PSTypeNameatributo:

function PrintContact
{
    param( [PSTypeName("Contact")]$contact )
    "Customer Name is " + $contact.First + " " + $contact.Last
    "Customer Phone is " + $contact.Phone 
}

En PowerShell 5

En PowerShell 5 todo cambia, y finalmente obtuvimos classy enumcomo palabras clave de idioma para definir tipos (no hay, structpero está bien):

class Contact
{
    # Optionally, add attributes to prevent invalid values
    [ValidateNotNullOrEmpty()][string]$First
    [ValidateNotNullOrEmpty()][string]$Last
    [ValidateNotNullOrEmpty()][string]$Phone

    # optionally, have a constructor to 
    # force properties to be set:
    Contact($First, $Last, $Phone) {
       $this.First = $First
       $this.Last = $Last
       $this.Phone = $Phone
    }
}

También obtuvimos una nueva forma de crear objetos sin usar New-Object: [Contact]::new()- de hecho, si mantienes tu clase simple y no defines un constructor, puedes crear objetos lanzando una tabla hash (aunque sin un constructor, no habría forma para hacer cumplir que todas las propiedades deben establecerse):

class Contact
{
    # Optionally, add attributes to prevent invalid values
    [ValidateNotNullOrEmpty()][string]$First
    [ValidateNotNullOrEmpty()][string]$Last
    [ValidateNotNullOrEmpty()][string]$Phone
}

$C = [Contact]@{
   First = "Joel"
   Last = "Bennett"
}
Jaykul
fuente
¡Gran respuesta! Solo agrego una nota de que este estilo es muy fácil para los scripts y aún funciona en PowerShell 5: Objeto nuevo PSObject -Property @ {prop here ...}
Ryan Shillington
2
En las primeras versiones de PowerShell 5, no podía usar New-Object con clases creadas con la sintaxis de clase, pero ahora puede hacerlo. SIN EMBARGO, si está usando la palabra clave class, su script está limitado solo a PS5 de todos modos, por lo que aún recomendaría usar la sintaxis :: new si el objeto tiene un constructor que toma parámetros (es mucho más rápido que New-Object) o casting de lo contrario, que es una sintaxis más limpia y más rápida.
Jaykul
¿Está seguro de que la verificación de tipos no se puede realizar con tipos creados con Add-Type? Parece funcionar en PowerShell 2 en Win 2008 R2. Digamos que yo defino contactel uso Add-Typecomo en su respuesta y luego crear una instancia: $con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }. A continuación, llamar a esta función obras: function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() }, pero llamar a esta función falla, x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() }. La llamada x 'abc'también falla con un mensaje de error apropiado sobre la transmisión. Probado en PS 2 y 4.
jpmc26
Por supuesto, puede verificar los tipos creados con Add-Type@ jpmc26, lo que dije es que no puede hacerlo sin compilar (es decir, sin escribirlo en C # y llamar Add-Type). Por supuesto, desde PS3 puede: hay un [PSTypeName("...")]atributo que le permite especificar el tipo como una cadena, que admite pruebas contra PSCustomObjects con el conjunto PSTypeNames ...
Jaykul
58

La creación de tipos personalizados se puede realizar en PowerShell.
Kirk Munro en realidad tiene dos excelentes publicaciones que detallan el proceso a fondo.

El libro Windows PowerShell en acción de Manning también tiene una muestra de código para crear un lenguaje específico de dominio para crear tipos personalizados. El libro es excelente en todos los aspectos, así que realmente lo recomiendo.

Si solo está buscando una forma rápida de hacer lo anterior, puede crear una función para crear el objeto personalizado como

function New-Person()
{
  param ($FirstName, $LastName, $Phone)

  $person = new-object PSObject

  $person | add-member -type NoteProperty -Name First -Value $FirstName
  $person | add-member -type NoteProperty -Name Last -Value $LastName
  $person | add-member -type NoteProperty -Name Phone -Value $Phone

  return $person
}
Steven Murawski
fuente
17

Este es el método de atajo:

$myPerson = "" | Select-Object First,Last,Phone
EBGreen
fuente
3
Básicamente, el cmdlet Select-Object agrega propiedades a los objetos que se le proporciona si el objeto aún no tiene esa propiedad. En este caso, está entregando un objeto String en blanco al cmdlet Select-Object. Agrega las propiedades y pasa el objeto a lo largo de la tubería. O si es el último comando en la tubería, genera el objeto. Debo señalar que solo uso este método si estoy trabajando en el indicador. Para los scripts, siempre uso los cmdlets Add-Member o New-Object más explícitos.
EBGreen
Si bien este es un gran truco, en realidad puede $myPerson = 1 | Select First,Last,Phone
acortarlo
Esto no le permite utilizar las funciones de tipo nativo, ya que establece el tipo de cada miembro como cadena. Teniendo en cuenta la contribución Jaykul anteriormente, revela cada nota como un miembro NotePropertydel stringtipo, es una Propertyde cualquier tipo que ha asignado en el objeto. Sin embargo, esto es rápido y funciona.
mbrownnyc
Esto puede causarle problemas si desea una propiedad de Longitud, ya que la cadena ya la tiene y su nuevo objeto obtendrá el valor existente, que probablemente no desee. Recomiendo pasar un [int], como muestra @RaYell.
FSCKur
9

La respuesta de Steven Murawski es excelente, sin embargo, me gusta el más corto (o más bien el select-object más ordenado en lugar de usar la sintaxis de agregar miembros):

function New-Person() {
  param ($FirstName, $LastName, $Phone)

  $person = new-object PSObject | select-object First, Last, Phone

  $person.First = $FirstName
  $person.Last = $LastName
  $person.Phone = $Phone

  return $person
}
Nick Meldrum
fuente
New-Objectni siquiera es necesario. Esto hará lo mismo:... = 1 | select-object First, Last, Phone
Roman Kuzmin
1
Sí, pero lo mismo que EBGreen anterior: esto crea una especie de tipo subyacente extraño (en su ejemplo sería un Int32.) Como vería si escribiera: $ person | gm. Prefiero que el tipo subyacente sea un PSCustomObject
Nick Meldrum
2
Veo el punto. Aún así, hay ventajas obvias de la intforma: 1) funciona más rápido, no mucho, pero para esta función en particular New-Personla diferencia es del 20%; 2) aparentemente es más fácil de escribir. Al mismo tiempo, utilizando este enfoque básicamente en todas partes, nunca he visto inconvenientes. Pero estoy de acuerdo: puede haber algunos casos raros en los que PSCustomObject sea mejor.
Roman Kuzmin
@RomanKuzmin ¿Sigue siendo un 20% más rápido si crea una instancia de un objeto personalizado global y lo almacena como una variable de secuencia de comandos?
jpmc26
5

Sorprendido, nadie mencionó esta opción simple (vs 3 o posterior) para crear objetos personalizados:

[PSCustomObject]@{
    First = $First
    Last = $Last
    Phone = $Phone
}

El tipo será PSCustomObject, aunque no un tipo personalizado real. Pero probablemente sea la forma más sencilla de crear un objeto personalizado.

Benjamin Hubbard
fuente
Consulte también esta publicación de blog de Will Anderson sobre la diferencia entre PSObject y PSCustomObject.
CodeFox
@CodeFox acaba de notar que el enlace está roto ahora
superjos
2
@superjos, gracias por la pista. No pude encontrar la nueva ubicación de la publicación. Al menos la publicación fue respaldada por el archivo .
CodeFox
2
aparentemente parece que se convirtió en un libro de Git aquí :)
superjos
4

Existe el concepto de PSObject y Add-Member que podría utilizar.

$contact = New-Object PSObject

$contact | Add-Member -memberType NoteProperty -name "First" -value "John"
$contact | Add-Member -memberType NoteProperty -name "Last" -value "Doe"
$contact | Add-Member -memberType NoteProperty -name "Phone" -value "123-4567"

Esto genera como:

[8] » $contact

First                                       Last                                       Phone
-----                                       ----                                       -----
John                                        Doe                                        123-4567

La otra alternativa (que conozco) es definir un tipo en C # / VB.NET y cargar ese ensamblado en PowerShell para usarlo directamente.

Definitivamente se recomienda este comportamiento porque permite que otros scripts o secciones de su script trabajen con un objeto real.

David Mohundro
fuente
3

Aquí está el camino difícil para crear tipos personalizados y almacenarlos en una colección.

$Collection = @()

$Object = New-Object -TypeName PSObject
$Object.PsObject.TypeNames.Add('MyCustomType.Contact.Detail')
Add-Member -InputObject $Object -memberType NoteProperty -name "First" -value "John"
Add-Member -InputObject $Object -memberType NoteProperty -name "Last" -value "Doe"
Add-Member -InputObject $Object -memberType NoteProperty -name "Phone" -value "123-4567"
$Collection += $Object

$Object = New-Object -TypeName PSObject
$Object.PsObject.TypeNames.Add('MyCustomType.Contact.Detail')
Add-Member -InputObject $Object -memberType NoteProperty -name "First" -value "Jeanne"
Add-Member -InputObject $Object -memberType NoteProperty -name "Last" -value "Doe"
Add-Member -InputObject $Object -memberType NoteProperty -name "Phone" -value "765-4321"
$Collection += $Object

Write-Ouput -InputObject $Collection
Florian JUDITH
fuente
Buen toque al agregar el nombre del tipo al objeto.
2014
0

Aquí hay una opción más, que usa una idea similar a la solución PSTypeName mencionada por Jaykul (y por lo tanto también requiere PSv3 o superior).

Ejemplo

  1. Cree un archivo TypeName .Types.ps1xml que defina su tipo. Por ejemplo Person.Types.ps1xml:
<?xml version="1.0" encoding="utf-8" ?>
<Types>
  <Type>
    <Name>StackOverflow.Example.Person</Name>
    <Members>
      <ScriptMethod>
        <Name>Initialize</Name>
        <Script>
            Param (
                [Parameter(Mandatory = $true)]
                [string]$GivenName
                ,
                [Parameter(Mandatory = $true)]
                [string]$Surname
            )
            $this | Add-Member -MemberType 'NoteProperty' -Name 'GivenName' -Value $GivenName
            $this | Add-Member -MemberType 'NoteProperty' -Name 'Surname' -Value $Surname
        </Script>
      </ScriptMethod>
      <ScriptMethod>
        <Name>SetGivenName</Name>
        <Script>
            Param (
                [Parameter(Mandatory = $true)]
                [string]$GivenName
            )
            $this | Add-Member -MemberType 'NoteProperty' -Name 'GivenName' -Value $GivenName -Force
        </Script>
      </ScriptMethod>
      <ScriptProperty>
        <Name>FullName</Name>
        <GetScriptBlock>'{0} {1}' -f $this.GivenName, $this.Surname</GetScriptBlock>
      </ScriptProperty>
      <!-- include properties under here if we don't want them to be visible by default
      <MemberSet>
        <Name>PSStandardMembers</Name>
        <Members>
        </Members>
      </MemberSet>
      -->
    </Members>
  </Type>
</Types>
  1. Importa tu tipo: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. Crea un objeto de tu tipo personalizado: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. Inicialice su tipo usando el método de secuencia de comandos que definió en el XML: $p.Initialize('Anne', 'Droid')
  4. Míralo; verá todas las propiedades definidas:$p | Format-Table -AutoSize
  5. Escriba llamando a un mutador para actualizar el valor de una propiedad: $p.SetGivenName('Dan')
  6. Míralo de nuevo para ver el valor actualizado: $p | Format-Table -AutoSize

Explicación

  • El archivo PS1XML le permite definir propiedades personalizadas en tipos.
  • No está restringido a los tipos .net como implica la documentación; para que pueda poner lo que quiera en '/ Tipos / Tipo / Nombre', cualquier objeto creado con un 'PSTypeName' que coincida heredará los miembros definidos para este tipo.
  • Los miembros añadidos a través de PS1XMLo Add-Memberse limitan a NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethod, y CodeMethod(o PropertySet/ MemberSet, aunque esos son sujetos a las mismas restricciones). Todas estas propiedades son de solo lectura.
  • Al definir un ScriptMethod, podemos burlar la restricción anterior. Por ejemplo, podemos definir un método (por ejemplo Initialize) que crea nuevas propiedades, estableciendo sus valores por nosotros; asegurando así que nuestro objeto tiene todas las propiedades que necesitamos para que funcionen nuestros otros scripts.
  • Podemos usar este mismo truco para permitir que las propiedades sean actualizables (aunque mediante un método en lugar de una asignación directa), como se muestra en el ejemplo SetGivenName.

Este enfoque no es ideal para todos los escenarios; pero es útil para agregar comportamientos de clase a tipos personalizados / se puede usar junto con otros métodos mencionados en las otras respuestas. Por ejemplo, en el mundo real probablemente solo definiría la FullNamepropiedad en el PS1XML, luego usaría una función para crear el objeto con los valores requeridos, así:

Más información

Eche un vistazo a la documentación o al archivo de tipo OOTB Get-Content $PSHome\types.ps1xmlpara inspirarse.

# have something like this defined in my script so we only try to import the definition once.
# the surrounding if statement may be useful if we're dot sourcing the script in an existing 
# session / running in ISE / something like that
if (!(Get-TypeData 'StackOverflow.Example.Person')) {
    Update-TypeData '.\Person.Types.ps1xml'
}

# have a function to create my objects with all required parameters
# creating them from the hash table means they're PROPERties; i.e. updatable without calling a 
# setter method (note: recall I said above that in this scenario I'd remove their definition 
# from the PS1XML)
function New-SOPerson {
    [CmdletBinding()]
    [OutputType('StackOverflow.Example.Person')]
    Param (
        [Parameter(Mandatory)]
        [string]$GivenName
        ,
        [Parameter(Mandatory)]
        [string]$Surname
    )
    ([PSCustomObject][Ordered]@{
        PSTypeName = 'StackOverflow.Example.Person'
        GivenName = $GivenName
        Surname = $Surname
    })
}

# then use my new function to generate the new object
$p = New-SOPerson -GivenName 'Simon' -Surname 'Borg'

# and thanks to the type magic... FullName exists :)
Write-Information "$($p.FullName) was created successfully!" -InformationAction Continue
JohnLBevan
fuente
PD. Para aquellos que usan VSCode, pueden agregar compatibilidad con PS1XML
JohnLBevan