PowerShell – efektywnie(j), część 4.
Troszkę to trwało nim udało mi się przysiąść do tego artykułu. Przyczyna jest prozaiczna: temat jest dość obszerny i nie wiem jak go ugryźć. Pisanie skryptów w PowerShellu nie wymaga wielkiego wysiłku, ale jeśli chcemy by nasz skrypt był przydatny dla innych i dla nas za pół roku – trzeba się nieco przyłożyć w czasie jego tworzenia. Uznałem jednak, że rzecz sama się nie zrobi. W najgorszym wypadku po prostu rozpiszę się nieco bardziej niż zwykle (ha, ha, ha… )
Od czego zacząć?
W zasadzie pierwsze pytanie jakie powinniśmy sobie zadać powinno brzmieć: czy chcemy napisać coś, co wykona serię operacji i da nam na koniec jakiś wynik (skrypt) czy bardziej interesuje nas biblioteka narzędzi (moduł). Dalej jest już prościej: dzielimy zadanie na proste czynności (funkcje), które następnie połączymy w całość. I o ile w przypadku modułów sprawa wydaje się “czysta” i dość naturalna, o tyle w wypadku skryptów musimy zwalczyć pokusę umieszczenia operacji w jednym, wielki worze.
Kod wielokrotnego użycia
Skrypty lubią rosnąć niby hydra: dodajemy do nich kolejne funkcjonalności, jednocześnie nie specjalnie dbając o to, by kod można było później wykorzystać. To moim zdaniem spory błąd: utrudnia ponowne wykorzystanie napisanego kodu w innym narzędziu, czyni narzędzie dość statycznym i dość silnie uwiązanym do wykorzystanej technologii. Może prosty przykład, by pokazać o co mi chodzi…
Naturalna kolej rzeczy: nasz skrypt może przyjąć jako parametr samą listę (przekazaną przez rurkę), OU w AD, plik txt/ csv z listą komputerów itd. Na wyjściu – podamy ścieżkę do pliku .csv w którym dane zostaną zapisane. To co się dzieje w środku zależy już od nas. Możemy więc albo zrobić to w formie funkcji, które następnie ustawimy w jednej “rurce” i uzyskamy pożądanych efekt. Albo wszystko zrobić za jednym zamachem, bez dziabania kodu na poszczególne fragmenty. Co tracimy?
- przejrzystość kodu (logika się “zlewa”)
- utrudnienia przy wymianie komponentu
- rozwiązując podobny problem musimy praktycznie kopiować cały skrypt, albo zacząć pisanie od początku
Problem, jaki może się pojawić, to konieczność zdefiniowania funkcji zanim ich użyjemy. Jak to w prosty sposób rozwiązać? Mój ulubiony trik to definiowanie na początku funkcji, która ma za zadanie tylko opisać zakładaną logikę – czy inaczej – szkic tego co chciałbym osiągnąć. Na ogół wygląda to tak:
#requires -version 2.0 <# Pomoc do skryptu #> function Invoke-Main { param ( $Parametr = $Script:Parametr, $DrugiParametr = $Script:DrugiParametr, $Path = $Script:Path ) Get-Foo -Parametr $Parametr | Test-Foo | ConvertTo-Bar -Drugi $DrugiParametr | Export-Csv -Path $Path } <# Tu definiujemy: * Get-Foo * Test-Foo * ConvertTo-Bar #> Invoke-Main
Jak widać – definicje funkcji następują przed jej wywołaniem (które ma miejsce na samym końcu skryptu) ale sposób ich wywołania znany jest od początku (zdefiniowany w pierwszej funkcji w skrypcie). Jeśli zechcę kiedyś zmienić finalny obiekt na ‘Boo’ zamiast funkcji ConvertTo-Bar zdefiniuję ConvertTo-Boo, zmienię nieco Invoke-Main i już – skrypt gotowy do użycia.
Funkcja w rurce – awansujemy.
Patrząc na składnię moich hipotetycznych funkcji od razu widać jedną rzecz: wszystkie “klocki” radzą sobie w rurce. To podstawa: pisząc funkcję niezdolną do pracy w środku pipe’a strzelamy sobie w stopę. By jednak współpraca przebiegała bezboleśnie – należy z całą pewnością przyzwyczaić się do pisania zaawansowanych funkcji. Wymagają one nieco dekoracji na wstępie, ale wartość dodaną trudno moim zdaniem przecenić. Druga rzecz, która być może w skryptach nie jest tak cenna, ale w modułach moim zdaniem absolutnie niezbędna: pomoc do funkcji. Wymaga to maciupeńkich nakładów pracy (odpowiednio skomponowany komentarz) a oszczędza konieczności czytania definicji funkcji za każdym razem. Myślę, że sam temat zaawansowanych funkcji jest zbyt obszerny, by go streścić tutaj, zostawię więc to na piątą część cyklu. Na rozbudzenie apetytu: malutka funkcja, która wykorzystuje część możliwości, które dają zaawansowane funkcje:
function Test-IsAlive { [CmdletBinding()] param ( [Parameter( ValueFromPipelineByPropertyName = $true, Mandatory = $true, HelpMessage = 'Nazwa komputera, ktory testujemy' )] [ValidatePattern('(?# Bez spacji w nazwach komputerow!)^\S+$')] [Alias('Name','CN','Nazwa')] [string]$ComputerName, [Parameter(ValueFromPipeline = $true)] [Alias('IO')] [PSObject]$InputObject ) process { Write-Verbose "Testuje komputer: $ComputerName" if (Test-Connection -ComputerName $ComputerName -Quiet -Count 1) { if (!$InputObject) { Write-Verbose 'Nic na wejsciu, tworzymy sami.' $InputObject = New-Object PSObject -Property @{ ComputerName = $ComputerName } } $InputObject | Add-Member NoteProperty IsAlive $true -PassThru } } }
Skutek? Mogę przez tą funkcję przepuścić dowolny obiekt, który posiada właściwość ComputerName, Name, Nazwa lub CN i jeśli właściwość ta nie zawiera spacji (ValidatePattern, z komentarzem, by nie straszyć ludzi niezrozumiałym regexpem) i da się ją “pingnąć” (preferowane więc będą obiekty pobrane z AD, lub innego zawierającego nazwę komputera) to na wyjściu dostanę ten sam obiekt “wzbogacony” o właściwość IsAlive. Jeśli spróbuję uruchomić funkcję bez parametru – poprosi mnie ona o wartość ComputerName. I rzecz nie bez znaczenia: dodanie wtrąceń typu Write-Verbose/ Debug działa bez potrzeby implementacji. [CmdletBinding()] daje mi te opcje za darmo. Mogę więc uruchomić funkcję z parametrem –Debug lub –Verbose i dowiedzieć się więcej o tym, co dzieje się w środku.
Wejście – wyjście.
Skrypty mają to do siebie, że czasem wolelibyśmy nie musieć pamiętać co do nich trzeba włożyć, oraz co byśmy chcieli z nich wyjąć. Z drugiej strony – czasem jednak chcemy mieć wpływ na to, co się stanie… PowerShell pozwala pogodzić te dwie sprzeczności. Rozwiązanie to wyciągnąć jak najwięcej elementów na zewnątrz skryptu (w postaci parametrów) i jednocześnie przypisać im wartość domyślną (by nie być zmuszonym podawać ich za każdym razem). Z wyjściem jest gorzej – optymalne rozwiązanie to parametr typu [switch], który na wyjściu da nam “żywe” obiekty. To co z nimi zrobimy dalej będzie zależeć już tylko od nas. I wilk syty, i owca cała.