Jak utworzyć niestandardowy typ w programie PowerShell do użytku przez moje skrypty?

88

Chciałbym móc zdefiniować i używać niestandardowego typu w niektórych moich skryptach PowerShell. Załóżmy na przykład, że potrzebowałem obiektu, który miałby następującą strukturę:

Contact
{
    string First
    string Last
    string Phone
}

Jak bym zabrał się do tworzenia tego, aby móc go używać w funkcji takiej jak:

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

Czy coś takiego jest możliwe, czy nawet zalecane w PowerShell?

Scott Saad
źródło

Odpowiedzi:

133

Przed PowerShell 3

Rozszerzalny system typów programu PowerShell pierwotnie nie pozwalał na tworzenie konkretnych typów, które można przetestować w stosunku do sposobu, w jaki zrobiłeś to w parametrze. Jeśli nie potrzebujesz tego testu, możesz zastosować dowolną z innych metod wymienionych powyżej.

Jeśli chcesz mieć rzeczywisty typ, do którego możesz rzutować lub sprawdzać typy, jak w przykładowym skrypcie ... nie można tego zrobić bez napisania go w C # lub VB.net i skompilowania. W PowerShell 2 możesz użyć polecenia „Add-Type”, aby zrobić to całkiem prosto:

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

Uwaga historyczna : w PowerShell 1 było jeszcze trudniej. Trzeba było ręcznie użyć CodeDom, na PoshCode.org jest bardzo staryskryptfunkcji nowej struktury, który pomoże. Twój przykład to:

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

Używanie Add-Typelub New-Structpozwoli ci faktycznie przetestować klasę w twojej param([Contact]$contact)i stworzyć nowe za pomocą $contact = new-object Contacti tak dalej ...

W programie PowerShell 3

Jeśli nie potrzebujesz „prawdziwej” klasy, na którą możesz rzucić, nie musisz używać metody Add-Member, którą przedstawili powyżej Steven i inni .

Od PowerShell 2 można użyć parametru -Property dla New-Object:

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

W PowerShell 3 mamy możliwość użycia PSCustomObjectakceleratora do dodania TypeName:

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

Nadal otrzymujesz tylko jeden obiekt, więc powinieneś utworzyć New-Contactfunkcję, aby upewnić się, że każdy obiekt wychodzi tak samo, ale teraz możesz łatwo sprawdzić, czy parametr „jest” jednym z tych typów, dekorując parametr PSTypeNameatrybutem:

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

W programie PowerShell 5

W PowerShell 5 wszystko się zmienia i w końcu otrzymaliśmy classi enumjako słowa kluczowe języka do definiowania typów (nie ma, structale to jest w porządku):

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

