Piszemy sobie moduł: C# (1)
Pisanie modułów skryptowych w PowerShellu to rzecz trywialna dla każdego, komu zdarzyło się w PowerShellu "popełnić" kilka skryptów. W ostateczności moduł taki możemy utworzyć wrzucając kilka funkcji do pliku z rozszerzeniem psm1. Jeśli mamy skrypty stanowiące swoiste biblioteki funkcji może wręcz wystarczyć zmiana nazwy pliku. Wreszcie – importować w ten sposób można również pliki z rozszerzeniem ps1, nic nie stoi więc na przeszkodzie by zwyczajny skrypt zacząć traktować jako moduł skryptowy. Dlatego właśnie postanowiłem przyjrzeć się dwóm pozostałym opcjom. Na pierwszy ogień idzie moduł binarny. A ponieważ z języków, w jakich można moduł taki napisać, najbliższy memu sercu jest C# – z niego właśnie w trakcie pisania modułu skorzystam.
Witaj świecie!
Czy zaskoczy kogokolwiek fakt, że pierwszym naszym poleceniem witać będziemy cały, otaczający nas świat? Gwoli ścisłości: o tym kogo będziemy witać zdecyduje nasz użytkownik. My jedynie zaoferujemy mu stosownego do tego narzędzia.
Moduł tworzyć będę w Visual Studio: pewnie da się prościej/ szybciej/ wygodniej, ale mi jakoś zawsze najłatwiej było zmusić do współpracy właśnie tę aplikację. Na początek stworzyć musimy projekt, wybierając jako nasz cel utworzenie biblioteki – tym właśnie są moduły w PowerShellu. Nazwa projektu stanie się docelowo nazwą naszego modułu.
Kolejna rzecz, o którą musimy zadbać, to dodać do projektu "System.Management.Automation.dll". W chwili obecnej najlepszym sposobem osiągnięcia tego celu jest zainstalowanie odpowiedniego pakietu NuGet:
Find-Package -Id Microsoft.PowerShell. | Where-Object Id -match ReferenceAssemblies Install-Package Microsoft.PowerShell.5.ReferenceAssemblies
Zainstalowanie pakietu doda również wspomnianą bibliotekę do listy referencji.
Pozostaje nam więc jedynie dopisać ją do przestrzeni nazw, z których nasz kod będzie korzystał. Zanim przystąpimy jednak do poprawiania samego kodu, warto zmienić też nazwę pliku/ opisywanej klasy. Następnie zmienimy opisywaną przez nas klasę: dopiszemy do niej atrybut Cmdlet, w którym umieścimy informację o tworzonym poleceniu. Zadbamy też o to, by tworzona przez nas klasa dziedziczyła po klasie PSCmdlet:
using System.Management.Automation; namespace Przywitania { [Cmdlet(VerbsCommunications.Write, "Hello")] public class WriteHelloCommand : PSCmdlet {
Przyjrzyjmy się temu, co w klasie takiej powinno się znaleźć
Deklarujemy parametry
Definiowanie parametrów w ramach funkcji zaawansowanych znamy dość dobrze: zaczynamy od (opcjonalnego) atrybutu Parameter(), następnie podajemy aliasy, wszelkiego rodzaju walidacje (w naszym wypadku: wymóg, by imię zaczynało się od wielkiej litery). W ramach atrybutu możemy uwzględnić informację, że parametr akceptuje wartości przekazywane przez potok, jest wymagany, możemy też podać pomoc wyświetlaną w momencie dopytywania o wartość parametru. Możemy też podać typ tworzonej zmiennej:
[Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName, Mandatory, HelpMessage = 'Imię osoby, którą witamy' )] [Alias("Imię")] [ValidatePattern('(?-i)^\p{Lu}')] [String]$Name
Definicja w C# różnić się będzie nieznacznie. Po prostu w pewnych elementach wymagane będzie podanie informacji jawnie. Oprócz tego wewnątrz klasy stosować będziemy prywatną właściwość, która zastąpi publiczny parametr:
[Parameter( ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Mandatory = true, HelpMessage = "Imię osoby, którą witamy", Position = 0 )] [Alias("Imię")] [ValidatePattern(@"(?-i)^\p{Lu}")] public string Name { get { return _name; } set { _name = value; } } private string _name;
Parametr mamy już przygotowany, pora go użyć.
Bloki kodu
W ramach funkcji w PowerShellu poszczególne bloki kodu, odpowiedzialne za etapy pracy potoku, umieszczamy w begin, process oraz end. Każdy blok jest do pewnego stopnia niezależny. W pierwszym na ogół tworzyć będziemy pomocnicze funkcje i zmienne wykorzystywane w ciele funkcji. Blok process ograniczamy do wszystkiego, co korzystać będzie z danych spływających w potoku. Wreszcie blok end, którego zadanie to usuwanie pozostałości po blokach poprzednich.
Tworząc cmdlety korzystać będziemy z podobnej procedury, z tym że zamiast tworzyć poszczególne bloki kodu, nadpiszemy metody BeginProcessing, ProcessRecord i EndProcessing naszą własną implementacją. Różnica polega na tym, że w przypadku klas metody te nie są nam niezbędne do tworzenia właściwości (zmiennych) i metod (funkcji) widocznych w całym cmdlecie: zamiast tego tworzyć możemy prywatne właściwości i metody. Nasze polecenie tego jednak nie wymaga, zaimplementujemy w nim po prostu poszczególne metody:
protected override void BeginProcessing() { WriteVerbose("Zaczynamy!"); } protected override void ProcessRecord() { WriteVerbose($"Przetwarzamy: {_name}"); WriteObject($"Witaj, {_name}!"); } protected override void EndProcessing() { WriteVerbose("Kończymy!"); }
Tym sposobem kończymy definiowanie naszego polecenia. Naturalnie, w przypadku prawdziwego polecenia dodalibyśmy jeszcze obsługę błędów, ale w naszym wypadku założymy, że błędy nie wystąpią. Pozostaje więc tylko zbudować nasz projekt i spróbować go użyć.
Budowanie, odrobaczanie
Tworząc projekty w Visual Studio, warto skorzystać z oferowanego przezeń debugera i w ten sposób sprawdzić, czemu nasze polecenie (z założenia bezbłędne) błąd jednak wygenerowało. Najprościej efekt ten osiągnąć konfigurując odpowiednio opcję debugowania w ramach naszego projektu:
Precyzujemy więc, że uruchamiać będziemy PowerShella, a wśród parametrów skorzystamy z opcji NoProfile (oszczędzi nam to czekania na załadowanie wszystkich elementów naszego profilu) NoExit (dzięki czemu po wykonaniu poleceń okno pozostanie otwarte, ułatwiając nam eksperymenty) i opcji Command (w której załaduje świeżo utworzony moduł). Pozwoli nam to na dokładną analizę modułu, łącznie z wykorzystaniem punktów zatrzymania:
Możemy się witać z całym światem przy pomocy skompilowanego cmdletu. Czemu jednak chcielibyśmy w ogóle to robić i co dalej z tak pięknie rozpoczętym modułem? O tym postaram się napisać w drugiej części.