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.

filtrowanie po wartości

potęgą PowerShell jest jego obiektowość, dzięki czemu cały język jest bardzo elastyczny. np filtrowanie wyjścia staje się bardzo proste:

ps|? name -match 'svc'

proste, jasne i przyjemne… i łatwo znaleźć na setkach tysiącach stron…

dla tego inne pytanie – a jak przefiltrować po wartościach wszystkich parametrów? np. “pokaż wszystkie parametry obiektu, które mają ‘name’ w nazwie”, albo “pokaż wszystkie wartości parametrów, które zawierają ‘abc'”. tego już tak łatwo zrobić się nie da. to będzie nieco nieprzyjemna przejażdżka, bo wymaga rozebrania obiektu na kawałki – czyli tracimy automatykę PS, która tak ślicznie pomaga w ‘normalnych’ zapytaniach, i grzebiemy we flakach.

aby osiągnąć cel trzeba skorzystać z ukrytego parametru ‘psobject’, który pokazuje wnętrzności obiektu. dodatkowo sprawę kompliqje fakt, że wyjście jest często tablicą obiektów a nie pojedynczym obiektem. pobawmy się ‘get-service’ i zrealizujmy takie zadania:

  • wyświetlmy wszystkie nazwy i wartości parametrów, które mają w nazwie ‘name’
  • wyświetlmy wszystkie nazwy i wartości parametrów, dla których wartość jest równa ‘true’

w pierwszej kolejności zobaczmy jak taki obiekt wygląda wewnątrz. najpierw klasycznie…:

PS C:\_scriptZ> Get-Service|gm


   TypeName: System.ServiceProcess.ServiceController

