image

В преддверии выпуска Windows 10 и новой, пятой, версии Powershell, хочу поговорить с вами о одном из наиболее серьезных нововведений этого языка — о классах. Начать наш разговор мне видится уместным с экземпляров класса — объектов — являющихся безусловно киллер-фичей языка сценариев Powershell. Простота и лаконичность упрощенного объектно-ориентированного подхода в языке автоматизации задач покорила не только большую, казалось бы, черствую, подобно 16-bit legacy, корпорацию, но и пользователей альтернативных операционных систем.

«Упрощенным» объектно-ориентированным я его назвал умышленно и хочу обратить на это ваше внимание. Объектно-ориентированные языки программирования предполагают ряд сущностей, таких как класс(тип), экземпляр класса, свойства и методы этого экземпляра, чаще называемого объектом. Powershell же, ловко оперируя объектами и их свойствами, практически полностью лишен методов и абсолютно полностью определяемых пользователем типов объектов (классов). Из часто используемых методов в голову приходят пожалуй лишь .trim() да .ToString(). Если дать еще минутку на парсинг дампа опыта написания скриптов на Powershell, всплывет еще что-то про Get-WMIObject.

Предлагаю освежить в памяти создание объекта в Powershell, хотя и для первого знакомства будет отлично.

Путь первый — Православный


Характерной особенностью Powershell, можно сказать его почерком, который легко узнается даже с десяти шагов, является его многословность. На первых порах это немного даже раздражает и вокруг монитора прилипают стикеры с сокращениями с символами: «gci, gc, gwmi, %, ?» и сокровенным — «ls alias:» (просмотр всех алиасов). Чуть позже немного отпускает и вместо пубертантного "?" начинают появляться хоть и не «Where-Object», но уже довольно уверенный «Where». Позже, когда количество строк кода переваливает за десятки тысяч, а написанных скриптов за сотни, приходит понимание, что многословность языка сказывается положительно как на скорости чтения самого скрипта, так и на качестве его поддержки коллегами. В этот момент в любимом редакторе Ruler смещается с 80 символов до 200, а по старым скриптам пускается скрипт автозамены. Хм, кажется я отвлекся.

Итак, вернемся. Первый способ создания объекта, как и весь Powershell, многословен, но это его плюс. Все слова простые, английские и для человека первый раз в глаза видящего этот язык, в общем-то, понятный в контексте языка:

$Name = 'Name'
$CustomObject = New-Object –TypeName PSObject
$CustomObject | Add-Member –MemberType NoteProperty –Name Name  –Value $Name
$CustomObject | Add-Member –MemberType NoteProperty –Name Date  –Value $(Get-Date)
$CustomObject | Add-Member –MemberType NoteProperty –Name Value –Value 'Value'

По-большому счету, только этот способ позволяет добавлять к объекту не только свойства, но и методы посредством "-MemberType ScriptMethod". К моему удивлению я ниразу не видел, что бы кто-то реализовывал какие-то методы в своих объектах. Признаюсь я и сам не сторонник методов в Powershell, вероятно причиной является то, что мой опыт объектно-ориентированного программирования не успел поразить мой мозг достаточно глубоко. Отчасти, не желание реализовывать методы, я готов списать на то, что методы пришлось бы расписывать в каждой функции, возвращающей кастомный объект, что безусловно менее удобно, нежели описать метод однажды в классе объекта.

Путь второй — Упрощенный


В этот раз для создания объекта используется хэш-таблица, которая передается все тому же коммандлету New-Object, при уменьшившемся количестве набранных символов, мы практически не потеряли в читаемости:

$Name = 'Name'
$Properties = @{}
$Properties.Name  = $Name
$Properties.Date  = $(Get-Date)
$Properties.Value = 'Value'
$CustomObject = New-Object –TypeName PSObject –Prop $Properties

Вообще, мне кажется хэш-таблицы немного недооценены авторами сценариев Powershell, даже ограничив в релизе язык только лишь хэш-таблицами и лишив — более толстых — объектов, язык бы ничуть не потерял своей мощи и стройности; хотя соглашусь с тем, что объекты выглядят перспективнее, о чем и поговорим в конце заметки.

