Powershell 7: eksperymentujemy #5

Wpis kończący serię o nowym Powershell.

Obsługa błędów w poprzednich wersjach PS była całkiem niezła. Jest try-catch, jest -ErrorAction, wreszcie jest zmienna $Error ze wszystkimi błędami, które do tej pory udało nam się wygenerować (zarówno krytycznymi jak i niekrytycznymi).

W wersji 7 pojawiła się jeszcze jedna komenda: Get-Error.

Proszę sobie spróbować podzielić przez zero albo wykonać Test-Connection do nieistniejącego adresu dns a potem uruchomić Get-Error:

Mnóstwo interesujących informacji na temat błędu; jest nawet konkretna lokalizacja (numer linii, pozycja w linii), w której błąd się pojawił.

Dla mnie osobiście najwygodniejsze jest tu to, że Get-Error w szybki sposób zwraca szczegóły *ostatniego* błędu w ładny, czytelny sposób. Co prawda wszystkie te informacje dało się wyciągnąć w poprzednich wersjach, ale wymagały nieco wygibasów. A tu – wszystko jak na tacy.

Na zakończenie dodam jeszcze, że tych pięć wpisów o Powershell 7 bynajmniej nie wyczerpuje wszystkich nowości w tej wersji. Opisałem tylko te, które społeczność zgromadzona wokół PS uznała za najbardziej przydatne.

Powershell 7: eksperymentujemy #4

Powershell 7 wprowadził dwa nowe operatory umożliwiające warunkowe uruchamianie poleceń w łańcuchu.

Wyobraźmy sobie, że chcemy najpierw uruchomić polecenie A a potem polecenie B, ale tylko jeżeli A zakończyło się sukcesem.

Czemu tak? Na przykład A może tworzyć nowy folder a B kopiować coś do niego – nie ma sensu uruchamiać B jeżeli A się nie udało.

Do tego celu służy operator &&:

New-Item -Type Directory -Name "TestFolder01" -ErrorAction SilentlyContinue | Out-Null && Write-Host "Udało się"

Powyższy kod spróbuje utworzyć nowy folder “TestFolder01”, a następnie wyświetli komunikat “Udało się” jeżeli wszystko poszło OK.

Można też w drugą stronę: czasami chcemy uruchomić jakąś komendę tylko wtedy, gdy jakaś inna komenda poległa z błędem. Wówczas użyjemy operatora ||:

New-Item -Type Directory -Name "TestFolder01" -ErrorAction SilentlyContinue | Out-Null || Write-Host "Nie udało się"

Tu znów próbujemy utworzyć folder TestFolder01, ale ponieważ taki folder już jest, operacja New-Item nie powiedzie się i dostaniemy na wyjściu komunikat “Nie udało się”.

Operatory && i || można oczywiście łączyć:

New-Item -Type Directory -Name "TestFolder02" -ErrorAction SilentlyContinue | Out-Null && Write-Host "Udało się" || Write-Host "Nie udało się"

Powyższa linijka za pierwszym razem da komunikat “Udało się”, a za drugim – “Nie udało się”. Od biedy można myśleć o parze && – || jak o operatorze trójwartościowym: wykonaj A, jeżeli się uda to wykonaj B, w przeciwnym razie C:

A && B || C

Powershell 7: eksperymentujemy #3

Po operatorze trójwartościowym i współbieżnej pętli ForEach przyszedł czas na Null-e.

Null to takie bydlę, które z jednej strony jest całkiem wygodnym konceptem, a z drugiej przysparza programistom (i twórcom języków programowania) mnóstwo kłopotów.

Null z definicji oznacza brak wartości. Nie: zero. Nie: pusty ciąg znaków. Nie. BRAK wartości.

Najwięcej problemów z NULL-em pojawia się przy operacjach boole-owskich: czy null AND true da w wyniku false? A może null?

Czy null OR true da w wyniku true? false? null?

Hmmm…

Niektóre języki próbują to rozwiązać poprzez wprowadzenie specjalnej wartości UNKNOWN: null AND true da w wyniku UNKNOWN. 3 + NULL – również. Ale tu pojawia się problem wyższego rzędu: skoro UNKNOWN samo w sobie jest wartością (specjalną, ale jednak), trzeba dorobić logikę, która ją uwzględnia… czy UNKNOWN and NULL dają na wyjściu UNKNOWN? A może NULL? A może 42? Hm.

