O'Reilly logo

Windows PowerShell for Developers by Douglas Finke

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Accelerating Delivery

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.

Scanning for const Definitions

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.

Reading a Single C# File

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.

Using Select-String

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.

Reading C# Files in a Directory

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.

Working with Template Engines

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.

The Engine

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.

A Single Variable

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>

Multiple Variables

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;}

Multiple Templates

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.

Complex Logic

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.

UML Style Syntax

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;}

Reading XML

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#.

Bonus Round

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.

Generating PowerShell Functions from C# Methods

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
Inject a GUI in your pipeline—showing methods on a C# object
Figure 4-1. Inject a GUI in your pipeline—showing methods on a C# object

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

Get Parameters

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
Showing C# parameters from method signatures
Figure 4-2. Showing C# parameters from method signatures

Pulling It All Together

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 inspection

  • Add a Where filter parameter, to be used when the methods are piped from GetMethods

  • 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.

Calling PowerShell Functions from C#

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.

Overriding C# Methods with PowerShell Functions

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.

The Breakdown

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.

Looking for PowerShell Functions

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.

Extracting Metadata and Generating C#

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.

The PowerShell Module

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.

Testing It All

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.

Summary

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.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required