Bagaimana cara membuat tipe kustom di PowerShell untuk digunakan skrip saya?

88

Saya ingin dapat menentukan dan menggunakan tipe kustom di beberapa skrip PowerShell saya. Misalnya, anggap saja saya membutuhkan objek yang memiliki struktur berikut:

Contact
{
    string First
    string Last
    string Phone
}

Bagaimana saya akan membuat ini sehingga saya bisa menggunakannya dalam fungsi seperti berikut:

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

Apakah sesuatu seperti ini mungkin, atau bahkan direkomendasikan di PowerShell?

Scott Saad
sumber

Jawaban:

135

Sebelum PowerShell 3

Sistem Jenis Dapat Diperluas PowerShell awalnya tidak memungkinkan Anda membuat jenis konkret yang dapat Anda uji dengan cara yang Anda lakukan di parameter Anda. Jika Anda tidak memerlukan tes itu, Anda baik-baik saja dengan metode lain yang disebutkan di atas.

Jika Anda menginginkan tipe aktual yang dapat Anda transmisikan atau gunakan pemeriksaan ketik, seperti dalam contoh skrip Anda ... itu tidak dapat dilakukan tanpa menuliskannya di C # atau VB.net dan kompilasi. Di PowerShell 2, Anda dapat menggunakan perintah "Add-Type" untuk melakukannya dengan cukup sederhana:

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

Catatan Sejarah : Di PowerShell 1 bahkan lebih sulit. Anda harus menggunakan CodeDom secara manual, ada fungsiskrip new-struct yang sangat lamadi PoshCode.org yang akan membantu. Contoh Anda menjadi:

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

Menggunakan Add-Typeatau New-Structakan membiarkan Anda benar-benar menguji kelas di Anda param([Contact]$contact)dan membuat yang baru menggunakan $contact = new-object Contactdan seterusnya ...

Di PowerShell 3

Jika Anda tidak memerlukan kelas "nyata" yang dapat Anda gunakan untuk melakukan cast, Anda tidak harus menggunakan cara Tambah Anggota seperti yang ditunjukkan Steven dan orang lain di atas.

Sejak PowerShell 2 Anda dapat menggunakan parameter -Property untuk New-Object:

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

Dan di PowerShell 3, kami mendapatkan kemampuan untuk menggunakan PSCustomObjectakselerator untuk menambahkan TypeName:

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

Anda masih hanya mendapatkan satu objek, jadi Anda harus membuat New-Contactfungsi untuk memastikan bahwa setiap objek keluar sama, tetapi sekarang Anda dapat dengan mudah memverifikasi parameter "adalah" salah satu dari jenis itu dengan menghias parameter dengan PSTypeNameatribut:

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

Di PowerShell 5

Di PowerShell 5 semuanya berubah, dan kami akhirnya mendapatkan classdan enumsebagai kata kunci bahasa untuk menentukan jenis (tidak ada structtapi tidak apa-apa):

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

