Comment créer un type personnalisé dans PowerShell pour mes scripts à utiliser?

88

Je voudrais pouvoir définir et utiliser un type personnalisé dans certains de mes scripts PowerShell. Par exemple, faisons comme si j'avais besoin d'un objet ayant la structure suivante:

Contact
{
    string First
    string Last
    string Phone
}

Comment pourrais-je créer ceci afin que je puisse l'utiliser dans une fonction comme celle-ci:

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

Est-ce que quelque chose comme ça est possible, voire recommandé dans PowerShell?

Scott Saad
la source

Réponses:

133

Avant PowerShell 3

Le système de type extensible de PowerShell ne vous permettait pas à l'origine de créer des types concrets que vous pouvez tester comme vous l'avez fait dans votre paramètre. Si vous n'avez pas besoin de ce test, vous êtes d'accord avec l'une des autres méthodes mentionnées ci-dessus.

Si vous voulez un type réel avec lequel vous pouvez transtyper ou vérifier le type, comme dans votre exemple de script ... cela ne peut pas être fait sans l'écrire en C # ou VB.net et le compiler. Dans PowerShell 2, vous pouvez utiliser la commande "Add-Type" pour le faire assez simplement:

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

Note historique : dans PowerShell 1, c'était encore plus difficile. Vous deviez utiliser manuellement CodeDom, il y a un très ancienscript defonction new-struct sur PoshCode.org qui vous aidera. Votre exemple devient:

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

Utiliser Add-Typeou New-Structvous permettra de tester la classe dans votre param([Contact]$contact)et d'en créer de nouvelles en utilisant $contact = new-object Contactet ainsi de suite ...

Dans PowerShell 3

Si vous n'avez pas besoin d'une "vraie" classe dans laquelle vous pouvez effectuer un cast, vous n'avez pas besoin d'utiliser la méthode d'ajout de membre que Steven et d'autres ont démontrée ci-dessus.

Depuis PowerShell 2, vous pouvez utiliser le paramètre -Property pour New-Object:

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

Et dans PowerShell 3, nous avons la possibilité d'utiliser l' PSCustomObjectaccélérateur pour ajouter un TypeName:

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

Vous n'obtenez toujours qu'un seul objet, vous devez donc créer une New-Contactfonction pour vous assurer que chaque objet sort de la même manière, mais vous pouvez maintenant facilement vérifier qu'un paramètre "est" l'un de ces types en décorant un paramètre avec l' PSTypeNameattribut:

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

Dans PowerShell 5

Dans PowerShell 5, tout change et nous avons finalement obtenu classetenum tant que mots-clés de langage pour définir les types (il n'y a pas structmais c'est ok):

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