По-большому счету разница между хэш-таблицой и объектом как раз и заключается в наличии методов, полезность которых, на мой взгляд, в скриптовом языке сомнительна. Попоробуйте выполнить пример из блока цитирования кода, расположенного выше, но опустив последнюю строку, в которой создается объект. После того, как мы в поле Name присвоили значение, мы уже можем к нему обращаться как $Properties.Name, при этом нигде выше мы не объявляли, что такое поле у нас вообще будет! Хэш-таблица уже ведет себя как объект, зачем нам создавать еще один такой же? Мало того, с хэш-таблицами можно работать и как с массивами обращаясь по индексу: $Properties['Name'].

В качестве примера работы с хэш-таблицами хочу привести код функции чтения значений ini-файла, по-моему она прекрасна:

function Get-IniContent  {
    Param (
        [String]$Filepath
    )
    $IniContent = @{}
    switch -Regex -File $Filepath {
        '^\[(.+)\]' {
            $Section = $matches[1]
            $IniContent[$Section] = @{}
            $CommentCount = 0
        }
        "^(;.*)$"  {
            $Value = $matches[1]
            $CommentCount = $CommentCount + 1
            $Name = 'Comment' + $CommentCount
            $IniContent[$Section][$Name] = $Value
        }
        '(.+?)\s*=(.*)' {
            $Name, $Value = $matches[1..2]
            $IniContent[$Section][$Name] = $Value
        }
    }
    Write-Output $IniContent
}
# Ed Wilson, Microsoft Scripting Guy


Третий путь — Короткий


Он простой и короткий, тут добавить нечего, не беру смелость советовать вам использовать его только в однострочниках и чем-то, что не будет выполняться больше пары раз, но советую. Советую не только потому, что я адепт механических клавиатур и получаю удовольствие от набора текста, а сколько потому, что читаемость и понятность вашего скрипта должна быть на первом месте. Задачи которые приходится автоматизировать зачастую и без того полны блэк-боксов, я думаю вы согласитесь — незачем добавлять к ним еще один на Powershell (ну и потому что с обфускацией скриптов отлично справляются регулярные выражения и незачем увеличивать энтропию =).

$Name = 'Name'
$CustomObject = [pscustomobject]@{
    Name  = $Name;
    Date  = $(Get-Date);
    Value = 'Value';
}


Четвертый путь — Вычислимый


Этот способ используется, в первую очередь, для модификации существующих объектов, получаемых, например, из конвейера. Речь о коммандлете Select-Object, с помощью него мы можем как уменьшать количество свойств объекта (например вычистить из результата работы коммандлета Receive-Job ненужные нам свойства в вроде RunspaceID), так и добавлять свои, в том числе вычисляя часть из них в процессе:

# вычислимый
$Name = 'Name'
$CustomObject = $Name | Select-Object @{Name='Name'; Expression = {$PSItem}}, @{Name='Date'; Expression = {Get-Date}}, @{Name='Value'; Expression={'Value'}}

# оставит только два свойства
$CustomObject | Select-Object Name, Date


Путь пятый — Враждебный (шучу)


Так как Powershell работает поверх CLR, на одном уровне с C#, например, то и использовать в нем средства предоставляемые этим языком нет никакой сложности:

Add-Type @'
public class CustomClass
{
    public string Name = "Name";
    public System.DateTime Date = System.DateTime.Now;
    public string Value = "Value";
}
'@
$CustomObject = New-Object CustomClass

Этот способ позволяет в дополнению к свойствам объекта, так же описать и методы, что вобщем-то очевидно.
Пример применения, скрывающий окно хоста консоли, показан ниже. Удобен если в скрипте есть формочка и «окно с досом» пугает пользователя:

$ShowWindow = '[DllImport("user32.dll")] public static extern bool ShowWindow(int handle, int state);'
Add-Type -name win -member $ShowWindow -namespace native
[native.win]::ShowWindow(([System.Diagnostics.Process]::GetCurrentProcess() | Get-Process).MainWindowHandle, 0)


Powershell 5 и классы


