nazwa skryptu, moduły i stos wywołania

Zakres

robiąc poprzedni wpis stwierdziłem, że największym problemem było pobranie nazwy skryptu uruchamiającego i spodziewałem się, że już jest to ogarnięte. chwilę potem znalazłem dodatkowe scenariusze udowadniające, że sprawa jest jeszcze trudniejsza i bardziej pogmatwana.

temat wydaje się prosty: uruchamiamy skrypt ‘skrypt.ps1’ i chcemy założyć plik logu ‘_skrypt.log’. przy normalnym wywołaniu nie ma najmniejszego problemu. do wyboru mamy kilka zmiennych automatycznych. główną taką zmienną jest $PSScript oraz linqjące do poszczególnych gałęzi skróty:

 

  • $PSCmdlet
  • $MyInvocation ($PSCmdlet.MyInvocation)
  • $PSScriptRoot
  • $PSCommandPath ($MyInvocation.MyCommand.Path)

hint: aby $PSCmdlet zainicjowało się prawidłowo, należy włączyć advanced functions np. przy pomocy cmdletbinding().

w czym zatem problem? otóż $PScmdlet jest inicjowany w momencie ładowania skryptu, i jest zmienną lokalną. mamy więc wiele scenariuszy, w których zamienna przyjmie różne wartości:

  • uruchomione bezpośrednio z linii poleceń (nie inicjuje zmiennych automatycznych, ale to co uruchamiamy zwróci wartość)
  • uruchomione z funkcji, zdefiniowanej w ramach tego samego pliq
  • uruchomione z funkcji, która ładuję bibliotekę
  • uruchomione z funkcji która uruchamia funkcję z innej biblioteki

aby to unaocznić przygotowałem scenariusz testowy – dostępny na GH, jeśli ktoś chciałby się pobawić samemu. jest tu dużo ciekawych niuansów i zabawy (np. niedostępność wartości $MyInvocation podczas debugowania).

hint: jeśli odpalisz plik test-context.ps1 z tego samego katalogu gdzie są moduły testowe, nie musisz ich instalować.

czy zatem nie da się wyciągnąć informacji o faktycznym źródle wywołania?

get-PSCallStack

da się – przy pomocy funkcji get-PSCallStack. Funkcja ta zwraca … właśnie ‘call stack’ czyli stos wywołania. przypatrzmy się, co konkretnie zwraca funkcja, zależnie od kontextu uruchomienia

  • bezpośrednio z linii poleceń:
C:\_ScriptZ :))o- Get-PSCallStack

Command Arguments Location
------- --------- --------
<ScriptBlock> {} <No file>
  • z linii poleceń, wywołanie funkcji z modułu
C:\_ScriptZ :))o- level1-callstack

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
<ScriptBlock> {} <No file>
  • ze skryptu:
C:\...PSCmdletTest :))o- .\test-contexts.ps1
WARNING: The names of some imported commands from the module 'level1module' include unapproved verbs that might make them less discoverable. To find the
commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a list of approved verbs, type Get-Verb.
PSCallStack in different contexts

directly from here

Command Arguments Location
------- --------- --------
test-contexts.ps1 {} test-contexts.ps1: line 8
<ScriptBlock> {} <No file>


from the function loaded by module

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
test-contexts.ps1 {} test-contexts.ps1: line 11
<ScriptBlock> {} <No file>


from the function in module L2 calling function in module L1

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
level2-callstack {} level2module.psm1: line 11
test-contexts.ps1 {} test-contexts.ps1: line 13
<ScriptBlock> {} <No file>


MtInvocation test from different contexts

first in-script PScommandPath (from myinvocation)
local myinvocation -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\test-contexts.ps1
now PScommandpath but run from the function, loaded in a module L1 ($myinvocation.command.path is empty for functions)
f1 PScommandPath -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1
similar, but function inside module L1 calls function in the same module
f1 via f2 PScommandPath -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1
more complex - call function in module L2, which is calling function in module L1
f1 via diferent module -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1

to co się jako pierwsze rzuca w oczy, równocześnie spójne z definicją ‘stosu wykonania’, to że wielkość stosu jest równa zagnieżdżeniu wywołania. z linii poleceń – jest tylko jeden element, bezpośrednio ze skryptu: 2, z funkcji w skrypcie: 3, a z funkcji wołającej funkcję: 4.

drugim elementem rzucającym się w oczy jest kolumna ‘location’ która pokazuje ścieżkę do pliq w którym nastąpiło wywołanie, wraz z linią. ponieważ jest to stos, kolejność elementów jest zawsze taka sama:

  • 0 (na dnie): wywołanie bezpośrednie, linia hosta
  • 1 : właściwy skrypt wywołujący
  • 2+: funkcje ładowane podczas przebiegu, wywołania pośrednie

i te dwie informacje dają nam już możliwość weryfikacji jak dana funkcja jest wywoływana (bezpośrednio z linii poleceń czy gdzieś ze skryptu) oraz dokładne sprawdzenie czy był to moduł, który, a może po prostu jakaś funkcja.

podsumowanie

funkcja get-pscallstack nie jest specjalnie popularna więc nie łatwo było na nią trafić. jednak przy pisaniu modułów lub jakiejś logiki wymagającej wiedzy o środowisq uruchomienia – okazuje się być dużo bardziej przydatna, niż niekontrolowalne $myInvocation, które zmieniają kontext i wartości.

eN.

install-module eNLib

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.

bezpieczne hardcodowanie hasła w skrypcie

bezpieczne hardocodowanie hasła?