Nous avons également une nouvelle façon de créer des objets sans utiliser New-Object: [Contact]::new()- en fait, si vous gardez votre classe simple et ne définissez pas de constructeur, vous pouvez créer des objets en castant une table de hachage (bien que sans constructeur, il n'y aurait aucun moyen pour garantir que toutes les propriétés doivent être définies):

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
la source
Très bonne réponse! Il suffit d'ajouter une note indiquant que ce style est très simple pour les scripts et fonctionne toujours dans PowerShell 5: Nouvel objet PSObject -Property @ {prop here ...}
Ryan Shillington
2
Dans les premières versions de PowerShell 5, vous ne pouviez pas utiliser New-Object avec des classes créées à l'aide de la syntaxe de classe, mais vous le pouvez maintenant. CEPENDANT, si vous utilisez le mot-clé class, votre script est limité à PS5 de toute façon, donc je recommanderais toujours d'utiliser la syntaxe :: new si l'objet a un constructeur qui prend des paramètres (c'est beaucoup plus rapide que New-Object) ou casting autrement, ce qui est à la fois une syntaxe plus propre et plus rapide.
Jaykul
Êtes-vous sûr que la vérification de type ne peut pas être effectuée avec les types créés à l'aide de Add-Type? Il semble fonctionner dans PowerShell 2 sur Win 2008 R2. Dire que je définis le contactutilisant Add-Typecomme dans votre réponse, puis créer une instance: $con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }. Ensuite , l' appel de cette fonction fonctionne: function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() }, mais d' appeler cette fonction échoue, x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() }. L'appel x 'abc'échoue également avec un message d'erreur approprié concernant la diffusion. Testé en PS 2 et 4.
jpmc26
Bien sûr, vous pouvez vérifier les types créés avec Add-Type@ jpmc26, ce que j'ai dit, c'est que vous ne pouvez pas le faire sans compiler (c'est-à-dire sans l'écrire en C # et appeler Add-Type). Bien sûr, à partir de PS3, vous pouvez - il existe un [PSTypeName("...")]attribut qui vous permet de spécifier le type sous forme de chaîne, qui prend en charge les tests contre PSCustomObjects avec le jeu PSTypeNames ...
Jaykul
58

La création de types personnalisés peut être effectuée dans PowerShell.
Kirk Munro a en fait deux excellents articles qui détaillent le processus à fond.

Le livre Windows PowerShell en action de Manning propose également un exemple de code permettant de créer un langage spécifique à un domaine pour créer des types personnalisés. Le livre est excellent tout autour, donc je le recommande vraiment.

Si vous cherchez simplement un moyen rapide de faire ce qui précède, vous pouvez créer une fonction pour créer l'objet personnalisé comme

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
la source
17

Voici la méthode de raccourci:

$myPerson = "" | Select-Object First,Last,Phone
EBGreen
la source
3
Fondamentalement, l'applet de commande Select-Object ajoute des propriétés aux objets qui lui sont attribués si l'objet ne possède pas déjà cette propriété. Dans ce cas, vous remettez un objet String vide à l'applet de commande Select-Object. Il ajoute les propriétés et passe l'objet le long du tuyau. Ou s'il s'agit de la dernière commande du tube, il génère l'objet. Je dois souligner que je n'utilise cette méthode que si je travaille à l'invite. Pour les scripts, j'utilise toujours les applets de commande Add-Member ou New-Object plus explicites.
EBGreen
Bien que ce soit un excellent truc, vous pouvez le rendre encore plus court:$myPerson = 1 | Select First,Last,Phone
RaYell
Cela ne vous permet pas d'utiliser les fonctions de type natif, car il définit le type de chaque membre sous forme de chaîne. Compte tenu de la contribution Jaykul ci - dessus, révèle chaque note de membre comme NotePropertydu stringtype, il est Propertyde quelque type que vous avez attribué à l'objet. Ceci est rapide et fait le travail cependant.
mbrownnyc
Cela peut vous poser des problèmes si vous voulez une propriété Length, car string l'a déjà et votre nouvel objet obtiendra la valeur existante - ce que vous ne voulez probablement pas. Je recommande de passer un [int], comme le montre @RaYell.
FSCKur
9

La réponse de Steven Murawski est excellente, mais j'aime le plus court (ou plutôt juste l'objet de sélection plus net au lieu d'utiliser la syntaxe d'ajout de membre):

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
la source
New-Objectn'est même pas nécessaire. Cela fera de même:... = 1 | select-object First, Last, Phone
Roman Kuzmin
1
Ouais, mais comme EBGreen ci-dessus - cela crée une sorte de type sous-jacent étrange (dans votre exemple, ce serait un Int32.) Comme vous le verriez si vous tapiez: $ person | gm. Je préfère que le type sous-jacent soit un PSCustomObject
Nick Meldrum
2
Je vois le point. Pourtant, il y a des avantages évidents de intmanière: 1) cela fonctionne plus vite, pas beaucoup, mais pour cette fonction particulière, New-Personla différence est de 20%; 2) il est apparemment plus facile à taper. En même temps, en utilisant cette approche pratiquement partout, je n'ai jamais vu d'inconvénients. Mais je suis d'accord: il peut y avoir de rares cas où PSCustomObject est un peu mieux.
Roman Kuzmin
@RomanKuzmin Est-ce encore 20% plus rapide si vous instanciez un objet personnalisé global et le stockez en tant que variable de script?
jpmc26
5

