Typy wyliczeniowe w PowerShellu, część 2
W pierwszej części opisałem pracę z typami wyliczeniowymi, które w PowerShellu istnieją out-of-the-box. Drugą część będzie poświęcona temu jak i po co moglibyśmy je tworzyć. Zacznę naturalnie od celu.
Po co mi to?
Czasem w PowerShellu chcielibyśmy zagwarantować, że pewne parametry będą przyjmować ściśle określone wartości. Np. typ komputera w naszej firmie to zawsze albo laptop, albo desktop, albo serwer. Narzędzia powinny korzystać tylko z tych wartości, a gdy ktoś spróbuje użyć innej (notebook, stacja robocza…) generować błąd. Można to osiągnąć na dwa sposoby: walidacja parametrów lub właśnie użycie typu wyliczeniowego. Prosta funkcja, która pozwala skorzystać z obu opcji:
function New-Asset { [CmdletBinding(DefaultParameterSetName = 'Enum')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'NoEnum', HelpMessage = 'Typ komputera.')] [ValidateSet('Laptop','Desktop','Server')] [string]$Typ, [Parameter(Mandatory = $true, ParameterSetName = 'Enum', HelpMessage = 'Rodzaj komputera.')] [rodzaj]$Rodzaj, [Parameter(Mandatory = $true, HelpMessage = 'Nazwa komputera.')] [string]$Nazwa ) switch ($psCmdlet.ParameterSetName) { NoEnum { $Script = { [string]$Nazwa = $args[0] [string]$Typ = $args[1] Export-ModuleMember -Variable * } $Arguments = $Nazwa, $Typ } Enum { $Script = { [string]$Nazwa = $args[0] [rodzaj]$Rodzaj = $args[1] Export-ModuleMember -Variable * } $Arguments = $Nazwa, $Rodzaj } } $Options = @{ ScriptBlock = $Script AsCustomObject = $true ArgumentList = $Arguments } New-Module @Options }
Zarówno parametr ‘Typ’ jak i parametr ‘Rodzaj’ mogą mieć tylko założone przez nas wartości. Niestety, ValidateSet nie dba o wielkość liter… Skutek?
Dużo większą kontrolę uzyskamy więc dzięki typom wyliczeniowym. Co jeszcze? ValidateSet byłby w tym wypadku zastosowany jedynie przy tworzeniu obiektu, jeśli skorzystamy z New-Module do tworzenia obiektu mamy możliwość utrwalania typów właściwości. W przypadku typu [string] daje nam to niewiele, ale skorzystanie z typu wyliczeniowego gwarantuje nam, że nawet modyfikując obiekt będziemy zmuszeni użyć właściwych wartości:
I ostatnia rzecz: tworząc cały moduł w oparciu o założenie, że nie pojawi nam się inny typ komputera, prościej jest zrealizować to przy pomocy typów wyliczeniowych. Zmiana “zestawu” też będzie banalna (modyfikujemy definicję klasy, zamiast modyfikować ValidateSet we wszystkich funkcjach). Wydaje mi się, że tych zalet warto czasem sięgnąć po typ wyliczeniowe. Przejdźmy więc do odpowiedzi na pytanie: jak.
Zaprogramujmy sobie typ.
W wersji pierwszej PowerShella nie było prostej metody na przeprowadzenie takiej operacji. W wersji drugiej mamy dostęp do polecenia Add-Type, które pozwala na kompilowanie kodu ‘w locie’. Dodanie klasy [rodzaj], którą użyłem w funkcji wymagało ode mnie wpisanie jednej, niezbyt skomplikowanej linii kodu:
Przy okazji: widać to, o czym pisałem w części pierwszej. Nowostworzona klasa pozwala używać tych samych metod, co jej ‘matka’ System.Enum. Oczywiście nic nie stoi na przeszkodzie, by klasę tą umieścić w jakiejś przestrzeni nazw, lub stworzyć własną przestrzeń, gdzie przechowywalibyśmy wszystkie stworzone przez nas typy wyliczeniowe. Tyle wersja druga PowerShella. W wersji trzeciej możemy oczekiwać jeszcze lepszego wsparcia dla tego typu zabiegów. Cytując Bruce’a Payette’a, członka zespołu tworzącego PowerShell i autora najlepszej, moim zdaniem, książki PowerShellowi poświęconej:
A common question is why the PowerShell team didn’t add some sort of native mechanism to create .NET types in v2. It’s because a) we were incredibly busy and b) we didn’t want to screw up. With the Dynamic Language Runtime (DLR) on the verge of becoming part of .NET (and it is as of .NET 4.0) the way to create types was in flux. Because we had to ship well before .NET 4 was out, we decided to wait one more release and let the dust settle. We’re keeping our fingers crossed that we’ll finally get native type definition capabilities into v3 (actually fingers, toes, eyes, and any other anatomical protuberances that come to mind).
Pozostaje trzymać również trzymać kciuki…? Na szczęście możemy zrobić nieco więcej: w PowerShellu możemy dość łatwo dodać słowo kluczowe ‘enum’, dzięki któremu sami będziemy mogli tworzyć typy wyliczeniowe w sposób analogiczny do tego, w jaki tworzy się je w C#:
function New-Enum { param ( [Parameter(Mandatory = $true, HelpMessage = 'Name of your enumerator.')] [string]$EnumName, [Parameter(Mandatory = $true, HelpMessage = 'Definition of this type.')] [scriptblock]$Definition, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Namespace ) Set-StrictMode -Version Latest $TypeDefinition = @" using System; $(if ($namespace) { "namespace $Namespace {" } ) public enum $EnumName { $($Definition.ToString()) } $(if ($namespace) { "}" } ) "@ Write-Verbose "Type will be defined using: `n$TypeDefinition" try { Add-Type -TypeDefinition $TypeDefinition -ErrorAction Stop } catch { Write-Error "Error creating type. Use -Verbose for more" Write-Verbose $_ } } $MyAlias = @{ Name = 'enum' Value = 'New-Enum' Description = 'Keyword to create new System.Enum types' } New-Alias @MyAlias
Od tej pory korzystając z aliasu ‘enum’ i parameterów na odpowiednich pozycjach możemy napisać wprost w konsoli:
Myślę, że mając typy wyliczeniowe na wyciągnięcie ręki trudno się będzie powstrzymać przed ich użyciem. Korzyści może nie są wielkie, ale z pewnością w wielu sytuacjach oszczędzą nam bólu głowy. Choćby dlatego, że pomagają wyeliminować w zasadzie najgorsze w debugowaniu błędy: literóweczki… 😉