Programovanie CmdLet-ov pre PowerShell v C# (2.)

Publikoval Michal Kočí dňa 2.7.2006 o 04:54 v kategórii PowerShell

V tomto príspevku, ktorý nadväzuje na predchádzajúci (Programovanie CmdLet-ov pre PowerShell v C#), sa posunieme pri tvorbe CmdLet-ov pre PowerShell (ďalej len PS) ďalej a ukážeme si najmä, ako cez pipeline prijímať a posielať objekty. Zmodifikujeme CmdLet Get-MsSqlDatabase aby posielal objekty s informáciami o databázach a nie iba ich názvy a naprogramujeme CmdLet Get-MsSqlTable, ktorý bude vedieť tieto objekty prijímať a na základe informácií v nich uvedených zistí tabuľky príslušných databáz. Pozrieme sa tiež na to, ako je možné CmdLet napísať tak, aby vedel prijímať rôzne sady parametrov a venovať sa budeme aj veľmi jednoduchému formátovaniu výstupu CmdLetu v tabuľkovej podobe.

Ako prvé budeme prerábať CmdLet Get-MsSqlDatabase tak, aby do výstupu neposielal reťazce, ale trochu komplexnejšie informácie v podobe objektov. Objekty budú triedy MsSqlDatabase, ktorú si pripravíme a ktorá bude obsahovať tieto dátové položky:

  • Name - názov databázy
  • Id - jedinečný identifikátor databázy definovaný databázovým serverom
  • CreationDate - dátum vytvorenia databázy
  • FileName - názov primárneho dátového súboru databázy
  • ServerName - názov servera, kde sa databáza nachádza

Trieda bude pre uľahčenie práce obsahovať aj konštruktor umožňujúci nastavenie všetkých vlastností triedy. Kód triedy je nasledujúci:

public class MsSqlDatabase
{
  public MsSqlDatabase()
  {
  }

  public MsSqlDatabase(
    Int16 id, 
    string name, 
    DateTime creationDate, 
    string fileName, 
    string serverName)
  {
    this.id = id;
    this.name = name;
    this.creationDate = creationDate;
    this.fileName = fileName;
    this.serverName = serverName;
  }

  private string name;
  public string Name
  {
    get { return name; }
    set { name = value; }
  }

  private Int16 id;
  public Int16 Id
  {
    get { return id; }
    set { id = value; }
  }

  private DateTime creationDate;
  public DateTime CreationDate
  {
    get { return creationDate; }
    set { creationDate = value; }
  }

  private string fileName;
  public string FileName
  {
    get { return fileName; }
    set { fileName = value; }
  }

  private string serverName;
  public string ServerName
  {
    get { return serverName; }
    set { serverName = value; }
  }
}

Nasledovne ešte zmodifikujeme metódu EndProcessing, v ktorej pre každú databázu vytvoríme inštanciu triedy MsSqlDatabase a túto potom pošleme na výstup. Dotaz na detaily ohľadom databázy, ktorý je posielaný na databázový server, modifikovať netreba, pretože ten všetky detaily doťahoval už v predchádzajúcom príklade (príspevku). Uvedená metóda bude teda po prerobení vyzerať takto:

protected override void EndProcessing()
{
  string cs = string.Format(
    "Data Source={0};" +
    "Initial Catalog=master;" +
    "Integrated Security=SSPI;", 
    serverName);
  string sc = "select dbid, name, crdate, filename " +
    "from master..sysdatabases order by name";

  SqlConnection c = new SqlConnection(cs);
  SqlCommand cmd = new SqlCommand(sc, c);

  try
  {
    c.Open();
    SqlDataReader dr = cmd.ExecuteReader();

    if(dr.HasRows)
    {
      while(dr.Read())
      {
        Int16 id = (Int16) dr["dbid"];
        string name = (string) dr["name"];
        DateTime creationDate = (DateTime) dr["crdate"];
        string fileName = (string) dr["filename"];

        MsSqlDatabase db = new MsSqlDatabase(id, name, creationDate, fileName, serverName);

        WriteObject(db, true);
      }
    }

    dr.Dispose();
  }
  finally
  {
    cmd.Dispose();
    c.Close();
    c.Dispose();
  }
}

Ak by sme teraz vyskúšali CmdLet, videli by sme, že na výstup ozaj posiela objekty a že PS ich zobrazuje všetky. Tak sa teraz môžeme pustiť do vývoja CmdLet-u Get-MsSqlTable, ktorý vypíše tabuľky nachádzajúce sa v konkrétnej databáze konkrétneho databázového servera. Jeho výstupom budú inštancie triedy MsSqlTable, ktorej štruktúra je obdobná ako štruktúra triedy MsSqlDatabase. Obsahovať bude nasledovné vlastnosti:

  • Name - názov tabuľky
  • Id - jedinečný identifikátor tabuľky definovaný databázovým serverom
  • CreationDate - dátum vytvorenia
  • DatabaseName - názov databázy, v ktorej sa tabuľka nachádza
  • ServerName -  názov servera, kde sa databáza a teda aj tabuľka nachádza

A rovnako ako trieda MsSqlDatabase bude aj trieda MsSqlTable obsahovať konštruktor umožňujúci naplnenie všetkých vlastností už pri konštrukcii objektu:

public class MsSqlTable
{
  public MsSqlTable()
  {
  }

  public MsSqlTable(Int32 id, string name, DateTime creationDate, string serverName, string databaseName)
  {
    this.id = id;
    this.name = name;
    this.creationDate = creationDate;
    this.serverName = serverName;
    this.databaseName = databaseName;
  }

  private string name;
  public string Name
  {
    get { return name; }
    set { name = value; }
  }

  private Int32 id;
  public Int32 Id
  {
    get { return id; }
    set { id = value; }
  }

  private DateTime creationDate;
  public DateTime CreationDate
  {
    get { return creationDate; }
    set { creationDate = value; }
  }

  private string databaseName;
  public string DatabaseName
  {
    get { return databaseName; }
    set { databaseName = value; }
  }

  private string serverName;
  public string ServerName
  {
    get { return serverName; }
    set { serverName = value; }
  }
}

Teraz je vhodný okamih na vytvorenie si predstavy o novotvorenom CmdLet-e Get-MsSqlTable. Aby tento vedel vypísať tabuľky nejakej databázy, potrebuje vedieť jej meno a na ktorom databázovom serveri sa nachádza. Treba sa však rozhodnúť, ako bude CmdLet prijímať tieto informácie, pričom máme v zásade dve možnosti:

  • CmdLet prijme parametre z príkazového riadku. Možnou nevýhodou v istých situáciách môže byť nemožnosť prijatia informácie o viacerých rôznych databázach počas jediného volania CmdLet-u.
  • CmdLet prijme parametre z pipeline, konkrétne prijme inštancie triedy MsSqlDatabase, čím získa informácie o databáze aj o serveri. V tomto prípade je možné cez pipeline poslať jeden ale aj viacero objektov typu MsSqlDatabase.

Tretia možnosť je, že CmdLet bude naprogramovaný tak, aby umožňoval obe spomenuté možnosti avšak pri každom spustení iba jednu z nich. Postupne ukážem všetky tri možnosti a začnem prvou z nich a to je príjem parametrov z príkazoveho riadku. CmdLet potrebuje prijať dva parametre a to:

  • DatabaseName - názov databázy
  • ServerName - názov servera, na ktorom sa databáza nachádza

O spôsobe akým sa mapujú parametre predané z príkazoveho riadku na vlastnosti triedy sme si vraveli v minulej časti, preto ho objasňovať už nebudem a rovno oddemonštrujem tieto dve vlastnosti v podobe zdrojového kódu:

[Cmdlet(VerbsCommon.Get, "MsSqlTable")]
public class GetMsSqlTableCommand: Cmdlet
{
  private string serverName;
  [Parameter(Mandatory=true, Position=0, ValueFromPipeline=false)]
  public string ServerName
  {
    get { return serverName; }
    set { serverName = value; }
  }

  private string databaseName;
  [Parameter(Mandatory=true, Position=1, ValueFromPipeline=false)]
  public string DatabaseName
  {
    get { return databaseName; }
    set { databaseName = value; }
  }
}

Potrebujeme ešte naprogramovať samotné zistenie tabuliek a ich odoslanie z CmdLet-u na čo si naprogramujeme metódu ProcessDatabase prijimajúcu dva parametre a to názov databázy a názov servera. Túto potom budeme volať z metódy EndProcessing v prípade že CmdLet parametre prijme z príkazového riadku a z metódy ProcessRecord ak ich prijme z pipeline:

private void ProcessDatabase(string srvName, string dbName)
{
  string cs = "Data Source={0};Initial Catalog={1};Integrated Security=SSPI;";
  cs = string.Format(cs, srvName, dbName);
  string sc = "select id, name, crdate from dbo.sysobjects where xtype=''U''";

  SqlConnection c = new SqlConnection(cs);
  SqlCommand cmd = new SqlCommand(sc, c);

  try
  {
    c.Open();
    SqlDataReader dr = cmd.ExecuteReader();

    if (dr.HasRows)
    {
      while (dr.Read())
      {
        Int32 id = (Int32) dr["id"];
        string name = (string) dr["name"];
        DateTime creationDate = (DateTime) dr["crdate"];

        MsSqlTable tbl = new MsSqlTable(id, name, creationDate, srvName, dbName);

        WriteObject(tbl, true);
      }
    }

    dr.Dispose();
  }
  finally
  {
    cmd.Dispose();
    c.Close();
    c.Dispose();
  }
}

Metóda je opäť podobná tej, ktorá zisťuje názvy databáz v CmdLet-e Get-MsSqlDatabase. Rovnako sa prípaja v kontexte prihláseného užívateľa avšak tentoraz sa nepripája k databáze master, ale k databáze ktorej zoznam tabuliek chce užívateľ získať. Metóda pre každú nájdenú tabuľku pripraví objekt typu MsSqlTable a tento zapíše na výstup. Ako bolo spomenuté v minulom príspevku, metódu treba volať z metódy EndProcessing, pretože parametre prijímame z príkazového riadku:

private MsSqlDatabase database;
[Parameter(Mandatory=true, Position=0, ValueFromPipeline=true)]
public MsSqlDatabase Database
{
  get { return database; }
  set { database = value; }
}

Čo však, ak by sme chceli, aby CmdLet prijímal namiesto parametrov z príkazového riadku objekt typu MsSqlDatabase z pipeline? Potom treba aby CmdLet obsahoval vlastnosť, na ktorú bude parameter z pipeline namapovaný. Táto vlastnosť bude rovnako označená atribútom ParameterAttribute, avšak vlastnosť ValueFromPipeline tohto atribútu bude mať hodnotu true. PS sa potom postará o automatické namapovanie každého prijatého objektu z pipeline na túto vlastnosť a po namapovaní zavolá metódu ProcessRecord. Metódu zavolá presne toľko krát, koľko objektov prijme. Preto volanie metódy ProcessDatabase budeme volať z metódy ProcessRecord:

protected override void ProcessRecord()
{
  ProcessDatabase(database.ServerName, database.Name);
}

Teraz je čas na krátku rekapituláciu znalostí:

  • Ak chceme, aby CmdLet prijímal parametre z príkazovej, vlastnosti predsatavujúce tieto parametre sú označené atribútom ParameterAttribute a vlastnosť ValueFromPipeline týchto atribútov je nastavená na false. Samotné spracovanie je vhodné mať v metóde EndProcessing.
  • Ak naopak chceme, aby CmdLet prijímal parametre z pipeline, vlastnosť predsatavujúca tento parameter je označená atribútom ParameterAttribute a vlastnosť ValueFromPipeline týchto atribútov je nastavená na true. Spracovanie musí byť v metóde ProcessRecord, ktorá je vždy zavolaná po každom namapovaní parametra z pipeline na vlastnosť CmdLet-u.

Ukážme si teraz, ako je možné náš CmdLet zavolať z PowerShell. Ak chceme vedieť aké tabuľky obsahuje databáza db1 na serveri dbsrv1, potom ak máme CmdLet naprogramovaný prvým spôsobom (príjem parametrov z príkazového riadku), môžeme použiť nasledovný príkaz:

Get-MsSqlTable -ServerName dbsrv1 -DatabaseName db1

Ak by sme chceli zistiť názvy tabuliek zo všetkých databáz na serveri dbsrv1 a CmdLet je naprogramovaný druhým spôsobom (príjem parametrov z pipeline), dosiahneme to takto:

Get-MsSqlDatabase -ServerName . | Get-MsSqlTable

Ak máme CmdLet naprogramovaný druhým spôsobom, je samozrejme možné nechať si vypísať tabuľky aj len jednej databázy ak použijeme filtrovanie výstupu z CmdLet-u Get-MsSqlDatabase a necháme si vyfiltrovať iba požadovanú databázu db1:

Get-MsSqlDatabase -ServerName dbsrv1 | Where-Object { $_.Name -eq "db1" } | Get-MsSqlTable

Finálne rozhodnutie pri špecifikácii parametrov pri tvorbe CmdLet-u by mohlo byť, ktorý spôsob prijímania parametrov uprednostníme. Každý ma isté výhody a nevýhody. Samozrejme, že asi najvhodnejšie by bolo aby CmdLet vedel fungovať jedným aj druhým spôsobom. A to sa dá dosiahnuť použitím sád parametrov. Sada parametrov je množina parametrov, ktoré spolu súviasia a musia byť preto spolu použité, nastavené všetky v prípade že je nastavený aspoň jeden. CmdLet Get-MsSqlTable bude definovať dve sady parametrov:

  • Prvá sada parametrov bude pomenovaná ServerAndDatabaseNames a bude obsahovať vlastnosti ServerName a DatabaseName. Ak bude zadaný názov servera, bude musieť byť zadaný aj názov databázy a naopak. Ak bude použitá táto sada parametrov, bude sa CmdLet tváriť ako keby bol naprogramovaný prvým spôsobom (príjem parametrov z príkazového riadku)
  • Druhá sada parametrov bude pomenovaná DatabaseInfo a bude obsahovať vlastnosť Database, ktorá je mapovaná na pipeline. Ak bude cez pipeline poslaný objekt (poslané objkekty) typu MsSqlDatabase, potom PS sa rozhodne pre použitie tejto sady parametrov. Vtedy sa CmdLet bude tváriť, ako keby bol naprogramovaný druhým spôsobom (príjem parametrov z pipeline)

Konfigurácia sád parametrov je jednoduchá, stačí atribútom ParameterAttribute vlastností CmdLet-u pridať vlastnosť ParameterSetName. V takomto prípade bude CmdLet obsahovať tri parametre a každý bude zaradený do nejakej sady parametrov:

private string serverName;
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = false, ParameterSetName="ServerAndDatabaseNames")]
public string ServerName
{
  get { return serverName; }
  set { serverName = value; }
}