uruchamiając skrypt, najbezpieczniej jest NIE hardcodować hasła. jeśli coś jest w skrypcie, to znaczy, że może być wykorzystane w nieautoryzowany sposób. jednak przykłady z życia pokazują, że czasem taka jest konieczność .. czy po prostu zlecenie (; przykład – obsługa wsparcia, która ma korzystać ze skryptu, który gdzieś tam sobie sięga. to ‘gdzieś tam’ to środowisko, w którym nawet owa obsługa nie ma konta.

podstawowe zasady bezpieczeństwa to 3xA (AAA) – Authentication, Authorization & Accounting. jeśli utworzymy konto serwisowe, które ma coś wykonywać w imieniu uruchamiającego, to aby nie złamać zasady AAA i być w stanie stwierdzić kto wykonał daną akcję, trzeba to przesunąć do miejsca, gdzie uruchamia się skrypt/aplikację i umieć to powiązać… dalej mogłoby być o SIEM i tak dalej… ale nie o tym tutaj. ogólnie najważniejsze – wykorzystanie konta serwisowego (w środowisq legacy-domain), czy aplikacji proxy (tak jak działa to w Azure) nie musi koniecznie być złem, ale pozostaje pytanie… *jak zamieścić hasło skrypcie tak, aby nie wyciekło wraz ze skryptem?*

najbezpieczniej byłoby wykorzystać certyfikat… tak to można zrobić np. dla aplikacji Azure. niestety w środowisq klasycznym, gdzie mamy np. wykonać akcję w domenie, nie jest to łatwe. najbezpieczniej byłoby umieszczenie serwisu aka proxy, który działa sobie na serwerze w owej domenie i do tegoż serwisu uwierzytelniać się certyfikatem. spoko ale to developerka i do ‘małych projekcików’ czy ‘drobnych narzędzi’ – po prostu jest za duża czasochłonność (czyt. koszt), nie wspominając o tym że napisanie własnego serwisu (np. wystawiającego REST API) to nie jest już skryptowanie. to developerka.

no więc zróbmy to, co da się zrobić tak, aby było szybko i *możliwie* bezpiecznie.

…na ile się da

skoro nie jesteśmy w stanie uwierzytelnić się certyfikatem, to przynajmniej zaszyfrujmy hasło certyfikatem. co nam to da:

  • dany skrypt będzie działał wyłącznie na tej maszynie gdzie jest certyfikat i dla tych osób, którym na to pozwolimy.

żeby była jasność – jeśli ktoś, kto ma złe intencje będzie tego skryptu używał, to sobie hasło odkoduje. ale po pierwsze – ‘pierwszy lepszy’ tego nie zrobi, po drugie – jeśli założylibyśmy takiej osobie konto, to i tak wykorzysta je do niecnych celów. trzeba wiedzieć że to może być rozwiązanie *good enough* i tylko w tym kontekście można nazwać je ‘bezpiecznym’. spory disclaimer ale nie chcę przypadkowych czytelników wprowadzić w błąd.

jak już wyjaśniłem co rozumiem przez ‘bezpieczne’, recepta:

  1. załóż konto w domenie, bez żadnych dodatkowych uprawnień
  2. dobrze byłoby poustawiać ‘deny’ dla takiego konta, przykrajając odpowiednio uprawnienia – wedle potrzeby, uznania i paranoia-level
  3. nadać uprawnienia do tego, do czego będzie wykorzystywane
  4. wygenerować odpowiedni certyfikat, zainstalować go i odpowiednio nadać uprawnienia
  5. zaszyfrować hasło certyfikatem
  6. i voila.. można go wykorzystać w skrypcie

punkty 1-3 pominę, natomiast 4-6 zaraz opiszę.

dzięki takiej konfiguracji – skrypt, nawet skopiowany czy przypadkowo udostępniony, na niewiele się zda. aby wydobyć hasło – trzeba mieć dostęp do klucza prywatnego certyfikatu.

generowanie certyfikatu

super proste, przy czym do tego celu cert musi spełniać odpowiednie funkcje, czyli (nomen-omen) kluczem jest dobry key-usage i document type:

$cert=New-SelfSignedCertificate -DnsName "script automation" -CertStoreLocation "Cert:\CurrentUser\My" -KeyUsage KeyEncipherment,DataEncipherment,KeyAgreement -Type DocumentEncryptionCert

$cert|Export-PfxCertificate -FilePath C:\temp\scriptautomation.pfx -Password (ConvertTo-SecureString -String "secret-string" -Force -AsPlainText)

dodatkowo można certyfikat usunąć z własnego komputera tak, aby jedna kopia była w pliq:

$cert|remove-Item

…albo zachować go jako backup a usunąć potem plik. up2u.

instalacja w środowisq docelowym

najłatwiej oczywiście kliknąć sobie na pliq pfx i wybrać opcję ‘local machine’. jeśli umieścimy w ‘current user’ to będzie dostępny tylko dla nas.

kolejną hiper-istotną kwestią jest *NIE włączanie* możliwości exportu klucza.

jeśli ktoś woli z PowerShell – należy użyć ‘import-pfxCertificate*bez* parametru -exportable.

Import-PfxCertificate -FilePath C:\temp\scriptautomation.pfx -Password (ConvertTo-SecureString -string "secret-string" -force -AsPlainText) -CertStoreLocation Cert:\LocalMachine\My\

ciekawostka: w PS 5.1 ten commandlet ma dodatkowy parametr, nieopisany w doc, protectPrivateKey, który pozwala wykorzystać Virtual Secure Mode.

to połowa sukcesu – bo do tak zaimportowanego certyfikatu, nikt bez uprawnień administratora nie będzie mógł skorzystać. należy nadać uprawnienia do klucza prywatnego:

  1. odpal ‘certlm.msc’ (zarządzanie certyfikatami komputera)
  2. wybierz certyfikat i kliknij PGM
  3. jest tam opcja ‘manage private keys…’

4. nadaj uprawnienie READ (nie FC!) dla wybranych osób

kodowanie i odkodowanie hasła

teraz już z górki. przygotowanie środowiska to bardziej wymagający element, ponieważ hasło można zaszyfrować za pomocą polecenia ‘protect-cmsMessage‘:

C:\_scriptz :))o- Protect-CmsMessage -To 'cn=script automation' -Content 'moje tajne hasło'
-----BEGIN CMS-----
MIIBtAYJKoZIhvcNAQcDoIIBpTCCAaECAQAxggFMMIIBSAIBADAwMBwxGjAYBgNVBAMMEXNjcmlw
dCBhdXRvbWF0aW9uAhB2BdZafxn2nkcWjQNvBi+pMA0GCSqGSIb3DQEBBzAABIIBAFY0TU8LAPl4
4IAWp9tGDOZK/bAEwQpZPix5dz8AoOpaDckpZ8dqatggEKq7vo4VX6wL4sBkImQoZrCZjkT7oCSi
N1YFDOIghjD7e2mIvEl/Gq2bReUbsFQ0iw6Wg1K8zVLBzDQAIUHgJvaV2Ssf4nCJ8WfW5d9UzmBB
JpHAmy7cdofboQt6hRz0wafGNivD47z2xNSSVyhqHVUzBt6NWmAYouFMGYGsBZBuOHXnQJzh+saG
Q5VpWxZGRHzM3igl8IgYDF75R+b/FSfciPu5Se2eeD6xxHrPj9rfLm37KVIab/4+QA/lRFVt4cwA
ztjhh8puyPqYgKCE7t0Tp5VNQv4wTAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQjmYztOm64boC
6XqnH/9kMYAgYTQlOXeGex7qMH5S5gVRKy/JOhfIdqlabP9e/N/6iRk=
-----END CMS-----