Mamy również nowy sposób tworzenia obiektów bez użycia New-Object: [Contact]::new()- tak naprawdę, gdybyś utrzymywał prostą klasę i nie definiował konstruktora, możesz tworzyć obiekty, rzucając tablicę haszy (chociaż bez konstruktora nie byłoby sposobu aby wymusić, że wszystkie właściwości muszą być ustawione):

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
źródło
Świetna odpowiedź! Dodam tylko, że ten styl jest bardzo łatwy dla skryptów i nadal działa w PowerShell 5: New-Object PSObject -Property @ {prop here ...}
Ryan Shillington,
2
We wczesnych wersjach PowerShell 5 nie można było używać New-Object z klasami utworzonymi przy użyciu składni klas, ale teraz jest to możliwe. JEDNAK, jeśli używasz słowa kluczowego class, twój skrypt i tak jest ograniczony tylko do PS5, więc nadal zalecałbym używanie składni :: new, jeśli obiekt ma konstruktor, który przyjmuje parametry (jest znacznie szybszy niż New-Object) lub rzutowanie w inny sposób, co jest zarówno czystszą składnią, jak i szybszym.
Jaykul,
Czy na pewno sprawdzania typów nie można przeprowadzić w przypadku typów utworzonych za pomocą Add-Type? Wydaje się, że działa w PowerShell 2 na Win 2008 R2. Powiedzieć zdefiniować contactużywając Add-Typejak w swojej odpowiedzi, a następnie utworzyć instancję: $con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }. Potem dzwoni Funkcja ta działa: function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() }, ale wywołanie tej funkcji nie powiedzie się, x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() }. Wywołanie x 'abc'również kończy się niepowodzeniem i pojawia się odpowiedni komunikat o błędzie dotyczący przesyłania. Testowane na PS 2 i 4.
jpmc26
Oczywiście możesz sprawdzić typy utworzone za pomocą Add-Type@ jpmc26, powiedziałem, że nie możesz tego zrobić bez kompilacji (tj .: bez pisania w C # i wywoływania Add-Type). Oczywiście z PS3 można - jest [PSTypeName("...")]atrybut, który pozwala określić typ jako string, który obsługuje testowanie z PSCustomObjects z ustawionym PSTypeNames ...
Jaykul
58

Tworzenie niestandardowych typów można wykonać w programie PowerShell.
Kirk Munro ma w rzeczywistości dwa świetne posty, które szczegółowo opisują proces.

Książka Windows PowerShell w akcji autorstwa Manninga zawiera również przykład kodu do tworzenia języka specyficznego dla domeny w celu tworzenia niestandardowych typów. Książka jest doskonała pod każdym względem, więc naprawdę ją polecam.

Jeśli szukasz szybkiego sposobu na wykonanie powyższego, możesz utworzyć funkcję do tworzenia niestandardowego obiektu, takiego jak

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
źródło
17

Oto metoda skrótów:

$myPerson = "" | Select-Object First,Last,Phone
EBGreen
źródło
3
Zasadniczo cmdlet Select-Object dodaje właściwości do obiektów, które jest mu dane, jeśli obiekt nie ma jeszcze tej właściwości. W tym przypadku przekazujesz pusty obiekt String do polecenia cmdlet Select-Object. Dodaje właściwości i przesuwa obiekt wzdłuż rury. Lub jeśli jest to ostatnie polecenie w potoku, wyprowadza obiekt. Powinienem zaznaczyć, że używam tej metody tylko wtedy, gdy pracuję z monitem. W przypadku skryptów zawsze używam bardziej wyraźnych poleceń cmdlet Add-Member lub New-Object.
EBGreen
Chociaż jest to świetna sztuczka, możesz ją jeszcze skrócić:$myPerson = 1 | Select First,Last,Phone
RaYell
Nie pozwala to na wykorzystanie funkcji typu natywnego, ponieważ ustawia typ każdego elementu członkowskiego jako ciąg. Biorąc pod uwagę wkład Jaykul powyżej, ujawnia każda nuta jako członek NotePropertyod stringtypu, to Propertybez względu na rodzaj przypisaniu w obiekcie. To jest szybkie i jednak spełnia swoje zadanie.
mbrownnyc
Może to powodować problemy, jeśli chcesz mieć właściwość Length, ponieważ łańcuch już to ma, a nowy obiekt otrzyma istniejącą wartość - której prawdopodobnie nie chcesz. Zalecam podanie [int], jak pokazuje @RaYell.
FSCKur
9

Odpowiedź Stevena Murawskiego jest świetna, jednak podoba mi się krótszy (a raczej po prostu schludniejszy obiekt wyboru zamiast używania składni add-member):

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
źródło
New-Objectnie jest nawet potrzebne. To zrobi to samo:... = 1 | select-object First, Last, Phone
Roman Kuzmin
1
Tak, ale to samo co EBGreen powyżej - tworzy to rodzaj dziwnego typu bazowego (w twoim przykładzie byłby to Int32.), Tak jak byś zobaczył, gdybyś wpisał: $ person | gm. Wolę, aby typem bazowym był PSCustomObject
Nick Meldrum,
2
Rozumiem, o co chodzi. Mimo to istnieją oczywiste zalety intsposobu: 1) działa szybciej, niewiele, ale dla tej konkretnej funkcji New-Personróżnica wynosi 20%; 2) najwyraźniej łatwiej jest wpisać. Jednocześnie, stosując to podejście praktycznie wszędzie, nigdy nie widziałem żadnych wad. Ale zgadzam się: mogą zdarzyć się rzadkie przypadki, gdy PSCustomObject jest trochę lepszy.
Roman Kuzmin
@RomanKuzmin Czy tworzenie instancji globalnego obiektu niestandardowego i przechowywanie go jako zmiennej skryptu jest o 20% szybsze?
jpmc26
5

Zaskoczony, nikt nie wspomniał o tej prostej opcji (w porównaniu z 3 lub nowszą wersją) do tworzenia obiektów niestandardowych:

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