private string databaseName;
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = false, ParameterSetName = "ServerAndDatabaseNames")]
public string DatabaseName
{
  get { return databaseName; }
  set { databaseName = value; }
}

private MsSqlDatabase database;
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ParameterSetName = "DatabaseInfo")]
public MsSqlDatabase Database
{
  get { return database; }
  set { database = value; }
}

Je dobré vedieť, že v niektorých prípadoch môže byť vhodné nastaviť CmdLet-u, ktorá sada parametrov sa má použiť primárne (defaultne), pretože jedna vlastnosť CmdLet-u môže byť vo viacerých sadách parametrov. Môže byť označená atribútom ParameterAttribute viac krát a každý atribút môže mať iné hodnoty pre vlastnosti tohto atribútu.

Ak by sme len pridali názvy sád parametrov do ktorých parametre patria a metódy ProcessRecord a EndProcessing by sme jednoducho zobrali z predchádzajúcich riešení, CmdLet by nefungoval tak ako by sme očakávali, pretože:

  • Ak by bola použitá sada parametrov ServerAndDatabaseNames, tak by sa okrem správneho vykonania kódu (zavolanie metódy ProcessDatabase) v metóde EndProcessing vykonalo presne jeden krát volanie metódy ProcessRecord, ktoré rovnako volá metódu ProcessDatabase a toto by zlyhalo, keďže vlastnosť Database nie je namapovaná na parameter z pipeline.
  • Ak by bola použitá sada parametrov DatabaseInfo, tak by okrem správneho vykonania kódu (zavolanie metódy ProcessDatabase) v metóde ProcessRecord vykonalo presne raz volanie metódy EndProcessing, ktoré tiež volá metódu ProcessDatabase a toto by zlyhalo, keďže vlastnosti ServerName a DatabaseName nie sú namapované na parametre z príkazového riadku.

