Come posso creare un tipo personalizzato in PowerShell per i miei script da utilizzare?


88

Vorrei essere in grado di definire e utilizzare un tipo personalizzato in alcuni dei miei script di PowerShell. Ad esempio, supponiamo che avessi bisogno di un oggetto che avesse la seguente struttura:

Contact
{
    string First
    string Last
    string Phone
}

Come dovrei creare questo in modo da poterlo utilizzare in funzione come il seguente:

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

Qualcosa di simile è possibile o addirittura consigliato in PowerShell?

Risposte:


135

Prima di PowerShell 3

Il sistema di tipi estensibili di PowerShell originariamente non ti consentiva di creare tipi concreti che puoi testare nel modo in cui hai fatto nel tuo parametro. Se non hai bisogno di quel test, stai bene con uno degli altri metodi sopra menzionati.

Se si desidera un tipo effettivo a cui eseguire il cast o il controllo del tipo, come nel proprio script di esempio ... non è possibile farlo senza scriverlo in C # o VB.net e compilare. In PowerShell 2, puoi utilizzare il comando "Add-Type" per farlo in modo abbastanza semplice:

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

Nota storica : in PowerShell 1 era ancora più difficile. Dovevi usare manualmente CodeDom, c'è una vecchia funzione new-struct script su PoshCode.org che ti aiuterà. Il tuo esempio diventa:

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

Usare Add-Typeo New-Structti permetterà di testare effettivamente la classe nel tuo param([Contact]$contact)e crearne di nuovi usando $contact = new-object Contacte così via ...

In PowerShell 3

Se non hai bisogno di una classe "reale" a cui puoi eseguire il cast, non devi usare il modo di aggiungere membri che Steven e altri hanno dimostrato sopra.

Da PowerShell 2 è possibile utilizzare il parametro -Property per New-Object:

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

E in PowerShell 3, abbiamo la possibilità di utilizzare l' PSCustomObjectacceleratore per aggiungere un TypeName:

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

Stai ancora ottenendo solo un singolo oggetto, quindi dovresti creare una New-Contactfunzione per assicurarti che ogni oggetto risulti uguale, ma ora puoi facilmente verificare un parametro "è" uno di quei tipi decorando un parametro con l' PSTypeNameattributo:

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

In PowerShell 5

In PowerShell 5 tutto cambia e finalmente abbiamo classe enumcome parole chiave della lingua per la definizione dei tipi (non c'è structma va bene):

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

Abbiamo anche un nuovo modo di creare oggetti senza usare New-Object: [Contact]::new()- infatti, se mantieni la tua classe semplice e non definisci un costruttore, puoi creare oggetti eseguendo il casting di una tabella hash (sebbene senza un costruttore, non ci sarebbe modo per imporre che tutte le proprietà devono essere impostate):

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

$C = [Contact]@{
   First = "Joel"
   Last = "Bennett"
}

Bella risposta! Aggiungo solo una nota che questo stile è molto semplice per gli script e funziona ancora in PowerShell 5: New-Object PSObject -Property @ {prop here ...}
Ryan Shillington

2
Nelle prime versioni di PowerShell 5 non era possibile utilizzare New-Object con classi create utilizzando la sintassi della classe, ma ora è possibile. TUTTAVIA, se stai usando la parola chiave class, il tuo script è comunque limitato solo a PS5, quindi consiglierei comunque di usare la sintassi :: new se l'oggetto ha un costruttore che accetta parametri (è molto più veloce di New-Object) o casting altrimenti, che è sia una sintassi più pulita che più veloce.
Jaykul

Sei sicuro che il controllo del tipo non possa essere eseguito con i tipi creati utilizzando Add-Type? Sembra funzionare in PowerShell 2 su Win 2008 R2. Di 'Definisco contactutilizzando Add-Typecome nella vostra risposta e quindi creare un'istanza: $con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }. Poi chiamando funziona questa funzione: function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() }ma chiamare questa funzione fallisce, x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() }. Anche la chiamata x 'abc'non riesce con un messaggio di errore appropriato sulla trasmissione. Testato in PS 2 e 4.
jpmc26

