Van start met MSH – bouw een wordcount cmdlet Introductie Microsoft Command Shell of kortweg MSH, ook gekend onder de codenaam Monad, vormt de volgende-generatie commandolijnomgeving voor het Windows-platform. In dit artikel bekijken we hoe je deze omgeving kan uitbreiden met eigen cmdlets (uitgesproken command-lets). Wat is MSH? MSH is een nieuwe commandolijn voor Windows die volledig gebaseerd is op .NET en gegroeid is uit WMIC. Het doel van deze nieuwe omgeving is de beheerbaarheid van applicaties via de commandolijn gemakkelijker te maken zowel vanuit het perspectief van de IT professional als de ontwikkelaar. Zo is het bijvoorbeeld de bedoeling om alles wat via een UI kan gedaan worden voor het beheer van een applicatie ook via de commandolijn toegankelijk – en dus scriptable – te maken. Eén van de eerste Microsoftproducten die volledig “MSH klaar” zal zijn, is Exchange 12. Samen met MMC 3.0 vormt Monad de hoeksteen van applicatie manageability in de nabije toekomst. Om MSH te downloaden ga je naar http://www.microsoft.com/downloads en zoek je op “Monad”. De versie die we in dit artikel zullen gebruiken is beta 3.1 en draait op het .NET Framework 2.0 RTM. Hoe werkt MSH? In elke commandolijnomgeving is er uiteraard nood aan een commando-concept. In Monad wordt dit gerealiseerd via zogenaamde cmdlets, in feite lichtgewicht commando’s. Dergelijke cmdlets zijn geen uitvoerbare bestanden maar instanties van .NET klassen en bevatten vaak slechts weinig code. Zo zal een cmdlet typisch gebruikt worden om achterliggende beheerstaken toegankelijk te maken vanuit de shell, zoals het aanmaken van een mailbox of een website of het beheren van een gebruiker. De globale architectuur van MSH wordt weergegeven in figuur 1. Bovenaan vinden we de “hosting interfaces” terug die de functionaliteit van MSH beschikbaar maken voor programma’s. De Monad Shell msh.exe is één van de programma’s die de “Monad Engine” host, maar ook andere programma’s kunnen dit doen (zoals MMC’s). De “Monad Engine” bestaat uit een gelaagde architectuur met een parser (waardoor cmdlets niet meer zelf verantwoordelijk zijn voor parsing), een pipeline processor om cmdlets aan elkaar te rijgen en een command processor die de uitvoering van cmdlets regelt. Dit alles wordt verder ondersteund via een aantal services zoals error handling, statusbeheer en een “Extended Type System” dat toegang tot zaken zoals properties en methoden op objecten regelt. Other Hosts Monad Shell Hosting Interfaces Script & Command Parser Pipeline Processor Command Processor Error & Event Handler Extended Type System Session State Monad Engine Cmdlets werken puur met objecten in plaats van strings in een klassieke commandolijnomgeving. Zo zal de cmdlet get-process bijvoorbeeld instanties van het type System.Diagnostics.Process teruggeven. De teruggegeven objecten kunnen dan via een pipeline doorgegeven worden aan een andere cmdlet, bijvoorbeeld om de resultaten te filteren en te sorteren, of administratieve acties te ondernemen. Een paar voorbeeldjes zijn weergegeven in figuur 2. MSH C:\ > get-process | sort-object -property CPU -Descending | selectobject -First 5 Handles ------846 461 2255 900 601 NPM(K) -----24 18 0 24 27 PM(K) ----57456 34784 0 32792 31076 WS(K) VS(M) ----- ----82336 307 62352 472 240 2 46980 146 25516 124 CPU(s) -----139.27 95.50 80.06 74.44 45.91 Id -888 6104 4 1972 5824 ProcessName ----------devenv WINWORD System explorer msnmsgr MSH C:\> $p = get-process | where { $_.ProcessName -eq “notepad” } MSH C:\> $p.Kill() MSH C:\ > get-process e* | stop-process -whatif What if: Performing operation "stop-process" on What if: Performing operation "stop-process" on (3676)". What if: Performing operation "stop-process" on (3692)". What if: Performing operation "stop-process" on What if: Performing operation "stop-process" on (1972)". Target "ehmsas (4392)". Target "ehrecvr Target "ehSched Target "ehtray (420)". Target "explorer Naast cmdlets zijn er ook providers. Deze vormen de interface naar een hiërarchische bron van gegevens, zoals een mappenstructuur maar bijvoorbeeld ook zaken zoals het register, Active Directory, een database en noem maar op. Ooit gedacht dat u met het het commando cd door om het even welke databron zou kunnen wandelen? Neem een kijkje in figuur 3 voor een voorbeeldje. MSH C:\> get-drive Name Provider CurrentLocation -----------------Alias Alias C FileSystem cert Certificate D FileSystem E FileSystem Env Environment Function Function HKCU Registry HKLM Registry Variable Variable X FileSystem Root ---- ------- C:\ \ D:\ E:\ HKEY_CURRENT_USER HKEY_LOCAL_MACHINE X:\ MSH C:\> cd HKLM:\SOFTWARE\Microsoft\.NETFramework MSH HKLM:\SOFTWARE\Microsoft\.NETFramework> dir v* Hive: Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework SKC --1 1 1 2 VC -0 0 0 0 Name ---v1.0 v1.0.3705 v1.1.4322 v2.0.50727 Property -------{} {} {} {} MSH HKLM:\SOFTWARE\Microsoft\.NETFramework> Om een globaal overzicht te krijgen van de mogelijkheden van MSH verwijzen we graag door naar de Hands-On Labs die ook via de Microsoft Download site beschikbaar zijn onder de noemer “Windows Monad Shell Beta 3 Documentation Pack”. Een eigen cmdlet bouwen: waar te beginnen? In dit artikel zullen we ons concentreren op het bouwen van een eigen cmdlet die de functionaliteit van het commando wc uit UNIX naar MSH brengt. Het commando wc wordt gebuikt om het aantal lijnen, woorden en karakters uit een bestand te tellen. Laten we beginnen met het aanmaken van een skelet voor onze eerste cmdlet. Maak hiervoor een nieuw C# Class Library project aan in Visual Studio 2005 en geef het de naam “wordcount”. Stel vervolgens onder de eigenschappen van het project “Output path” in op de installatiemap van Monad (hier is dat C:\Program Files\Microsoft Command Shell\v1.0 – u dient msh.exe hierin terug te vinden), dit voor zowel de Debug als Release configuratie (zie figuur 4). Voeg tenslotte een referentie toe naar System.Management.Automation.dll uit de Monadinstallatiemap. Dit doet u door in de Solution Explorer rechts te klikken op References en Add Reference te kiezen. Onder het tabblad Browse kunt u dan de te refereren assembly aanduiden zoals weergegeven in figuur 5. Tijd om code te schrijven. Hernoem Class1.cs naar WordCount.cs en vervang de code door volgende basiscode: using System; using System.Management.Automation; [Cmdlet("get", "wordcount")] public class WordCount : MshCmdlet { } Het definiëren van een cmdlet bestaat erin over te erven van de basisklasse MshCmdlet en het attribuut Cmdlet toe te passen. Dit attribuut bevat twee parameters, een verb en een noun. In MSH worden deze door een koppelteken van elkaar gescheiden bij het oproepen van de cmdlet. In ons geval wordt dit dus get-wordcount. Compileer het project, waarmee u uw eerste (niet-functionele) cmdlet hebt gebouwd. De code vullen we later in maar eerst bouwen we een shell die onze cmdlet zal hosten. Start hiertoe de Microsoft Command Shell op en ga naar de installatiemap via cd. Bouw nu een nieuwe shell via make-shell zoals weergegeven in figuur 6. MSH C:\Program Files\Microsoft Command Shell\v1.0> make-shell -out demo –ns MSDN.Demos -ref wordcount.dll Microsoft(R) Command Shell MakeKit Copyright (C) 2005 Microsoft Corporation. All rights reserved. Shell demo.exe is created successfully. MSH C:\Program Files\Microsoft Command Shell\v1.0> Make-shell is de cmdlet voor de MakeKit tool waarmee nieuwe shell executables kunnen gecreëerd worden. Op deze manier kunt u bijvoorbeeld een speciale shell bouwen voor het beheer van een serverapplicatie. Onze oproep naar make-shell bouwt een nieuwe shell genaamd demo.exe in de (shell-)namespace MSDN.Demos en refereert de assembly wordcount.dll waarin onze cmdlet zich bevindt. Herinner u dat we eerder het uitvoerpad van ons Visual Studio 2005 project ingesteld hadden op de installatiemap van MSH waardoor wordcount.dll meteen op de juiste plaats staat. Om de nieuwe shell correct te laten werken dienen we deze eerst te registeren in het register. We kunnen dit rechtstreeks vanuit MSH uitvoeren dankzij de registry provider (zie figuur 7). MSH C:\Program Files\Microsoft Command Shell\v1.0> cd HKLM:\SOFTWARE\Microsoft\MSH\1\ShellIds MSH HKLM:\SOFTWARE\Microsoft\MSH\1\ShellIds> new-item -type directory MSDN.Demos.demo Hive: Microsoft.Management.Automation.Core\Registry::HKEY_LOCAL_MACHINE\SOFT WARE\Microsoft\MSH\1\ShellIds SKC --0 VC Name -- ---0 MSDN.Demos.demo Property -------{} MSH HKLM:\SOFTWARE\Microsoft\MSH\1\ShellIds> cd MSDN.Demos.demo MSH HKLM:\SOFTWARE\Microsoft\MSH\1\ShellIds\MSDN.Demos.demo> newproperty -path . -property path -value 'C:\Program Files\Microsoft Command Shell\v1.0\demo.exe' MshPath : Microsoft.Management.Automation.Core\Registry::HKEY_LOCAL_MACHI NE\SOFTWARE\Microsoft\MSH\1\ShellIds\MSDN.Demos.demo MshParentPath : Microsoft.Management.Automation.Core\Registry::HKEY_LOCAL_MACHI NE\SOFTWARE\Microsoft\MSH\1\ShellIds MshChildName : MSDN.Demos.demo MshDrive : HKLM MshProvider path : Microsoft.Management.Automation.Core\Registry : C:\Program Files\Microsoft Command Shell\v1.0\demo.exe Ga nu via Verkenner naar de MSH-installatiemap en start demo.exe op. In deze shell wordt get-wordcount reeds herkend, hoewel het resultaat nog niet bijster interessant is: MSH C:\Program Files\Microsoft Command Shell\v1.0> get-wordcount MSH C:\Program Files\Microsoft Command Shell\v1.0> Sluit deze shell terug af. De cmdlet parameteriseren... Tijd voor het echte werk. We beginnen met het definiëren van de parameters voor onze cmdlet: private string[] files; /// <summary> /// Lijst met bestanden waarop “wordcount” uitgevoerd zal worden. /// </summary> [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] public string[] FullName { get { return files; } set { files = value; } } De parameters worden als properties gedeclareerd. Voor de werking van onze cmdlet hebben we slechts één parameter nodig die alle te behandelen bestanden bevat in een array. Alle parameters worden gemarkeerd via een Parameter-attribuut. Met de property Mandatory stellen we in dat de FullName-parameter verplicht is. Via Position = 0 wordt de parameter als eerste ingesteld. Tot slot wordt ValueFromPipelineByPropertyName ingesteld waardoor pipelining mogelijk wordt gemaakt. Hierdoor zal het mogelijk worden de uitvoer van get-childitems (of kortweg dir) te gebruiken als invoer voor onze cmdlet. Om dit mogelijk te maken werd de parameternaam overigens als FullName ingesteld. ...en implementeren Nu is het tijd voor de eigenlijke implementatie. Deze bevindt zich in de methode ProcessRecord die opgeroepen wordt voor elk record dat behandeld moet worden. Bij een alleenstaande aanroep wordt deze methode slechts éénmaal opgeroepen. Is de cmdlet echter geschakeld in een pipelinestructuur dan wordt deze voor elk doorgegeven object opgeroepen. /// <summary> /// Doe lijn-, woord- en karakter-telling. /// </summary> protected override void ProcessRecord() { // // Resultaat-object dat zal worden teruggegeven. // WordCountResult res = new WordCountResult(); // // Behandel elk van de opgegeven ‘bestanden’. // foreach (string file in files) { ProviderInfo provider; // // Ondersteuning voor wildcard-syntax. // foreach (string path in GetResolvedProviderPathFromMshPath(file, out provider) ) { // // Ondersteuning voor -whatif en -confirm opties. // if (ShouldProcess("Count words in " + path)) { // // Doe de eigenlijke verwerking (hulpmethode). // Process(path, res); // // Ondersteuning voor -verbose optie. // WriteVerbose(path + " " + res); } } } // // Rapporteer het resultaat. // WriteObject(res); } Deze code voert volgende taken uit. Eerst wordt een object klaargemaakt dat teruggegeven zal worden. Vermits MSH werkt met objecten kan elk type hiervoor gebruikt worden. De definitie van dit type is terug te vinden in figuur 8. /// <summary> /// Represents a result from the wordcount cmdlet. /// </summary> public class WordCountResult { /// <summary> /// The number of lines count. /// </summary> public int Lines; /// <summary> /// The number of words count. /// </summary> public int Words; /// <summary> /// The number of characters count. /// </summary> public int Characters; /// <summary> /// Provides a friendly string representation for the object. /// </summary> /// <returns>String with line, word and character count information.</returns> public override string ToString() { return Lines + " " + Words + " " + Characters; } } Vervolgens wordt elk bestand behandeld, afkomstig uit de parameter-array. Vermits het ook mogelijk is via wildcards te werken en we nood hebben een concrete paden om het bestand in te lezen, moeten deze omgezet worden naar een absoluut pad. Dit gebeurt via in de geneste foreach-lus die GetResolvedProviderPathFromMshPath oproept om een lijst van te behandelen bestanden te verkrijgen. Hierna wordt gecontroleerd of het huidige item effectief behandeld dient te worden, wat aan de gebruiker gevraagd kan worden via een -confirm parameter. Het antwoord van de gebruiker is toegankelijk via een oproep naar ShouldProcess. Om dit mogelijk te maken dient het Cmdlet attribuut echter aangepast te worden als volgt: [Cmdlet("get", "wordcount", SupportsShouldProcess = true)] Uiteindelijk wordt een helpermethode opgeroepen om het eigenlijke werk te verrichten en het resultaatobject in te vullen (zie figuur 9 voor de code hiervan). De oproep naar WriteVerbose wordt gebruikt om de optie -verbose te ondersteunen, wat handig is om meer informatie te krijgen over wat het commando aan het doen is. private void Process(string file, WordCountResult res) { // // Gebruik Disposable patroon voor veilig gebruik van resources. // using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read)) using (StreamReader reader = new StreamReader(fs)) { try { for (string line; (line = reader.ReadLine()) != null; res.Lines++) { bool firstWordFound = false; bool inWord = false; foreach (char c in line) { res.Characters++; if (!firstWordFound && !char.IsWhiteSpace(c)) { firstWordFound = true; inWord = true; res.Words++; } if (firstWordFound) { if (inWord && char.IsWhiteSpace(c)) inWord = false; if (!inWord && !char.IsWhiteSpace(c)) { inWord = true; res.Words++; } } } } } catch (Exception ex) { // // Rapporteer een fout aan de MSH engine. // WriteError(new ErrorRecord(ex, "FileError", ErrorCategory.ObjectNotFound, file)); } } } En om te besluiten wordt het resultaat aan de gebruiker teruggegeven via WriteObject waarna het kan afgedrukt worden op het scherm of doorgegeven kan worden in de pipeline. Onze cmdlet in actie Als alles goed loopt, is onze cmdlet nu na compilatie volledig functioneel. Tijd om te compileren en demo.exe op te starten dus. In de installatiemap van MSH staan een heleboel .txt bestanden met informatie over diverse cmdlets. Laten we even onze getwordcount cmdlet hierop loslaten: MSH C:\Program Files\Microsoft Command Shell\v1.0> get-wordcount About_Alias.help.txt Lines Words Characters Files ------------------ ----157 1038 6503 {about_Alias.hel... Om het typwerk te verlichten zullen we eerst een alias creëren via set-alias: MSH C:\Program Files\Microsoft Command Shell\v1.0> set-alias wc getwordcount Nu kunnen we ook gewoon wc gebruiken om de cmdlet op te roepen. Om u ervan te vergewissen dat get-wordcount effectief een object teruggeeft, probeert u het volgende: MSH C:\Program Files\Microsoft Command Shell\v1.0> $count = wc about_Alias.help.txt MSH C:\Program Files\Microsoft Command Shell\v1.0> $count Lines ----157 {about_Alias.hel... Words ----1038 Characters Files ---------- ----6503 MSH C:\Program Files\Microsoft Command Shell\v1.0> "Aantal woorden: " + $count.Words Aantal woorden: 1038 En wat met wildcards? Een voorbeeldje: MSH C:\Program Files\Microsoft Command Shell\v1.0> wc *.txt Lines ----6334 Words ----36314 Characters Files ---------- ----250462 {*.txt} Met deze oproep wordt get-wordcount slechts éénmaal opgeroepen met één bestandsnaam, namelijk *.txt. Uiteindelijk wordt dus slechts één WordCountResultobject teruggegeven in dit geval. Bij het gebruik van pipelining ligt de situatie anders: MSH C:\Program Files\Microsoft Command Shell\v1.0> get-childitem *.txt | wc Lines ----- Words ----- Characters Files ---------- ----- 157 1038 6503 {C:\Program 75 490 3319 {C:\Program 248 1611 9841 {C:\Program File... File... File... ... Nu geeft get-childitem telkens een object terug dat netjes wordt doorgegeven aan de getwordcount cmdlet. Hierdoor worden bestanden één per één behandeld. Veronderstel dat we nu het totale woorden willen tellen voor alle bestanden tesamen, dan kunnen we foreach inschakelen: MSH C:\Program Files\Microsoft Command Shell\v1.0> get-item *.txt | get-wordcount | foreach { $x = 0 } { $x += $_.Words } { "Total words: " + $x } Total words: 36314 Of waarom geen gebruik maken van het WordCountResult-object? MSH C:\Program Files\Microsoft Command Shell\v1.0> get-item *.txt | get-wordcount | foreach { $x = new-object WordCountResult } { $x.Words += $_.Words; $x.Characters += $_.Characters; $x.Lines += $_.Lines } { "Total: " + $x } Total: 6334 36314 250462 Conclusie Naast het bouwen van generieke cmdlets zoals het get-wordcount voorbeeld zijn applicatiespecifieke cmdlets niet te versmaden om de beheerbaarheid van een applicatie te verhogen. Vandaag kunt u zich reeds voorbereiden op deze evolutie door goede interfaces te definiëren voor de klassen die instaan voor management taken. Later kunt u deze dan ook via MSH toegankelijk maken onder de vorm van cmdlets en voorgoed vriendschap sluiten met de collega’s uit het IT-departement.