Як створити власний тип у PowerShell для використання моїх сценаріїв?


88

Я хотів би мати можливість визначати та використовувати спеціальний тип у деяких моїх сценаріях PowerShell. Наприклад, давайте зробимо вигляд, що мені потрібен був об’єкт, який мав таку структуру:

Contact
{
    string First
    string Last
    string Phone
}

Як би я створив це, щоб я міг використовувати його у такій функції:

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

Чи можливо щось подібне або навіть рекомендовано в PowerShell?

Відповіді:


133

До PowerShell 3

Розширювана система типів PowerShell спочатку не дозволяла вам створювати конкретні типи, які ви можете перевірити як у вашому параметрі. Якщо вам не потрібен цей тест, ви добре працюєте з будь-яким із інших методів, згаданих вище.

Якщо вам потрібен фактичний тип, до якого ви можете передати або перевірити тип, як у вашому прикладі сценарію ... це неможливо зробити, не написавши його на C # або VB.net і не скомпілювавши. У PowerShell 2 ви можете скористатися командою "Додати тип", щоб зробити це досить просто:

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

Історична примітка : у PowerShell 1 це було ще складніше. Вам довелося вручну використовувати CodeDom, на PoshCode.org є дуже стара функція new-struct script, яка допоможе. Вашим прикладом стає:

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

Використання Add-Typeабо New-Structдозволить вам фактично перевірити клас у вашому param([Contact]$contact)і створити нові за допомогою $contact = new-object Contactтощо ...

У PowerShell 3

Якщо вам не потрібен "справжній" клас, до якого ви можете перейти, вам не доведеться використовувати спосіб Add-Member, який продемонстрували Стівен та інші .

З PowerShell 2 ви можете використовувати параметр -Property для New-Object:

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

А в PowerShell 3 ми отримали можливість використовувати PSCustomObjectприскорювач для додавання TypeName:

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

Ви все ще отримуєте лише один об’єкт, тому вам слід створити New-Contactфункцію, щоб переконатися, що кожен об’єкт виходить однаковим, але тепер ви можете легко перевірити параметр "є" одним із таких типів, прикрасивши параметр PSTypeNameатрибутом:

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

У PowerShell 5

У PowerShell 5 все змінюється, і нарешті ми отримали classі enumяк ключові слова для мови для визначення типів (немає, structале це нормально):

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

Ми також отримали новий спосіб створювати об’єкти без використання New-Object: [Contact]::new()- насправді, якщо ви тримали свій клас простим і не визначали конструктор, ви можете створювати об’єкти, додаючи хеш-таблицю (хоча без конструктора не було б можливості щоб забезпечити встановлення всіх властивостей):

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

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

Чудова відповідь! Просто додавши примітку, що цей стиль дуже простий для сценаріїв і все ще працює в PowerShell 5: New-Object PSObject -Property @ {prop here ...}
Райан Шилінгтон,

2
У перших випусках PowerShell 5 ви не могли використовувати New-Object з класами, створеними з використанням синтаксису класів, але зараз ви можете. Втім, якщо ви використовуєте ключове слово class, ваш сценарій у будь-якому випадку обмежений лише PS5, тому я все одно рекомендую використовувати синтаксис :: new, якщо об'єкт має конструктор, який приймає параметри (це набагато швидше, ніж New-Object) або кастинг в іншому випадку, що є і більш чистим синтаксисом, і швидшим.
Jaykul

Ви впевнені, що перевірку типу неможливо здійснити за допомогою типів, створених за допомогою Add-Type? Здається, це працює в PowerShell 2 на Win 2008 R2. Скажімо , я визначаю , contactвикористовуючи , Add-Typeяк у відповіді , а потім створити екземпляр: $con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }. Потім виклику ця функція працює: function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() }, але виклик цієї функції не вдається, x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() }. x 'abc'Також не вдається зателефонувати із відповідним повідомленням про помилку про трансляцію. Випробувано в PS 2 та 4.
jpmc26,

Звичайно, ви можете перевірити типи, створені за допомогою Add-Type@ jpmc26, але я сказав, що це неможливо зробити без компіляції (тобто: не записуючи це на C # і не викликаючи Add-Type). Звичайно, з PS3 ви можете - є [PSTypeName("...")]атрибут, який дозволяє вказати тип як рядок, який підтримує тестування на PSCustomObjects з набором PSTypeNames ...
Jaykul

58

Створення власних типів можна виконати в PowerShell.
Кірк Манро насправді має два чудові пости, які детально деталізують процес.

У книзі Windows PowerShell In Action від Manning також є зразок коду для створення мови домену для створення власних типів. Книга чудова навколо, тому я дуже рекомендую її.

Якщо ви просто шукаєте швидкий спосіб зробити вищезазначене, ви можете створити функцію для створення власного об’єкта типу

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

Це метод ярлика:

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

3
В основному командлет Select-Object додає властивості до об’єктів, які йому надаються, якщо об’єкт ще не має цієї властивості. У цьому випадку ви передаєте порожній об’єкт String до командлета Select-Object. Він додає властивості і передає об'єкт уздовж труби. Або якщо це остання команда в каналі, вона виводить об'єкт. Слід зазначити, що я використовую цей метод лише тоді, коли працюю за підказкою. Для сценаріїв я завжди використовую більш чіткі командлети Add-Member або New-Object.
EBGreen

Хоча це чудовий трюк, насправді ви можете зробити його ще коротшим:$myPerson = 1 | Select First,Last,Phone
RaYell

Це не дозволяє використовувати функції власного типу, оскільки він встановлює тип кожного члена як рядок. З огляду на Jaykul внесок вище, показує кожен член банкнота як NotePropertyз stringтипу, це Propertyбудь-якого типу ви призначили в об'єкті. Це швидко і робить свою роботу.
mbrownnyc

Це може викликати проблеми, якщо вам потрібна властивість Length, оскільки рядок це вже має, і ваш новий об’єкт отримає існуюче значення - чого ви, мабуть, не хочете. Я рекомендую пройти [int], як показує @RaYell.
FSCKur

9

Відповідь Стівена Муравського є чудовою, однак мені подобається коротший (а точніше просто акуратніший об'єкт select замість використання синтаксису 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-Objectнавіть не потрібно. Це зробить те саме:... = 1 | select-object First, Last, Phone
Роман Кузьмін

1
Так, але те саме, що EBGreen вище - це створює якийсь дивний базовий тип (у вашому прикладі це буде Int32.), Як ви побачили б, якби ви ввели: $ person | гм. Я вважаю за краще, щоб основним типом був PSCustomObject
Нік

2
Я бачу сенс. Проте є очевидні переваги intспособу: 1) він працює швидше, не набагато, але для цієї конкретної функції New-Personрізниця становить 20%; 2) це, мабуть, легше друкувати. У той же час, використовуючи цей підхід в основному скрізь, я ніколи не бачив жодних недоліків. Але я погоджуюсь: трапляються рідкісні випадки, коли PSCustomObject стає набагато кращим.
Роман Кузьмін