Inne języki zakładają po prostu, że jakakolwiek operacja z NULL-em da w wyniku NULL.

Jeszcze inne konwertują niejawnie NULL na 0.

Różnych algorytmów postępowania z NULL-ami jest całkiem sporo (niektóre się wzajemnie wykluczają!), ale ja dziś nie o tym.

Ja dziś o tym, jak rozpoznać NULL-a w Powershell oraz co z tego wynika.

Zacznę od tego, że implementacja NULL w Powershell nie jest byt elegancka. W wersji 5.x na przykład można było zrobić coś takiego:

$zmienna = $null
$zmienna -eq $null

Wynik powyższego dawał na wyjściu True. Ale już:

[string] $innazmienna = $null
$innazmienna -eq $null

… generował na wyjściu False. Wystarczy więc dodać deklarację typu, żeby się rozjechało. Jeżeli chcieliśmy sprawdzić “null-owatość” zmiennej typu string, musieliśmy się łapać “brudnych” chwytów typu [String]::Empty lub im podobnych. Groch z kapustą.

Jeszcze gorzej było z NULL-ami zwracanymi przez zapytania SQL. Żeby sprawdzić, czy dostaliśmy NULL, trzeba było używać [DBNull]::Value, co prowadziło do jeszcze większego zamieszania.

Powershell 7.0 próbuje to wszystko uporządkować – czy robi to prawidłowo czy nie, chyba nie da się jednoznacznie określić, krzaków po drodze jest mnóstwo. Ale jest postęp.

Ci z Czytelników, którzy liznęli w życiu nieco SQL-a wiedzą o istnieniu operatora COALESCE, który zwraca pierwszą napotkaną wartość parametru niebędącego NULL-em. Innymi słowy jeżeli a i b są NULL, a c i d nie są, wówczas COALESCE(a, b, c, d) zwróci wartość zmiennej c, bo jest to pierwszy parametr niebędący NULL. I tak dalej.

W Powershell 7 pojawił się operator ??, który robi mniej więcej to samo, tylko trochę inaczej:

$x = "iks"
$y = "igrek"
$z = "zet"
$a ?? $b ?? $c ?? $x ?? $y ?? $z

Powyższy kod da na wyjściu “iks”, ponieważ zarówno $a jak też $b i $c są NULL, a $x jest pierwszą zmienną niebędącą NULL.

Drugim ważnym operatorem, który pojawił się w PS7, jest operator “podstaw jeżeli NULL”, który najpierw sprawdza, czy po lewej stronie jest NULL i jeżeli tak, podstawia do lewej strony to, co jest po prawej, a jeżeli nie, to nic nie robi. Przykład:

$a ??= "literka a"
$a ??= "literka b"
$a ??= "literka c"

Tutaj na dzień dobry $a jest NULL (bo nie ma takiej zmiennej, a nieistniejące zmienne w PS są zawsze NULL), więc najpierw do $a zostanie podstawiony tekst “literka a”. Potem próbujemy tam podstawić “literka b”, ale operator ??= “widzi”, że po lewej stronie ma już jakąś wartość (“literka a”) i zignoruje podstawienie. To samo dzieje się w kolejnej linii, tej z literką c.

Wniosek końcowy? Operatory ?? oraz ??= mogą zaoszczędzić nam zbędnego klepania if-ów, ale trzeba ich używać z głową. Jak wszystkiego zresztą.

Powershell 7: eksperymentujemy #2

Dziś drugi wpis z serii nowinek w nowym Powershell. Tym razem weźmiemy na tapet operator trójargumentowy zwany czasem operatorem warunkowym (po naszemu: ternary operator). Czyli pytajnik z dwukropkiem.

Operator ów to nic innego jak sprawdzenie jakiegoś warunku, a następnie zwrócenie wartości X jeżeli warunek ów jest prawdziwy bądź też Y jeżeli nie jest.

Przykład:

$c = $a -eq $b ? 3 : 4

Powyższy kod należy rozumieć tak: sprawdź czy $a równa się $b i jeżeli tak, to podstaw za $c trójkę, a jeżeli nie – czwórkę.

Inny przykład:

1..10 | Foreach-Object {$dzien = Get-Random "Poniedzialek","Wtorek","Sroda","Czwartek","Piatek","Sobota","Niedziela"; $ma_r = $dzien -like "*r*" ? "$dzien ma R" : "$dzien nie ma R"; $ma_r}

