Как создать собственный тип в 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», чтобы сделать это довольно просто:

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

Историческая справка : в PowerShell 1 было еще сложнее. Вам пришлось вручную использовать CodeDom, на PoshCode.org есть очень старыйскрипт новой структуры, который поможет. Ваш пример становится:

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

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

В PowerShell 3

Если вам не нужен «настоящий» класс, к которому вы можете привести, вам не нужно использовать способ добавления члена, который Стивен и другие продемонстрировали выше.

Начиная с 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.
jpmc, 26 01
Конечно, вы можете проверять типы, созданные с помощью Add-Type@ jpmc26, я сказал, что вы не можете сделать это без компиляции (т.е. без написания на C # и вызова Add-Type). Конечно, с PS3 вы можете - есть [PSTypeName("...")]атрибут, который позволяет вам указать тип в виде строки, который поддерживает тестирование с использованием PSCustomObjects с установленным PSTypeNames ...
Jaykul 01
58

Создавать собственные типы можно в PowerShell.
У Кирка Манро есть два замечательных поста, в которых подробно описывается процесс.

В книге « Windows PowerShell In Action» Мэннинга также есть образец кода для создания специфичного для предметной области языка для создания настраиваемых типов. Книга отличная во всех отношениях, поэтому очень рекомендую.

Если вы просто ищете быстрый способ сделать это, вы можете создать функцию для создания настраиваемого объекта, например

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
EBGreen
источник
3
По сути, командлет Select-Object добавляет свойства к объектам, которые ему присваиваются, если объект еще не имеет этого свойства. В этом случае вы передаете пустой объект String командлету Select-Object. Он добавляет свойства и передает объект по трубе. Или, если это последняя команда в конвейере, она выводит объект. Я должен отметить, что я использую этот метод только в том случае, если я работаю в командной строке. Для сценариев я всегда использую более явные командлеты Add-Member или New-Object.
EBGreen 02
Хотя это отличный трюк, вы можете сделать его еще короче:$myPerson = 1 | Select First,Last,Phone
RaYell
Это не позволяет вам использовать функции собственного типа, поскольку он устанавливает тип каждого члена как строку. Учитывая Jaykul вклад выше, показывает каждый член банкнота как NotePropertyиз stringтипа, это Propertyлюбого типа вы назначили в объекте. Это быстро и работает.
mbrownnyc 02
Это может вызвать проблемы, если вам нужно свойство Length, поскольку оно уже есть в строке, и ваш новый объект получит существующее значение, которое вам, вероятно, не нужно. Я рекомендую передавать [int], как показывает @RaYell.
FSCKur
9

Ответ Стивена Муравски великолепен, однако мне нравится более короткий (или, скорее, более аккуратный объект выбора вместо использования синтаксиса добавления члена):

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 | gm. Я предпочитаю, чтобы базовый тип был PSCustomObject,
Ник Мелдрам,
2
Я вижу суть. Тем не менее, преимущества intспособа очевидны : 1) работает быстрее, ненамного, но для данной функции New-Personразница составляет 20%; 2) набирать видимо легче. В то же время, используя такой подход практически везде, я никогда не видел недостатков. Но я согласен: могут быть некоторые редкие случаи, когда PSCustomObject лучше.
Роман Кузьмин
@RomanKuzmin Будет ли все еще на 20% быстрее, если вы создадите глобальный пользовательский объект и сохраните его как переменную сценария?
jpmc26 01
5

Удивленный, никто не упомянул этот простой вариант (vs 3 или новее) для создания пользовательских объектов:

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

Типом будет PSCustomObject, а не фактический настраиваемый тип. Но, вероятно, это самый простой способ создать собственный объект.

Бенджамин Хаббард
источник
См. Также это сообщение в блоге Уилла Андерсона о различии PSObject и PSCustomObject.
CodeFox 01
@CodeFox только что заметил, что ссылка сейчас не работает
superjos
2
@superjos, спасибо за подсказку. Мне не удалось найти новое местоположение сообщения. По крайней мере, пост был заархивирован .
CodeFox
2
похоже, что это превратилось в книгу Git здесь :)
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, упомянутому Джайкулом (и, следовательно, также требуется 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
ДжонЛБеван
источник
пс. Для тех, кто использует VSCode, вы можете добавить поддержку PS1XML
JohnLBevan,