Laden...

[Artikel] PowerShell Cmdlet's

Erstellt von egrath vor 17 Jahren Letzter Beitrag vor 17 Jahren 26.819 Views
egrath Themenstarter:in
871 Beiträge seit 2005
vor 17 Jahren
[Artikel] PowerShell Cmdlet's

Die Entwicklung eines Microsoft PowerShell CmdLets

Unter einem Cmdlet versteht man eine zusätzliche erweiterung der in der PowerShell integrierten Befehle. Alle Befehle welche standardmässig mitgeliefert werden sind Cmdlets und sind in den folgenden DLL's enthalten:*Microsoft.PowerShell.Commands.Management.dll *Microsoft.PowerShell.Commands.Utility.dll

Zu finden sind diese (und noch weitere, später in diesem Artikel benötigten) DLL's im Verzeichnis "C:\Programme\Reference Assemblys\Microsoft\WindowsPowerShell\v1.0"

Ein Cmdlet ist technisch gesehen also nichts anderes, als eine managed DLL welche eine (oder mehrere) Klasse(n) enthält die von PSCmdlet erbt, ein spezielles Attribut besitzt und die entsprechende Funktionalität implementiert. Wie dies in der Praxis aussieht wollen wir uns nun ansehen. Ziel soll es sein, ein Cmdlet zu entwickeln welches als Argument eine Windows NT Batch Datei entgegennimmt, diese analysiert und die entsprechenden SET Befehle in das PowerShell Equivalent umwandelt. Benutzt kann dieses dann beispielsweise werden um die Umgebungsvariablen für Visual Studio in der PS zu setzen.

Der Anfang (Das A-und-O des Cmdlets)

Beginnen wir ein neues Projekt und referenzieren folgende DLL's:*System.Management.Automation.dll

Diese ist leider nicht in der .NET Assembly Liste des Referenzdialogs eingetragen und muss daher händisch aus oben genanntem Verzeichnis eingebunden werden. Der erste Schritt besteht nun darin, die Klasse zu deklarieren welche die Funktionalität des Cmdlets enthält:


using System;
using System.Management.Automation;

namespace egrath.powershell.cmdlets
{
    [Cmdlet( VerbsCommon.Set, "Batchvars" )]
    public class SetBatchvarsCommand : PSCmdlet
    {
    }
}

Was ist hier besonderes zu beachten:

Die Klasse enthält das Attribut "Cmdlet", welches als Parameter den sg. Verb und Noun enthält. PS Cmdlets bestehen in deren Namen immer aus diesen beiden Kombinationen. Dabei ist das Verb das "Was" und der Noun das "Worauf". Es gibt folgenden Verb's:

Add, Clear, Copy, Get, Join, Lock, Move, New, Remove, Rename, Select, Set, Split, Unlock  

Man ist allerdings nicht gezwungen diese zu benutzen und kann als ersten Parameter auch einen String mit dem gewünschten Verb übergeben. Ich würde der Konsistenz wegen davon aber abraten. Der zweite Parameter ist das Noun und bezeichnet worauf das Cmdlet angewandt wird. Dieser Name ist frei vergebbar und sollte dem Sinn des Cmdlets entsprechen. In unserem Fall nennen wir unser Cmdlet also "Set-Batchvars".

Der Klassenname sollte das Verb und den Noun wiederspiegeln, gefolgt von "Command". Diese Nomiklatur ist nicht bindent, sollte aber trotzdem eingehalten werden.

Eingabe von Daten (Der Benutzer)

Wir benötigen als Parameter den Namen jenes Batch Jobs den wir bearbeiten sollen. Aus diesem Grund müssen wir uns einen entsprechenden Parameter definieren:


private string m_BatchName;

[Parameter( Mandatory=true, Position=0, HelpMessage="Name of NT Batch File")]
public string BatchName
{
    get
    {
        return m_BatchName;
    }

    set
    {
        m_BatchName = value;
    }
}