Wynik:

Czwartek ma R
Niedziela nie ma R
Sobota nie ma R
Sobota nie ma R
Poniedziałek nie ma R
Czwartek ma R
Niedziela nie ma R
Środa ma R
Niedziela nie ma R
Wtorek ma R

Tym razem wylosowaliśmy sobie losowo dziesięć razy nazwę dnia tygodnia i sprawdziliśmy każdorazowo czy nazwa ma w środku literę R czy nie (dlaczego? Bo “Niedzierla”, jak mówi stary kawał).

Implementacja operatora trójwartościowego w Powershell 7.0 jest dość “goła”:

Po pierwsze, nie da się użyć wyniku operatora w charakterze zmiennej (innymi słowy nie da się podstawić do wyniku operatora żadnej wartości). W C jest to jak najbardziej możliwe:

( x > y ? a : b) = 7

Powyższy kod w C podstawi siódemkę do zmiennej a jeżeli x>y, w przeciwnym razie do b. W Powershell podobny zabieg skończy się komunikatem o błędzie składni

Po drugie zaś powershellowa wersja nie obsługuje składni “skróconej”; w C możemy napisać:

a = b ?: c

Tu z kolei jeżeli b jest prawdą, wartość zmiennej a nie zmieni się, a jeżeli nie, podstawimy tam wartość zmiennej c. Innymi słowy powyższa linijka jest równoważna:

a = b ? a : c

W Powershell użycie składni “skróconej” nie przejdzie – również zostaniemy opluci komunikatem błędu.

Wniosek końcowy: pomimo niepełnej implementacji operatora trójwartościowego mamy od teraz okazję skrócenia tego:

if($a -ne $b) {$x = 7} else {$x = 9}

Do tego:

$x = $a -ne $b ? 7 : 9

Prawda, że prościej?

Powershell 7: eksperymentujemy #1

Microsoft wypuścił niedawno nowego Powershella, wersja 7. Podobno ma on zjednoczyć i ujednolicić wszystkie poprzednie wersje i biorąc pod uwagę wysiłki i inwestycje giganta z Redmond może się okazać, że się uda.

Optymistycznie dopisałem na końcu dzisiejszego tytułu numerek jeden w nadziei, że rozwinie mi się z tego seria wpisów – wersja 7 ma mnóstwo smakowitych przypraw, których brakowało w wersji 5.1 (oraz Core, czyli 6.x), naprawdę jest o czym pisać. Póki co serii nie ma, ale od czegoś trzeba zacząć. A najlepiej od najsmaczniejszego kawałka: nowy Powershell obsługuje równoległe pętle ForEach!

Coś, czego dotychczas strasznie brakowało, w końcu jest w zasięgu ręki i to nawet całkiem amatorskiej. Nie trzeba doktoratów ani studiowania diagramów Harela, żeby załapać ideę.

O sssso chozzzzi?

Chozzzi o to, że najnowszy Powershell dopuszcza w poleceniu foreach opcję -Parallel, której użycie sprawi, że kolejne iteracje pętli będą się wykonywać bez oczekiwania na zakończenie poprzednich. Innymi słowy – w tym samym czasie!

Efekt może być całkiem piorunujący: jeżeli pętla “biegnie” po różnych maszynach, wywołując kod zdalnie na każdej z nich, wówczas możemy liczyć na skrócenie czasu proporcjonalne do ilości zdalnych maszyn.

Ale nawet bez takich hopsztosów, na maszynie lokalnej (wielowątkowej) też można się całkiem nieźle zabawić.

Spójrzmy na najprostszy możliwy przykład: wykonamy stukrotne losowanie liczby całkowitej między 20 a 30, następnie poczekamy sobie za każdym razem tyle milisekund, ile wylosowaliśmy. Najpierw po staremu:

1..100 | foreach {Start-Sleep -Milliseconds (Get-Random -Minimum 20 -Maximum 30)}

Pukamy Enter, czekamy chwilę… i nic, wykonuje się bez błędów. Ale też bez żadnych interesujących nas informacji. Spróbujmy więc zmierzyć czas trwania naszego eksperymentu:

Measure-Command {1..100 | foreach {Start-Sleep -Milliseconds (Get-Random -Minimum 20 -Maximum 30)}}

Teraz jest dużo ciekawiej. Oto wynik:

