Van start met MSH – bouw een wordcount cmdlet

advertisement
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.
Download