Kami juga mendapatkan cara baru untuk membuat objek tanpa menggunakan New-Object: [Contact]::new()- pada kenyataannya, jika Anda membuat kelas Anda tetap sederhana dan tidak mendefinisikan konstruktor, Anda dapat membuat objek dengan mentransmisikan hashtable (meskipun tanpa konstruktor, tidak mungkin untuk menegakkan bahwa semua properti harus disetel):

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
sumber
Jawaban yang bagus! Hanya menambahkan catatan bahwa gaya ini sangat mudah untuk skrip dan masih berfungsi di PowerShell 5: New-Object PSObject -Property @ {prop here ...}
Ryan Shillington
2
Di rilis PowerShell 5 awal Anda tidak bisa menggunakan New-Object dengan kelas yang dibuat menggunakan sintaks kelas, tetapi Anda sekarang bisa. NAMUN, jika Anda menggunakan kata kunci class, skrip Anda hanya terbatas pada PS5, jadi saya tetap akan merekomendasikan menggunakan :: sintaks baru jika objek memiliki konstruktor yang mengambil parameter ( jauh lebih cepat daripada New-Object) atau mentransmisikan sebaliknya, yang merupakan sintaks yang lebih bersih dan lebih cepat.
Jaykul
Apakah Anda yakin pemeriksaan jenis tidak dapat dilakukan dengan jenis yang dibuat menggunakan Add-Type? Tampaknya berfungsi di PowerShell 2 pada Win 2008 R2. Katakanlah saya mendefinisikan contactmenggunakan Add-Typeseperti dalam jawaban Anda dan kemudian membuat sebuah contoh: $con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }. Kemudian memanggil fungsi ini berfungsi:, function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() }tetapi pemanggilan fungsi ini gagal x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() },. Panggilan x 'abc'juga gagal dengan pesan kesalahan yang sesuai tentang transmisi. Diuji di PS 2 dan 4.
jpmc26
Tentu saja Anda dapat memeriksa jenis yang dibuat dengan Add-Type@ jpmc26, apa yang saya katakan adalah Anda tidak dapat melakukannya tanpa kompilasi (yaitu: tanpa menuliskannya di C # dan memanggil Add-Type). Tentu saja, dari PS3 Anda bisa - ada [PSTypeName("...")]atribut yang memungkinkan Anda untuk menentukan jenis sebagai string, yang mendukung pengujian terhadap PSCustomObjects dengan PSTypeNames set ...
Jaykul
58

Membuat tipe kustom dapat dilakukan di PowerShell.
Kirk Munro sebenarnya memiliki dua pos hebat yang merinci prosesnya secara menyeluruh.

Buku Windows PowerShell In Action by Manning juga memiliki contoh kode untuk membuat bahasa khusus domain untuk membuat tipe kustom. Buku ini sangat bagus secara keseluruhan, jadi saya sangat merekomendasikannya.

Jika Anda hanya mencari cara cepat untuk melakukan hal di atas, Anda dapat membuat fungsi untuk membuat objek khusus seperti

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

Ini adalah metode pintas:

$myPerson = "" | Select-Object First,Last,Phone
EBGreen
sumber
3
Pada dasarnya, cmdlet Select-Object menambahkan properti ke objek yang diberikan jika objek belum memiliki properti itu. Dalam hal ini Anda menyerahkan objek String kosong ke cmdlet Select-Object. Ia menambahkan properti dan meneruskan objek di sepanjang pipa. Atau jika itu adalah perintah terakhir dalam pipa, itu akan mengeluarkan objek. Saya harus menunjukkan bahwa saya hanya menggunakan metode ini jika saya bekerja pada prompt. Untuk skrip, saya selalu menggunakan cmdlet Add-Member atau New-Object yang lebih eksplisit.
EBGreen
Meskipun ini adalah trik yang bagus, Anda sebenarnya dapat membuatnya lebih pendek:$myPerson = 1 | Select First,Last,Phone
RaYell
Ini tidak memungkinkan Anda untuk menggunakan fungsi tipe asli, karena ini menetapkan tipe setiap anggota sebagai string. Mengingat kontribusi Jaykul di atas, mengungkapkan catatan masing-masing anggota sebagai NotePropertydari stringjenis, itu adalah Propertydari jenis apa pun yang Anda telah ditugaskan dalam objek. Ini cepat dan berhasil.
mbrownnyc
Ini mungkin memberi Anda masalah jika Anda menginginkan properti Panjang, karena string sudah memilikinya dan objek baru Anda akan mendapatkan nilai yang ada - yang mungkin tidak Anda inginkan. Saya sarankan untuk memberikan [int], seperti yang ditunjukkan @RaYell.
FSCKur
9

Jawaban Steven Murawski bagus, namun saya suka yang lebih pendek (atau lebih tepatnya hanya objek-pilih yang lebih rapi daripada menggunakan sintaks anggota tambahan):

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
sumber
New-Objectbahkan tidak dibutuhkan. Ini akan melakukan hal yang sama:... = 1 | select-object First, Last, Phone
Roman Kuzmin
1
Ya, tapi sama seperti EBGreen di atas - ini menciptakan semacam tipe dasar yang aneh (dalam contoh Anda ini adalah Int32.) Seperti yang Anda lihat jika Anda mengetik: $ person | gm. Saya lebih suka memiliki tipe yang mendasari menjadi PSCustomObject
Nick Meldrum
2
Saya mengerti maksudnya. Namun, ada keuntungan yang jelas dari intcara: 1) bekerja lebih cepat, tidak banyak, tetapi untuk fungsi khusus ini New-Personperbedaannya adalah 20%; 2) tampaknya lebih mudah untuk mengetik. Pada saat yang sama, menggunakan pendekatan ini pada dasarnya di mana-mana, saya belum pernah melihat kekurangannya. Tapi saya setuju: mungkin ada beberapa kasus yang jarang terjadi saat PSCustomObject lebih baik.
Roman Kuzmin
@RomanKuzmin Apakah masih 20% lebih cepat jika Anda membuat instance objek kustom global dan menyimpannya sebagai variabel skrip?
jpmc26
5