Die Attribute für die Property geben an, dass es sich dabei um einen notwendigen Parameter an Eingabeposition 0 handelt. Für den Fall dass dieser nicht auf der Kommandozeile übergeben wird, fordert die PS den Benutzer auf diesen anzugeben - und zeigt nach Wunsch des Benutzers den String der durch "HelpMessage" definiert wurde.

Bearbeiten von Daten (Das Arbeitspferd)

Die eigentliche Funktionalität welche unser Cmdlet implementieren soll, kann dadurch stattfinden, dass wir eine der drei folgenden Methoden überschreiben:*BeginProcessing - Wird beim starten des Cmdlets aufgerufen. *ProcessRecord - Wird für jeden Parameter welches sich in der Pipeline befindet aufgerufen. *EndProcessing - Wird beim beenden des Cmdlets aufgerufen.

Genauere Informationen über den Ablauf der Cmdlets gibt es in der MSDN im Artikel "Cmdlet Lifecycle". Für unseren Zweck reicht es, wenn wir die Methode BeginpProcessing überschreiben und darin die von uns gewünschte Arbeit erledigen:


protected override void BeginProcessing()
{
    Console.Out.WriteLine( "Beginning NT-Batch Processing ... {0}", m_BatchName );

    StreamReader reader = new StreamReader( m_BatchName );
    string batchContent = reader.ReadToEnd();
    reader.Close();

    batchContent = batchContent.Replace( "echo off", "echo on" );  // Turn on echoing

    Random rand = new Random();
    string tempFileName = String.Format( "{0}\\{1}.{2}.cmd",
                                        System.Environment.GetEnvironmentVariable( "TEMP" ),
                                        "batchtemp",
                                        rand.Next() );

    Console.Out.WriteLine( "Writing new Batch Job to temporary location: {0}", tempFileName );

    StreamWriter writer = new StreamWriter( tempFileName );
    writer.Write( batchContent );
    writer.Close();

    ProcessStartInfo batchProcessInfo = new ProcessStartInfo( String.Format( "{0}\\cmd.exe", System.Environment.GetFolderPath(Environment.SpecialFolder.System )));
    batchProcessInfo.Arguments = String.Format( "/c {0}", tempFileName );
    batchProcessInfo.RedirectStandardOutput = true;
    batchProcessInfo.UseShellExecute = false;
    batchProcessInfo.CreateNoWindow = true;

    Console.Out.WriteLine( "Executing: {0}", batchProcessInfo.FileName );

    Process batchProcess = new Process();
    batchProcess.StartInfo = batchProcessInfo;
    try
    {
        batchProcess.Start();
    }
    catch( Exception e )
    {
        Console.Out.WriteLine( e.ToString() );
        return;
    }

    // Reading all variables and put them into a dictionary
    string[] inputData = batchProcess.StandardOutput.ReadToEnd().Split( System.Environment.NewLine.ToCharArray() );
    Dictionary<string,string> varDict = new Dictionary<string,string>();
    foreach( string line in inputData )
    {
        string newLine;
        newLine = line.Replace( String.Format( "{0}>", System.Environment.CurrentDirectory ), String.Empty );
        if( newLine.Contains( "set" ))
        {
            // Initializing Tokenizer
            StringTokenizer tokenizer = new StringTokenizer();
            ArrayList seperators = new ArrayList();
            seperators.Add( "=" );
            seperators.Add( " " );
            tokenizer.Seperators = seperators;
            tokenizer.TokenString = newLine;
            tokenizer.ResetTokenizer();

            varDict.Add( tokenizer[1], tokenizer[2] );
        }
    }

    // Set all variables
    foreach( KeyValuePair<string,string> pair in varDict )
    {
        Console.Out.WriteLine( "Setting Variable [{0}] to [{1}]", pair.Key, pair.Value );
        PSVariable var = new PSVariable( pair.Key, pair.Value, ScopedItemOptions.None );
        this.SessionState.PSVariable.Set( var );
        WriteObject( var );
    }

    File.Delete( tempFileName );
    base.BeginProcessing();
}

