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?

z uniwersalnym inicjowaniem pliq logu i tzw. ’scopes’ dla zmiennych. idea jest trywialna, ale w praktyce okazało się to zabójcze. np:
  • 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
    }
zmienna $logFile jest wykorzystana jako flaga – jeśli nie jest zainicjowana, odpalana jest 'start-logging’. zmienna jest eksportowana, więc można jej potem używać wewnątrz własnych funkcji np. żeby poinformować, 'zapis przebiegu jest zapisany w $logFile’.
drugim problemem/ciekawostką, nad którą chwilę musiałem się pogłowić, było umożliwienie funkcji konsumpcji dowolnej ilości parametrów. znów teoretycznie – sprawa prosta. wystarczy dodać 'ValueFromRemainingArguments=$true’ i pozamiatane…. tak przynajmiej wynikało z licznych przykładów dostępnych w necie. tyle, że te przykłady zawsze pokazywały sytuację, gdzie był tylko jeden parametr – a tu, funkcja ma ich kilka. i za każdym razem wywalało błąd, próbując podstawić pod nie wartość, która miała iść jako 'message’. podobną funkcjonalość ma write-host. wszystkie przykłady zadziałają:
write-log "jakis text"
write-log jakis text
write-log jakis -type ok text
z tą różnicą, że write-host zapisze wszystkie parametry w jednej linii, write-log – w oddzielnych liniach. zgubna okazała się … poprawność zapisu parametrów. kiedy deklaracja nagłówka była 'pełna’, przekazywanie nie działało prawidłowo:
    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
    )
natomiast po usunięciu deklaracji prametrów udało się uzyskać porządany efekt:
    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
    )
nie wiem czy 'pedanteryjna’ wersja deklaracji pozwalająca na pożądany efekt, a póki co nie udało mi się znaleźć sposobu na zdebugowanie write-host. ale pewnie kiedyś poświęcę czas, żeby go podejrzeć i przeanalizować.

2o/8o

choć write-log używam od dawien-dawna, czyli większość pracy niby była zrobiona, okazało się, że to jest to 2o% pracy, robiące 8o% wyniq. kiedy zacząłem robić z tego moduł, z myślą o publikacji, pozostałe 8o% pracy, stanowiące 2o% tego, co widać to:
  • 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ł
mam nadzieję, że komuś ułatwi pracę (:
eN.
-o((:: sprEad the l0ve ::))o-

Comments (1)

  1. Sfinks

    Odpowiedz

    Napewno ułatwi, ale muszę się przegryźć przez całość. Pisanie skryptów do użycia przez innych, to dopiero całochłonne zajęcie. Sam na to wpadłem, jak robiłem kilka prostych rzeczy dla Helpdesk’u. Logika to przysłowiowe 5-10 minut, pół dnia to opis i obsługa błędów.

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Time limit is exhausted. Please reload CAPTCHA.