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:
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:
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:
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:
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í:
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:
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:
Preto je potrebné zabezpečiť, aby volanie metódy ProcessDatabase bolo volané
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:
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.