Preto je potrebné zabezpečiť, aby volanie metódy ProcessDatabase bolo volané

  • z metódy EndProcessing len ak je použitá sada parametrov ServerAndDatabaseNames
  • z metódy ProcessRecord len ak je použitá sada parametrov DatabaseInfo

Tým, že trieda GetMsSqlTableCommand dedí z triedy PsCmdlet (všimnite si že už nededí z triedy Cmdlet, pretože potrebuje rozšírenú funkcionalitu), obsahuje aj vlastnosť ParameterSetName, ktorú PS naplní názvom použitej sady parametrov. Túto vlastnosť je možné kontrolovať v metódach ProcessRecord a EndProcessing a samotné spracovanie vykonať len ak nadobúda nami požadovenú hodnotu. Potom metódy ProcessRecord a EndProcessing v tretej verzii CmdLet-u vyzerajú nasledovne:

protected override void ProcessRecord()
{
  if (ParameterSetName == "DatabaseInfo")
  {
    ProcessDatabase(database.ServerName, database.Name);
  }
}

protected override void EndProcessing()
{
  if (ParameterSetName == "ServerAndDatabaseNames")
  {
    ProcessDatabase(serverName, databaseName);
  }
}

Ostáva už len celý projekt prekopilovať, nainštalovať a zaregistrovať SnapIn v PS. Toto bolo tiež ukázané v predchádzajúcom príspevku. Minule som ešte slúbil, že ukážem ako sa dá modifikovať výstup CmdLet-u. Keďže dokumentácia k PS je ešte veľmi biedna, tak sa obmedzím na jednoduché formátovanie v podobe tabuľky. Keď si necháte zobraziť tabuľky jednej databázy napríklad nasledovným príkazom