Typem będzie PSCustomObject, a nie rzeczywisty typ niestandardowy. Ale to prawdopodobnie najłatwiejszy sposób na stworzenie niestandardowego obiektu.

Benjamin Hubbard
źródło
Zobacz także ten wpis na blogu autorstwa Willa Andersona na temat różnicy między PSObject i PSCustomObject.
CodeFox
@CodeFox właśnie zauważył, że link jest teraz uszkodzony
superjos
2
@superjos, dzięki za podpowiedź. Nie mogłem znaleźć nowej lokalizacji postu. Przynajmniej post został zabezpieczony przez archiwum .
CodeFox,
2
najwyraźniej wygląda na to, że zmieniło się to tutaj w książkę Gita :)
superjos
4

Istnieje koncepcja PSObject i Add-Member, której możesz użyć.

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

To daje takie wyniki jak:

[8] » $contact

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

Inną alternatywą (o której wiem) jest zdefiniowanie typu w C # / VB.NET i załadowanie tego zestawu do programu PowerShell w celu bezpośredniego użycia.

Takie zachowanie jest zdecydowanie zalecane, ponieważ umożliwia innym skryptom lub sekcjom skryptu pracę z rzeczywistym obiektem.

David Mohundro
źródło
3

Oto trudna ścieżka tworzenia niestandardowych typów i przechowywania ich w kolekcji.

$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
źródło
Miły akcent z dodaniem nazwy typu do obiektu.
2014 o 22:43
0

Oto jeszcze jedna opcja, która wykorzystuje podobny pomysł do rozwiązania PSTypeName wspomnianego przez Jaykula (a zatem wymaga również PSv3 lub nowszego).

Przykład

  1. Tworzenie TypeName .Types.ps1xml plik definiujący typ. Np . 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. Zaimportuj swój typ: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. Utwórz obiekt własnego typu: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. Zainicjuj swój typ za pomocą metody skryptu zdefiniowanej w XML: $p.Initialize('Anne', 'Droid')
  4. Spójrz na to; zobaczysz wszystkie zdefiniowane właściwości:$p | Format-Table -AutoSize
  5. Wpisz wywołanie mutatora w celu zaktualizowania wartości właściwości: $p.SetGivenName('Dan')
  6. Spójrz ponownie, aby zobaczyć zaktualizowaną wartość: $p | Format-Table -AutoSize

Wyjaśnienie

  • Plik PS1XML umożliwia definiowanie niestandardowych właściwości typów.
  • Nie ogranicza się do typów .net, jak sugeruje dokumentacja; więc możesz umieścić to, co lubisz w „/ Types / Type / Name”, każdy obiekt utworzony z pasującym „PSTypeName” odziedziczy elementy zdefiniowane dla tego typu.
  • Członkowie dodani za pośrednictwem PS1XML lub Add-Membersą ograniczone do NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethod, i CodeMethod(lub PropertySet/ MemberSet, choć te podlegają takim samym ograniczeniom). Wszystkie te właściwości są tylko do odczytu.
  • Definiując a ScriptMethodmożemy oszukać powyższe ograniczenie. Np. Możemy zdefiniować metodę (np. Initialize), Która tworzy nowe właściwości, ustawiając dla nas ich wartości; w ten sposób upewniając się, że nasz obiekt ma wszystkie właściwości potrzebne do działania innych naszych skryptów.
  • Możemy użyć tej samej sztuczki, aby umożliwić aktualizację właściwości (aczkolwiek za pomocą metody, a nie bezpośredniego przypisania), jak pokazano w przykładzie SetGivenName.

To podejście nie jest idealne dla wszystkich scenariuszy; ale jest przydatny do dodawania zachowań klasowych do typów niestandardowych / może być używany w połączeniu z innymi metodami wymienionymi w innych odpowiedziach. Np. W prawdziwym świecie prawdopodobnie zdefiniowałbym FullNamewłaściwość tylko w PS1XML, a następnie użyłbym funkcji do stworzenia obiektu z wymaganymi wartościami, na przykład:

Więcej informacji

Zapoznaj się z dokumentacją lub plikiem typu OOTB w Get-Content $PSHome\types.ps1xmlposzukiwaniu inspiracji.

# 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
źródło
ps. Dla tych, którzy używają VSCode, możesz dodać obsługę PS1XML
JohnLBevan