Wie erstelle ich in PowerShell einen benutzerdefinierten Typ für meine Skripte?

87

Ich möchte in einigen meiner PowerShell-Skripts einen benutzerdefinierten Typ definieren und verwenden können. Nehmen wir zum Beispiel an, ich brauche ein Objekt mit der folgenden Struktur:

Contact
{
    string First
    string Last
    string Phone
}

Wie würde ich vorgehen, um dies zu erstellen, damit ich es in folgenden Funktionen verwenden kann:

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

Ist so etwas in PowerShell möglich oder sogar empfohlen?

Scott Saad
quelle

Antworten:

132

Vor PowerShell 3

Mit dem Extensible Type System von PowerShell konnten Sie ursprünglich keine konkreten Typen erstellen, die Sie anhand der Parameter testen können. Wenn Sie diesen Test nicht benötigen, können Sie eine der anderen oben genannten Methoden anwenden.

Wenn Sie einen tatsächlichen Typ möchten, den Sie wie in Ihrem Beispielskript umwandeln oder mit dem Sie ihn überprüfen können, können Sie dies nicht tun, ohne ihn in C # oder VB.net zu schreiben und zu kompilieren. In PowerShell 2 können Sie den Befehl "Typ hinzufügen" verwenden, um dies ganz einfach zu tun:

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

Historischer Hinweis : In PowerShell 1 war es noch schwieriger. Sie mussten CodeDom manuell verwenden, es gibt ein sehr altes Funktions- New- Struct - Skript auf PoshCode.org, das helfen wird. Ihr Beispiel wird:

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

Mit Add-Typeoder New-Structkönnen Sie die Klasse in Ihrer Klasse testen param([Contact]$contact)und neue erstellen, indem Sie $contact = new-object Contactusw. verwenden ...

In PowerShell 3

Wenn Sie keine "echte" Klasse benötigen, in die Sie umwandeln können, müssen Sie nicht die Add-Member-Methode verwenden, die Steven und andere oben demonstriert haben .

Seit PowerShell 2 können Sie den Parameter -Property für New-Object verwenden:

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

In PowerShell 3 haben wir die Möglichkeit, mithilfe des PSCustomObjectBeschleunigers einen Typnamen hinzuzufügen:

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

Sie erhalten immer noch nur ein einzelnes Objekt. Sie sollten daher eine New-ContactFunktion erstellen, um sicherzustellen, dass jedes Objekt gleich ausgegeben wird. Sie können jetzt jedoch leicht überprüfen, ob ein Parameter "ist" einer dieser Typen ist, indem Sie einen Parameter mit dem folgenden PSTypeNameAttribut dekorieren :

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

In PowerShell 5

In PowerShell 5 ändert sich alles und wir haben endlich classund enumals Sprachschlüsselwörter zum Definieren von Typen (es gibt keine, structaber das ist in Ordnung):

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