Get-MsSqlTable -ServerName dbsrv1 -DatabaseName db1

tak ich PS zobrazí vo forme zoznamu, ktorý nie vždy musí byť pre Vás vyhovujúci. Je to ekvivalent príkazu:

Get-MsSqlTable -ServerName dbsrv1 -DatabaseName db1 | Format-List

Ak uprednostňujete výpis v podobe tabuľky, môžete si výstup nechať preformátvať nasledovným príkazom:

Get-MsSqlTable -ServerName dbsrv1 -DatabaseName db1 | Format-Table

Bohužial, nie každému musí vyhovovať podoba, v akej je tabuľkový výstup zobrazený, keďže údajov je v jednom riadku veľa a tieto sú potom krátené. Niektoré údaje možno zobrazovať primárne nechcete a niektoré chcete zobrazovať v inej podobe (napríklad dátum). V takomto prípade máte možnosť vytvoriť formátovací súbor a tento zaregistrovať v PS. Jednoduchý príklad pomocou ktorého si necháte zobraziť v tabuľkovej podobe iba dva stĺpce (názov tabuľky a dátum vytvorenia) a údaje si necháte zoskupiť podľa databázy je vidieť na nasledovnom XML súbore, ktorý je oným formátovacím súborom:

<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
  <ViewDefinitions>
    <View>
      <Name>MsSqlTable</Name>
      <ViewSelectedBy>
        <TypeName>Mifko.Ps.MsSqlSnapIn.MsSqlTable</TypeName>
      </ViewSelectedBy>
      <GroupBy>
        <PropertyName>DatabaseName</PropertyName>
      </GroupBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Label>Name</Label>
            <Width>32</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Creation Date</Label>
            <Width>14</Width>
            <Alignment>right</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>Name</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>$_.CreationDate.ToString("dd.MM.yyyy")</ScriptBlock>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