Вот мы и подошли к самому главному, тому о чем в первую очередь я хотел бы поговорить, несмотря на то, что вот уже 99 строк (Word Wrap Column 120) углубляюсь в пространные разговоры о перипетиях синтаксиса: в Powershell 5 стали доступны классы (это не обман =). Все написанное ниже относится в первую очередь к превью языка, доступного как в комплекте с Windows 10, так и в несколько урезанном виде для других операционных систем.

Сначала я решил описать рыбу класса и его применения полную шаблонных слов вроде Example, FirstProperty и подобных, и лишь за тем описать что-то рабочее и близкое к телу. Посмотрев на это понял, что упрощая усложнил, ибо порой нет ничего хуже скучных шаблонных описаний, поэтому ниже я покажу как создать класс логера для вашего сценария:

# описание класса
class Logger {
    # свойство
    [String]$LogPath

    # конструктор
    Logger([String]$NewLogPath) {
        $This.LogPath  = $NewLogPath
        New-Item -Type File $This.LogPath -Force
    }

    # метод
    [void]Add([String]$Value) {
        '[{0}] {1}' -f $(Get-Date), $Value |
            Out-File $This.LogPath -Append -Encoding default
    }
}

$MyLogger = [Logger]::New('C:\temp\test.log')
$MyLogger.Add('Initial log entry')


Получившийся результат:
PS C:\Users\rbobot> Get-Content C:\temp\test.log
[4/4/2015 4:23:22 PM] Initial log entry

Итак, немного слов по синтаксису нашего минимального пригодного к работе класса:
— конструктор класса именуется так же как и сам класс, при этом конструктор можно не описывать, в этом случае вызовется конструктор по-умолчанию, но и определить свойства при создании мы не сможем;
— при обращении к свойствам класса внутри конструктора и методов используется ключевое слово $This;
— описание методов начинается с указания типа возвращаемого значения, в том случае если метод не возвращает ничего следует указать ключевое слово [void];
— при создании экземпляра класса используется синтаксис вида: [Имя класса]::New();

Из отмеченного выше, лично у меня глаз цепляется лишь за синтаксис создания экземпляра класса остальное выглядит логичным. С одной стороны для создания экземпляра класса ожидаешь увидеть уже знакомый New-Object –TypeName, с помощью которого мы создавали объекты как описанные на Powershell, так и заимствованные из C#.
С другой, этот коммандлет не предполагает определения свойств и объект создается конструктором по-умолчанию, возможно к релизу синтаксис создания экземпляра пользовательского класса изменится на более Powershell-Way, путем расширения параметров коммандлета New-Object.

Расширим немного наш класс, перегрузив метод Add и добавив функцию, которая будет получать наш объект параметром и логировать свои действия:

class Logger {
    [String]$LogPath
    [String]$CodePage

    Logger([String]$NewLogPath, [String]$NewCodePage) {
        $This.LogPath  = $NewLogPath
        $This.CodePage = $NewCodePage

        New-Item -Type File $This.LogPath -Force
    }

    [void]Add([String]$Value) {
        '[{0}] {1}' -f $(Get-Date), $Value |
            Out-File $This.LogPath -Append -Encoding $This.CodePage
    }
    [void]Add([String]$Type, [String]$Value) {
        '[{0}] {1} {2}' -f $(Get-Date), $Type, $Value |
            Out-File $This.LogPath -Append -Encoding $This.CodePage
    }
    [UInt64]GetSize() {
        return (Get-Item $This.LogPath).Length
    }    
}

function New-SomeJob {
    Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [String]$Job,
        [Logger]$Logger
    )
    Process {
        $JobResult = '{0} {1}' -f $Job, 'job'
        $Logger.Add($JobResult)
    }
}


$MyLogger = [Logger]::New('C:\temp\test.log', 'UTF8')
$MyLogger.Add('Initial log entry')
$MyLogger.Add('Warning:', 'Warning log entry')
'First', 'Second' | New-SomeJob -Logger $MyLogger
$MyLogger.Add('Last log entry')
$MyLogger.GetSize()


Получившийся результат:
PS C:\Users\rbobot> $MyLogger.GetSize()
204

