Spis treści:

Kategoria:PowerShell


Skrypty PowerShell dla programisty, część 2

Najczęściej używane skrypty PowerShell w pracy programisty - ikona konsoli

PowerShell nie jest raczej narzędziem pierwszej potrzeby w codziennej pracy programisty i architekta. Najważniejszy z pewnością jest kompilator. Niektórzy mają pecha i ich głównym narzędziem staje się Word i Excel - wiadomo, ktoś musi tworzyć dokumentacje i raporty dla przełożonych. Od czasu do czasu trzeba się jednak oderwać od kodu i zaprzyjaźnić się z innymi narzędziami. Ot choćby z systemem zarządzania bazami danych, narzędziami do projektowania i planowania prac. Gdzieś tam na dalszym planie czasami pojawiają się narzędzia do automatyzacji pewnych powtarzalnych czynności.

Zawsze w takich momentach staram się przypomnieć, i polecam to innym, po co tak naprawdę powstały komputery, skąd się wywodzą i jakie było ich przeznaczenie. Temat jest bardzo interesujący i może kiedyś go rozwinę. Dziś napiszę krótko: miały to być urządzenia, które wyręczają człowieka, odciążają go przy realizacji niektórych zadań. O ile programistom łatwo zrozumieć ten automatyzm w kodzie programu, o tyle automatyzacja zadań bardziej administracyjnych pozostaje poza zasięgiem wielu. Wydaje się, że większość woli klikać. Tworzenie użytkowników Windows, przydzielanie ich do grup, kontrola uprawnień, pobieranie i przetwarzanie dokumentów lub definicji interfejsów z sieci, rejestracja komponentów COM, zakładanie kont IIS - każda z tych czynności może się prędzej czy później pojawić w pracy programisty, a z czasem na dobre zadomowić. A czynność powtarzana wiele razy staje się irytująca. Jednym z rozwiązań, które dzisiaj postaram się przedstawić, będzie wykorzystanie narzędzie PowerShell. Jakiś czas temu o nim pisałem i był on wykorzystywany w kontekście prostych czynności (wpis można znaleźć w artykule Najczęściej używane skrypty PowerShell w pracy programisty). Dziś pokażę zagadnienia nieco bardziej skomplikowane.

Usługi z definicją WSDL - generowanie interfejsu C#

WSDL to język definiowania usług internetowych. Różni programiści, niezależnie od ich prymarnego języka programowania, muszą w jakimś stopniu go zgłębić. Teoretycznie język ten powinien być pewnego rodzaju umową, obietnicą pomiędzy dwoma systemami. Powinien definiować jakie komunikaty mogą być wysyłane (funkcje) i jakie argumenty mogą te komunikaty przenosić. Przyznam, że nie widziałem jeszcze rozwiązania, w którym prace rozpoczynałyby się od definicji WSDL. Znacznie częściej kontrakt definiowany jest w jakiejś aplikacji do projektowania graficznego lub pisany jako zwykły interfejs w języku Java, C# czy też VB. Te interfejsy są przede wszystkim znacznie czytelniejsze, co w konsekwencji doprowadza do mniejszej liczby błędówWSDL powstał z potrzeby pogodzenia różnych systemów pisanych w różnych językach i nie jest to pierwsza próba tego typu. Spośród takich inicjatyw, według mnie bardziej przystępnych, był język IDL (skrót rozwijany jako Interface Description Language lub Interface Definition Language). Sam język został opracowany jeszcze w poprzednim stuleciu a jego składnia bardzo przypomina to, co można zobaczyć w językach C/C++. Istniało mnóstwo narzędzi, generatorów, które tłumaczyły zapis IDL ma odpowiednie klasy najbardziej popularnych języków.. Odstawiając na bok dyskusję na temat wad i zalet tego języka trzeba przyznać, że stał się jakimś standardem. Co ma zrobić programista C#? Ma się tego języka uczyć? Nie zaszkodzi, ale w praktyce rzadko będzie taka wiedza przydatna. Najczęściej wykorzystuje się jakieś zewnętrzne narzędzie do wygenerowania interfejsu w znanym sobie języku. Użytkownicy Visual Studio robią to często korzystając z opcji Add Service Reference. Gdybyśmy jednak zechcieli zautomatyzować ten proces, moglibyśmy użyć PowerShell. Popatrzmy na poniższy listing:

$WsdlUrl = "http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL"
$WorkingDir = "D:\Temp"
$InterfaceName = "IWeather"
$WindowsSDK = "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\x64\"

$WsdlApp = Join-Path $WindowsSDK wsdl.exe
$WsdlPath = "$WorkingDir\$InterfaceName.wsdl"
New-item $WsdlPath -Type File -Value (Invoke-WebRequest $WsdlUrl).Content -Force | % {"Writing File '$_'"}
Invoke-Expression "& '$WsdlApp' $WsdlPath /nologo /l:CS /serverInterface /out:$WorkingDir\$InterfaceName.cs"

Pozwoliłem sobie skorzystać z publicznie dostępnej definicji interfejsu http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL. Kolejne deklaracje to parametry dość prostego skryptu: $WorkingDir to katalog wynikowy, $InterfaceName to nazwa generowanego interfejsu, $WindowsSDK to katalog SDK Windows. To tam znajduje się aplikacja pozwalające wykonać konwersję: wsdl.exe.

Aby sam skrypt jeszcze uprościć, stworzyłem wcześniej dwie zmienne określające kolejno: $WsdlApp - ścieżka do aplikacji konwertującej, $WsdlPath - ścieżka docelowa pliku WSDL. Prawdziwy skrypt to tak naprawdę dwie ostatnie linijki. Dzięki przedostatniej w katalogu docelowym zapisywany jest plik WSDL (polecenie New-Item) pobrany z internetu (polecenie Invoke-WebRequest). Ostatnia linijka to wywołanie programu wsdl.exe z odpowiednimi parametrami.

Sam skrypt można naturalnie zamknąć w odpowiedniej funkcji i wywoływać jako element składowy szerszego skryptu.

Zdarza się, że definicja WSDL umieszczona jest w kilku plikach lub nie potrzebujemy bazowego pliku WSDL. W takim przypadku można sobie skrypt jeszcze bardziej uprościć i wywołać generowanie bezpośrednio ze strony:

$WsdlUrl = "http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL"
$WorkingDir = "D:\Temp"
$InterfaceName = "IWeather"
$WindowsSDK = "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\x64\"

$WsdlApp = Join-Path $WindowsSDK wsdl.exe
Invoke-Expression "& '$WsdlApp' $WsdlUrl /nologo /l:CS /serverInterface /out:$WorkingDir\$InterfaceName.cs"

Interfejs generowany jest maszynowo dlatego nie należy spodziewać się ładnego układu. Podobnie można wygenerować interfejs korzystając z programu svcutil:

$WsdlUrl = "http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL"
$WorkingDir = "D:\Temp"
$InterfaceName = "IWeather"
$WindowsSDK = "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\x64\"

$WsdlApp = Join-Path $WindowsSDK svcutil.exe
Invoke-Expression "& '$WsdlApp' $WsdlUrl /nologo /l:CS /out:$WorkingDir\$InterfaceName.cs"

Parametry są prawie takie same jak wcześniej. Zmienia się tylko nazwa aplikacji, co oczywiste, oraz lista parametrów (brak /serverInterface).

Wywoływanie MSBuild

