В преддверии выпуска 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)
eosfor
06.04.2015 19:04Не знаю зачем они это сюда привнесли. Я как бы не против, но мне кажется что они зря усложняют
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.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 до сих пор есть, там всё куда круче.
Не вижу того, что именно круче, у меня обратный опыт, нет например интерактивной консоли.Ivan_83
07.04.2015 18:01-1До си или .NET ему по удобству не дорасти.
Те если нужно много кодить то лучше сразу выбрать инструмент который с этим лучше справится.
Visual Basic в умелых руках мог делать почти всё что и си. vb script я не пользовался.
F1oyd
07.04.2015 07:45Привет, Толян. А можно в метод Add передать неопределенное количество параметров
а-ля [void]Add(params[String[]]$Values). Извини за вражеский синтаксис.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)
ApeCoder
Всегда предпочитал алиасы (их совсем же немного) и