Days : 0
Hours : 0
Minutes : 0
Seconds : 3
Milliseconds : 130
Ticks : 31308312
TotalDays : 3.62364722222222E-05
TotalHours : 0.000869675333333333
TotalMinutes : 0.05218052
TotalSeconds : 3.1308312
TotalMilliseconds : 3130.8312

Potrzebujemy aż tylu informacji? Moim zdaniem wystarczy w zupełności jeden wynik, w milisekundach:

(Measure-Command {1..100 | foreach {Start-Sleep -Milliseconds (Get-Random -Minimum 20 -Maximum 30)}}).TotalMilliseconds

Wynik:

3124.9469

OK, czyli widzimy wyraźnie, że stukrotne oczekiwanie między 20 a 30 milisekund wykonuje się około trzech sekund. Oczekiwałbym raczej okolic dwóch i pół sekundy, ale pewnie trzeba jeszcze uwzględnić narzut samej pętli (Powershell jest językiem wysokopoziomowym, nie jest więc demonem prędkości).

A teraz skosztujmy magii najnowszej wersji i spróbujmy naszą pętelkę uwspółbieżnić:

(Measure-Command {1..100 | foreach -Parallel {Start-Sleep -Milliseconds (Get-Random -Minimum 20 -Maximum 30)}}).TotalMilliseconds

Magia polega na dodaniu opcji -Parallel do pętli foreach. Teraz kod wykonuje się w czasie około 1315 milisekund! A więc na oko jakieś dwa i pół razy szybciej.

Spróbujemy teraz trochę rozciągnąć nasz eksperyment w czasie: zamiast losować dziesiątki milisekund, będziemy losować setki. Dzięki temu zmniejszymy nieco efekt narzutu pętli i innych etceterów i dostaniemy bardziej miarodajne wyniki (chociaż będziemy musieli nieco dłużej poczekać).

Po staremu:

(Measure-Command {1..100 | foreach {Start-Sleep -Milliseconds (Get-Random -Minimum 100 -Maximum 120)}}).TotalMilliseconds

Wynik: 11690 milisekund.

A teraz po nowemu:

(Measure-Command {1..100 | foreach -Parallel {Start-Sleep -Milliseconds (Get-Random -Minimum 100 -Maximum 120)}}).TotalMilliseconds

Wynik: 3527 milisekund, czyli prawie trzyipółkrotnie szybciej.

Przy jeszcze większym wydłużeniu czasu wykonywania pojedynczego przebiegu pętli okazałoby się, że czas wykonania z opcją -Parallel zbliża się asymptotycznie do 20% czasu wykonania bez tej opcji. Dzieje się tak, ponieważ domyślnie Powershell rozkłada pętlę z opcją -Parallel na pięć rdzeni (a raczej wątków). Nawet jeżeli mamy procesor ośmio- czy szesnastowątkowy, to i tak domyślnie wykorzystamy tylko pięć.

Chyba że…

Chyba że skorzystamy z innej opcji zatytułowanej -ThrottleLimit, która nakazuje korzystać z tylu wątków, ile chcemy (ale oczywiście nie więcej, niż jest akurat pod ręką).

Poniższy kod pokazuje zależność między czasem wykonania a ilością wykorzystanych wątków. Mam procesor ośmiowątkowy i da się to zauważyć na poniższym przykładzie:

foreach($n in 1..16){$n.ToString()+":"+(measure-command {1..100 | foreach -ThrottleLimit $n -Parallel {Start-Sleep -Milliseconds (Get-Random -Minimum 50 -Maximum 70)}}).TotalMilliseconds.ToString()}

Wynik:

1:12022.7903
2:5984.0288
3:4086.215
4:2888.1541
5:2182.0602
6:1805.0725
7:1602.9308
8:1385.5676
9:1377.71
10:1325.2778
11:1338.5241
12:1350.8028
13:1388.9493
14:1301.6482
15:1385.6124
16:1383.6232

Powyższe słupki należy interpretować następująco: po lewej stronie dwukropka mamy wartość parametru ThrottleLimit (czyli wymaganą liczbę współbieżnych wątków), po prawej zaś – czas wykonania stu losowych oczekiwań między 50 a 70 milisekund każde.