Ovviamente puoi controllare i tipi creati con Add-Type@ jpmc26, quello che ho detto è che non puoi farlo senza compilare (cioè senza scriverlo in C # e chiamare Add-Type). Ovviamente, da PS3 puoi: c'è un [PSTypeName("...")]attributo che ti consente di specificare il tipo come stringa, che supporta i test su PSCustomObjects con il set PSTypeNames ...
Jaykul

58

La creazione di tipi personalizzati può essere eseguita in PowerShell.
Kirk Munro ha in realtà due ottimi post che descrivono dettagliatamente il processo.

Il libro Windows PowerShell In Action di Manning include anche un esempio di codice per la creazione di un linguaggio specifico del dominio per creare tipi personalizzati. Il libro è eccellente in tutto, quindi lo consiglio davvero.

Se stai solo cercando un modo rapido per eseguire quanto sopra, potresti creare una funzione per creare l'oggetto personalizzato come

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
}

17

Questo è il metodo di scelta rapida:

$myPerson = "" | Select-Object First,Last,Phone

3
Fondamentalmente, il cmdlet Select-Object aggiunge proprietà agli oggetti che vengono fornite se l'oggetto non dispone già di tale proprietà. In questo caso si passa un oggetto String vuoto al cmdlet Select-Object. Aggiunge le proprietà e passa l'oggetto lungo il tubo. Oppure, se è l'ultimo comando nella pipe, restituisce l'oggetto. Devo sottolineare che utilizzo questo metodo solo se lavoro al prompt. Per gli script, utilizzo sempre i cmdlet Add-Member o New-Object più espliciti.
EBGreen

Anche se questo è un ottimo trucco, puoi renderlo ancora più breve:$myPerson = 1 | Select First,Last,Phone
RaYell

Ciò non consente di utilizzare le funzioni di tipo nativo, poiché imposta il tipo di ogni membro come stringa. Dato contributo Jaykul sopra, rivela ogni nota come membro NotePropertydi stringtipo, si tratta di una Propertydi qualsiasi tipo che avete assegnato nell'oggetto. Questo è veloce e fa il lavoro però.
mbrownnyc

Questo potrebbe darti problemi se desideri una proprietà Length, poiché la stringa l'ha già e il tuo nuovo oggetto riceverà il valore esistente, che probabilmente non vuoi. Consiglio di passare un [int], come mostra @RaYell.
FSCKur

9

La risposta di Steven Murawski è ottima, tuttavia mi piace il più breve (o meglio solo l'oggetto di selezione più ordinato invece di usare la sintassi 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
}

New-Objectnon è nemmeno necessario. Questo farà lo stesso:... = 1 | select-object First, Last, Phone
Roman Kuzmin

1
Sì, ma lo stesso di EBGreen sopra - questo crea una sorta di strano tipo sottostante (nel tuo esempio sarebbe un Int32.) Come vedresti digitando: $ person | gm. Preferisco che il tipo sottostante sia un PSCustomObject
Nick Meldrum

2
Capisco il punto. Tuttavia, ci sono evidenti vantaggi di intway: 1) funziona più velocemente, non molto, ma per questa particolare funzione New-Personla differenza è del 20%; 2) è apparentemente più facile da digitare. Allo stesso tempo, usando questo approccio praticamente ovunque, non ho mai visto alcun inconveniente. Ma sono d'accordo: potrebbero esserci alcuni rari casi in cui PSCustomObject è un po 'migliore.
Roman Kuzmin

@RomanKuzmin È ancora più veloce del 20% se si istanzia un oggetto personalizzato globale e lo si memorizza come variabile di script?
jpmc26

5

Sorpreso nessuno ha menzionato questa semplice opzione (vs 3 o successiva) per la creazione di oggetti personalizzati:

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