MSBuild to narzędzie do kompilowania i budowania projektów pisanych w Visual Studio. Jeżeli pracujemy w środowisku Visual Studio, najłatwiejszym sposobem na zbudowanie aplikacji jest naciśnięcie przycisku Run, F5 lub Ctrl + F5, w zależności od trybu. Jeżeli chcemy opublikować nasze rozwiązanie na serwerze FTP, na dysku lub w obszarze IIS - możemy z tego środowiska wybrać opcję Publish... i zdefiniować akcje jakie ma wykonać Visual Studio. Trzeba jednak wiedzieć, że Visual Studio nie wykonuje tych akcji samodzielnie. Posługuje się narzędziem MSBuild. Co najważniejsze - MSBuild może być wywoływane bez pośrednictwa Visual Studio. Automatyzacja tego narzędzia jest szczególnie przydatna, jeżeli mamy wiele projektów, wiele środowisk (produkcyjne, testowe, integracyjne), wiele serwerów, kilka aplikacji i trzeba na nich wszystkich coś umieścić. Można oczywiście klikać w Visual Studio, ręcznie kopiować pliki do katalogów wirtualnych IIS, ale można ten proces zautomatyzować.

Nie zamierzam bardzo rozwijać tematu publikacji, ale muszę wspomnieć o kilku krokach, które trzeba wykonać jednorazowo. Po pierwsze, należy mieć projekt. Po drugie, należy skonfigurować profil publikacji. Aby to zrobić należy kliknąć prawym przyciskiem myszy na projekcie w Visual Studio i wybrać Publish.... Tam można ustalić co się ma dziać z plikami (mogą być umieszczone w katalogu na dysku, na jakimś serwerze przez FTP, w usłudze IIS, w chmurze Azure), określić sposób uwierzytelnienia i autoryzacji i kilka innych rzeczy, w zależności od sposobu publikacji. Ten zestaw czynności można potem nazwać pod postacią Profilu publikacji. To zwykły plik XML, który, w zależności od wybranych ustawień, może przyjąć na przykład taką postać:

<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <SiteUrlToLaunchAfterPublish />
    <publishUrl>d:\Temp\Deploy</publishUrl>
    <DeleteExistingFiles>True</DeleteExistingFiles>
  </PropertyGroup>
</Project>

Ten profil kopiuje potrzebne do uruchomienia pliki do katalogu D:\Temp\Deploy, kasując wcześniej całą zawartość katalogu. Znajduje się on w katalogu Properties\PublishProfiles projektu, ma rozszerzenie *.pubxml oraz nazwę Local.pubxml. To Local to właśnie nazwa profilu. Co nam to daje? Otóż pozwala na zautomatyzowanie tego procesu w PowerShell. Popatrzmy na przykładowy skrypt:

$MsBuild = $env:systemroot + "\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe";
$Configuration = "DEBUG"
$SlnPath = "C:\Users\pady\Documents\visual studio 2012\Projects\SampleWcfService\SampleWcfService.sln"
$expression = "$MsBuild ""$SlnPath"" /p:Configuration=$Configuration /p:DeployOnBuild=true /p:PublishProfile=Local /t:build /v:minimal "
Invoke-Expression "& $expression"

Skrypt można oczywiście rozszerzyć publikując w prostej pętli do kilku różnych lokalizacji.

Pobieranie rozmiarów katalogów

Zapewne każdy ma inne powody, dla których potrzebuje pobrać rozmiary katalogów. Może to być potrzeba oszczędnego gospodarowania szybkim ale mało pojemnym dyskiem SSD, kontrola rozmiarów katalogów jakiegoś serwera z logami aplikacji czy też dowolna inna - skrypt będzie ten sam. Popatrzmy w jaki sposób można pobrać rozmiar bieżącego katalogu:

ls -Directory |
  select @{N='Directory';E={$_.Name}}, @{N='Size';E={(ls -Path $_.FullName -Recurse | measure Length -Sum).Sum}} |
  sort Size -Descending

Problem może się pojawić z przełącznikiem -Directory, który nie jest dostępny w starszych wersjach PowerShell. W takim przypadku należy nieco zmodyfikować skrypt:

ls | 
  ?{ $_.PSIsContainer } |
  select @{N='Directory';E={$_.Name}}, @{N='Size';E={(ls -Path $_.FullName -Recurse | measure Length -Sum).Sum}} |
  sort Size -Descending