Obiger Code erledigt im Prinzip folgende Arbeit: Zuerst wird der angegebene Batch-Job gelesen und so modifiziert, dass dieser mit eingeschaltenm echo ausgeführt wird. Anschliessend führen wir diesen aus und speichern die ausgabe in einer String Variable zwischen. Aus dieser lesen wir nun die SET Befehle, speichern diese in einem Dictionary zwischen und setzen diese im Anschluss in der PowerShell. Bitte zu beachten, dass ich beim Parsen der SET Zeilen die nicht inkludierte Klasse "StringTokenizer" verwende (welche sich aber natürlich im beiliegenden Quellcode befindet)

Das installieren und ausführen

Um unser Cmdlet nun auch ausführen zu können, bedienen wir uns im ersten Schritt des Tools "installutil" welches mit dem Framework mitinstalliert wird. Da dieses in der Assembly eine entsprechende Klasse erwartet, müssen wir natürlich auch diese implementieren. Das könnte für unser beispiel einfach so aussehen:


using System;
using System.ComponentModel;
using System.Management.Automation;

namespace egrath.powershell.cmdlets
{
    [RunInstaller( true )]
    public class SetBatchvarsSnapIn : PSSnapIn
    {
        public SetBatchvarsSnapIn() : base()
        {
        }

        public override string Name
        {
            get
            {
                return "SetBatchvarsSnapin";
            }
        }

        public override string Vendor
        {
            get
            {
                return "egrath";
            }
        }

        public override string VendorResource
        {
            get
            {
                return String.Format( "{0},{1}", Name, Vendor );
            }
        }

        public override string Description
        {
            get
            {
                return "The Set-Batchvars Cmdlet; Parses and executes NT Batch jobs and sets their variables in the PS";
            }
        }

        public override string DescriptionResource
        {
            get
            {
                return String.Format( "{0},{1}", Name, Description );
            }
        }
    }
}

Wir erben hierbei von PSSnapIn und überschreiben die entsprechenden Eigenschaften. In diesen setzen wir beispielsweise den Namen, den Hersteller und die Beschreibung. Diese Daten können später innerhalb der PS angezeigt werden.

Sobald wir nun unsere fertig übersetzte DLL vor uns haben installieren wir diese mit:

installutil dll-name

Das ist aber noch nicht alles. Mittels "get-pssnapin -registered" sollten wir nun überprüfen ob die Registrierung des Cmdlets auch funktioniert hat. Es sollte dieser (oder ein ähnlicher Output) dargestellt werden:


Name : SetBatchvarsSnapin
PSVersion : 1.0
Description : The Set-Batchvars Cmdlet; Parses and executes NT Batch jobs and sets their variables in the PS

Wenn dies der Fall ist, so müssen wir das Cmdlet noch zur Liste der Snap-In's hinzufügen. Auch dies geschieht mit einem PS Befehl:

add-pssnapin SetBatchvarsSnapin

So das wars fürs erste. Das Cmdlet kann nun wie ein gewöhnlicher Befehl ausgeführt werden. Da diese Prozedur allerdings etwas umständlich ist, können wir auch die gesamten Einstellungen der aktuellen PS Sitzung exportieren und später beim starten der PS erneut aufrufen. Damit ersparen wir uns obige Punkte. Das geschieht auf folgendem Weg:

export-console C:\mysettings.psc1

Die Verknüpfung über die die PS gestartet wird, erweitern wir nun um den Parameter "-PSConsoleFile c:\mysettings.psc1". Damit wird das Settings File beim starten geparst und die Einstellungen entsprechend vorgenommen.

Der komplette Quellcode des Artikels kann hier heruntergeladen werden. Viel Spass beim rumexperimentieren!