PS C:\Users\rbobot> Get-Content C:\temp\test.log
[4/6/2015 10:43:26 AM] Initial log entry
[4/6/2015 10:43:26 AM] Warning: Warning log entry
[4/6/2015 10:43:26 AM] First job
[4/6/2015 10:43:26 AM] Second job
[4/6/2015 10:43:26 AM] Last log entry

По-поводу вышепроцитированного можно отметить лишь то, что в методах, возвращающих значения, используется ключевое слово «return», использование которого в Powershell не рекомендуется в силу того, что оно как и Write-Output является синтаксическим сахаром, но при этом не соответствует стилистике Powershell. Покрайней мере так было раньше.

В заключение хотел бы спросить у вас, считаете ли вы классы в Powershell необходимой фичей или все же не стоит делать из Powershell еще один объектно-ориентированный язык программирования добавляя сущности без надобности?

Комментарии (9)


  1. ApeCoder
    06.04.2015 10:27

    Всегда предпочитал алиасы (их совсем же немного) и

    $CustomObject = New-Object PSObject –Prop @{ Name = "Myname"; Value = "Value"  } 
    


  1. VoidEx
    06.04.2015 13:58

    А чем «ls alias:» от «alias» отличается?


    1. rbobot Автор
      06.04.2015 14:10

      Да ни чем, просто это «Drive-Enabled» провайдер, подобный остальным, список которых доступен через Get-PSProvider


  1. eosfor
    06.04.2015 19:04

    Не знаю зачем они это сюда привнесли. Я как бы не против, но мне кажется что они зря усложняют


  1. Ivan_83
    06.04.2015 22:03

    Какая реальная польза от классов скриптовой среде?

    "$ShowWindow = '[DllImport(«user32.dll»)] public static extern bool ShowWindow(int handle, int state);'" — какой в этом смысл?

    был же Visual Basic, vb script до сих пор есть, там всё куда круче.

    PS: строго говоря handle — HWND, оно вроде как платформо зависимо должно быть, и с х64 будет 64 бита, в то время как state — int.


    1. rbobot Автор
      07.04.2015 06:38

      Не могу точно сказать какая реальная польза от классов, сам задаюсь этим вопросом. Могу лишь предположить, что возможно майкрософт и может быть ее партнеры столкнулись с тем, что количество кода на powershell сильно увеличивается и классы позволяют лучше структурировать код, возможно powershell будет продвигаться как встраиваемый язык и на больших конфигурациях классы, опять же, помогут. С другой, возможно это связано с тем, что программистов объектно ориентированных языков сильно больше, нежели процедурных и классы позволят им оставаться в зоне комфорта и привычных парадигм. Какие-то такие мысли у меня на этот счет.

      "$ShowWindow = '[DllImport(«user32.dll»)] public static extern bool ShowWindow(int handle, int state);'" — это решение не из раздела лучших практик, стоит ее рассматривать как демонстрацию возможностей языка и рантайма.

      был же Visual Basic, vb script до сих пор есть, там всё куда круче.

      Не вижу того, что именно круче, у меня обратный опыт, нет например интерактивной консоли.


      1. Ivan_83
        07.04.2015 18:01
        -1

        До си или .NET ему по удобству не дорасти.
        Те если нужно много кодить то лучше сразу выбрать инструмент который с этим лучше справится.

        Visual Basic в умелых руках мог делать почти всё что и си. vb script я не пользовался.


  1. F1oyd
    07.04.2015 07:45

    Привет, Толян. А можно в метод Add передать неопределенное количество параметров
    а-ля [void]Add(params [String[]]$Values). Извини за вражеский синтаксис.


    1. rbobot Автор
      07.04.2015 09:09
      +1

      Почему бы и нет?

          [void]Add([String[]]$Value) {
              foreach ($String in $Value) {
                  '[{0}] {1}' -f $(Get-Date), $String |
                      Out-File $This.LogPath -Append -Encoding $This.CodePage
              }
          }
      
      [...]
      $MyLogger = [Logger]::New('C:\temp\test.log', 'UTF8')
      [String[]]$StringArray += 'First'
      [String[]]$StringArray += 'Second'
      $MyLogger.Add($StringArray)