Jeżeli ustawimy się w katalogu Windows, wtedy wynik zapytania mógłby wyglądać tak:

Directory                             Size
---------                             ----
winsxs                         14786587644
System32                        4833714627
assembly                        3918770675
Microsoft.NET                   1571950262
SysWOW64                        1323971850
SoftwareDistribution            1168631522
Fonts                            461994375
Temp                             391250840
symbols                          242750976
inf                              143892639
IME                              143546732
(pominięte kolejne wpisy)

Gdy wersja PowerShell jest bardzo stara (2.0), może się również pojawić problem z argumentem -Path. Wtedy należy wprowadzić kolejną modyfikację:

ls | ?{ $_.PSIsContainer } |
  select @{N='Directory';E={$_.Name}}, @{N='Size';E={(ls -LiteralPath $_.FullName -Recurse | measure Length -Sum).Sum}} |
  sort Size -Descending

Przyznam, że takie przedstawienie wyników nie zawsze jest odpowiednie. Jeżeli operujemy plikami rzędu gigabajtów, mnogość cyferek nie pozwala od razu określić rozmiaru katalogu. Ten problem implikuje jeszcze jedną modyfikację skryptu - przeliczenie rozmiaru na megabajty. Drugi problem to odczyt plików ukrytych, które domyślnie są ignorowane. Aby je uwzględnić, należy dodać jeszcze parametr -Force. Na koniec warto jeszcze ładnie dopasować rozmiar tabelki dokładając bloczek Format-Table, w skrócie ft z przełącznikiem -AutoSize. Rezultat końcowy poniżej:

ls | ?{ $_.PSIsContainer } |
  select @{N='Directory';E={$_.Name}}, @{N='Size';E={(ls -LiteralPath $_.FullName -Recurse -Force | measure Length -Sum).Sum}} |
  sort Size -Descending |
  select Directory,Size,@{N='Size [MB]';E={"{0:N2}" -f ($_.Size/1MB)}} |
  ft -AutoSize

Pobieranie rozmiarów katalogów wersja końcowa

Tak przygotowany skrypt może być jeszcze nieznacznie zmodyfikowany. Ja w swojej wersji wzorcowej kopiuj-wklej-pozmieniaj dokładam jeszcze limit na liczbę rezultatów. Jeżeli skrypt puścimy na dysku/katalogu z dużą liczbą podkatalogów, wynik może mieć kilka stron. Najczęściej analizuje się tylko kilka, może kilkanaście pierwszych rekordów i nie ma potrzeby zaśmiecać sobie głowę pozostałymi. Popatrzmy na skrypt końcowy:

ls | ?{ $_.PSIsContainer } |
  select @{N='Directory';E={$_.Name}}, @{N='Size';E={(ls -LiteralPath $_.FullName -Recurse -Force | measure Length -Sum).Sum}} |
  sort Size -Descending |
  select Directory,Size,@{N='Size [MB]';E={"{0:N2}" -f ($_.Size/1MB)}} -First 10 |
  ft -AutoSize

Wynik działania takiego skryptu, uruchomionego w katalogu Windows, pokazany jest poniżej:

Directory                   Size Size [MB]
---------                   ---- ---------
winsxs               15036239072 14 339,68
System32              5017551469 4 785,11
assembly              3918770902 3 737,23
Microsoft.NET         1571950262 1 499,13
SysWOW64              1324214910 1 262,87
SoftwareDistribution  1168631522 1 114,49
Fonts                  471700903 449,85
Temp                   391250840 373,13
symbols                242750976 231,51
inf                    143892639 137,23

Inne kombinacje, monitorowanie wielu niezależnych katalogów, prezentowanie wyników w postaci drzewa to już nieco bardziej skomplikowane skrypty, nadające się na oddzielny artykuł.

Wyszukiwanie plików zawierających podaną frazę