Wir haben auch eine neue Möglichkeit zum Erstellen von Objekten ohne Verwendung New-Object: [Contact]::new()- Wenn Sie Ihre Klasse einfach halten und keinen Konstruktor definieren, können Sie Objekte erstellen, indem Sie eine Hashtabelle umwandeln (obwohl es ohne Konstruktor keine Möglichkeit gibt um zu erzwingen, dass alle Eigenschaften festgelegt werden müssen):

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
quelle
Gute Antwort! Fügen Sie einfach einen Hinweis hinzu, dass dieser Stil für Skripte sehr einfach ist und dennoch in PowerShell 5 funktioniert: PSObject-Eigenschaft für neue Objekte @ {prop here ...}
Ryan Shillington
2
In den frühen PowerShell 5-Versionen konnten Sie New-Object nicht mit Klassen verwenden, die mit der Klassensyntax erstellt wurden, aber jetzt. Wenn Sie jedoch das Schlüsselwort class verwenden, ist Ihr Skript ohnehin nur auf PS5 beschränkt. Daher würde ich die Verwendung der :: new-Syntax empfehlen, wenn das Objekt einen Konstruktor hat, der Parameter akzeptiert (es ist viel schneller als New-Object) oder Casting ansonsten, was sowohl sauberer als auch schneller ist.
Jaykul
Sind Sie sicher, dass die Typprüfung nicht mit Typen durchgeführt werden kann, die mit erstellt wurden Add-Type? Es scheint in PowerShell 2 unter Win 2008 R2 zu funktionieren. Angenommen, ich definiere contactmit Add-Typewie in Ihrer Antwort und erstelle dann eine Instanz : $con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }. Dann funktioniert das Aufrufen dieser Funktion : function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() }, das Aufrufen dieser Funktion schlägt jedoch fehl x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() }. Der Aufruf x 'abc'schlägt auch mit einer entsprechenden Fehlermeldung zum Casting fehl. Getestet in PS 2 und 4.
jpmc26
Natürlich können Sie Typen überprüfen, die mit Add-Type@ jpmc26 erstellt wurden. Ich habe gesagt, dass Sie dies nicht ohne Kompilieren tun können (dh ohne es in C # zu schreiben und aufzurufen Add-Type). Natürlich können Sie von PS3 aus - es gibt ein [PSTypeName("...")]Attribut, mit dem Sie den Typ als Zeichenfolge angeben können, das das Testen gegen PSCustomObjects mit festgelegten PSTypeNames unterstützt ...
Jaykul
58

Das Erstellen benutzerdefinierter Typen kann in PowerShell erfolgen.
Kirk Munro hat tatsächlich zwei großartige Beiträge, die den Prozess gründlich beschreiben.

Das Buch Windows PowerShell In Action von Manning enthält auch ein Codebeispiel zum Erstellen einer domänenspezifischen Sprache zum Erstellen benutzerdefinierter Typen. Das Buch ist rundum ausgezeichnet, daher kann ich es nur empfehlen.

Wenn Sie nur nach einer schnellen Möglichkeit suchen, dies zu tun, können Sie eine Funktion zum Erstellen eines benutzerdefinierten Objekts erstellen

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
quelle
17

Dies ist die Verknüpfungsmethode:

$myPerson = "" | Select-Object First,Last,Phone
EBGreen
quelle
3
Grundsätzlich fügt das Cmdlet Select-Object Objekten Objekte hinzu, die angegeben werden, wenn das Objekt diese Eigenschaft noch nicht besitzt. In diesem Fall übergeben Sie dem Cmdlet Select-Object ein leeres String-Objekt. Es fügt die Eigenschaften hinzu und leitet das Objekt entlang der Pipe weiter. Wenn es sich um den letzten Befehl in der Pipe handelt, wird das Objekt ausgegeben. Ich sollte darauf hinweisen, dass ich diese Methode nur verwende, wenn ich an der Eingabeaufforderung arbeite. Für Skripte verwende ich immer die expliziteren Cmdlets Add-Member oder New-Object.
EBGreen
Obwohl dies ein großartiger Trick ist, können Sie ihn sogar noch kürzer machen:$myPerson = 1 | Select First,Last,Phone
RaYell
Auf diese Weise können Sie die nativen Typfunktionen nicht verwenden, da der Typ jedes Elements als Zeichenfolge festgelegt wird. In Anbetracht Jaykul Beitrag oben, offenbart jedes Mitglied Note als NotePropertyder stringTyp, ist es eine wie Propertyauch immer geartete Sie in dem Objekt zugewiesen haben. Dies ist schnell und erledigt den Job.
mbrownnyc
Dies kann zu Problemen führen, wenn Sie eine Length-Eigenschaft wünschen, da die Zeichenfolge diese bereits hat und Ihr neues Objekt den vorhandenen Wert erhält - den Sie wahrscheinlich nicht möchten. Ich empfehle, ein [int] zu übergeben, wie @RaYell zeigt.
FSCKur
9

Steven Murawskis Antwort ist großartig, aber ich mag das kürzere (oder besser gesagt das sauberere Auswahlobjekt, anstatt die Syntax für Add-Member zu verwenden):

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
quelle
New-Objectwird nicht einmal benötigt. Dies wird das gleiche tun:... = 1 | select-object First, Last, Phone
Roman Kuzmin
1
Ja, aber das gleiche wie bei EBGreen oben - dies erzeugt eine Art seltsamen zugrunde liegenden Typ (in Ihrem Beispiel wäre es ein Int32.), Wie Sie sehen würden, wenn Sie Folgendes eingeben würden: $ person | gm. Ich bevorzuge es, wenn der zugrunde liegende Typ ein PSCustomObject ist
Nick Meldrum
2
Ich verstehe den Punkt. Dennoch gibt es offensichtliche Vorteile des intWeges: 1) es arbeitet schneller, nicht viel, aber für diese spezielle Funktion New-Personbeträgt der Unterschied 20%; 2) es ist anscheinend einfacher zu tippen. Gleichzeitig habe ich mit diesem Ansatz praktisch überall keine Nachteile gesehen. Aber ich stimme zu: Es kann einige seltene Fälle geben, in denen PSCustomObject besser ist.
Roman Kuzmin
@RomanKuzmin Ist es immer noch 20% schneller, wenn Sie ein globales benutzerdefiniertes Objekt instanziieren und als Skriptvariable speichern?
jpmc26
5

Überrascht erwähnte niemand diese einfache Option (vs 3 oder höher) zum Erstellen von benutzerdefinierten Objekten:

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