ciekawostka: przy tym mechaniźmie szyfrowania (Cryptographic Message Syntax), uruchomienie tego polecenia, da za każdym razem inny wynik

jeśli ktoś nie pamięta nazwy certu, to można je szybko wylistować:

C:\_scriptz :))o- ls Cert:\LocalMachine\My\

PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\My

Thumbprint Subject
---------- -------
B643393E34BDC10BE2266F307F63A712578416F1 CN=script automation

wkleić sobie ten ciąg do skryptu i kiedy potrzebne jest hasło, użyć unprotect-CMSMessage… np. tak:

[string]$encPass="-----BEGIN CMS----- 
MIIBtAYJKoZIhvcNAQcDoIIBpTCCAaECAQAxggFMMIIBSAIBADAwMBwxGjAYBgNVBAMMEXNjcmlw
dCBhdXRvbWF0aW9uAhB2BdZafxn2nkcWjQNvBi+pMA0GCSqGSIb3DQEBBzAABIIBAFY0TU8LAPl4
4IAWp9tGDOZK/bAEwQpZPix5dz8AoOpaDckpZ8dqatggEKq7vo4VX6wL4sBkImQoZrCZjkT7oCSi
N1YFDOIghjD7e2mIvEl/Gq2bReUbsFQ0iw6Wg1K8zVLBzDQAIUHgJvaV2Ssf4nCJ8WfW5d9UzmBB
JpHAmy7cdofboQt6hRz0wafGNivD47z2xNSSVyhqHVUzBt6NWmAYouFMGYGsBZBuOHXnQJzh+saG
Q5VpWxZGRHzM3igl8IgYDF75R+b/FSfciPu5Se2eeD6xxHrPj9rfLm37KVIab/4+QA/lRFVt4cwA
ztjhh8puyPqYgKCE7t0Tp5VNQv4wTAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQjmYztOm64boC
6XqnH/9kMYAgYTQlOXeGex7qMH5S5gVRKy/JOhfIdqlabP9e/N/6iRk=
-----END CMS-----"

try {
  $cert = Get-ChildItem Cert:\LocalMachine\My\B643393E34BDC10BE2266F307F63A712578416F1 -ErrorAction stop
} catch {
  write-log "missing authentication certificate. quitting" -type error
  retrun $null
}
  if(-not $cert.HasPrivateKey) {
  write-log "missing decription factor. quitting." -type error
  return $null
}
  $certCN = $cert.Subject
try {
  [securestring]$secStringPassword = ConvertTo-SecureString (Unprotect-CmsMessage -Content $encPass -To $certCN -ErrorAction stop) -AsPlainText -Force
} catch {
  write-log "error decrypting auth password $($_.exception)" -type error
  return $null
}
[pscredential]$creds = New-Object System.Management.Automation.PSCredential ('CORP\SVC-contactCreator', $secStringPassword)

inne scenariusze i metody

jakiś czas temu linkowałem dostęp do EXO certyfikatem. wygodne.

a podczas uruchamiania własnych skryptów, i tam gdzie wystarczą zwykłe credsy, nie chce mi się wpisywać hasła. zapisuję więc je sobie do pliq:

if(test-path $credsFile) {
    $myCreds = Import-CliXml -Path $credsFile
    write-log "used saved credentials in $credsFile" -type info
} else {
    $myCreds=Get-Credential
    if($NULL -eq $myCreds) {
        write-log 'Cancelled.' -type error
        exit -3
    }
    if($saveCredentials) {
        $myCreds | Export-Clixml -Path $credsFile
        write-log "credentials saved as $credsFile" -type info
    }
}

get-Credential używa DPAPI i w ‘zamkniętym’ środowisq (np. na własnym lapq) w zupełności wystarcza. z tego co kojarzę szyfrowane jest kluczem użytkownika i komputera więc taki plik, aby był odczytany, musi być w tym samym środowisq. mimo wszystko w ‘otwartym’ środowisq raczej nie należy stosować takich metod.

eN.

 

125. WGUiSW – VSC okiełznane

wypluwałem już swoje frustracje dot. Visual Studio Code w poprzednich wpisach… i choć nadal są aktualne, to jak to w większości bywa, uczymy się żyć z niedoskonałościami, jeśli zalety przeważają wady.

na najbliższym WGUiSW, we wtorek 1.XII, będę miał SNACKa (krótka prezentacja, 2o min), na której pokażę czemu, pomimo niestabilności i dość wysokiego progu wejścia w VSC, jednak warto zainteresować się tym edytorem.

