PowerShell–efektywniej, część 6.
W poprzedniej części opisałem składnię, dzięki której funkcja zmienia się w funkcję zaawansowaną. Dziś skupię się na tym, co z tego wynika przy używaniu takiej funkcji.
Debug i Verbose
W wielu językach – zarówno programowania jak i skryptowych – pierwszą metodą sprawdzania co poszło nie tak jest wyświetlanie na ekran informacji, które mogą pomóc ustalić, gdzie popełniliśmy błąd. To generuje dwa problemy:
- nie usunięte komunikaty pojawiają się w najmniej odpowiednim momencie
- ponowne “włączenie” komunikatów po ich usunięciu jest niemożliwe
PowerShell ma bardzo przydatną właściwość: generuje nieco więcej strumieni niż domyślne wyjście standardowe i wyjście błędów: mamy dodatkowo ostrzeżenia, informacje rozszerzone (verbose) i komunikaty pomagające debugować (debug). Komunikaty verbose i debug można z powodzeniem pozostawić (jeśli są cenzuralne ) a w razie potrzeby – włączyć na żądanie. W funkcjach zwykłych wymagałoby to zmiany $DebugPreference i $VerbosePreference na poziomie sesji. Funkcje zaawansowane dają nam możliwość użycia po prostu jednego z parametrów: –Debug lub –Verbose. Dzięki temu automagicznie zmienia się wartość odpowiedniej zmiennej i wszystkie komunikaty pojawiają się w odpowiednich strumieniach. Dodajmy więc komunikaty dotyczące zawartości naszych parametrów:
Write-Verbose "Format: $Format" Write-Verbose "Liczba: $Liczba" Write-Verbose "Slowo: $Slowo" if ($Format) { Write-Debug "Wygląda na to, że używamy liczby." }
Begin, Process, End
Funkcja w PowerShellu może mieć trzy odseparowane części, co ułatwia zdecydowanie pracę w potokach i dzięki czemu każda funkcja może działać podobnie jak prawdziwy cmdlet (jeśli chodzi o funkcjonalność, nie o wydajność). Mamy więc:
- begin gdzie można zainicjować połączenia, zdefiniować funkcje pomocnicze, przetworzyć zmienne podawane “w linii”
- process stanowiący serce funkcji działającej w rurce: tu możemy sięgnąć do parametrów pobieranych z poprzedniej komendy i przekazać informację dalej
- end który sprząta na koniec: zabija połączenia, czyści pamięć ze zbędnych elementów
Jeśli zamierzamy korzystać tylko z parametrów podawanych w linii – nasza funkcja może mieć jedno, zbiorcze “ciało”. Jeśli jednak chcemy skorzystać z pipe’a – absolutnym minimum jest dodanie części process, pozostałe dwie pozostają wówczas opcjonalne. Begin ma sens wtedy, gdy potrzebujemy pewną operację przeprowadzić dokładnie raz na początku, a wykonywanie jej dla każdego elementu wpadającego w rurkę jest stratą czasu, czy wręcz może doprowadzić do nieoczekiwanych rezultatów. End działa podobnie – tylko operacja musi mieć miejsce dokładnie raz na końcu. Cała reszta – powinna trafić do bloku process. Zaawansowane funkcje tracą wiele jeśli pozbawimy ich tej troistości.
PSCmdlet
Zaawansowane funkcja ma jeszcze jedną wielką zaletę: w jej wnętrzu dostępna jest automatyczna zmienna ($PSCmdlet), która w zasadzie daje nam dostęp do tych samych metod i właściwości, jakie ma programista piszący cmdlet w C# lub VB.NET. Na blogu zespołu PowerShell jakiś czas temu opublikowana była prosta funkcja, która pozwala zajrzeć do wnętrza tej zmiennej, sprowadza się to właściwie do:
function Test-PSCmdlet { [CmdletBinding()] param() $p = $PSCmdlet function prompt { "Test-PSCmdlet> " } $host.EnterNestedPrompt() }
Dzięki temu możemy poeksperymentować ze zmienną $p (która jest kopią $PSCdmlet). W praktyce na ogół wykorzystuje się dwa jej elementy:
- właściwość ParameterSetName – pozwala ustalić, który zestaw parametrów jest wykorzystywany
- metody ShouldProcess i ShouldContinue – pozwalają zastosować parametry WhatIf oraz Confirm.
Ponieważ budowana przez nas zaawansowana funkcja wspiera ShouldProcess (a więc metody Shoud* będą w niej dostępne), oraz założyliśmy kilka różnych zestawów parametrów, więc wykorzystamy i jedno, i drugie:
process { if ($psCmdlet.ShouldProcess( "Cel: $Slowo $Liczba", "Operacja: Wyświetl" )) { switch ($psCmdlet.ParameterSetName) { Domyslny { "{0} to moje slowo" -f $Slowo } Liczby { "Moja liczba: {0:$Format}" -f $Liczba } } } }
Ponieważ część parametrów zamierzamy opcjonalnie pobierać z rurki – możemy do nich sięgać dopiero w bloku process. Jeśli mamy dwa zestawy parametrów na ogół wystarczy zwykły if – jeśli zestawów jest więcej, opcjonalnym rozwiązaniem może okazać się switch. Jak widać operacja przeprowadzana nie jest specjalnie niebezpieczna, ale czasem jednak funkcja napisana przez nas może nieść z sobą pewne ryzyko – warto wtedy wiedzieć jak zaimplementować parametry –WhatIf i –Confirm.
I tym optymistycznym akcentem kończę temat zaawansowanych funkcji. Jeszcze tylko nasza zaawansowana funkcja w pełnej krasie:
function Get-Foo { [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'Domyslny' )] param ( [Parameter( Mandatory = $true, HelpMessage = 'Pomocy!', ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Liczby' )] [double]$Liczba, [Parameter( Mandatory = $true, HelpMessage = 'Pomocy!', ValueFromPipelineByPropertyName = $true, Position = 0, ParameterSetName = 'Domyslny' )] [ValidatePattern('^\w+$')] [string]$Slowo, [Parameter( Position = 1, ParameterSetName = 'Liczby', Mandatory = $true, HelpMessage = 'Pomozcie mi!' )] [ValidateSet('P4','N4','C4')] [string]$Format ) begin { Write-Verbose "Format: $Format" Write-Verbose "Liczba: $Liczba" Write-Verbose "Slowo: $Slowo" if ($Format) { Write-Debug "Wygląda na to, że używamy liczby." } } process { if ($psCmdlet.ShouldProcess( "Cel: $Slowo $Liczba", "Operacja: Wyświetl" )) { switch ($psCmdlet.ParameterSetName) { Domyslny { "{0} to moje slowo" -f $Slowo } Liczby { "Moja liczba: {0:$Format}" -f $Liczba } } } } }
Następną część zamierzam poświęcić kolejnej funkcjonalności, przydatnej zarówno w funkcjach (również zaawansowanych) jak i w skryptach: pomocy generowanej w oparciu o komentarze.