Il tipo sarà PSCustomObject, non un vero tipo personalizzato. Ma è probabilmente il modo più semplice per creare un oggetto personalizzato.


Vedi anche questo post sul blog di Will Anderson sulla differenza tra PSObject e PSCustomObject.
CodeFox

@CodeFox ha appena notato che il collegamento è interrotto ora
superjos

2
@superjos, grazie per il suggerimento. Non sono riuscito a trovare la nuova posizione del post. Almeno il post è stato sostenuto dall'archivio .
CodeFox

2
a quanto pare sembra che si sia trasformato in un libro Git qui :)
superjos

4

C'è il concetto di PSObject e Add-Member che potresti usare.

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

Questo produce come:

[8] » $contact

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

L'altra alternativa (di cui sono a conoscenza) è definire un tipo in C # / VB.NET e caricare quell'assembly in PowerShell per l'uso diretto.

Questo comportamento è decisamente incoraggiato perché consente ad altri script o sezioni del tuo script di funzionare con un oggetto reale.


3

Ecco il percorso difficile per creare tipi personalizzati e archiviarli in una raccolta.

$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

Bel tocco con l'aggiunta del nome del tipo all'oggetto.
dal

0

Ecco un'altra opzione, che utilizza un'idea simile alla soluzione PSTypeName menzionata da Jaykul (e quindi richiede anche PSv3 o superiore).

Esempio

  1. Creare un TypeName .Types.ps1xml file di definizione del vostro tipo. Ad esempio 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 il tuo tipo: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. Crea un oggetto del tuo tipo personalizzato: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. Inizializza il tuo tipo usando il metodo di script che hai definito nell'XML: $p.Initialize('Anne', 'Droid')
  4. Guardarlo; vedrai tutte le proprietà definite:$p | Format-Table -AutoSize
  5. Digita chiamando un mutatore per aggiornare il valore di una proprietà: $p.SetGivenName('Dan')
  6. Guardalo di nuovo per vedere il valore aggiornato: $p | Format-Table -AutoSize

Spiegazione

  • Il file PS1XML consente di definire proprietà personalizzate sui tipi.
  • Non è limitato ai tipi .net come suggerisce la documentazione; così puoi mettere quello che ti piace in "/ Tipi / Tipo / Nome" qualsiasi oggetto creato con un "PSTypeName" corrispondente erediterà i membri definiti per questo tipo.
  • I membri aggiunti attraverso PS1XMLo Add-Membersono limitate a NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethod, e CodeMethod(o PropertySet/ MemberSet, anche se questi sono soggetti alle stesse restrizioni). Tutte queste proprietà sono di sola lettura.
  • Definendo a ScriptMethodpossiamo imbrogliare la restrizione di cui sopra. Es. Possiamo definire un metodo (es. Initialize) Che crea nuove proprietà, impostandone i valori per noi; assicurandoci così che il nostro oggetto abbia tutte le proprietà di cui abbiamo bisogno per il funzionamento degli altri nostri script.
  • Possiamo usare questo stesso trucco per consentire alle proprietà di essere aggiornabili (anche se tramite metodo piuttosto che assegnazione diretta), come mostrato nell'esempio SetGivenName.

Questo approccio non è ideale per tutti gli scenari; ma è utile per aggiungere comportamenti di tipo classe a tipi personalizzati / può essere usato insieme ad altri metodi menzionati nelle altre risposte. Ad esempio, nel mondo reale definirei la FullNameproprietà solo in PS1XML, quindi utilizzerei una funzione per creare l'oggetto con i valori richiesti, in questo modo:

Ulteriori informazioni

Dai un'occhiata alla documentazione o al file di tipo OOTB Get-Content $PSHome\types.ps1xmlper l'ispirazione.

# 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

ps. Per coloro che utilizzano VSCode, è possibile aggiungere il supporto PS1XML
JohnLBevan
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.