Объекты и немного о классах в Powershell 5.0
В преддверии выпуска 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)
Вот мы и подошли к самому главному, тому о чем в первую очередь я хотел бы поговорить, несмотря на то, что вот уже 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 еще один объектно-ориентированный язык программирования добавляя сущности без надобности?