Como faço para criar um tipo personalizado no PowerShell para meus scripts usarem?

88

Eu gostaria de poder definir e usar um tipo personalizado em alguns dos meus scripts do PowerShell. Por exemplo, vamos fingir que eu precisava de um objeto que tinha a seguinte estrutura:

Contact
{
    string First
    string Last
    string Phone
}

Como eu faria para criar isso para que pudesse usá-lo em uma função como a seguinte:

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

Algo assim é possível ou mesmo recomendado no PowerShell?

Scott Saad
fonte

Respostas:

133

Antes do PowerShell 3

O Extensible Type System do PowerShell originalmente não permitia que você criasse tipos concretos que você pudesse testar da maneira como fez em seu parâmetro. Se você não precisa desse teste, está satisfeito com qualquer um dos outros métodos mencionados acima.

Se você quiser um tipo real para o qual possa lançar ou verificar o tipo, como em seu script de exemplo ... não pode ser feito sem escrever em C # ou VB.net e compilar. No PowerShell 2, você pode usar o comando "Add-Type" para fazer isso de forma bastante simples:

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

Nota histórica : No PowerShell 1 era ainda mais difícil. Você tinha que usar o CodeDom manualmente, há umscript de nova estrutura de função muito antigoem PoshCode.org que ajudará. Seu exemplo se torna:

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

Usar Add-Typeou New-Structpermitirá que você realmente teste a classe em seu param([Contact]$contact)e faça novas usando $contact = new-object Contacte assim por diante ...

No PowerShell 3

Se você não precisa de uma classe "real" para a qual possa lançar, você não precisa usar o método Add-Member que Steven e outros demonstraram acima.

Desde o PowerShell 2, você pode usar o parâmetro -Property para New-Object:

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

E no PowerShell 3, conseguimos usar o PSCustomObjectacelerador para adicionar um TypeName:

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

Você ainda está recebendo apenas um único objeto, então deve criar uma New-Contactfunção para garantir que todos os objetos sejam iguais, mas agora você pode facilmente verificar se um parâmetro "é" um desses tipos decorando um parâmetro com o PSTypeNameatributo:

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

No PowerShell 5

No PowerShell 5 tudo muda e finalmente obtivemos classe enumcomo palavras-chave de linguagem para definir tipos (não há, structmas está tudo bem):

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

Também temos uma nova maneira de criar objetos sem usar New-Object: [Contact]::new()- na verdade, se você mantiver sua classe simples e não definir um construtor, você pode criar objetos lançando uma tabela de hash (embora sem um construtor, não haveria maneira para fazer com que todas as propriedades sejam definidas):

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
fonte
Ótima resposta! Apenas adicionando uma observação de que este estilo é muito fácil para scripts e ainda funciona no PowerShell 5: New-Object PSObject -Property @ {prop here ...}
Ryan Shillington
2
Nas primeiras versões do PowerShell 5, você não podia usar New-Object com classes criadas usando a sintaxe de classe, mas agora você pode. NO ENTANTO, se você estiver usando a palavra-chave class, seu script é limitado apenas ao PS5 de qualquer maneira, então eu ainda recomendo usar a sintaxe :: new se o objeto tiver um construtor que leva parâmetros (é muito mais rápido do que New-Object) ou lançar de outra forma, que é uma sintaxe mais limpa e mais rápida.
Jaykul
Tem certeza de que a verificação de tipo não pode ser feita com tipos criados usando Add-Type? Parece funcionar no PowerShell 2 no Win 2008 R2. Digamos que eu definem contactusando Add-Typecomo na sua resposta e, em seguida, criar uma instância: $con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }. Então, chamar essa função funciona function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() }:, mas chamar essa função falha x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() },. A chamada x 'abc'também falha com uma mensagem de erro apropriada sobre a transmissão. Testado em PS 2 e 4.
jpmc26 01 de
Claro que você pode verificar os tipos criados com Add-Type@ jpmc26, o que eu disse é que você não pode fazer isso sem compilar (ou seja: sem escrever em C # e chamar Add-Type). Claro, no PS3 você pode - há um [PSTypeName("...")]atributo que permite especificar o tipo como uma string, que oferece suporte a testes em PSCustomObjects com o conjunto PSTypeNames ...
Jaykul
58

A criação de tipos personalizados pode ser feita no PowerShell.
Kirk Munro na verdade tem dois ótimos posts que detalham o processo completamente.

O livro Windows PowerShell In Action de Manning também contém um exemplo de código para criar uma linguagem específica de domínio para criar tipos personalizados. O livro é excelente em todos os aspectos, então eu realmente o recomendo.

Se você está apenas procurando uma maneira rápida de fazer o acima, pode criar uma função para criar o 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
fonte
17

Este é o método de atalho:

$myPerson = "" | Select-Object First,Last,Phone
EBGreen
fonte
3
Basicamente, o cmdlet Select-Object adiciona propriedades aos objetos que são fornecidos se o objeto ainda não tiver essa propriedade. Nesse caso, você está entregando um objeto String em branco para o cmdlet Select-Object. Ele adiciona as propriedades e passa o objeto ao longo do tubo. Ou se for o último comando no tubo, ele produz o objeto. Devo ressaltar que só utilizo esse método se estou trabalhando no prompt. Para scripts, sempre uso os cmdlets Add-Member ou New-Object mais explícitos.
EBGreen
Embora este seja um ótimo truque, você pode torná-lo ainda mais curto:$myPerson = 1 | Select First,Last,Phone
RaYell
Isso não permite que você utilize as funções de tipo nativo, pois define o tipo de cada membro como string. Dada contribuição Jaykul acima, revela cada nota membro como um NotePropertyde stringtipo, é uma Propertyde qualquer tipo que você atribuiu no objeto. Isso é rápido e faz o trabalho.
mbrownnyc
Isso pode gerar problemas se você quiser uma propriedade Length, uma vez que a string já a possui e seu novo objeto obterá o valor existente - o que você provavelmente não deseja. Eu recomendo passar um [int], como mostra @RaYell.
FSCKur
9

A resposta de Steven Murawski é ótima, no entanto, gosto do mais curto (ou melhor, apenas do objeto de seleção mais limpo em vez de usar a sintaxe adicionar membro):

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
fonte
New-Objectnem mesmo é necessário. Isso fará o mesmo:... = 1 | select-object First, Last, Phone
Roman Kuzmin
1
Sim, mas o mesmo que EBGreen acima - isso cria um tipo de tipo estranho subjacente (em seu exemplo seria um Int32.) Como você veria se digitasse: $ person | gm. Eu prefiro que o tipo subjacente seja um PSCustomObject
Nick Meldrum
2
Eu vejo o ponto. Ainda assim, existem vantagens óbvias de intforma: 1) funciona mais rápido, não muito, mas para esta função em particular New-Persona diferença é de 20%; 2) é aparentemente mais fácil de digitar. Ao mesmo tempo, usando essa abordagem basicamente em todos os lugares, nunca vi nenhuma desvantagem. Mas eu concordo: pode haver alguns casos raros em que PSCustomObject é um pouco melhor.
Roman Kuzmin
@RomanKuzmin Ainda é 20% mais rápido se você instanciar um objeto personalizado global e armazená-lo como uma variável de script?
jpmc26
5

