install-module eNLib
nie tylko udało mi się skończyć* moduł eNLib, ale również opublikować w PowerShell Gallery. przy okazji tego drobnego sukcesu chciałem podzielić się kilkoma doświadczeniami z pisania modułów. wszędobylska zasada Pareto (lub dla bardziej dociekliwych – prawo Zipfa) okazała się sporym wyzwaniem – im bliżej końca, tym trudniej. dla tego słowo opisu samej biblioteki, oraz z czym były największe problemy przy jego wykończeniu.
tak, że teraz instalacja biblioteki sprowadza się do trywialnego 'install-module eNLib’ a źródła można również pobrać z GitHub.
[*skończyć w metodyce CI/CD, czyli najważniejsze gotowe, a z resztą jakoś to będzie (; ]
eNLib
typowa biblioteka użytkowa – kiedy skryptów pisze się dużo, bardzo szybko okazuje się, że pewne funkcje ułatwiające życie, wykorzystuje się w niemal każdym skrypcie. dla mnie taką funkcją jest write-log – funkcja, która forkuje wszelkie informacje zarówno na ekran jak do pliq. dzięki temu nie muszę zastanawiać się nad 'verbose’ [który ma swoje wady] i mam gwarancję, że wszystko co widzę, będzie również w logu. koniec zastanawiania się, że jakiś błąd śmignął na ekranie i nie wiadomo co to. łatwiej też troubleshootować – 'prześlij mi log z przebiegu’ i wszystko będzie jasne.
żeby logować do pliq, należy go zainicjalizować. i z tym było najwięcej zabawy, o czym za chwilę. drugą (czy też raczej zerową) funkcją, jest zatem start-logging, inicjalizująca zmienną $logFile i sam plik logu. jeśli wywoła się write-log bez uprzedniej inicjalizacji – zostanie automatycznie wykonana. wywołanie jawne pozwala na użycie dodatkowych atrybutów – założenie logu o zadanej nazwie, w zadanym folderze lub w katalogu 'documents’ użytkownika.
każdy plik logu zawiera podstawowe informacje dot. środowiska uruchomieniowego, każdy wpis ma timestamp a dodatkowo można tagować linijki błędów/ostrzeżeń/ok przy pomocy parametru 'type’ – nie tylko pokazuje w innym kolorze, co pomaga podczas pracy bieżącej, ale dodaje tak 'ERROR:’ czy 'INFO:’ w logu, pomagając przeszukiwać go później:
*logging initiated 02/06/2021 09:30:41 in C:\Users\nexor\OneDrive\Documents\Logs\_console-2102060930.log *script parameters: <none> *************************************************** 09:30:41> test 09:30:42> ERROR: test error
jasna sprawa, jako wiadomość można podać dowolny obiekt, który zostanie skonwertowany do tekstu. szczegóły oraz przykłady użycia – 'man write-log -examples’, 'man start-logging -full’ – więc nie rozpisuję się.
ponadto moduł zawiera funkcje
- new-randomPassword – generuje ciąg znaków o zadanej złożoności
- get-AnswerBox – Formsowe okienko OK/Cancel
- get-valueFromInputBox – Formsowe okienko pozwalające na wpisanie promptu użytkownika
- get-ExchangeConnectionStatus – weryfikacja połączenia z Exchange [Ex/EXO]
- connect-Azure – weryfikacja połączenia z Azure
te funkcje są wersjach słabo przetestowanych i na pewno nie są dopieszczone tak jak write-log, którego po prostu używam nagminnie [już nawet odruchowo wpisuje wrtite-log zamiast write-host (; ]. na pewno powoli będę nad nimi pracował – skoro są opublikowane, to będą potrzebowały trochę więcej atencji.
Z czym było najwięcej zabawy?
- jeśli funkcja odpalana jest bezpośrednio z konsoli, to $MyInvocation ma inną wartość niż…
- …kiedy odpali się ze skryptu. ale co jeśli…
- …funkcja uruchamiana jest przez inną funkcję, która wywołana jest ze skryptu?
przyjąłem default, że plik logu będzie miał nazwę '_<nazwa-skryptu>-<datestamp>.log’ . i żeby ową nazwę skryptu pobrać, trzeba zastosować inną metodę, zależnie od kontekstu – opisanego wyżej. 3ci opisany przypdek, choć wydaje się być najbardziej złożony, jest równocześnie domyślnym:
- nie chcemy pamiętać i inicjalizować logu, więc po prostu w skrypcie zamieszczamy 'write-log „coś tam”’
- funkcja write-log wykrywa, że to pierwsze użycie, więc woła start-logging.
- dodatkowo obie funkcje są w module, a więc w zewnętrznym skrypcie
zmienne typu $MyInvocation to zmienne automatyczne, tworzone w trakcie egzekucji skryptu. maja więc określony kontekst, czy też zakres (scope) i w każdym będą miały inną, lokalną wartość. do tego nie można założyć konkretnego scenariusza – muszą być obsłużone wszystkie. z tego powodu kawałek inicjalizacji musiał zostać przeniesiony do samego write-log, choć łamie to logikę [powinno być wszystko w start-logging]:
#if function is called without pre-initialize with start-logging, run it to initialize log. if( [string]::isNullOrEmpty($script:logFile) ){ #these need to be calculated here, as $myinvocation context changes giving library name instead of script if( [string]::isNullOrEmpty($MyInvocation.PSCommandPath) ) { #it's run directly from console. $scriptBaseName = 'console' } else { $scriptBaseName = ([System.IO.FileInfo]$($MyInvocation.PSCommandPath)).basename } if([string]::isNullOrEmpty($MyInvocation.PSScriptRoot) ) { #it's run directly from console $logFolder = [Environment]::GetFolderPath("MyDocuments") + '\Logs' } else { $logFolder = "$($MyInvocation.PSScriptRoot)\Logs" } $logFile = "{0}\_{1}-{2}.log" -f $logFolder,$scriptBaseName,$(Get-Date -Format yyMMddHHmm) start-Logging -logFileName $logFile }
write-log "jakis text" write-log jakis text write-log jakis -type ok text
param( #message to display - can be an object [parameter(ValueFromRemainingArguments=$true,mandatory=$false,position=0)] $message, #adds description and colour dependently on message type [parameter(mandatory=$false,position=1)] [string][validateSet('error','info','warning','ok')]$type, #do not output to a screen - logfile only [parameter(mandatory=$false,position=2)] [switch]$silent, # do not show timestamp with the message [Parameter(mandatory=$false,position=3)] [switch]$skipTimestamp )
param( #message to display - can be an object [parameter(ValueFromRemainingArguments=$true,mandatory=$false,position=0)] $message, #adds description and colour dependently on message type [string][validateSet('error','info','warning','ok')]$type, #do not output to a screen - logfile only [switch]$silent, # do not show timestamp with the message [switch]$skipTimestamp )
2o/8o
- testy, testy i jeszcze raz testy – w różnych konfiguracjach. i tak nie zrobiłem wszystkich – choćby z wersji core.
- opisanie funkcji wraz z przykładami.
- zapanowanie nad inicjalizacją logu bez względu na sposób uruchomienia
- prawidłowe przygotowanie do publikacji jako moduł
Sfinks