Terkejut tidak ada yang menyebutkan opsi sederhana ini (vs 3 atau lebih baru) untuk membuat objek khusus:

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

Jenisnya adalah PSCustomObject, bukan jenis kustom sebenarnya. Tapi ini mungkin cara termudah untuk membuat objek kustom.

Benjamin Hubbard
sumber
Lihat juga posting blog ini oleh Will Anderson tentang perbedaan PSObject dan PSCustomObject.
CodeFox
@CodeFox baru saja menyadari bahwa tautannya rusak sekarang
superjos
2
@superjos, terima kasih atas petunjuknya. Saya tidak dapat menemukan lokasi baru dari postingan tersebut. Setidaknya posting itu didukung oleh arsip .
CodeFox
2
rupanya sepertinya itu berubah menjadi buku Git di sini :)
superjos
4

Ada konsep PSObject dan Add-Member yang bisa Anda gunakan.

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

Output ini seperti:

[8] » $contact

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

Alternatif lain (yang saya ketahui) adalah untuk menentukan tipe di C # / VB.NET dan memuat rakitan itu ke PowerShell untuk digunakan secara langsung.

Perilaku ini sangat dianjurkan karena memungkinkan skrip atau bagian lain dari skrip Anda bekerja dengan objek yang sebenarnya.

David Mohundro
sumber
3

Berikut adalah cara yang sulit untuk membuat jenis kustom dan menyimpannya dalam koleksi.

$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
sumber
Sentuhan yang bagus dengan menambahkan nama tipe ke objek.
oɔɯǝɹ
0

Berikut satu opsi lagi, yang menggunakan ide serupa dengan solusi PSTypeName yang disebutkan oleh Jaykul (dan karenanya juga memerlukan PSv3 atau lebih tinggi).

Contoh

  1. Buat file TypeName .Types.ps1xml yang mendefinisikan tipe Anda. Misal 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. Impor tipe Anda: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. Buat objek jenis kustom Anda: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. Inisialisasi tipe Anda menggunakan metode skrip yang Anda tentukan di XML: $p.Initialize('Anne', 'Droid')
  4. Lihat itu; Anda akan melihat semua properti ditentukan:$p | Format-Table -AutoSize
  5. Ketik memanggil mutator untuk memperbarui nilai properti: $p.SetGivenName('Dan')
  6. Lihat lagi untuk melihat nilai yang diperbarui: $p | Format-Table -AutoSize

Penjelasan

  • File PS1XML memungkinkan Anda untuk menentukan properti kustom pada tipe.
  • Ini tidak terbatas pada tipe .net seperti yang tersirat dalam dokumentasinya; sehingga Anda dapat meletakkan apa yang Anda suka di '/ Jenis / Jenis / Nama' objek apa pun yang dibuat dengan 'PSTypeName' yang cocok akan mewarisi anggota yang ditentukan untuk jenis ini.
  • Anggota ditambahkan melalui PS1XMLatau Add-Memberdibatasi untuk NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethod, dan CodeMethod(atau PropertySet/ MemberSet; meskipun mereka tunduk pada pembatasan yang sama). Semua properti ini hanya baca.
  • Dengan mendefinisikan a ScriptMethodkita bisa menipu batasan di atas. Misalnya, kita dapat mendefinisikan metode (misalnya Initialize) yang membuat properti baru, menyetel nilainya untuk kita; sehingga memastikan objek kami memiliki semua properti yang kami butuhkan agar skrip kami yang lain berfungsi.
  • Kita dapat menggunakan trik yang sama ini untuk memungkinkan properti dapat diupdate (meskipun melalui metode daripada penetapan langsung), seperti yang ditunjukkan pada contoh SetGivenName.

Pendekatan ini tidak ideal untuk semua skenario; tetapi berguna untuk menambahkan perilaku seperti kelas ke tipe khusus / dapat digunakan bersama dengan metode lain yang disebutkan dalam jawaban lain. Misalnya di dunia nyata, saya mungkin hanya mendefinisikan FullNameproperti di PS1XML, lalu menggunakan fungsi untuk membuat objek dengan nilai yang diperlukan, seperti:

Info lebih lanjut

Lihatlah dokumentasi , atau file tipe OOTB Get-Content $PSHome\types.ps1xmluntuk inspirasi.

# 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
sumber
ps. Bagi mereka yang menggunakan VSCode, Anda dapat menambahkan dukungan PS1XML
JohnLBevan