Der Typ ist PSCustomObject, jedoch kein tatsächlicher benutzerdefinierter Typ. Aber es ist wahrscheinlich der einfachste Weg, ein benutzerdefiniertes Objekt zu erstellen.

Benjamin Hubbard
quelle
Siehe auch diesen Blog-Beitrag von Will Anderson zum Unterschied zwischen PSObject und PSCustomObject.
CodeFox
@ CodeFox gerade bemerkt, dass Link jetzt gebrochen ist
Superjos
2
@ Superjos, danke für den Hinweis. Ich konnte den neuen Speicherort der Post nicht finden. Zumindest wurde der Beitrag vom Archiv gesichert .
CodeFox
2
offenbar sieht es so aus, dass in einem Git Buch drehte hier :)
superjos
4

Es gibt das Konzept von PSObject und Add-Member, das Sie verwenden können.

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

Dies gibt aus wie:

[8] » $contact

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

Die andere Alternative (die mir bekannt ist) besteht darin, einen Typ in C # / VB.NET zu definieren und diese Assembly zur direkten Verwendung in PowerShell zu laden.

Dieses Verhalten wird definitiv empfohlen, da andere Skripte oder Abschnitte Ihres Skripts mit einem tatsächlichen Objekt arbeiten können.

David Mohundro
quelle
3

Hier ist der schwierige Weg, um benutzerdefinierte Typen zu erstellen und in einer Sammlung zu speichern.

$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
quelle
Nizza Touch mit dem Hinzufügen des Typnamens zum Objekt.
oɔɯǝɹ
0

Hier ist eine weitere Option, die eine ähnliche Idee wie die von Jaykul erwähnte PSTypeName-Lösung verwendet (und daher auch PSv3 oder höher erfordert).

Beispiel

  1. Erstellen Sie eine TypeName .Types.ps1xml- Datei, die Ihren Typ definiert. ZB 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. Importieren Sie Ihren Typ: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. Erstellen Sie ein Objekt Ihres benutzerdefinierten Typs: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. Initialisieren Sie Ihren Typ mit der Skriptmethode, die Sie im XML definiert haben: $p.Initialize('Anne', 'Droid')
  4. Schau es dir an; Sie sehen alle definierten Eigenschaften:$p | Format-Table -AutoSize
  5. Geben Sie den Aufruf eines Mutators ein, um den Wert einer Eigenschaft zu aktualisieren: $p.SetGivenName('Dan')
  6. Schauen Sie es sich noch einmal an, um den aktualisierten Wert zu sehen: $p | Format-Table -AutoSize

Erläuterung

  • Mit der PS1XML-Datei können Sie benutzerdefinierte Eigenschaften für Typen definieren.
  • Es ist nicht auf .net-Typen beschränkt, wie in der Dokumentation angegeben. So können Sie in '/ Types / Type / Name' ein beliebiges Objekt einfügen, das mit einem passenden 'PSTypeName' erstellt wurde. Es erbt die für diesen Typ definierten Mitglieder.
  • Mitglieder hinzugefügt durch PS1XMLoder Add-Membersind beschränkt auf NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethod, und CodeMethod(oder PropertySet/ MemberSet, obwohl diejenigen , unterliegen den gleichen Einschränkungen). Alle diese Eigenschaften sind schreibgeschützt.
  • Durch die Definition von a können ScriptMethodwir die obige Einschränkung betrügen. ZB Wir können eine Methode (z. B. Initialize) definieren, die neue Eigenschaften erstellt und deren Werte für uns festlegt. So stellen Sie sicher, dass unser Objekt alle Eigenschaften hat, die wir benötigen, damit unsere anderen Skripte funktionieren.
  • Wir können denselben Trick verwenden, um zu ermöglichen, dass die Eigenschaften aktualisiert werden können (allerdings über eine Methode anstelle einer direkten Zuweisung), wie in den Beispielen gezeigt SetGivenName.

Dieser Ansatz ist nicht für alle Szenarien ideal. Dies ist jedoch nützlich, um benutzerdefinierten Typen klassenähnliches Verhalten hinzuzufügen. / Kann in Verbindung mit anderen in den anderen Antworten genannten Methoden verwendet werden. In der realen Welt würde ich wahrscheinlich nur die FullNameEigenschaft in PS1XML definieren und dann eine Funktion verwenden, um das Objekt mit den erforderlichen Werten zu erstellen, wie folgt:

Mehr Info

Schauen Sie sich die Dokumentation oder die OOTB-Datei an, Get-Content $PSHome\types.ps1xmlum sich inspirieren zu lassen.

# 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
quelle
ps. Für diejenigen, die VSCode verwenden, können Sie PS1XML-Unterstützung hinzufügen: code.visualstudio.com/docs/languages/…
JohnLBevan