@RomanKuzmin Чи все-таки це на 20% швидше, якщо створити екземпляр глобального користувацького об'єкта та зберегти його як змінну сценарію?
jpmc26,

5

Здивований, ніхто не згадав про цей простий варіант (проти 3 або пізнішої версії) для створення користувацьких об'єктів:

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

Тип буде PSCustomObject, а не власне власний тип. Але це, мабуть, найпростіший спосіб створити власний об’єкт.


Дивіться також цю публікацію в блозі Віла Андерсона про різницю PSObject та PSCustomObject.
CodeFox

@CodeFox щойно помітив, що посилання зараз порушено
superjos

2
@superjos, дякую за підказку. Мені не вдалося знайти нове місце посту. Принаймні пост був підкріплений архівом .
CodeFox

2
судячи по всьому, схоже , що перетворилися в книгу Гіта тут :)
superjos

4

Існує концепція PSObject та Add-Member, якими ви могли б скористатися.

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

Це виводить, як:

[8] » $contact

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

Інша альтернатива (про яку мені відомо) - це визначення типу в C # / VB.NET і завантаження цієї збірки в PowerShell для безпосереднього використання.

Ця поведінка, безумовно, заохочується, оскільки вона дозволяє іншим сценаріям або розділам вашого сценарію працювати з фактичним об’єктом.


3

Ось важкий шлях для створення власних типів та збереження їх у колекції.

$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

Гарний дотик із додаванням імені типу до об’єкта.
oɔɯǝɹ

0

Ось ще один варіант, який використовує ідею, подібну до рішення PSTypeName, згаданого Jaykul (і, отже, також вимагає PSv3 або вище).

Приклад

  1. Створіть файл TypeName .Types.ps1xml, який визначає ваш тип. Наприклад 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. Імпортуйте свій тип: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. Створіть об’єкт власного типу: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. Ініціалізуйте свій тип, використовуючи метод сценарію, який ви визначили в XML: $p.Initialize('Anne', 'Droid')
  4. Подивись на це; ви побачите всі визначені властивості:$p | Format-Table -AutoSize
  5. Введіть виклик мутатора, щоб оновити значення властивості: $p.SetGivenName('Dan')
  6. Подивіться ще раз, щоб побачити оновлене значення: $p | Format-Table -AutoSize

Пояснення

  • Файл PS1XML дозволяє визначати власні властивості типів.
  • Це не обмежується типами .net, як випливає з документації; так що ви можете помістити те, що вам подобається, у '/ Types / Type / Name', будь-який об'єкт, створений із відповідним 'PSTypeName', успадкує члени, визначені для цього типу.
  • Члени додані через PS1XMLабо Add-Memberобмежені NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethod, і CodeMethod(або PropertySet/ MemberSet; хоча ті можуть бути тими ж обмеженнями). Усі ці властивості доступні лише для читання.
  • Визначивши a, ScriptMethodми можемо обдурити вищезазначене обмеження. Наприклад, ми можемо визначити метод (наприклад Initialize), який створює нові властивості, встановлюючи їх значення для нас; таким чином гарантуючи, що наш об'єкт має всі властивості, необхідні для роботи інших наших сценаріїв.
  • Ми можемо використати цей самий фокус, щоб дозволити властивості оновлювати (хоча за допомогою методу, а не прямого призначення), як показано в прикладах SetGivenName.

Цей підхід не є ідеальним для всіх сценаріїв; але корисний для додавання поведінки, подібної до класу, до власних типів / може використовуватися разом з іншими методами, згаданими в інших відповідях. Наприклад, у реальному світі я б, мабуть, лише визначав FullNameвластивість у PS1XML, а потім використовував функцію для створення об’єкта з необхідними значеннями, наприклад:

Більше інформації

Подивіться на документацію або файл типу OOTB Get-Content $PSHome\types.ps1xmlдля натхнення.

# 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. Для тих, хто використовує VSCode, ви можете додати підтримку PS1XML
JohnLBevan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.