In this chapter weâll work through different types of text extraction and manipulation. This functionality ties into creating code generators, which take over the task of writing repetitive infrastructure code, thereby eliminating grunt work. PowerShellâs ability to work in this wayâreading text, XML, and DLL metadataâenables productivity and consistency while driving up the quality of the deliverable.
Being able to rip through numerous source code files looking for text in a specific context and extracting key information is super useful; primarily, it means we can locate key information quickly. Plus, because we can generate a .NET object with properties, we can easily pipe the results and do more work easily. For example, we can:
Export the results to a CSV and do analysis on them with Excel.
Catalog strings for use by QA/QC.
Create lists of classes, methods, and functions.
Letâs get started exploring this useful and timesaving functionality.
The examples in this section read C# files looking for strings containing the word const
, extracting the variable name and value. Scanning for strings across files takes many formsâsearching SQL files, PowerShell scripts, JavaScript files, and HTML files, just to name a few. Once the information is extracted, you can use it again in many waysâfor example, cataloging strings for internationalization, analyzing code, creating indexes of class methods and functions, and locating global variables. The list goes on and on.
public const int Name = "Dog"; const double Freezing = 32;
This reader will look for const
definitions in C# files like the previous one and produce the following output:
FileName Name Value -------- ---- ----- test1.cs Name "Dog" test1.cs Freezing 32
I will show two versions of the code. The first will read a single file, and the second will search a directory for all C# files and process them. Both examples are nearly identical, differing only in how I work with the Select-String
cmdlet.
This is an example of a single C# file, test.cs. It has three const
variables definedâtwo scoped at the class level and one at the method level.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { public const string Test = "test"; public const int TestX = 1; static void Main(string[] args) { const double PI = 3.14; } } }
Next up, weâll cover the PowerShell script to scan and extract the const
pattern.
Itâs import to note that we are doing pattern matching here, not parsing. If one of these lines of code is in a comment, this reader will find it because it cannot tell the difference between a comment and a ârealâ line of code.
The reader will find these const
definitions and then output them in this format. This is an array of PowerShell objects, each having three properties: FileName
, Name
, and Value
.
$regex = "\s+const\s+\w+\s+(?<name>.*)\s+=\s+(?<value>.*);" Select-String $regex .\test.cs | ForEach { $fileName = $_.Path ForEach($match in $_.Matches) { New-Object PSObject -Property @{ FileName = $fileName Name = $match.Groups["name"].Value Value = $match.Groups["value"].Value } } }
Here is the result:
FileName Name Value -------- ---- ----- test.cs Test "test" test.cs TestX 1 test.cs PI 3.14
Select-String
finds text in files or strings. For UNIX folks, this is equivalent to grep
. In this example, we are using a regular expression with the named groups "name"
and "value"
. Select-String
can also find text using the âSimpleMatch
keyword, meaning Select-String
will not interpret the pattern as a regular expression statement.
So, the parameters weâre passing are the pattern and filename. If matches are found, they are piped to a ForEach
. We capture the $fileName
from the property $_.Path
($_
is the current item in the pipeline) and then pipe the matches ($_.Matches)
to another ForEach
. In the ForEach
we create a new PSObject
on the fly with three properties, FileName
, Name
, and Value
. Where did Name
and Value
come from? They came from the named groups in the regular expression.
We extracted data and created a custom output type using Select-String
and New-Object PSObject
. We can rip through any text-based file, searching for information, and then present it as a .NET object with properties. We could have even piped this data to Export-Csv .\MyFile.CSV
, which converts it to comma-separated values (CSV) and saves it to a file. Then we could do an Invoke-Item .\MyFile.CSV
, opening the file in Excel, parsed and ready to go.
In this example, we use Select-String
again. The difference is weâre doing a dir
for files ending in .cs and then piping them to Select-String
. From there, the process is the same as before.
$regex = "\s+const\s+\w+\s+(?<name>.*)\s+=\s+(?<value>.*);" dir *.cs | Select-String $regex | ForEach { $fileName = $_.Path ForEach($match in $_.Matches) { New-Object PSObject -Property @{ FileName = $fileName Name = $match.Groups["name"].Value Value = $match.Groups["value"].Value } } }
Here is the result:
FileName Name Value -------- ---- ----- test.cs Test "test" test.cs TestX 1 test.cs PI 3.14 test1.cs Color "Red" test1.cs Name "Dog" test1.cs Freezing 32
PowerShell simplifies the process of traversing directories to search for patterns in the text and transforming the results into objects with properties. We could further pipe these results to other PowerShell built-in cmdlets or to our own functions in order to do all kinds of work for us.
Consider refactoring this script by varying either the regex or files you want to search for, but keeping the same type of output.
This is a two-foot dive into what you can do using PowerShellâs Select-String
, regular expressions, and objects with properties. There is an entire ocean of possibilities you can apply this extraction technique to with text files. Once the strings have been extracted and are in the form of a list of PowerShell objects, you can generate a wide variety of output, including HTML documentation and many other programmatic elements.
A template engine is software that is designed to process templates and content to produce output documents. Writing a simple template engine in PowerShell is straightforward. This approach lets us write many different types of templates in text and then leverage PowerShell to dynamically generate a fileâs content based on variables or more complex logic.
Template engines typically include features common to most high-level programming languages, with an emphasis on features for processing plain text. Such features include:
Variables and functions
Text replacement
File inclusion
Conditional evaluation and loops
Because we are using PowerShell to write the engine, we not only get all these benefits, but we can also use all of PowerShellâs features, cmdlets, and functionality.
The parameter $ScriptBlock
is the script block weâll pass in a later example. To execute it, we use the &
(call operator). Invoke-Template
supports the keyword Get-Template
. We define this keyword simply by creating a function named Get-Template
. Here we nest that function inside the Invoke-Template
function. Get-Template
take one parameter, $TemplateFileName
.
function Invoke-Template { param( [string]$Path, [Scriptblock]$ScriptBlock ) function Get-Template { param($TemplateFileName) $content = [IO.File]::ReadAllText( (Join-Path $Path $TemplateFileName) ) Invoke-Expression "@`"`r`n$content`r`n`"@" } & $ScriptBlock }
In essence, this example has three moving parts: the execution of the script block, which calls Get-Template
; the reading of that fileâs contents, using the .NET Frameworkâs System.IO.File.ReadAllText
static method; and finally, PowerShellâs Invoke-
Expression
, which evaluates the content just read as though it were a here-string.
Notice how Invoke-Template
takes a -ScriptBlock
as a second parameter. Practically speaking, Invoke-Template
is an internal domain-specific language (DSL), meaning we have the entire PowerShell ecosystem available to us and can get really creative inside this script block, calling cmdlets, getting templates, and generating code. This opens the door for lots of automation possibilities, saving us time and effort and reducing defects in our deliverables.
Letâs use the template engine in a simple example. I set up this template in a file called TestHtml.htm in the subdirectory etc.
<h1>Hello $name</h1>
We use an HTML tag plus PowerShell syntax to define the variable for replacement, $name
. Here are contents of the TestHtml.htm. Note, this is the verbose version. We explicitly specify the parameter names âPath
, -ScriptBlock
, and -TemplateName
.
# dot-source it . .\Invoke-Template.ps1 Invoke-Template -Path "$pwd\etc" -ScriptBlock { $name = "World" Get-Template -TemplateFileName TestHtml.htm }
Hereâs the terse approach, letting PowerShell bind the parameters:
# dot-source it . .\Invoke-Template.ps1 Invoke-Template "$pwd\etc" { $name = "World" Get-Template TestHtml.htm }
While the intent of code is clearer using named parameters, I prefer less typing and typically write my code as terse as possible. Both versions are valid because of PowerShellâs parameter binding.
Here is our result:
<h1>Hello World</h1>
Expanding on the theme of variable replacement, weâll replace two variables. The template is a blend of C# and PowerShell variables; after the variable replacement, itâll be a C# property.
public $type $name {get; set;}
And now, the script:
Invoke-Template "$pwd\etc" { $type = "string" $name = "FirstName" Get-Template properties.txt }
Invoke-Template
stitches the variables and template together, and I think it is important to extrapolate here. You can have any number of Invoke-Template
calls in a single script, each pointing to a different filepath for its set of templates. Plus, the code inside the script block can be far more involved in setting up numerous variables and calling Get-Template
multiple times, pulling in any number of templates.
Here is our result:
public string FirstName {get; set;}
Say we want to create both public and private C# variables. We do this by calling different templates. In this example, I am demoing multiple templates. I want to create two properties, a string FirstName
and a DateTime Date
. For the Date
property though, I want a get
and a private set
. I create a file in the etc directory called privateSet.txt and stub what I want to generate.
Here are the contents of Test-MultipleVariableTemplate.ps1:
# dot-source it . .\Invoke-Template.ps1 Invoke-Template "$pwd\etc" { $type = "string" $name = "FirstName" Get-Template properties.txt $type = "DateTime" $name = "Date" Get-Template privateSet.txt }
This is incredibly useful; for example, we can write PowerShell code that reads the schema of a SQL table, grabs the column names and datatypes, and generates an entire C# class that maps our table to an object. Yes, there are other tools that do this, but just a few lines of PowerShell will handle these key processes and give you control of the entire workflow. Plus, most off-the-shelf products canât always give us fine-grained control over the acquisition, processing, and output of the results. There are always exceptions.
Here is our result:
public string FirstName {get; set;} public DateTime Date {get; private set;}
This is just a small sampling of what is possible with Invoke-Template
. Itâs a very powerful way to organize simple text replacement and get a lot done. Now letâs move on to some more involved scripts.
In this example, weâre using the built-in Import-Csv
cmdlet to read a CSV (comma-separated value) file.
Type, Name string, LastName int, Age
Here, weâre piping the contents of the CSV to ForEach
, setting the appropriate variables, and finally calling the template properties.txt.
Invoke-Template "$pwd\etc" { Import-Csv $pwd\properties.csv | ForEach { $type = $_.Type $name = $_.Name Get-Template properties.txt } }
Here is our result:
public string LastName {get; set;} public int Age {get; set;}
The template is the same as the previous example, and the PowerShell script to create it is nearly identical, the main difference being that the input here is from a CSV file.
We can continue to add properties to the CSV file, rerun the script, and code generate as many C# properties as we need. With a little creativity, we might view this as a first step in code generating an entire C# class, ready for compilation.
To demonstrate how flexible PowerShell is, I created a file containing properties in UML syntax and then used the built-in PowerShell cmdlet Import-Csv
to read the file and convert it to an array of PowerShell objects, each having the properties Name
and Type
. By default, Import-Csv
reads the first line and uses it to name the properties. I override that by specifying Name
and Type
in the âHeader
property. I also override the default delimiter â,
â setting the âDelimiter
property to â:
â.
LastName : string FirstName : string Age : int City : string State : string Zip : int . .\Invoke-Template.ps1 Invoke-Template "$pwd\etc" { Import-Csv -Path .\Uml.txt -Header "Name","Type" -Delimiter ":" | ForEach { $name = $_.Name $type = $_.Type Get-Template properties.txt } }
With a little imagination, you can work up a number of interesting, useful formats that make it simple to represent information and then transform it into many other types of output.
Here is our result:
public string LastName {get; set;} public string FirstName {get; set;} public int Age {get; set;} public string City {get; set;} public string State {get; set;} public int Zip {get; set;}
PowerShell is not limited to reading CSV files, so we have options. As a developer, I use XML as a part of my daily diet. Here, Iâll play off the previous example of generating C# properties, this time using XML to drive the input to the process.
<properties> <property> <type>string</type> <name>City</name> </property> <property> <type>string</type> <name>State</name> </property> <property> <type>string</type> <name>Zip</name> </property> </properties>
Letâs read the XML and convert it:
Invoke-Template "$pwd\etc" { ([xml](Get-Content .\Properties.xml)).properties.property | ForEach { $type = $_.type $name = $_.name Get-Template properties.txt } }
This is the same script as the complex logic version in the previous example, but instead of reading from a CSV file with Import-Csv
, we now read the file using Get-Content
, applying the PowerShell [xml]
accelerator and dot notation over the nodes.
Here is the result:
public string City {get; set;} public string State {get; set;} public string Zip {get; set;}
There it isâthe transformation of XML data into C #properties. The separation of the text being replaced from the PowerShell that processes the input really highlights the essence of using PowerShell. This handful of scripts processes and transforms information into very readable and maintainable C#.
Next weâll invoke all three scripts one after the other. The PowerShell engine takes care of handling the output from all of them. Weâre bringing together information from three disparate sources.
.\Test-MultipleVariableTemplate.ps1 .\Test-ComplexLogicTemplate.ps1 .\Test-ReadXMLTemplate.ps1
We can easily pipe this to Set-Content Person.cs, and we are well on our way to generating code that compiles. Hereâs the result:
public string FirstName {get; set;} public string LastName {get; set;} public int Age {get; set;} public string City {get; set;} public string State {get; set;} public string Zip {get; set;}
Using template engines and PowerShell, we have tremendous reach. We can pull information from numerous sourcesâa database, Excel, a web service, or a web page, just to name a few. Plus, we can call Get-Template
multiple times in the same script, each instance pointing to different templates, and produce a number of different outputs.
Next, weâre going to compile a C# class, MyMath
, on the fly, using the built-in Add-Type
cmdlet. Note, Add-Type
also lets us load either a DLL or C# source file. Now we have a new type, MyMath
, loaded in our PowerShell session. We can use the methods on the .NET Frameworkâs System.Type
class, like GetMethods()
, on this type to get information.
$code = @" public class MyMath { public int MyAdd(int n1, int n2) { return n1 + n2; } public int MySubtract(int n1, int n2) { return n1 - n2; } public int MyMultiply(int n1, int n2) { return n1 * n2; } public int MyDivide(int n1, int n2) { return n1 / n2; } public void MyTest() {System.Console.WriteLine("Test");} } "@ Add-Type -TypeDefinition $code
Here we take the output of GetMethods()
and display it in a GUI using Out-GridView
(see Figure 4-1).
[MyMath].GetMethods() | Where {$_.Name -like "My*"} | Out-GridView
As you know, PowerShell is based on .NET, so here we tap into the framework and use GetMethods()
on the type MyMath
. First, weâll create the variable $code
to hold our C# class and its methods. Then, Add-Type
will compile the code in the current PowerShell session. Lastly, we use brackets []
around the name of our class MyMath
, indicating to PowerShell that it is a type, and then we can call GetMethods()
. I frequently use this approach when working with C# code/DLLs at the command line. I have used the âlong formâ of the code in the script example for clarity. When I do this at the command line, I like the pithy version better because it saves time, effort, and keystrokes.
In PowerShell v3, it gets simplerâcleaner, less noise, fewer keystrokes, and more essence. Here the Where
syntax loses the curly braces, and the $_
:
[MyMath].GetMethods() | Where Name -like "My*" | Out-GridView
Now weâll take the last line of PowerShell from the previous example and pipe it to ForEach
, calling the .NET GetParameters()
method. Then weâll pipe it to Out-GridView
and get a nice summary of parameter information on MyMath
code implementation, as shown in Figure 4-2.
[MyMath].GetMethods() | Where {$_.Name -like "My*"} | ForEach { $_.GetParameters() } | Out-GridView
If we wanted, we could type this by hand to get full access to MyMath
in PowerShell. PowerShell is an automation platform; Iâm a lazy coder, so Iâll write a script to make that happen.
$MyMath = New-Object MyMath function Invoke-MyAdd ($n1, $n2) {$MyMath.MyAdd($n1, $n2)} function Invoke-MySubtract ($n1, $n2) {$MyMath.MySubtract($n1, $n2)} function Invoke-MyMultiply ($n1, $n2) {$MyMath.MyMultiply($n1, $n2)} function Invoke-MyDivide ($n1, $n2) {$MyMath.MyDivide($n1, $n2)} function Invoke-MyTest () {$MyMath.MyTest()}
Wrapping MyMath
in PowerShell functions is a gateway to many capabilities. For example, we can interact with MyMath
at the command line or in scripts, write tests, and pipe results to the rest of the PowerShell ecosystem. PowerShell enables us to compose code in ways we canât in a system language like C#. In this simple example, I let PowerShell handle parameters through parameter binding so I can focus less on mechanics and more on problem solving:
Invoke-MyAdd 1 3 1..10 | ForEach {Invoke-MyAdd $_ $_} | ForEach {Invoke-MyMultiply $_ $_}
Iâve shown PowerShell code that can get the methods and parameters for an object that is loaded into a PowerShell session. The next script will combine these, and using a here-string, will create the PowerShell functions that fully wrap MyMath
signatures in a PowerShell way.
One line gets a bit funky, however. In the Get-Parameter
function, I have "`$$($_.Name)"
; this is needed in order to generate the $n1
. I use the PowerShell escape character `
before the first $
; otherwise, PowerShell would interpret that as $$
. That is a PowerShell automatic variable, which contains the last token in the last line received. The $($_.Name)
is a subexpression, and is a simple rule to memorize when you want to expand variables in strings.
function Get-Parameter ($target) { ($target.GetParameters() | ForEach { "`$$($_.Name)" } ) -join ", " } @" `$MyMath = New-Object MyMath $([MyMath].GetMethods() | Where {$_.Name -like "My*"} | ForEach { $params = Get-Parameter $_ @" function Invoke-$($_.Name) ($params) {`$MyMath.$($_.Name)($($params))} "@ }) "@
Generating PowerShell wrappers is a scalable approach, as compared to manually transforming the C# method signatures to PowerShell functions. In addition, if our C# code is still changing, we have a single script solution to wrapping our C# functions and make them PowerShell ready. Again, this saves us time and effort, and weâll have fewer finger errors.
Here is our result:
function Invoke-MyAdd ($n1, $n2) {$MyMath.MyAdd($n1, $n2)} function Invoke-MySubtract ($n1, $n2) {$MyMath.MySubtract($n1, $n2)} function Invoke-MyMultiply ($n1, $n2) {$MyMath.MyMultiply($n1, $n2)} function Invoke-MyDivide ($n1, $n2) {$MyMath.MyDivide($n1, $n2)} function Invoke-MyTest () {$MyMath.MyTest()}
This example is for illustration purposes. With some additional thought and work, though, we can make it generic by parameterizing the final snippet. We can:
Add a
$Type
parameter, which lets us pass in any type for inspectionAdd a
Where
filter parameter, to be used when the methods are piped fromGetMethods
Add a variable name parameter, so we donât have to hardcode
$MyMath
One final thought: the text manipulation tools that PowerShell brings to the table are invaluable for doing many types of transforms. In the next sections, youâll see a few more. These ideas are not new. PowerShellâs deep integration into Windows and the .NET Framework are what makes it possible for developers to optimize their efforts.
Next, weâll compile more C# and then create a custom object rather than a PSModuleInfo
object using New-Module
and the âAsCustomObject
property. Weâll create a single PowerShell function called test
and store it in the variable $module
so we can pass it to the constructor in the C# class. Finally, weâll call the C# InvokeTestMethod
. InvokeTestMethod
looks up the PowerShell test
function in the module that was passed in the constructor. If the function is found, Invoke
is called, all the ducks line up, and PowerShell prints "Hello World"
.
Note
This next example using Add-Type
will work if youâre using PowerShell v3.
If you are using PowerShell v2 and have not added powershell.exe.config to point to .NET 4.0, see Appendix A.
If youâre not sure what version of the .NET runtime your session is using, type $PSVersionTable
and look for the CLRVersion
entry.
Add-Type @" using System.Management.Automation; public class InvokePSModuleMethod { PSObject module; public InvokePSModuleMethod(PSObject module) { this.module = module; } public void InvokeTestMethod() { var method = module.Methods["test"]; if(method != null) method.Invoke(); } } "@ $module = New-Module -AsCustomObject { function test { "Hello World" | Out-Host } } (New-Object InvokePSModuleMethod $module).InvokeTestMethod()
Thatâs a long trek to get Hello World printed; we could have just typed "Hello World"
at the command line, after all. But thereâs a method to the madness.
In the next section, we will use these pieces to create a visitor that uses PowerShell v3âs new access to the abstract syntax tree (AST). We will read PowerShell source code and extract information by parsing it, not just scanning for text patterns.
Note
A hat tip goes to Jason Shirk, one of the PowerShell teamâs language experts, who shared the technique.
OK, Iâve shown you how to pull out the metadata from compiled C# code and generate PowerShell functions to wrap it. This is extremely useful when youâre exploring a new .NET DLL. We can quickly extract key information about the component and start kicking the tires right from the command line. Plus, because the .NET component is wrapped in PowerShell functions, we can seamlessly plug into the PowerShell ecosystem, further optimizing our time and effort. For example, if the component returns arrays of objects, we can use the Where
, Group
, and Measure
cmdlets to filter and summarize information rapidly.
Now weâll move on to overriding C# base class methods with PowerShell functions.
The next example extracts metadata from a .NET DLL, generates C# methods overriding the base class methods, and creates a constructor that takes a PowerShell module.
Each of the C# methods doing the override uses the technique in the previous section to look up the method in the PowerShell module and call it with the correct parameters.
Iâm using the AST capabilities of PowerShell v3 to demonstrate the technique of extracting method signatures from C# and then injecting a PowerShell module to override the implementation. This is valid for PowerShell v2 and can be applied to .NET solutions employing inheritance.
Iâm going to break this script down into a few sections: the metadata extraction of the PowerShell v3 AstVisitor
methods, the subsequent C# code generation that puts the PowerShell âhooksâ in place, and the creation of the PowerShell custom object using New-Module
. This example will have a PowerShell function called VisitFunction
and mirrors the method I override in the base class AstVisitor
. This PowerShell function will be called each time a function is found in our source script. VisitFunction
takes $ast
as a parameter and contains all the information about the function that has been matched in our source script. Iâll be pulling out only the name and line number where it was matched.
In this source script, we want to find where all the functions are defined.
function test1 {"Say Hello"} 1..10 | % {$_} function test2 {"Say Goodbye"} 1..10 | % {$_} function test3 {"Another function"} #function test4 {"This is a comment"}
We can see three functions named test1
, test2
, test3
, and they are on lines 1, 3, and 5. The last function, test4
, is a comment. I included it for two reasons. First, if we were scanning the file using Select-String
and pattern matching on function
, this would show up in the results and be misleading. Second, with the AST approach, test4
will be recognized as a comment and therefore not included in the results of our search for functions.
While it is easy to scan a file visually, if Iâm looking at a large script with many functions, Iâd like an automated way to know what and where my functions are. Plus, if I can extract this information programmatically, the potential is there to automate many other activities.
Here weâll generate something a little more complex, leveraging the Invoke-Template
we built before. The goal is to create a C# class that has all of the override methods found in System.Management.Automation.Language.AstVisitor
. This is equivalent to being in Visual Studio, inheriting from AstVisitor
, overriding each method, and then providing an implementation.
public override AstVisitAction $FunctionName($ParameterName ast) { var method = module.Methods["$FunctionName"]; if (method != null) { method.Invoke(ast); } return AstVisitAction.Continue; }
The implementation we want to provide, for each overridden method, is a lookup for that function name in the module/custom object passed from PowerShell. If one is found, weâll invoke it and pass it the AST for the declaration being visited.
[System.Management.Automation.Language.AstVisitor].GetMethods() | Where { $_.Name -like 'Visit*' } | ForEach { $functionName = $_.Name $parameterName = $_.GetParameters()[0].ParameterType.Name Get-Template AstVisitAction.txt }
This is the template that gets it done; the file is named AstVisitAction.txt.
Now we move on to the PowerShell code snippet thatâll figure out the FunctionName
and ParameterName
and invoke the template that does the code generation.
The GetMethods()
method returns a list of methods on the Type System.Management.Automation.Language.AstVisitor
. Weâre filtering the list of methods to only the ones whose names begin with Visit*
âthat is, Where { $_.Name -like 'Visit*' }
. In the ForEach
, we grab the name of the function $_.Name
and the name of the parameter type being passed to it, $_.GetParameters()[0].ParameterType.Name
.
using System; using System.Management.Automation; using System.Management.Automation.Language; public class CommandMatcher : AstVisitor { PSObject module; public CommandMatcher(PSObject module) { this.module = module; } $methodOverrides }
The template sets up references, a constructor, and a backing store for the module being passed in. The key piece is the $methodOverrides
variable. This will contain all the text generated from the previous template, AstVisitAction.txt.
. .\Invoke-Template.ps1 Invoke-Template $pwd\etc { $methodOverrides = Invoke-Template $pwd\etc { [System.Management.Automation.Language.AstVisitor].GetMethods() | Where { $_.Name -like 'Visit*' } | ForEach { $functionName = $_.Name $parameterName = $_.GetParameters()[0].ParameterType.Name Get-Template AstVisitAction.txt } } Get-Template CommandMatcher.txt }
This is the completed script that generates a C# class ready for compilation. This class handles visiting any PowerShell source, calling out to a PowerShell function to handle the node that is visited. Weâll go over that next.
Fortunately, itâs not necessary to understand the recursive descent parser mechanism. The fundamental point here is the metadata extraction and code generation, which is the glide path to using the Add-Type
cmdlet and compiling useful code on the fly in the current context.
Now that we have code-generated all of the overrides for the base class AstVisitor
, we will create a PowerShell module to pass to it that will be called back every time a PowerShell function definition is detected.
$m = New-Module -AsCustomObject { $script:FunctionList = @() function VisitFunctionDefinition ($ast) { $script:FunctionList += New-Object PSObject -Property @{ Kind = "Function" Name = $ast.Name StartLineNumber = $ast.Extent.StartLineNumber } } function GetFunctionList {$script:FunctionList} }
We store this in the variable $m
, and will pass it to the constructor later.
I added a helper function, GetFunctionList
, which returns the script scoped variable. FunctionList
is initialized to an empty array to start and is populated in VisitFunctionDefinition
.
Each time a function declaration is matched, the PowerShell function VisitFunctionDefinition
is invoked. We then emit a PowerShell object with three parameters, Kind
, Name
, and StartLineNumber
. We hardcode Kind
, for simplicity, and get the other two values from the data passed in the $ast
variable.
Weâll now create a reusable helper function that takes a PowerShell script and returns the AST that can be âvisitedâ; letâs call it Get-Ast
. Next, weâll ânewâ up the CommandMatcher
we built in C# during the code-generation phase and pass in $m
, which contains our PowerShell module with the function we want to invoke. The variable $ast
contains the AST of the script passed in the here-string. The variable $ast
is a System.Management.Automation.Language.ScriptBlockAst
, and the method we want to invoke is Visit()
. We will pass $matcher
, our custom visitor, to it. Finally, we will call $m.GetFunctionList()
, displaying the details about the functions that were found.
function Get-Ast { param([string]$script) [System.Management.Automation.Language.Parser]::ParseInput( $script, [ref]$null, [ref]$null ) } $matcher = New-Object CommandMatcher $m $ast = Get-Ast @' function test {"Say Hello"} 1..10 | % {$_} function test1 {"Say Goodbye"} 1..10 | % {$_} function test2 {"Another function"} '@ $ast.Visit($matcher) $m.GetFunctionList()
This correctly finds the three functions in our test script, displaying the name of the function and the line it is on as follows:
Name StartLineNumber Kind ---- --------------- ---- test 1 Function test1 3 Function test2 5 Function
You can easily rework this to process a single script or an entire directory of scripts. In addition, you can add a filename as a property, thus enabling filtering of function names and filenames. This way, we can semantically scan any number of PowerShell scripts for a particular function name and quickly locate the file and line number where it lives.
We can also add more functions to the PowerShell module to match on parameters, variable expressions, and more. From there, we could create a new PSObject
with the properties we wanted and then weâd have a list of key information about our scripts that we could programmatically act on.
Using PowerShellâs System.Management.Automation.Language
library like this is only one application of what the library can do. There is a lot to explore here that is beyond the scope of this book. If youâre familiar with the tool ReSharper from JetBrains and its ability to refactor C# code, youâll have an idea of the potential of System.Management.Automation.Language
. For example, you could use it to rename part of a PowerShell function name and ripple that change through an entire script accurately. Another example is extracting a section of PowerShell code as a function, naming it, adding it to the script, and replacing where it came from with the new function name. Doing static analysis along the lines of the lint tool PSLint (http://bit.ly/bI9sLz)? No problem with System.Management.Automation.Language
.
This doesnât come for free. You need to learn the ins and outs of this library. There is much potential here for some great open source tools for PowerShell as well as opportunities to learn more about what this platform offers.
In this chapter, I showed several ways to use PowerShell to work with information, transform it, and position it for consumption elsewhere. The information was stored in C# files and text files, and it was even extracted directly from compiled DLLs. These ideas can also be extended to SQL Server schemas, XML, JSON, and even Microsoft Excel. Because itâs based on .NET, PowerShell easily integrates with all of these tools.
As a developer, I reuse and expand these approaches for every project I work on. I actively seek out patterns in the workflow and automate them. This has numerous benefits. Code generation has been around as long as software languages. PowerShellâs deep integration to the .NET platform and its game-changing object pipeline optimizes the development effort. Being able to crack open a DLL and inspect methods and parametersâall from within a subexpression in a here-stringâand then compile it on the fly in a single page of code enables developers to iterate through ideas more rapidly.
Finally, being able to extend C# solutions by invoking PowerShellâand here is the keyâwithout having to touch the original C# code, is huge. As you might know, scripting languages are sometimes referred to as glue languages or system integration languages. PowerShell, being based on .NET, takes this definition to a whole new level.
Get Windows PowerShell for Developers now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.