Surpreso, ninguém mencionou esta opção simples (vs 3 ou posterior) para criar objetos personalizados:

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

O tipo será PSCustomObject, não um tipo personalizado real. Mas provavelmente é a maneira mais fácil de criar um objeto personalizado.

Benjamin Hubbard
fonte
Veja também esta postagem do blog de Will Anderson sobre a diferença entre PSObject e PSCustomObject.
CodeFox 01 de
@CodeFox acabou de notar que o link está quebrado agora
superjos
2
@superjos, obrigado pela dica. Não consegui encontrar o novo local da postagem. Pelo menos a postagem foi salva pelo arquivo .
CodeFox de
2
aparentemente parece que virou um livro Git aqui :)
superjos
4

Existe o conceito de PSObject e Add-Member que você pode usar.

$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"

Isso resulta como:

[8] » $contact

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

A outra alternativa (que eu saiba) é definir um tipo em C # / VB.NET e carregar esse assembly no PowerShell para uso direto.

Esse comportamento é definitivamente encorajado porque permite que outros scripts ou seções de seu script trabalhem com um objeto real.

David Mohundro
fonte
3

Aqui está o caminho difícil para criar tipos personalizados e armazená-los em uma coleção.

$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
fonte
Toque agradável ao adicionar o nome do tipo ao objeto.
oɔɯǝɹ
0

Aqui está mais uma opção, que usa uma ideia semelhante à solução PSTypeName mencionada por Jaykul (e, portanto, também requer PSv3 ou superior).

Exemplo

  1. Crie um arquivo TypeName .Types.ps1xml definindo seu tipo. Ex 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. Importe seu tipo: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. Crie um objeto do seu tipo personalizado: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. Inicialize seu tipo usando o método de script que você definiu no XML: $p.Initialize('Anne', 'Droid')
  4. Olhe para isso; você verá todas as propriedades definidas:$p | Format-Table -AutoSize
  5. Digite chamando um modificador para atualizar o valor de uma propriedade: $p.SetGivenName('Dan')
  6. Olhe novamente para ver o valor atualizado: $p | Format-Table -AutoSize

Explicação

  • O arquivo PS1XML permite definir propriedades personalizadas nos tipos.
  • Não está restrito aos tipos .net como a documentação indica; então você pode colocar o que quiser em '/ Tipos / Tipo / Nome' qualquer objeto criado com um 'PSTypeName' correspondente irá herdar os membros definidos para este tipo.
  • Membros adicionado através PS1XMLou Add-Memberse restringem a NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethod, e CodeMethod(ou PropertySet/ MemberSet; embora estes são sujeitos às mesmas restrições). Todas essas propriedades são somente leitura.
  • Ao definir um ScriptMethod, podemos enganar a restrição acima. Por exemplo, podemos definir um método (por exemplo Initialize) que cria novas propriedades, definindo seus valores para nós; garantindo assim que nosso objeto tenha todas as propriedades de que precisamos para nossos outros scripts funcionarem.
  • Podemos usar esse mesmo truque para permitir que as propriedades sejam atualizáveis ​​(embora por meio de método em vez de atribuição direta), conforme mostrado no exemplo SetGivenName.

Essa abordagem não é ideal para todos os cenários; mas é útil para adicionar comportamentos de classe a tipos personalizados / pode ser usado em conjunto com outros métodos mencionados nas outras respostas. Por exemplo, no mundo real eu provavelmente só definiria a FullNamepropriedade no PS1XML e, em seguida, usaria uma função para criar o objeto com os valores necessários, assim:

Mais informações

Dê uma olhada na documentação ou no arquivo do tipo OOTB Get-Content $PSHome\types.ps1xmlpara se inspirar.

# 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
fonte
ps. Para aqueles que usam VSCode, você pode adicionar suporte PS1XML
JohnLBevan