Jak widać czas ten sukcesywnie spada wraz ze wzrostem liczby użytych wątków, ale tylko do ośmiu – potem się stabilizuje. Dzieje się tak, ponieważ nie da się na ośmiowątkowym procesorze wykonywać więcej niż ośmiu rzeczy na raz. Jeżeli pracownik potrzebuje dwóch godzin na wykopanie łopatą dziury metr na metr na metr, to dwunastu pracowników nie wykopie tej dziury w dziesięć minut, tak samo jak dziewięć kobiet nie donosi pojedynczej ciąży w miesiąc. No nie da się i już. Pozabijają się prędzej tymi łopatami niż cokolwiek wykopią.

A teraz przykład nieco bardziej praktyczny. Porównamy sobie czas wykonania polecenia PING w wersji po kolei oraz współbieżnie:

(Measure-Command {"www.google.com","www.bing.com","www.apple.com" | ForEach-Object {ping $_ | Out-Null}}).TotalMilliseconds

Wynik: 9168 milisekund

Mniej więcej się zgadza: ping wysyła domyślnie cztery pingi, czeka między każdym z nich jedną sekundę (czyli między czterema pingami czeka trzy sekundy). Trzy sekundy na każdą z trzech domen, razem dziewięć sekund z niedużym hakiem.

A teraz po nowemu:

(Measure-Command {"www.google.com","www.bing.com","www.apple.com" | ForEach-Object -Parallel {ping $_ | Out-Null}}).TotalMilliseconds

Wynik: 3104 milisekundy. Trzy razy szybciej, bo wszystkie trzy pingi wykonywały się w tym samym czasie.

Proste?

No ba!

Przestrzegę jeszcze, żeby nie nadużywać opcji -Parallel, ponieważ nie jest ona panaceum na wszelkie problemy z wydajnością naszego kodu. Jeżeli na przykład chcemy policzyć sumy kontrolne plików w folderze, wówczas okaże się, że wąskim gardłem jest mechanizm dostępu do plików i próbując przyspieszyć tylko sobie pogorszymy sytuację:

(Measure-Command {Get-ChildItem -File -Recurse | Select-Object -First 100 | foreach {(Get-FileHash $_.FullName).Hash}}).TotalMilliseconds

Wynik: 185 ms

To samo po nowemu:

(Measure-Command {Get-ChildItem -File -Recurse | Select-Object -First 100 | foreach -Parallel {(Get-FileHash $_.FullName).Hash}}).TotalMilliseconds 

Wynik: 2381 ms (tak, prawie 13 razy wolniej!) Próbujemy odczytać pięć różnych plików na raz, z tego samego dysku. Całkiem jak z tymi łopatami i kopaniem dołów…

Jeszcze gorzej będzie, jeżeli zechcemy w takiej równoległej pętli modyfikować wartość zmiennej. Zobaczmy na przykładzie prostego sumowania pierwszych stu liczb naturalnych:

$suma = 0; 1..100|foreach {$suma+=$_}; Write-Output $suma

Wynik: 5050, wszystko ok. A teraz z opcją -Parallel:

 $suma = 0; 1..100|foreach -Parallel {$suma+=$_}; Write-Output $suma

Wynik: 0.

WTF?

Nie chciało mi się już kopać w czeluściach Jęternetów, ale obstawiam, że lokalne zmienne nie działają w pętli -Parallel tak samo jak w “normalnym” skrypcie. Musiałbym doczytać o zasięgach widoczności zmiennych, a już mi się nie chce. Może któryś z Czytelników jest bardziej ode mnie w temacie i zechce się podzielić?

Tak czy siak, końcowy wniosek na dziś jest taki, że warto używać opcji -Parallel, ale trzeba to robić z głową! Najlepiej wtedy, gdy w pętli wołamy coś zewnętrznego, co długo trwa i gania na różnych maszynach.

Za dużo spacji – wersja w Powershell

Jakiś czas temu pokazałem jak za pomocą krótkiego kodu w SQL usunąć wszystkie wielokrotne spacje w tekście i zastąpić je spacjami pojedynczymi.

https://xpil.eu/PCJ1Y

Dziś króciutko ten sam trick w Powershell, w razie gdyby ktoś kiedyś potrzebował pozbyć się wielokrotnych spacji z tekstu w windowsowym skrypcie.

$tekst = "Ala     ma kota      i    dwie     wydry."
$tekst.Replace(" ", "{}").Replace("}{", "").Replace("{}", " ")

Efekt?