Dla wielu użytkowników najlepszym wyjaśnieniem jest nazwanie tej operacji grep. Grep to program napisany pierwotnie dla systemu Unix (obecny również w innych systemach tej rodziny, także w linii Linux), służący do wyszukiwania w plikach wskazanych wyrażeń. To narzędzie tak popularne, że doczekało się nawet swojej branżowej nazwy. Tak jak od nazwy Google powstało słowo googlować, tak od grep powstało grepować, czyli wyszukiwać wzorce w plikach. Przypuśćmy, że chcemy odnaleźć wszystkie wystąpienia słowa XmlDocument w plikach *.cs. Napisalibyśmy taki oto fragment:

ls *.cs -Recurse | Select-String -Pattern "XmlDocument"

Wynikiem działania takiego skryptu mogłaby być poniższa lista wystąpień:

SoapTester\Core\DigestHeader.cs:43:            var doc = new XmlDocument();
SoapTester\Core\TestExecutor.cs:43:            var responseDoc = new XmlDocument();
SoapTester\Core\TestExecutor.cs:56:        private SoapResult SendMessage(XmlDocument doc, Request request)
SoapTester\Core\TestExecutor.cs:70:        private XmlDocument LoadDocument(string path)
SoapTester\Core\TestExecutor.cs:72:            var doc = new XmlDocument();
SoapTester\Core\TestExecutor.cs:77:        private XmlDocument ProcessTemplate(XmlDocument doc)
SoapTester\Core\XmlFormatter.cs:8:        public static string Format(XmlDocument doc)
SoapTester\Core\XmlFormatter.cs:27:            var doc = new XmlDocument();

Można oczywiście stosować nieco bardziej skomplikowane reguły. Gdybyśmy na przykład pamiętali, że szukamy czegoś zaczynającego się od Xml, następna litera to coś pomiędzy A i C, a wielkość liter ma znaczenie, napisalibyśmy tak:

ls *.cs -Recurse | Select-String -Pattern "Xml[A-C]" -CaseSensitive

Moglibyśmy otrzymać następujący rezultat:

SoapTester\Core\Xml\XmlTemplate.cs:17:                foreach (XmlAttribute attr in rootNode.Attributes)
SoapTester\Core\Request.cs:11:        [XmlAttribute("action")]
SoapTester\Core\Request.cs:14:        [XmlAttribute("template")]
SoapTester\Core\Variable.cs:7:        [XmlAttribute("name")]
SoapTester\Core\Variable.cs:10:        [XmlAttribute("value")]
SoapTester\Core\Variable.cs:13:        [XmlAttribute("xpath")]

Wynik jest odpowiednio sformatowany, ale, tak jak w większości przypadków podczas pracy w PowerShell, zwracane są obiekty i można sobie taki wynik odpowiednio filtrować. Załóżmy, że wzorzec występuje gdzieś na początku pliku i nie chcemy wyświetlać dopasowań, które nas nie interesują. Prosty filtr może bardzo łatwo spowodować wyświetlenie setek, może nawet tysięcy rekordów - stanie się wtedy bezużyteczny. Popatrzmy na przykład:

ls *.cs -Recurse | Select-String -Pattern "Xml" | where {$_.LineNumber -lt 5}

Tym razem możemy dostać wyniki w postaci:

SoapTester\Core\Request.cs:2:using System.Xml.Serialization;
SoapTester\Core\TestExecutor.cs:1:using SoapTester.Core.Xml;
SoapTester\Core\Variable.cs:1:using System.Xml.Serialization;
SoapTester\Core\XmlFormatter.cs:4:    using System.Xml;

Wszystkie wyniki mają numer linii mniejszy niż 5.

Podsumowanie

Druga część skryptów PowerShell dla programisty jest bardziej złożona. Nie wyczerpuje to jednak wszystkich możliwości. Ja postaram się gromadzić takie skrypty i umieszczać je w kolejnych wpisach. Zachęcam też do dzielenia się pomysłami i rozwiązaniami w komentarzach.

Kategoria:PowerShell

, 2016-06-21

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?