Name                      MemberType    Definition
----                      ----------    ----------
Name                      AliasProperty Name = ServiceName
RequiredServices          AliasProperty RequiredServices = ServicesDependedOn
Disposed                  Event         System.EventHandler Disposed(System.Object, System.EventArgs)
Close                     Method        void Close()
Continue                  Method        void Continue()
CreateObjRef              Method        System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
Dispose                   Method        void Dispose(), void IDisposable.Dispose()
Equals                    Method        bool Equals(System.Object obj)
ExecuteCommand            Method        void ExecuteCommand(int command)
GetHashCode               Method        int GetHashCode()
GetLifetimeService        Method        System.Object GetLifetimeService()
GetType                   Method        type GetType()
InitializeLifetimeService Method        System.Object InitializeLifetimeService()
Pause                     Method        void Pause()
Refresh                   Method        void Refresh()
Start                     Method        void Start(), void Start(string[] args)
Stop                      Method        void Stop()
WaitForStatus             Method        void WaitForStatus(System.ServiceProcess.ServiceControllerStatus desiredStat...
CanPauseAndContinue       Property      bool CanPauseAndContinue {get;}
CanShutdown               Property      bool CanShutdown {get;}
CanStop                   Property      bool CanStop {get;}
Container                 Property      System.ComponentModel.IContainer Container {get;}
DependentServices         Property      System.ServiceProcess.ServiceController[] DependentServices {get;}
DisplayName               Property      string DisplayName {get;set;}
MachineName               Property      string MachineName {get;set;}
ServiceHandle             Property      System.Runtime.InteropServices.SafeHandle ServiceHandle {get;}
ServiceName               Property      string ServiceName {get;set;}
ServicesDependedOn        Property      System.ServiceProcess.ServiceController[] ServicesDependedOn {get;}
ServiceType               Property      System.ServiceProcess.ServiceType ServiceType {get;}
Site                      Property      System.ComponentModel.ISite Site {get;set;}
StartType                 Property      System.ServiceProcess.ServiceStartMode StartType {get;}
Status                    Property      System.ServiceProcess.ServiceControllerStatus Status {get;}
ToString                  ScriptMethod  System.Object ToString();

a potem… od środka:

PS C:\_scriptZ> (get-service).psobject

Members : {Count = Length, int Length {get;}, long LongLength {get;}, int Rank {get;}...}
Properties : {Count = Length, int Length {get;}, long LongLength {get;}, int Rank {get;}...}
Methods : {void Set(int , System.Object ), System.Object&, mscorlib, Version=4.0.0.0, Culture=neutral, Publ
          icKeyToken=b77a5c561934e089 Address(int ), System.Object Get(int ), System.Object GetValue(Params
          int[] indices), System.Object GetValue(int index), System.Object GetValue(int index1, int index2
          ), System.Object GetValue(int index1, int index2, int index3), System.Object GetValue(long index)
          , System.Object GetValue(long index1, long index2), System.Object GetValue(long index1, long inde
          x2, long index3), System.Object GetValue(Params long[] indices)...}
ImmediateBaseObject : {aciseagent, AdobeARMservice, AdobeFlashPlayerUpdateSvc, agiptd...}
BaseObject : {aciseagent, AdobeARMservice, AdobeFlashPlayerUpdateSvc, agiptd...}
TypeNames : {System.Object[], System.Array, System.Object}

nas będą interesować ‘properties’, których nazwy zawierają ‘name’. nie będę się bawił w step-by-step, myślę, że taki początek wystarczy, a całą resztę trzeba po prostu usiąść i się pobawić. mamy podstawy więc teraz trzeba po PowerShellowemu zapisać zadanie “wyświetl wszystkie usługi (obiekty), następnie dla każdej usługi przefiltruj te parametry, który mają w nazwie ‘name’ a następnie wyświetl tylko nazwę i wartość”. po takim zapisie to już bułka z masłem:

Get-Service|%{$_.psobject.properties|? name -match 'name'}|ft name,value

a teraz drugie zadanie – i tu robi się na prawdę pasqdnie, bo wartości mogą być różnych typów i też są obiektami. ponadto samo ‘property, value’ będzie bezużyteczne bez dodatkowej informacji – nazwy usługi dla której je znaleźliśmy. poniżej rozwiązanie, zachęcam do zrozumienia – czyli samodzielnych testów:

get-service|%{$sn=$_.name;$_.psobject.members|?{$_.membertype -eq 'property' -and $_.value -eq $true}}|select @{N='ServiceName';E={$sn}},name,value

żeby nie było za łatwo… i tego nie potrafię wyjaśnić – z jakiegoś powodu ‘Stopped’ jest traktowany jako $true. fragment wyjścia:

ServiceName Name Value
----------- ---- -----
aciseagent CanStop True
AdobeARMservice CanStop True
AdobeFlashPlayerUpdateSvc Status Stopped
agiptd CanStop True
AJRouter Status Stopped
ALG Status Stopped
ApHidMonitorService CanS... True
ApHidMonitorService CanStop True
AppHostSvc CanP... True
AppHostSvc CanS... True
AppHostSvc CanStop True
AppIDSvc Status Stopped
Appinfo CanStop True
AppMgmt Status Stopped
AppReadiness Status Stopped

… ale już nie chce mi się zagłębiać dalej.

sztuka dla sztuki? niekoniecznie. często trafiam na scenariusz, prostszy niż zaprezentowane, gdzie dostaję obiekt o dużej liczbie parametrów – np. obiekty EXO – i chcę sprawdzić czy gdzieś nie siedzi jakaś wartość, przy czym nie mam pojęcia jak może nazywać się parametr. o ile w ogóle istnienie. np. wyświetlmy wszystkie URI jakie są skonfigurowane dla grupy o365:

PS C:\_scriptZ> (Get-UnifiedGroup -Identity GuestGroupTest).psobject.Members.value|Select-String 'http'

https://outlook.office365.com/owa/?path=/group/GuestGroupTest@w-files.pl/calendar
https://outlook.office365.com/owa/?path=/group/GuestGroupTest@w-files.pl/mail
https://outlook.office365.com/owa/?path=/group/GuestGroupTest@w-files.pl/people
https://outlook.office365.com/EWS/Exchange.asmx/s/GetUserPhoto?email=GuestGroupTest@w-files.pl

to tak w ramach przygotowań do ShareCon365 (; zapraszam na moją sesję!

PS. aahhh… jeśli ktoś czytał z uwagą, to powinien zadać pytanie – “a jakie jeszcze ukryte parametry mają obiekty?“. i jeśli ktoś takie pytanie by zadał, otrzymałby zapewne odpowiedź: “<your-command>|gm -force

eN.

 

 

 

 

OneLiner vs Script, stopki i gorączka

OneLiner nie jest skryptem

pasqdny dzień. migrena, potem gorączka, a jak mam gorączkę to muszę coś napisać. czasem głupoty, ale dziś może coś sensownego. i pisząc skrypt wstrzyqjący stopki dla OWA i mobile w EXO dla paru tysięcy userów, pomyślałem o tym czemu z OneLinera zrobiło się ponad 4oo linii kodu w dwóch skryptach. nie wszyscy to rozumieją, a jeśli ktoś skrypty pisać musi, lub lubi, to powinien to bardzo dobrze (z)rozumieć. dla tego wpis, poświęcony klepaczom kodu, którzy chcą robić to lepiej.

pisanie skryptów to praca dość niewdzięczna i mało opłacalna. kiedyś sądziłem, że jak powymiatam w pisaniu PS to będę mógł na tym sensownie zarobić. fakty są takie, że za skrypty rzadko ktoś chce płacić. “bo przecież to jedno polecenie”, albo że tego się nie da używać. no i w sumie… muszę się zgodzić. skrypty na zlecenie zdarzają się, ale rzadko.

w zasadzie robię już trochę inne rzeczy, ale czasem wspieram 2gą i 3cią linię. PowerShell to moje hobby, ale w pracy oczywiście czasem się przydaje. skrypty rzadko bo:

  • 1sza linia wsparcia boi się literek. jak coś nie ma GUI, to jest nie-do-użycia. nie ma znaczenia czy jest dokumentacja, i czy to w ogóle robi coś skomplikowanego, ale klawiatura dla pierwszej linii służy do logowania. też pewnie już niedługo, bo będą się logować biometrią a w dużej mierze zostaną zastąpieni chatbotami. a więc pisanie automatyzacji w postaci skryptu dla nich – nie ma sensu.
  • 2ga linia wsparcia owszem, uruchomi skrypt. ale jeśli coś się wysypie, pokaże się jakiś błąd… koniec. skrypt do śmieci, strach, nie chcemy. oczywiście tutaj jest już sporo wyjątqw, osób które po prostu *potrzebują* automatyzacji, więc się uczą. a czasem nawet lubią.
  • 3cia linia wsparcia… jest nieliczna i dość samowystarczalna. ale zazwyczaj wolą zassać jakiś skrypt z netu a potem szukać 1oo łorkeraundów jak go użyć do swoich potrzeb, niż coś napisać. ludzie wiedzą jak ‘coś osiągnąć’ – ale napisanie skryptu to inna para kaloszy.

to oczywiście subiektywna ocena, na podstawie doświadczeń własnych. wniosek jednak nasuwa się prosty – chcesz zarobić na kodowaniu, to weź się za coś, co ma interfejs, bo linia poleceń nadal straszy. jest dla nerdów, geeków i popraprańców. ewentualnie – ostatnia deska ratunq.

OneLiner nie jest skryptem. jest… łanlajnerem. nie będę wchodził w filozoficzno-semantyczne dywagacje i definicje – dla mnie nie jest to skrypt. zastosowanie onelinera jest wtedy, kiedy robi się coś bieżącego, trzeba zrobić raz i na szybko, i samemu kontroluje się przebieg. zazwyczaj to tylko jeden w wielu kroqw w długiej pracy. skrypt dla odmiany – to coś co pisze się, żeby było. żeby zostało i żeby można było tego użyć wiele razy i wielu warunkach… a co najważniejsze – żeby użyć tego mogli inni.

taka jest moja definicja.

to samo inaczej

dostałem skrypt mailem – nawet nie oneliner, miał ponad 1oo linii kodu. i prośba “weź tu tylko braqje takiego dyngsa i już. easypeasy… aaa i to ma być uruchamiane przez adminów potem”. oj. czyli celem nie jest to, co robi skrypt, tylko automatyzacja powtarzalnego zadania, przez osoby z 2giej linii. to zmienia wszystko.

zajrzałem do środka – jak na robotę admina, spoko. co prawda z ‘dyngsem’ by sobie nie poradził i zrobił zapętloną enumerację – czyli wydłużył przebieg do kwadratu, co przy tysiącach elementów i przebiegu w okolicy 3-4h, nagle zrobiłoby się 3-4 dni (; … ale widać, że ogólnie ogarnia. a jak zatem powinien wyglądać skrypt ‘dla kogoś’, dla 2giej linii? kilka wskazówek:

  • włącz cmdletbinding – proste, a daje dodatkowe możliwości np. tryb verbose i pełną obsługę parametrów.
  • zrób dobre opisy! wykorzystaj to, co daje PS i dobrze udoqmentuj skrypt. przyda się zarówno innym jak i Tobie
  • nie nadawaj nazw zmiennych typu ‘$a’ czy ‘$tmp’ – każdy normalny edytor ma intellisense, więc nie bój się wpisać ‘$tymczasowyImportDanych’ – przecież edytor to sam dokończy, a kod staje się czytelniejszy
  • jeśli niezbędny jest import danych – musisz założyć, że ktoś zamiast pliq tekstowego spróbuje załadować obrazek, że csv będzie miał złe kolumny albo że ktoś w ogóle go ominie. weryfikacja danych jest jednym z najpracochłonniejszych elementów – a ludzie są leniwi. dobra weryfikacja danych wejściowych to mniej błędów
  • OBSŁUGA BŁĘDÓW. to jedna z najtrudniejszych rzeczy – nie tylko przy skryptach. ale w przypadq skryptów, istnieje przesąd, że obsługa błędów jest niepotrzebna albo że w ogóle jej nie ma. jeśli oddajesz skrypt komuś, to niemal każda operacja powinna być otoczona try/catch lub weryfikacją zmiennej wyjściowej działania. inaczej skrypt będzie się sypał i nikt nie będzie widział czemu, albo nawet, że się sypie. to powoduje, że kod puchnie niebotycznie bo zamiast jednej, prostej linijki nagle robi się dziesięć, ale bez tego będzie scenariusz: nie zadziałało, nie wiem, boję się, kasuję.

oczywiście takich przykazań można by mnożyć i wypisywać przez pół nocy, ale wybrałem te, których zazwyczaj nie znajduję nawet w skryptach zassanych z technet/codeplex/github czy innych repo – teoretycznie stworzonych do konsumpcji przez innych. tak jak zasadą House MD dla wsparcia jest ‘użytkownik zawsze kłamie’, tak dla skypciarzy (a w zasadzie i w ogóle ogólnie) jest ‘ludzie są leniwi’. robią minimum. chcesz napisać skrypt dobrze – napisz tak, jak byś sam chciał go użyć, pierwszy raz go widząc, nie bądź leniwy.

przykładowy szkielet skryptu, który może posłużyć jako szablon:

<#
.SYNOPSIS
    skrypt ogólnie robi to i tamto
.DESCRIPTION
    tutaj już pełna instrukcja co i jak, w jaki sposób przygotować dane. jakie są warunki działania, kiedy sie może 
    wywalić, i wszystko inne, co w skrócie nazywa się 'dokumnetacją'
.EXAMPLE 
    .\set-ExampleSctipt.ps1
    dodaj informacje jak skrypt odpalić 
.EXAMPLE 
    .\set-ExampleSctipt.ps1 -dataFile abc.txt -param2 3
    nie szczędź przykładów i opisów! jeśli sam zaczynasz korzystać z czegoś, czego nie znasz, to pomaga
.INPUTS
    plik txt - płaski plik zawierające dane oddzielone enterem. to pedanteria, ale warto dodać 
.OUTPUTS
    ten skrypt nie ma outputu - ale warto to napisać
.NOTES
    nExoR ::))o-
    ver.20190716
    20190716 warto trzymać sobie wersję. nawet jeśli trzymasz repo na GitHubie - warto mieć loga z inormacją o zmianach
    20190715 w końcu ktoś dostaje nową wersję - chciałby wiedzieć co się zmieniło. nie robisz tego tylko dla siebie. 
    20180101 ...ty po roku też nie będziesz pamiętał
#>

[cmdletbinding()]
param (
    [parameter(Mandatory=$false,position=0)]
        [string]$dataFile,
    #zrób opis po co jest parametr - tutaj będzie to import danych
    [parameter(Mandatory=$false,position=1)]
        [string]$param2,
    #tutaj co prawda głupie param - ale nie bój się nadawać nazw parametrów, które przedstawiają ich znaczenie
    [parameter(Mandatory=$false,position=2)]
        [string]$logFile="_set_example-$(Get-Date -Format yyMMddHHmm).log"
    #dobre logowanie jest super ważne! jak potem sprawdzisz co zadziałało a co nie? 
)

#logowanie warto zrobić do pliq i opcjonalnie - na ekran. dla tego zawsze korzystam 
#z funkcji która mi to forqje na oba wyjścia
function write-log {
    param(
        [string]$text
    )
    if($text -match '\[WRN\]') {
        write-host -ForegroundColor Magenta $text
    } elseif ($text -match '\[ERR\]') {
        write-host -ForegroundColor Red $text
    } else {
        write-verbose $text
    }
    $text|out-file $LogFile -Append
}

$someData= Import-Csv $dataFile -Delimiter $delimiter -Encoding Default
#weryfikacja poprawności csv dla pliku danych - czy na pewno to taki plik, jaki sie oczeqje?
$expectedHeader=@("kolumna 1","innakolumna")
$csvHeader=$someData|get-Member -MemberType NoteProperty|select-object -ExpandProperty Name 
$missingColumn=@()
foreach($headElement in $expectedHeader) {
    if($csvHeader -notcontains $headElement) {
        Write-log "[ERR] brakuje kolumny $headElement w pliku $dataFile"
        $missingColumn+=$headElement
    }
}
if($missingColumn) {
    write-log -text "[ERR] nieprawidłowy format pliku wsadowego"
    exit -3
}

$data=import-csv -delimiter ';' $dataFile

$data

$VerbosePreference = "continue"
write-log -text "done."

nie ma tu żadnego try/catch… ale jest przykład jak obsłużyć dane wejściowe i nie oszczędzać na opisach i nazwach parametrów.

jedni biegają albo chodzą na imprezy, ja sobie skrobię skrypty (; a jeśli chcesz programować zawodowo – to raczej sięgnij po VS i zacznij ogarniać jakieś .NETy, pytongi czy inne duże frameworki.

gorączka spadła, czas usnąć. miłego kodowania!

eN.

PS. contains nie jest tym, czym się wydaje.

SSPR vs contact info

ciekawy case dotyczący uruchamiania Self-Service Password Reset (SSPR) i zarazem lekcja – workaround to nie solution q:

zgłosił się do mnie zapłakany klient, że uruchamiają SSPR i muszą jakoś dostarczyć dane użytkowników do portalu. do niedawna nie było to możliwe (uservoice z 2o18), ale od jakiegoś czasu Microsoft w swojej wielkości dostarczył… no właśnie, co dostarczył? obejście problemu opisane tutaj. ‘czemu obejście?’ zapyta ktoś, kto uważnie przestudiuje, sprawdzi i stwierdzi że działa…

…ano problem polega na tym, że informacje do SSPR brane są z atrybuty ‘StrongAuthenticationUserDetails’. ten atrybut ma kilka cech odróżniających go od zwykłego atrybutu np. ‘telephoneNumber’ a jednym z nich jest fakt… że nie wypełnia wizytówki użytkownika. jest niewidzialny.

i teraz taki myk…

tak wygląda użytkownik, który wszedł na portal i wypełnił sobie dane do MFA/SSPR:

PS> get-msoluser -UserPrincipalName testuser01@w-files.pl|select -ExpandProperty strongauthenticationuserdetails


ExtensionData : System.Runtime.Serialization.ExtensionDataObject
AlternativePhoneNumber :
Email : moj@email.com
OldPin :
PhoneNumber : +48 2323232323
Pin :

i wyświetlmy sobie właściwości w AAD:

PS > Get-AzureADUser -SearchString testuser01@w-files.pl|select othermails,mobile,telephonenumber

OtherMails Mobile TelephoneNumber
---------- ------ ---------------
{}

a teraz, za artykułem, prepopuluję (dziś super po polsq) atrybuty:

Set-AzureADUser -ObjectId testuser02@w-files.pl -OtherMails @('innymail@wtf.com') -Mobile '+48 666232323' -TelephoneNumber '+48 999999999'

no i output takiej operacji:

PS > Get-AzureADUser -SearchString testuser02@w-files.pl|select othermails,mobile,telephonenumber

OtherMails Mobile TelephoneNumber
---------- ------ ---------------
{innymail@wtf.com} +48 666232323 +48 999999999

get-msoluser -UserPrincipalName testuser02@w-files.pl|select -ExpandProperty strongauthenticationuserdetails
<empty>

co się dzieje dalej? hasło dla testuser02 resetuję i działa. kiedy próbuję dostać się do portalu o365, dostaję standardowe pytanie o uzupełnienie obligatoryjnych pól do zabezpieczenia konta (czyli te do MFA, które zapisują się w StrongAuthenticationUserDetails).

a więc to …’rozwiązanie’ nie działa dla MFA, jest obejściem dla SSPR, i dane ‘zabezpieczające’ stają się *publicznie* dostępne dla wszystkich w organizacji ROTFLMAO.

eN.

Oneliner wyświetlający administratorów AAD/o365

trochę zabawa żeby udowodnić co może PS, bo można łatwiej, ale na pewno w większej ilości kroqw.

scenariusz: wyświetlić wszystkich adminów i ich role w AAD. niby proste, mamy do dyspozycji get-AzureADDirectoryRole i get-AzureADDirectoryRoleMember. a więc we w miarę prostej postaci można to zrobić tak:

Get-AzureADDirectoryRole|%{$rn=$_.displayname;Get-AzureADDirectoryRoleMember -ObjectId $_.objectid|? objectType -eq 'user'|select displayname,userprincipalname,@{N='role';E={$rn}}}

już tutaj zapytanie jest dość złożone. z ciekawych elementów, które warto wyjaśnić:

  • $rn=$_.displayname jest ściśle powiązane z faktem, żeby potem wykorzystać je w selekcie. bez tego utracona by została informacja dotycząca roli – a więc byłaby lista użytkowników, bez informacji o tym, o którą rolę chodzi.
  • ..i tak w selekcie następuje ‘wstrzyknięcie’ tej informacji poprzez @{N=’role’;E={$rn}} – utworzona zostaje dodatkowa kolumna o nazwie ‘role’.

niemniej to nie koniec ‘zabawy’ bo otrzymamy płaską listę, na której jeden login może występować wiele razy – dla każdej roli w której się pojawia. mamy oczywiście ‘group-object’ które pozwoli połączyć to w jedną całość… tylko co ukaże się naszym oczom nie będzie przyjemne.

problem, który się pojawia – jak sformatować to, co wypluwa ‘group-object’? w dużym skrócie – należy się przyjrzeć temu, co ten zwraca, i potraktować go… jak każdy inny obiekt. przyjmując, że wyniki poprzedniego polecenia mam w zmiennej ‘$admins’, przyjrzyjmy się co będzie po zgrupowaniu:

$admins|group userprincipalname|select -ExpandProperty group -first 1

DisplayName UserPrincipalName role
----------- ----------------- ----
Administrator nexor@w-files.pl Power BI Service Administrator
Administrator nexor@w-files.pl Service Support Administrator
Administrator nexor@w-files.pl Exchange Service Administrator
Administrator nexor@w-files.pl SharePoint Service Administrator

a więc ‘grupa’, trzyma dla każdego wpisu, wszystkie informacje o zgrupowanych obiektach (jako kolekcja obiektów) – ze względu na to zagnieżdżenie, nie jest to zbyt proste do pracy. do tego trzeba będzie pracować na kolekcji – bo jeśli ktoś występuje w kilq rolach, to mamy liczbę mnogą. finalnie odpowiedź jest taka:

Get-AzureADDirectoryRole|%{$rn=$_.displayname;Get-AzureADDirectoryRoleMember -ObjectId $_.objectid|? objectType -eq 'user'|select displayname,userprincipalname,@{N='role';E={$rn}}}|Group-Object userprincipalname |select name,@{N='displayname';E={$_.group.displayname|select -unique}},@{N='roles';E={($_.group.role|sort) -join ','}}

można teraz całość wypluć do CSV [ |export-csv c:\temp\admins.csv -nti -deli ‘;’ ] i zrobić raport w Excelu (:

eN.

zaokrąglanie liczb (PowerShell)

liczby można zaokrąglić/sformatować na kilka sposobów.

można np. ‘rzutować typ’ [casting] przez co liczba zostanie odpowiednio sformatowana. np. interesuje nas liczba całkowita z działania:

PS C:\_scriptZ> 1/3
0,333333333333333
PS C:\_scriptZ> [int](1/3)
0

można też bawić się formatowaniem ciągu znaków [string] co jest zwłaszcza przydatne jeśli potrzebujemy dopełnienia – np. dopełnienie z przodu do trzech cyfr – 001, 010, 100:

PS C:\_scriptZ> 1..100|%{$_.toString("000")}
001
002
[...]
092
093
[...]
099
100

zaokrąglenie liczby do konkretnej ilości po przecinq – albo jak wyżej, poprzez konwersję string…

PS C:\_scriptZ> (1/3).toString("##.#")
,3
PS C:\_scriptZ> (1/3).toString("#.###")
,333
PS C:\_scriptZ> (1/3).toString("0.###")
0,333
PS C:\_scriptZ> (44/3).toString("0.###")
14,667

…albo przez funkcje matematyczne. te dają trochę większą kontrolę.

PS C:\_scriptZ> [math]::round((1/3))
0
PS C:\_scriptZ> [math]::round((2/3))
1
PS C:\_scriptZ> [math]::round((2/3),3)
0,667

przy obu działaniach trzeba mieć pewność na jakim typie danych się operuje, więc można je stosować zależnie.

…a cały wpis zainspirowany był poszukaniem sensownej metody na wyciągnięcie części całkowitej ułamka, ale nie zaokrąglaniu. wszystkie testowane funkcje – czy round, czy cast czy format string – zaokrąglają automatycznie. czyli 0,6 = 1. a ja potrzebuję zera z przodu. i tutaj klasa matematyczna sprawdza się najlepiej [podłoga z dołu i sufit z góry (; ]:

PS C:\_scriptZ> [math]::floor(2/3)
0
PS C:\_scriptZ> [math]::floor(1/3)
0
PS C:\_scriptZ> [math]::ceiling(1/3)
1
PS C:\_scriptZ> [math]::ceiling(2/3)
1

eN.

 

 

 

 

konwersja XLSX do CSV metodą przeciągnij i upuść

drag’n’drop wygląda jakoś bardziej naturalnie… ale co tam. niech będzie po polsq (;

taka sytuacja…

spora część mojej pracy to zbieranie i analiza danych z systemów – AD, AAD, Ex, EXO, SfB i tak dalej. piszę sobie do tego pałerszelki i wrzucam to do CSV. no niestety – ale cały czas nie mogę się przestawić na jakąś sensowną bazę danych, ale to nie na dziś. potem takie CSVki łatwo jest otworzyć, edytować, analizować w Excel – oczywiście już jako xlsx. wszyscy Excel znają (LoL, raczej potrafią go otworzyć i coś wpisać, bo niestety poziom obsługi podstawowych narzędzi jest niestety, na równie podstawowym poziomie), dzięki o365 mamy wspaniałe funkcje koedycji – a więc w prosty sposób cały zespół może pracować na pojedynczym źródle.

żeby odświeżyć dane w excel, znów muszę wyeksportować go do CSV. czyli:

  • otworzyć arkusz
  • zrobić export (kilka kliknięć, wybór formatu, nie pomylić się, bo jest kilka formatów CSV itd…)
  • zamknąć.

nie lubię klikać, a więc fajnie byłoby mieć ikonkę na pulpicie, przeciągam plik XLS i voila! CSVka sobie czeka w ustalonym miejscu.

taki przydługi wstęp, ale taki dzień. jak to zrobić?

automatyczna konwersja XLSX2CSV via drag’n’drop

  1. trzeba sobie machnąć prosty skrypcik PS, który konwersji dokona. np taki:

convert-XLSX2CSV.ps1

param(
  [string]$fileName
)

if(-not (test-path $fileName)) {
  write-host "$fileName not found. exitting"
  exit
}

write-host "converting $fileName ..."

$file=get-childItem $fileName
if($file.Extension -ne '.xlsx') {
  write-host "$fileName doen't look like excel file. exitting"
  exit
}
try{
  $Excel = New-Object -ComObject Excel.Application
} catch {
  $Error
  exit
}
$Excel.Visible = $false
$Excel.DisplayAlerts = $false
$wb = $Excel.Workbooks.Open($fileName)
$wb.Worksheets[1].SaveAs("c:\temp\" + $File.BaseName + ".csv", 6,$null,$null,$null,$null,$null,$null,$null,'True')
$Excel.Quit()

write-host -ForegroundColor Green "convertion done. saved as c:\temp\$($file.BaseName).csv"
write-host "press any key to finish."

$HOST.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | OUT-NULL
$HOST.UI.RawUI.Flushinputbuffer()

nic skomplikowanego. jeśli ma być auto trzeba pamiętać, że ścieżki muszą być zahardcodowane, a parametry wejścia najlepiej ograniczyć do nazwy pliq.

2. zrobić skrót na pulpicie, który przyjmie plik drag’n’drop. jeśli zrobimy skrót do ps1 – nie zadziała. skrót musi być do pliq exe. a więc proste:

  • utwórz nowy skrót
  • jako plik do uruchomienia:

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -noprofile -file C:\_scriptZ\convert-xlsx2csv.ps1

…no i w zasadzie tyle. teraz wystarczy upuścić plik Excel na skrócie i życie jest dłuższe o kilka ładnych kliqw (=

bezpośrednia edycja XLSX

a nie można by tak bezpośrednio na excelu pracować? ano możnaby. nawet na serwerze. jest taka fajna biblioteka, która pozwala zaimportować się bez instalacji samego excel. nawet kiedyś o tym pisałem. ale obsługa tego skryptem, jest mówiąc oględnie – mało przyjemna. praca na zwykłych obiektach to czysta przyjemność.

eN.