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.