Ala ma kota i dwie wydry.

Jak widać metoda działa identycznie. Najpierw zamieniamy każdą spację na {}, potem usuwamy każdą parę }{, na koniec zamieniamy pozostałe {} na pojedynczą spację.

Prawdę mówiąc sposób ten nie przestaje mnie zadziwiać. To jedna z tych rzeczy, które są proste algorytmicznie, krótkie w zapisie i działają uniwersalnie dla dowolnych tekstów.

Udajemy, że myślimy

Ludzie często udają. Udają różne rzeczy, z najdziwniejszych powodów. Ja na przykład udaję, że umiem mówić po angielsku. Udaję tak dobrze, że przez ostatnich czternaście (bez mała) lat na emigracji nikt się nie zorientował.

A komputery udają, że wcale nie są inteligentne, bo jakby się wydało, że istnieje już AI mądrzejsze od człowieka, to szybko doszłoby do krwawej elektronicznej rzezi, bo człowiek nie lubi jak mu się ktoś wymandrza.

Dziś nauczymy się jak w Powershell napisać skrypt, który udaje, że myśli.

Czemu?

Bo się da 😉

Żeby było śmieszniej (ale jakoś nie jest, chociaż ciągle próbuję, żeby było) skrypt ten będzie poza tym kompletnie bezużyteczny. Jeżeli ktoś chciałby go uniebezużytecznić (czyli uczynić mniej bezużytecznym, czyli uczynić bardziej użytecznym, czyli żeby coś robił poza udawaniem), pokażę w którym miejscu można próbować.

Najpierw zadamy sobie jednak jedno bardzo ważne pytanie: skąd wiadomo jak komputer ma udawać, że myśli w taki sposób, żeby człowiek dał się nabrać?

Można zrobić test Turinga. Ale na to jestem – póki co – za cienki w uszach.

Można też wrzucić na ekran dużo ruszających się cyferków i literków tak jak zrobili to bracia Wachowscy w czasach, kiedy bracia byli jeszcze braćmi i wyjść z założenia, że człowiek widzący na ekranie coś, czego kompletnie nie rozumie, przyjmie automatycznie, że jest to coś bardzo Mądrego, a więc na pewno musi Myśleć.

Żeby jednak nie zeżreć w tym celu całego ekranu…

Nie, wystarczy tego ględzenia. Zabierzmy się do konkretów.

[console]::CursorVisible = $false;
[int] $i = 0;
$sha = New-Object System.Security.Cryptography.SHA256Managed;
$utf8 = [System.Text.Encoding]::UTF8;
while ($true) {
    $cp = $host.UI.RawUI.CursorPosition;
    $sha | % { $_.ComputeHash($utf8.GetBytes(($i += 1).ToString())) } | % { $_.ToString("x2") } | Write-Host -NoNewline;
    $host.UI.RawUI.CursorPosition = $cp;
    Start-Sleep -Milliseconds 50;
}

Efekt?

Skrypt bierze kolejne liczby całkowite, zamienia je na tekst i generuje z nich hasz SHA2_256, a potem wyświetla go na ekranie.

Czego się tu nauczyliśmy?

  1. $host.UI.RawUI.CursorPosition powie nam jakie są aktualne współrzędne kursora tekstowego w bieżącej konsoli.
  2. Powyższy dynks jest zapisywalny! A więc zapisanie współrzędnych do powyższej zmiennej przesunie nam kursor w zadane miejsce!
  3. Wykonanie metody ToString(“x2”) na danych binarnych zwróci nam te same dane zakodowane szesnastkowo.
  4. Klasa System.Security.Cryptography.SHA256Managed oferuje metodę ComputeHash (domyślnie używającą SHA2_256) – a więc można za jej pomocą haszować dane w naszym skrypcie. Przypominam oczywiście o istnieniu wbudowanej funkcji Get-FileHash, która zrobi to samo z plikiem – jednak nie ma w Powershell wbudowanego odpowiednika do wyliczania haszów ze zmiennych.
  5. Krótkie drzemki przynoszą ulgę

Aha, obiecałem, że pokażę gdzie powyższy skrypt może zawierać kod, który robi coś pożytecznego tj. coś innego niż udawanie, że myśli.

Kłamałem.

Pchełki Powershell: proste logowanie z cherbatką

Ja wiem, że jestem z dolnych okolic ujemnego IQ, ale żeby “herbatka” napisać przez ce-ha?

Zaraz się wyjaśni.

Czasem mamy do napisania skrypt w Powershell. I nie jednolinijkowy, który kopiuje plik z folderu A do folderu B, tylko taki bardziejszy. Taki, co to i do bazy danych się podłączy, i do jakiegoś webserwisu na ten przykład, a to w rejestrze grzebnie, a to jakiegoś CSS-a czy HTML-a wygeneruje, czy co tam jeszcze kto potrzebuje. No taki bardziejszy.

A jak powszechnie wiadomo[citation needed] czym bardziej bardziejszy skrypt tym większe szanse na popełnienie błędu. A skoro będą błędy to już na dzień dobry warto pomyśleć o jakimś systemie logowania różnych informacji do pliku. Albo chociaż na ekran. A najlepiej jedno i drugie.

Do tego jeszcze żeby było prosto i szybko.

Powershell ma filtry.

A cebula ma warstwy i co z tego? Jakie, kurna, znów filtry? O co temu gościowi znów qu’est-ce que c’est?

Filtr w Powershellu to nic innego jak funkcja bez argumentów, którą można “karmić” danymi wyłącznie przez rurkę.

Czyli po naszemu pipe.

O, tak wygląda rurka: |

A tak wyglądają dwie rurki obok siebie: ||

Użytkownicy linuksów są z rurką obeznani, bo tam bez rurki się w zasadzie nie da. Windowsiarze przebrzydli może też wiedzą, co to rurka. A może nie…

Rurka działa o, tak:

jakaś-komenda | jakaś-inna-komenda

Działa to w ten sposób, że dane wygenerowane przez pierwszą komendę (tę po lewej stronie rurki) są wysyłane do drugiej komendy (tej po prawej stronie).

Na przykład:

dir | sort

Powyższe wykona najpierw polecenie dir (czyli wylistowanie wszystkich plików i katalogów w bieżącym miejscu) a następnie zamiast wywalić to na ekran, wyśle do polecenia sort, które – niespodzianka – posortuje te nasze pliki i katalogi alfabetycznie i dopiero wtedy wyświetli na ekranie, posortowane. Chyba że za sort umieścimy kolejną rurkę, wtedy ta posortowana list plików i folderów trafi do kolejnego polecenia. I tak dalej. Można w ten sposób budować całkiem interesujące i zaawansowane mechanizmy przetwarzania danych.

Ale do rzeczy, moiściewi, do rzeczy, miało być o logowaniu i o debilach ortograficznych!

No dobra. Logowanie. Filtry. Lecimy:

Filter DoLog {
    $LogName = "$PSScriptRoot\log\$(Get-Date -Format 'yyyyMMdd').log"
    (Get-Date -Format "yyyyMMddhhmmss.fff ") + $_ | Tee-Object $LogName -Append
}

Od tej pory możemy w naszym skrypcie przepuszczać różne rzeczy przez DoLog, za pomocą rurki. Na przykład o tak:

"Bardzo Ważny Komunikat" | DoLog

Powyższa komenda wypisze na ekranie “Bardzo Ważny Komunikat”, poprzedzony znacznikiem czasowym, a także wyśle ten sam komunikat do pliku 20191018.log w podfolderze [log] w lokalizacji naszego skryptu.

Fajne?

Fajne, tylko że nie zadziała, bo nie mamy folderu [log].

Ja z lenistwa zawsze tworzę ten folder ręcznie, ale można to zautomatyzować jak się komuś mocno chce. Mi się nie chce.

A co z tą nieszczęsną cherbatką?

Jeżeli przyjrzymy się treści naszego filtra DoLog zobaczymy tam polecenie Tee-Object, które działa analogicznie do linuksowego tee. Czyli kopiujemy (w locie) dane, jedna kopia ląduje na ekranie a druga – w pliku o podanej nazwie. Opcja -Append zaś zagwarantuje, że kolejne komunikaty będą dopisywane na koniec pliku czyli dokładnie tak, jak powinien działać log. “Tee” wygląda trochę jak “Tea” z błędem, stąd cherbatka.

Aha, $_ reprezentuje w Powershell “to”, czyli w tym przypadku zawartość wylewającą się z rurki. A w innym przypadku – na przykład bieżący element iterowanej właśnie kolekcji, albo jeszcze coś innego. Ale to już może kiedy indziej.