oczywiście to tylko przekąska do właściwych prezentacji – Krzyśka i Roberta oraz Hobby Marcina. czyli jak zwykle: warto poświęcić popołudnie na trochę wiedzy (:

eN.

w-files on github

GH

moja Babcia mawiała: “przyszła kryska na matyska“. może nie w takim terminalnym znaczeniu, ale i do mnie przyszło to, co nieuniknione – przesiadka na GitHub.

to oczywiście proces, każdy skrypt muszę odkurzyć a wiadomo… jak się patrzy na swój kod sprzed kilq lat, to czasem kończy się na … napisaniu od nowa. tak było np. ze skryptem, służącym do ściągania darmowych eBooków MSPress.

public vs customer

większość skryptów przygotowuję w ramach różnych projektów. pomimo, że staram się używać prawidłowych zasad pisania, to jednak przerobienie skryptu napisanego dla konkretnego środowiska na uniwersalny, czasem wręcz nie ma sensu. każde środowisko, każdy klient, każdy projekt – to tak specyficzna kombinacja, że nawet sens danej automatyzacji jest tak specyficzny, że nie da się go ‘zuniwersalizować’.

to dość ciekawa lekcja – wziąć coś, co uważa się za dobrze napisane i przygotować to do opublikowania. nagle okazuje się, że komentarze słabe, że jakieś zmienne głupio poopisywane, że braqje obsługi błędów, bo przecież dla siebie nie trzeba…. w sumie te tematy gdzieś przez WF się przewijają, ale z początkowego pomysłu żeby po prostu opublikować bibliotekę, już po kilq minutach wiedziałem, że to byłby duży błąd.

ale powolutq będę tą bibliotekę rozszerzał.

VSC po miesiącu

Visual Studio Code zagościło już na moich wszystkich stacjach. ale cały czas jest powodem wielu frustracji – to przykład szwajcarskiego scyzoryka. ma wszystko, ale jak się chce nim coś konkretnego to okazuje się że problem – za krótki, żeby chleb ukroić, za wąski i za ostry do masła, niby wszystko się da, ale w ręq nie leży dobrze… itd…

najprostsze rzeczy nagle okazują się skomplikowane… ale cóż. jak już się to dostosuje, to sporo ułatwia. jednak tak ogólnie: nie polecam. chyba, że ktoś na prawdę trzepie tych skryptów na potęgę i to nie sam tylko w teamie. z gita można korzystać ze zwykłej linii poleceń więc ISE+git też jest dobrym zestawem. ale liczę na to, że ten zainwestowany czas się kiedyś zwróci… poza tym jest challenge (;

PPB eN.

GUI w skryptach – porównanie

Zadanie

zadanie jest proste – napisać GUI, które pozwoli wyłączyć maszynę w Azure z opcją force. tego normalnie z interfejsu zrobić się nie da, a operacja potrzebna. chodzi o przeniesienie zadania z 2giej czy 3ciej linii wsparcia do 1szej, czyli nie ma opcji na jakąkolwiek linię poleceń. musi być GUI.

 

poniżej dwa skrypty, które realizują to samo zadanie. jeden napisany w tym co PS daje [z małym wyjątkiem], drugi w Formsach. przykład ma posłużyć do porównania tych metod i pozwolić się zastanowić jakie są ‘za’ i ‘przeciw’ dla obu.

(prawie) natywny PowerShell

pełny skrypt można pobrać z GitHuba, poniżej omówienie fragmentów.

założeniem tej wersji jest szybkość działania i uniwersalność. nie korzystam póki co z wersji PS Core ale na pewno korzystanie z biblioteki forms nie będzie możliwe. dla Core można korzystać z XAML UI, którego również jeszcze nie testowałem. jak już pisałem – jedną z bolączek pisania skryptów z GUI jest już sam dobór bibliotek i decyzja ‘gdzie ten skrypt będzie uruchamiany i przez kogo’ /:

dla tego ‘natywne’ skrypty zachowują lepszą kompatybilność i uniwersalność.

niestety natywnych kontrolek jest niezmiernie mało. najważniejszą jest out-GridView, które umożliwia wyświetlenie listy lub obiektów w postaci interaktywnej tabeli, pozwalając na sortowanie, filtrowanie etc. pozwala również na wybranie pojedynczego lub wielu elementów – czyli może być przydatny również wtedy, kiedy chcemy pozwolić użytkownikowi wybrać obiektów do dalszego przetworzenia. i choć daje wiele możliwości, to równocześnie bardzo niewielką kontrolę.

fragment kodu do którego się odwołuję to:

function select-RG {
  param(
    $RGList 
  )
  write-host -ForegroundColor Yellow "Select Resource Group VM resides on"
  $RG=$RGList|out-gridview -title 'Select Resource Group' -OutputMode Single
  if([string]::isNullOrEmpty($RG)) {
    write-host 'cancelled by user. quitting' -foreground Yellow
    exit -1
  }
  write-host "$($RG.ResourceGroupName) chosen. reading VMs..."
  return $RG.ResourceGroupName
}

w szczególności linia pokazująca listę odczytanych Resource Groups w postaci interaktywnej. Out-GridView ‘daje-to-co-daje’ czyli nie da się kontrolować wyglądu okna. to co możemy zrobić to:

  • title: ustawić tytuł okna. ponieważ nie jest to coś, co rzuca się specjalnie w oczy, staram się poprzedzać to dodatkowo odpowiednim komunikatem na ekranie. zwiększy to prawdopodobieństwo, że użytkownik tego nie przegapi i będzie widział po co w ogóle jakieś okno mu wyskaqje na ekran.
  • OutputMode: przyjmuje wartości ‘single’ lub ‘multiple’ dzięki czemu możemy kontrolować czy oczekujemy pojedynczego wyniku czy listy. np. jeśli piszemy skrypt, który usuwa pliki – chcemy mieć możliwość wybrania od razu wielu. tutaj użyte jest ‘single’ co bloqje opcję multi-choice. dodatkowo ten parametr *zastępuje* passthru.
  • PassThru: to starsza wersja, nie jestem pewien w której wersji pojawiło się OutputMode… w każdym razie sam ‘ogv’ [bo taki ma alias] po prostu wyświetla. można się pobawić i zamknąć. tyle. natomiast dodanie opcji PassThru [lub użycie OutputMode] spowoduje dodanie guzika wyboru ‘OK’, po wybraniu którego wybrane elementy zostaną przesłane jako wyniki [pipelined].

siła tkwi w prostocie. chwilę później jednak można zobaczyć:

do {
  #choose RG
  $ResourceGroupName=select-RG -RGList $RGsInThisSub
  $VMlist=get-VMList -RGname $ResourceGroupName
  if([string]::isNullOrEmpty($VMlist)) {
    write-host "This Resource Group do not contain any VMs. Do you want to select another RG?"
    switch (
      [System.Windows.Forms.MessageBox]::show($this,"Choose another Resource Group?",'CONFIRM','OKCancel') 
    ) {
      'OK' {
        #do nothing - VMList is null which will trigger loop
      }
      'Cancel' {
        Write-Host -ForegroundColor Yellow "operation cancelled by the user. quitting."
        exit -1
        } 
    }
  }
} while( [string]::isNullOrEmpty($VMlist) )

czemu nagle pojawia się Forms? to niestety sqcha twórców PowerShell – o ile WScript dysponował prostym okienkiem OK/Cancel, o ile jest OGV w PS… o tyle nie ma takiego prostego okienka OK/Cancel w PS.

co ciekawe, można takie okienko wyświetlić bez Forms! w taki sposób:

Add-Type -AssemblyName PresentationFramework
[System.Windows.MessageBox]::show("Choose another Resource Group?",'CONFIRM','OKCancel')

pomimo, że wyglądają bardzo podobnie, i dają niemal ten sam efekt, pod spodem wydarzają się trochę inne rzeczy. jeden jest częścią PresentationFramework drugi Forms, mają inne konstruktory. lepiej używać Formsowego z dwóch powodów:

  • zazwyczaj to nie będzie jedyny element GUI, więc jeśli importujesz biblioteki .NET, lepiej już wybrać tą bardziej rozbudowaną w tym kierunq
  • wersja z PF nie pozwala w konstruktorze na zdefiniowanie okna nadrzędnego – magiczne ‘$this’ na pierwszej pozycji show($this,”Choose another Resource Group?”,’CONFIRM’,’OKCancel’)  – a w efekcie takie okienko często lądowało pod spodem czegoś aktywnego.

a co z readKey?!

jak już coś pisaliście co wymaga interakcji, powinniście zadać pytanie – czemu nie użyć readKey? natywne, nie wymaga importu bibliotek, szybsze … i tak dalej.

zgoda. ale celem nadrzędnym jest wygoda endusera i założenie, że nie jest bystrym inżynierem (; kiedy zrobiłem readKey, a skrypt miał mieć GUI, sam łapałem się na tym, że czekałem i czekałem… aż zorientowałem się, że to skrypt czeka na mnie, a nie odwrotnie. załamanie logiki – tutaj interfejs a tu linia poleceń – jest bardzo złym podejściem. albo decydujemy się robić skrypt tak, albo inaczej – ale musi być spójnie. inaczej ani nie jest łatwy w użyciu ani prosty w strukturze.

readKey to nie GUI tylko metoda na interakcję bez GUI.

wersja Forms

całość do pobrania z GitHub . kilka uwag n00ba dotyczących pisania PS+Forms.

pierwszą rzeczą jaka rzuca się oczy to narzut ilości kodu. do wykonania mamy jedno polecenie (jedna linijka). w wersji opisywanej wcześniej wyszło 126 linii kodu, tutaj mamy już prawie 19o. jasne się staje że podstawowym problemem będzie możliwy bałagan wynikający z samej ilości. dla tego części dotyczące rysowania samego interfejsu warto oddzielić od reszty. niektórzy decydują się na utworzenie projektu, w ramach którego jest kilka oddzielnych pliqw, i definicje interfejsu wyrzuca się do takiego oddzielnego pliq. spoko, ale ze względu na spójność, przy takich małych toolach wolę trzymać wszystko w pojedynczym pliq. łatwiej go przesłać do niedoświadczonych użytkowników, wytłumaczyć że to taki plik i już, niż kazać tworzyć jakąś strukturę, czy martwić się, że ktoś kawałek usunął czy nie skopiował. ale to oczywiście kwestia wyboru – ważne, żeby sobie oczyścić kod i możliwie mocno oddzielić logikę od definicji.

nie jest to oczywiście do końca możliwe, bo tak, jak opisywałem poprzednio, logika jest de facto związana z interfejsem – trigger-action. klikniemy guzik to uruchomi się jakiś kawałek. całą sekwencyjność szlag trafia.

ja póki co przyjąłem metodę tworzenia regionów – widać znaczniki #region i #endregion które dzielą skrypt na bloki – tutaj 3 bloki – interfejs, akcje interfejsu i cała reszta. zaleta jest taka, że większość edytorów rozpoznaje ten znacznik i pozwala zwinąć tą część kodu żeby nie pasqdziła widoq.

na koniec

niewątpliwie pisanie skryptów z GUI jest dużo trudniejsze i dużo bardziej czasochłonne. tak, jak przy zwykłym skrypcie 6o-8o% czasu to praca nad logiką działania, tak tutaj, ok. 9o% czasu zjadło mi badanie reguł interakcji między poszczególnymi elementami. ok, uczę się więc potem będzie łatwiej, ale nawet przy tak banalnym skrypciq sprawdzenie czy odpowiednio wyczyszczą się wartości konkretnych kontrolek, czy finalnie ustawiona/przeczytana zmienna jest prawidłowa, czy może pozostało coś z klikania usera, czy guziki są aktywne kiedy trzeba, do tego jak to zoptymalizować, że nie ładowało w kółko po pół minuty…

do tego commandlety Az są… hmm.. nie działają zbyt dobrze q: connect-AzAccount się regularnie [niedeterministycznie] zawiesza po zabiciu okna logowania [cancel], zawiesza się dużo bardziej w Vistual Studio Code, a get-AzContex nie implementuje prawidłowo ErrorAction SilentlyContinue…. żeby tak podać najważniejsze. ogólnie to dość trudna biblioteka do współpracy, bez względu na GUI.

eN.

GUI w skryptach – wstęp

GUI w skryptach?

zasadniczo jestem przeciwny takiemu pomysłowi. celem skryptów jest automatyzacja, a ich największą zaletą jest przetwarzanie potokowe – wyjście jednego skryptu staje się wejściem dla innego. dodanie GUI łamie te zasady – przede wszystkim zakłada interakcję, podczas gdy punktem wyjścia dla skryptu jest podanie parametrów ‘up front’ dające automatyzację danego zadania. 

GUI jest oczywiście niezbędny dla 1szej/2giej linii, ale również pojedyncze zadania łatwiej przeważnie wyklikać. można też wyobrazić sobie rozwiązanie ‘hybrydowe’ [takie modne słowo w dzisiejszych czasach… od paznokci poprzez samochody aż po architekturę IT] – skrypt przyjmuje parametr [np. nazwę pliq wejściowego], a kiedy go nie ma, pokazuje jakiś file-picker czy coś… nie jest to koszerne, ale koniec-końców decyduje zamawiający.

w ostatnim czasie musiałem wrócić do poważnego skryptowania [yyyeeeeey! (= ] właśnie z myślą o narzędziach dla 1szej linii. w zwizq z tym musiałem zmierzyć się z pisaniem pełnego GUI w PowerShell. w związq z tym kilka wniosków i spostrzeżeń.

GUI w PS – możliwości

w oprogramowaniu GUI jestem n00bem. ostatnie aplikacje okienkowe pisałem w Javie na studiach. do tego jeszcze moja ogólna niechęć do takiego pisania… a więc wpis cenny dla tych, którzy są na tej samej drodze. 

są trzy podstawowe metody uzyskania interfejsu dla skryptów:

  • natywne metody PS – jest ich oczywiście niezmiernie mało i są bardzo ograniczone. tak na prawdę ogranicza się do albo sczytywania z linii poleceń [nie bardzo GUI, ale jest interakcja) albo out-GridView – który jest genialną metodą do zrobienia szybkiego ‘pickera’ wartości. dodałbym też tutaj funkcję
    [System.Windows.MessageBox]::Show(MessageBody,Title,ButtonType,Image)
    która natywna dla PS nie jest, ale dobrze uzupełnia możliwości.
  • wykorzystanie obiektu DCOM wscript.shell  który daje możliwość wyświetlenia prostego okienka podobnie do .NET. osobiście mierzi mnie łączenie tak odległych technik jak PS i WScript – IMHO to błąd i zamieszczam wyłącznie żeby napisać: “nie korzystać! to jest legacy crap
  • no i oczywiście pełnoprawne GUI – albo poprzez biblioteki Forms lub WPF. 

dokładniejszy opis w przyszłości, co kiedy jak i z jakim efektem, a póki co

narzędzie dla skryptów na poważnie

zwykłe ISE przestaje wystarczać – nie tyle z powodu samego GUI, co ogólnie tego, że ostatnio piszę więcej. zacisnąłem zęby i po raz kolejny przesiedziałem nad odpaleniem VSCode. VSC niestety jest makabrycznym narzędziem, jednak jego potencjał i choćby możliwości integracyjne są na tyle ważne, że cóż.. płaczę, ale walczę. kiedy to było tylko hobby, odpuszczałem zniechęcony, ale kiedy trzeba bardziej na poważnie, musiałem się złamać

  • VSC jest uniwersalnym narzędziem developerskim. dwa kluczowe słowa: uniwersalne i developerskie – w zasadzie wskazują jego wady. uniwersalne – wszystkiego jest za dużo, 8o% opcji jest zbędne przy pisaniu w PS. po instalacji dostajemy coś, czego do pisania w PS nie da się używać – a więc czeka nas czytanie serii artykułów i wpisów na forum, które pluginy zainstalować, jak skonfigurować, które z tysięcy opcji jak ustawić żeby uzyskać jakiś sensowny efekt.
    to co kiedyś było potęgą produktów MS – integralność i trzymanie się pojedynczych standardów – załamało się wraz z “powszechną linuxyzacją”. dziś wszystko trzeba wybrać z pośród dziesiątek lub setek różnych dodatków i zaczynają się problemy że ‘ten dodatek jest szybszy a tamten ma taką opcję, a inny gryzie się z tamtym ale ma tą funkcję..’ .. i tak zaczyna się żmudna żonglerka dobrania sobie zestawu pluginów tak, aby mieć maksymalnie dużo wymaganych funkcji, równocześnie nie przeciążyć środowiska zbytecznymi śmieciami …
  • …które doprowadzą do niestabilności. podstawowe zastrzeżenie jakie mam do VSC to niestabilność, a zwłaszcza inteliSens, który regularnie przestaje działać w trakcie pisania. skrót “alt-ctrl-p, reload” już mi wszedł w krew /: [muszę poszukać pluginu, który mi doda tą operację pod jednym guzikiem q:]. o ile ISE zawieszało się w bardzo szczególnych przypadkach [np. korzystając z commadletów Exchange] o tyle VSC wywala się jak archetypiczny open source. poprawki wydawane typowo agile – czyli update za updatem i trzymanie kciuków, czy dziś będzie się wywalać się bardziej….  welcome to open source .. and agile.    

czemu warto pocierpieć? przy pewnej wielkości czy skomplikowaniu skryptu, nie mówiąc o projekcie w którym tych skryptów jest wiele, ale nawet kiedy po porostu trzeba przy nich popracować więcej – ISE to za mało. lepsze wyświetlanie składni, czy masy skrótów klawiszowych, znacznie przyspieszają pracę. integracja z repo, projekty… jest wiele dodatków, bez których byłoby więcej ręcznej pracy – czyli to co jest wadą, przy zainwestowaniu czasu, staje się podstawową przewagą.

jeśli więc czeka cię przesiadka – zarezerwuj sobie przynajmniej tydzień nieskończonej cierpliwości. będzie za to nagroda w postaci dużych możliwości i jeszcze większego potencjału. jeśli jednak nie korzystasz z repo, piszesz hobbystycznie lub okazjonalnie – ISE to wszystko co potrzebne i szkoda czasu i stresu. 

pisanie GUI

niestety VSC nie pomaga zbytnio w pisaniu GUI. może są dodatki, których jeszcze nie znalazłem, ale ogólnie – oprogramowanie GUI w edytorze nie-wizualnym to porażka. kilka elementów i robią się setki linii kodu ciężkie do ogarnięcia. ustawianie guzików zmieniając ich położenie wartościami X/Y na czuja – makabra. interfejs najlepiej się rysuje w interfejsie, dla tego najsensowniej jest mieć coś wizualnego. chciałoby się powiedzieć Visual Studio… huh, ale VS do pisania skryptów PS, to jak instalacja Exchange on prem w pełnej redundancji do wysyłania kilq emali. 

w skrócie – cały czas jestem w trakcie poszukiwania jakiegoś lekkiego designera interfejsu. te, które testowałem online były … jak to aplikacje online – siermiężne i ograniczone. 

forms vs UWP vs WPF

już sam wybór tego w czym ten interfejs ma być napisany nie jest łatwy. formsy wygrywają ilością dostępnych przykładów, UWP [chyba] umiera, WPF niby jest trendem, nowsze, nowocześniejsze i bardziej uniwersalne… ale czy przyszłościowe? no właśnie – nawet po przeczytaniu kilq artów, jeśli nie siedzi się w temacie, sam dobór biblioteki nie jest trywialny. jeśli się uczyć, chciałoby się nauczyć czegoś, co choć przez kilka lat się przyda. a to w obecnym tempie zmian niełatwe zadanie. 

osobiście póki co zdecydowałem się na formsy – stare, dobre, oklepane, dużo przykładów, a póki Microsoft nie zrezygnuje z Windows na rzecz jakiejś hybrydowej dystrybucji [chciałoby się powiedzieć Lindows, ale to już było] , zgromadzona wiedza powinna żyć przez jakiś czas.

zmiana logiki

ciekawym aspektem jest zmiana paradygmatu całej logiki działania. skrypty, poniekąd z samej definicji skryptu – przetwarzane są sekwencyjnie linia po linii. tak też logika pisania jest ‘liniowa’. GUI to z kolei ‘rozproszona logika’ pozaczepiania pod elementy interfejsu. całość nie jest już jak opowiadanie:

begin { początkiem }
process { rozwinięciem }
end { i końcem }

w zamian staje się fragmentarycznym kodem, sterowanym akcjami z kliknięć czy innych list. dla skrypciarza z krwi i kości to jest inny świat. dziwny i nie do końca logiczny. 

GUI + skrypt

na tym etapie nie mam jeszcze wyrobionych ‘best practice’ więc poszuqję. jednym z pomysłów było zadziałanie tak, jak to przez pewien czas projektował Microsoft [i co mi się bardzo podobało] – interfejs służył do zebrania informacji, wizualizacji elementów, ale ostateczna akcja to był po prostu skrypt uruchomiony z parametrami, których wartości były zebrane z interfejsu.

takie podejście jest świetne jednak w momencie kiedy pisze się duże, rozbudowane narzędzie, aplikację pozwalającą na wykonanie wielu zadań. jednak drobny tool, którego celem jest właśnie ograniczenie wyborów i dostosowanie do konkretnego zadania w konkretnym środowisq powoduje, że rozdzielenie na dwa niezależne skrypty [jeden to GUI do zebrania parametrów, drugi do wykonania akcji] tworzy dużą nadmiarowość. ponadto takie podejście choć ‘ładne’ i logiczne, utrudnia uruchomienie osobom, które tych skryptów się już i tak boją. to te niuanse – łatwiej komuś nieqmatemu przesłać jeden plik do uruchomienia, niż kilka, z opisem jak zrobić bibliotekę, czy jak przygotować środowisko. w regularnej aplikacji załatwia to instalator, przy skrypcie to już nie takie proste. 

z kolei zaletami oddzielenia GUI od logiki jest łatwiejsze panowanie nad samym wykonaniem, no i dla osób, które GUI nie potrzebują – od razu jest narzędzie. większa uniwersalność. mam nadzieję pokazać przykłady za jakiś czas.

praktyka pokaże który sposób wygra, choć widzę coraz więcej zalet rozbicia tego monolitycznego podejścia na rzecz bardziej uniwersalnej metody…

kompromis

skoro skrypty nie są do pisania GUI, przesiadka jest trudna, to czemu po prostu nie zacząć pisać np. w C#? 

proste: mimo-wszystko łatwiej nauczyć się oprogramowania samych Formsów, niż całego języka. na drugi dzień byłem w stanie narysować prosty interfejs, podpiąć logikę i zbudować jakiś działający skrypt. a to dla tego, że cała faktyczna logika, aktywna część działania była tym, co znam, w postaci commandletów. nauczenie się napisania tego w C# i poznanie całej specyfiki samego języka, zajęła by znacznie więcej czasu. choć podobny do PS – jest czyś zupełnie innym. no i podstawowa rzecz – nie zamierzam być developerem [może kiedyś, hobbystycznie] a tego rodzaju zadania są ‘szybkim wsparciem’, a nie moim codziennym zajęciem. póki co nauka C# byłaby stratą czasu. 

jak mawiają: ‘życie składa się z kompromisów’. kompromisem jest tutaj wyważenie efektu i czasu. pozostają zalety skryptu: kod nie jest kompilowany, łatwo go przeczytać a w razie potrzeby zmodyfikować ‘gdziekolwiek’, bo bez konieczności rekompilacji, commandlety dają obszerne biblioteki o olbrzymich możliwościach, a równocześnie można dopiąć kawałek GUI – i to wszystko w bardzo krótkim czasie.

może Frankenstein, ale żywy i myślący. 

eN. 

 

nazwa plików wyjściowych – zabawa regex

nazwy pliqw

nazewnictwo pliqw wyjściowych wydaje się sprawą trywialną. w najprostszej wersji, wygodnej zwłaszcza dla pliqw tymczasowych można to zrobić tak:

[System.IO.Path]::GetRandomFileName()

generuje to unikalne nazwy pliqw z uniknięciem konfliktów, tego pokroju: “4toqskhi.qbk”.

ale w przypadq logów lub wyjścia, które będzie dalej wykorzystywane, pliki muszą mieć bardziej konkretne nazwy, i pomagać nie pogubić się w przypadq wielu uruchomień i konieczności kontroli co-do-czego. większość skryptów, odruchowo, zaopatruję w funkcję logowania, a sam plik logu, aby wiadomo było czego dotyczy, ma nazwę skryptu:

$logFile="_$( ([io.fileinfo]$MyInvocation.MyCommand.Definition).BaseName )-$(Get-Date -Format yyMMddHHmm).log"

dzięki temu logi zaczynają się podkreślnikiem (wygodne sortowanie i wyróżnik), zawierają nazwę skryptu (wiadomo czego log dotyczy) oraz daty (dzięki czemu łatwiej zorientować się, o który chodzi). np tak:

skrypt: new-CloudUser.ps1 

log: _new-CloudUser-202008051001.log

ale log to jedno, a wyjście do dalszego przetwarzania to inna para kaloszy. podstawowym typem pliq z jakim pracuję to CSV – głównie ze względu na łatwość w dostarczaniu wyników dla biznesu (w postaci xlsx) oraz oczywiście – niezmierną łatwość w pracy z tym formatem. okazuje się, że w wielu scenariuszach, przy których plik jest przetwarzany przez biznes i ma do niego z powrotem trafić po obróbce, najlepiej, żeby nazwa odzwierciedlała nazwę pliq wejściowego. a przy kilqkrotnym przetwarzaniu powinna być jakaś inkrementalna wartość liczbowa. można to oczywiście osiągnąć na wiele sposobów, ale lubię eleganckie sztuczki, wykorzystujące fajne mechanizmy – np. takie jak regex.

lekcja poprawnego wyrażania się

choć z wyrażeń regularnych korzystam od bardzo dawna i wkładam wiele wysiłq, żeby je okiełznać, to nadal pozostają dużym wyzwaniem. są IMHO jedną z najtrudniejszych do opanowania technik i nadal czuję się n00bem, nie potrafiąc ogarnąć backreferences czy wykorzystania pewnych elementów w ramach przetwarzania regex. to jest świetna gimnastyka dla umysłu, więc często tworzę regexy tak, jak niektórzy grają w sudoq. oczywiście przy wielu zastosowaniach lepszej metody ani bardziej wydajnej nie ma. inaczej trzeba by pisać setki pozagnieżdżanych IFów czy switchy, a w niektórych przypadkach nawet to nie da rezultatu.

Ale często chodzi o zwykły “challange” – bo jak się czegoś nauczyć inaczej, niż korzystając z tego?

a więc w celach edukacyjnych, przeanalizuję taką funkcję:

$fileInput=get-Item $inputCSV
if($fileInput.BaseName -match '-prepped(?<inc>\d{0,2})') {
    $outputCSV=$fileInput.BaseName -replace '-prepped\d{0,2}-[\d]+',"-prepped$( if($Matches.inc) { (([decimal]$Matches.inc)+1).toString("00") } else { "02" } )-$(Get-Date -Format yyMMddHHmm).csv"
} else {
    $outputCSV="{0}-prepped01-{1}.csv" -f $fileInput.BaseName,(Get-Date -Format yyMMddHHmm)
}

założenie: chcę aby przetworzony plik miał suffix ‘prepped‘ z numerkiem dopełnionym zerami, wskazujący na liczbę przetworzeń.

algorytm jest taki:

  • biorę nazwę pliq wejściowego – np. ‘myData’
  • nie wiem czy to pierwszy przebieg, więc najpierw sprawdzam czy nazwa pliq zawiera już suffix – “IF()”.
  • trochę nie po kolei – ale zacznę od ‘nie, nie zawiera’, ponieważ tu sprawa jest prosta – po prostu tworzę nazwę “<nazwaPliqWejsciowego>-prepped01-<dataPrzetworzenia>.csv” – co widać po ELSE
  • ciekawszy jest przypadek ‘tak, zawiera’ – jak wyciągnąć numer i go zwiększyć?

tutaj w sukurs przychodzą wyrażenia regularne. w PowerShell można stosować je niemal wszędzie, a każdym razie wszędzie tam, gdzie używamy funkcji ‘match’ lub ‘replace’ (ale pozwala na to również wiele poleceń). klauzula ‘if’ zawiera:

$fileInput.BaseName -match '-prepped(?<inc>\d{0,2})'

a ciekawym fragmentem jest oczywiście “(?<inc>\d{0,2})“, który oznacza:

  • nawiasy () to tzw. capture group. to, co zostanie dopasowane do szablonu opisanego wewnątrz nawiasów, zostanie zapisane jako zmienna w tablicy wyniqw. w PowerShell wyniki dopasowań przetrzymywane są w automatycznej zmiennej $Matches, będącą tablicą dopasowań.
  • konstrukcja “?<inc>” nadaje temu dopasowaniu nazwę (‘named capture group’) . w ramach zmiennej $Matches wyniki przechowywane są jako tablica, więc aby wybrać konkretny element, nie mogąc przewidzieć ilości wyników czy ich kolejności, najlepiej nadać wynikowi nazwę.
  • “\d” oznacza dowolną cyfrę. można to również zapisać jako “[0-9]”
  • {0,2} oznacza liczbę wystąpień poprzedzającej definicji – tu: cyfry.

czyli ciągi spełniające definicję szablonu i zwracające ‘True’ to np:

  • “mojanazwa-prepped-cośtu.csv”
  • “mojanazwa-prepped1-cośtu.csv”
  • “mojanazwa-prepped01-cośtu.csv”
  • “mojanazwa-prepped23-cośtu.csv”

w zmiennej $Matches, pod nazwą ‘inc’ będzie przechowywana liczba… a w zasadzie ‘ciąg znaków składający się z cyfr’ bo będzie to [string]. chyba, żeby nie. bo jeśli mamy tylko “-prepped-” to element ‘inc’ się nie pojawi (będzie $null) – czyli w pierwszym prezentowanym przykładzie, wartość “$Matches.inc” będzie $null.

po przejściu ‘IF’ wykonywany jest ‘replace’, który również korzysta z regex. tutaj pojawia się dodatkowo “[\d]+” i nie ma nawiasów okrągłych:

$outputCSV=$fileInput.BaseName -replace ‘-prepped\d{0,2}-[\d]+‘,”-prepped$( if($Matches.inc) { (([decimal]$Matches.inc)+1).toString(“00”) } else { “02” } )-$(Get-Date -Format yyMMddHHmm).csv

ciąg spełniający szablon opisany pogrubioną czcionką, zostanie zastąpiony wyliczeniem, opisanym na pomarańczowo. a co się tam dzieje?

  • nawiasy kwadratowe ‘[]’ mają zgoła odmienną funkcję niż okrągłe. ich celem jest określenie dopuszczalnego zbioru kryteriów. np. [abc] oznacza, iż dowolna POJEDYNCZA litera spełni warunek, jeśli będzie literą a, b lub c. czyli dla wyrażenia regex ‘[abc]’ dla słowa ‘żaba’, będą trzy pozytywne wyniki – ‘a’, ‘b’ oraz ‘a’.
C:\_scriptz> [regex]$rxz='[abc]'
C:\_scriptz> $rxz.Matches('zaba')|select index,value

Index Value
----- -----
1 a
2 b
3 a
  • plus ‘+’ oznacza, że liter musi być jedna lub więcej. standardowe wyszukiwanie jest ‘zachłanne’ [greedy], czyli zwróci maksymalny ciąg spełniający warunek. czyli ‘[abc]+’ zwróci maksymalnie długie ciągi znaków, składający się z liter a, b oraz c
PS C:\_scriptz> [regex]$rxz='[abc]+'
PS C:\_scriptz> $rxz.Matches('zaba')|select index,value

Index Value
----- -----
1 aba
  • w tym szablonie nie ma zwracanej grupy (nawiasów okrągłych) … chciałbym ‘przechwycić’ wartość liczbową po ‘prepped’ aby ją zwiększyć, więc powinna być… ale to za chwilę.

co się dzieje w funkcji generującej wynik?

"-prepped$(
    if($Matches.inc) {
        ( ([decimal]$Matches.inc)+1 ).toString("00")
    } else {
        "02"
    }
)-$(Get-Date -Format yyMMddHHmm).csv"

ciąg spełniający opisane wcześniej warunki, zostanie zastąpiony:

  • ciągiem ‘-prepped’
  • następnie jest typowe dla PowerShell wyliczenie zmiennej – “$( <kod> )”
  • (if) wyliczenie zależy czy znalezione zostały liczby, czyli czy $Matches.inc istnieje czy jest $null. jest to nieco dziwna konstrukcja tutaj, ponieważ zmienna $Matches przechowuje wyniki… z poprzedniego wyszukiwania – tego, który był w IFie, tego z nawiasami ‘()’ – to tam tworzony jest element ‘inc’ i dla tego nie ma nawiasów w samym ‘replace’. co prawda istnieje możliwość wykorzystania wyników dopasować z obecnie przetwarzanego  ‘replace’, ale dopiero w wersji 6 PowerShell. dla wersji 5 i niższych, rozwiązanie jest mniej eleganckie, a takie wersje są na większości serwerów…
  • jeśli jakiś ciąg liczbowy jest, to trzeba go zmienić w liczbę [wymusić interpretację jako liczba] i zwiększyć o jeden: [decimal]$Matches.inc+1 . następnie chcę mieć pewność, że zostanie dopełniony zerami – czyli np. ‘3’ stanie się ’03’. to realizuje funkcja toString(“00”)
  • (else) jeśli liczby nie ma, czyli jest samo ‘prepped’, to dodaję “02” jako drugi przebieg.
  • no i oczywiście wstawiam aktualną datę przebiegu

już sam fakt tego, ile trzeba zrobić opisu, żeby wyjaśnić malutkiego regexa pokazuje jaką kryje w sobie moc… a więc zamiast sudoq – proponuję przerobić sobie jakieś wyszukiwanie na wyrażenie regularne (:

eN.