Súbor je vhodné pomenovať názvom SnapIn-u s príponou Fromat.ps1xml a mať ho uložený v tom istom adresári, kde sa nachádza dll súbor samotného SnapIn-u. V PS je potrebné ho zaregistrovať príkazom:

Update-FormatData -PrependPath "MsSqlPsSnapIn.Format.ps1xml"

Súbor nebude detailnejšie popisovať, nakoľko mi príde ako taký docela prehľadný, najmä kvôli tomu, že sa jedná o XML súbor. A koniec koncov, keďže chýba detailnejší popis formátu tohto súboru, pokročilejšie formátovanie je obvykle nutné spraviť metódou pokus-omyl a tú podrobne popisovať nemá zmysel.

Tak a to je všetko. Snáď Vás tento dvojpríspevkový popis programovania jednoduchých CmdLet-ov naštartoval a pevne dúfam, že sa mi podarilo ukázať aké jednoduché je rozšíriť si PowerShell o nový, na mieru šitý CmdLet. Celý projekt opätovne ponúkam vo forme zip súboru (MsSqlSnapIn.v2.0.zip, 17KB) a obsahuje nasledovné súbory:

  • MsSqlPsSnapIn.cs obsahuje SnapIn v podobe triedy MsSqlPsSnapIn
  • MsSqlDatabase.cs obsahuje triedu MsSqlDatabase, ktorá zapúzdruje informácie o databáze
  • MsSqlTable.cs obsahuje triedu MsSqlTable, ktorá zapúzdruje informácie o tabuľke
  • GetMsSqlDatabaseCommand.cs obsahuje CmdLet v podobe triedy GetMsSqlDatabaseCommand
  • GetMsSqlTableCommand.cs obsahuje CmdLet v podobe triedy GetMsSqlTableCommand - táto trieda je zakomentovaná, slúži len na demonštráciu ako by tento CmdLet vyzeral, ak by mal prijímať parametre iba z príkazového riadku
  • GetMsSqlTableCommand2.cs obsahuje CmdLet v podobe triedy GetMsSqlTableCommand - táto trieda je zakomentovaná, slúži len na demonštráciu ako by tento CmdLet vyzeral, ak by mal prijímať parametre iba pipeline
  • GetMsSqlTableCommand3.cs obsahuje CmdLet v podobe triedy GetMsSqlTableCommand - tento CmdLet prijíma parametre z príkazového riadku alebo z pipeline
  • MsSqlPsSnapIn.Format.ps1xml je jednoduchý ukážkový formátovací súbor

Mohlo by ťa tiež zaujímať

Páčil sa ti príspevok?

Zdieľaj príspevok alebo si ho odlož na neskôr

Sleduj ma

Ak nechceš premeškať príspevky ako je tento, sleduj ma na Twitteri, alebo ak máš RSS čítačku, môžeš sledovať môj RSS kanál.