Surpris, personne n'a mentionné cette option simple (vs 3 ou version ultérieure) pour créer des objets personnalisés:

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

Le type sera PSCustomObject, mais pas un type personnalisé réel. Mais c'est probablement le moyen le plus simple de créer un objet personnalisé.

Benjamin Hubbard
la source
Voir aussi ce billet de blog de Will Anderson sur la différence entre PSObject et PSCustomObject.
CodeFox
@CodeFox vient de remarquer que le lien est rompu maintenant
superjos
2
@superjos, merci pour l'indice. Je n'ai pas pu trouver le nouvel emplacement du message. Au moins, le message a été sauvegardé par les archives .
CodeFox
2
Apparemment, cela ressemble à un livre Git ici :)
superjos
4

Il y a le concept de PSObject et Add-Member que vous pouvez utiliser.

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

Cela produit comme:

[8] » $contact

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

L'autre alternative (dont je suis conscient) consiste à définir un type dans C # / VB.NET et à charger cet assembly dans PowerShell pour une utilisation directe.

Ce comportement est définitivement encouragé car il permet à d'autres scripts ou sections de votre script de fonctionner avec un objet réel.

David Mohundro
la source
3

Voici le chemin difficile pour créer des types personnalisés et les stocker dans une collection.

$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
la source
Belle touche avec l'ajout du nom du type à l'objet.
oɔɯǝɹ
0

Voici une autre option, qui utilise une idée similaire à la solution PSTypeName mentionnée par Jaykul (et nécessite donc également PSv3 ou supérieur).

Exemple

  1. Créez un fichier TypeName .Types.ps1xml définissant votre type. Par exemple 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. Importez votre type: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. Créez un objet de votre type personnalisé: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. Initialisez votre type à l'aide de la méthode de script que vous avez définie dans le XML: $p.Initialize('Anne', 'Droid')
  4. Regarde ça; vous verrez toutes les propriétés définies:$p | Format-Table -AutoSize
  5. Tapez appeler un mutateur pour mettre à jour la valeur d'une propriété: $p.SetGivenName('Dan')
  6. Regardez-le à nouveau pour voir la valeur mise à jour: $p | Format-Table -AutoSize

Explication

  • Le fichier PS1XML vous permet de définir des propriétés personnalisées sur les types.
  • Il n'est pas limité aux types .net comme l'indique la documentation; afin que vous puissiez mettre ce que vous voulez dans '/ Types / Type / Name', tout objet créé avec un 'PSTypeName' correspondant héritera des membres définis pour ce type.
  • Les membres ajoutés par PS1XMLou Add-Membersont limités à NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethodet CodeMethod(ou PropertySet/MemberSet , bien que ceux -ci sont soumis aux mêmes restrictions). Toutes ces propriétés sont en lecture seule.
  • En définissant un, ScriptMethodnous pouvons tricher la restriction ci-dessus. Par exemple, nous pouvons définir une méthode (par exempleInitialize ) qui crée de nouvelles propriétés, en définissant leurs valeurs pour nous; garantissant ainsi que notre objet possède toutes les propriétés dont nous avons besoin pour que nos autres scripts fonctionnent.
  • Nous pouvons utiliser cette même astuce pour permettre aux propriétés d'être mises à jour (bien que via une méthode plutôt qu'une affectation directe), comme indiqué dans l'exemple SetGivenName.

Cette approche n'est pas idéale pour tous les scénarios; mais est utile pour ajouter des comportements de classe à des types personnalisés / peut être utilisé en conjonction avec d'autres méthodes mentionnées dans les autres réponses. Par exemple, dans le monde réel, je définirais probablement uniquement la FullNamepropriété dans PS1XML, puis utiliserais une fonction pour créer l'objet avec les valeurs requises, comme ceci:

Plus d'informations

Jetez un œil à la documentation ou au fichier de type OOTB Get-Content $PSHome\types.ps1xmlpour vous inspirer.

# 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
la source
ps. Pour ceux qui utilisent VSCode, vous pouvez ajouter le support PS1XML
JohnLBevan