A templating engine using PowerShell expressions

[Update Januari 5, 2008: fixed a small bug, content was always saved in encoding ANSI, resulting in the loss of special characters.

Changed the line:

Set-Content -Path $destination -value $expandedText

to

Set-Content -Path $destination -value $expandedText -encoding $Encoding

The attached file is updated as well.

]

While working on our Software Factory for SharePoint 2007 solutions I needed to do some simple template expansion. First choice would be to use the Text Templates from the DSL toolset as available in the Visual Studio 2005 SDK, and write the templates in the T4 template language. Problem is that the template expansion I need to do right now needs to expand variables, function  and expressions in the PowerShell language. So I created a small PowerShell script to implement the Template-Expand command to do just that. First some simple explanatory but useless examples:

$a='template'
function MyFunction($action) { if ($action -eq 1) { 'a function'} else { 'WRONG!' }}
./Template-Expand -text 'This is a [[$a]] test to execute [[MyFunction -action 1]] and to add 2+3=[[2+3]] '

Results in:

This is a template test to execute a function and to add 2+3=5

You can also assign the output to a variable like in the following example. In this example I changed the default left and right markers [[ and ]] to the same syntax  used in the T4 template language:

$result = ./Template-Expand -leftMarker '<#=' -rightMarker '#>' -text 'This is a <#= $a #> test to execute <#= MyFunction -action 1 #> and to add 2+3=<#= 2+3 #>'

The variable $result now contains the expanded template text.

Nota bene that the markers are used to construct our matching regular expression as follows: [regex]"$leftMarker(.*?)$rightMarker", so the marker strings must escape special regular expression characters. The default value for the left marker is for example "\[\[".

I also added some extra options, like the possibility to read the template from a file, and write the expanded template text to a destination file using the options -path and -destination.

If you have a template file template.txt with the following content:

<values>
    <value>
[[    
    $a=10
    $b=20
    $a*$b
    for ($i=0; $i -lt 3; $i++)
    {
        $i*5
        $i*10
    }
]]    
    </value>
</values>

You can execute the template expansion with the following command:

./template-expand -path template.txt -destination templateexpanded.txt

This will result in a file templateexpanded.txt with the following content:

<values>
    <value>
200 0 0 5 10 10 20    
    </value>
</values>

I know, the example is useless, but you get the drift;-) Important thing to notice in the example, expressions can consist of multiple lines!

You can also define functions within your template as in the following example:

[[
function SayHelloWorld
{
    "Hello world!"
}
]]
And then he said:
[[SayHelloWorld]]

If you want to have the configuration of variables and functions in a separate powershell file, use the -psConfigurationPath. The specified file (which must have a .ps1 extension) will be sourced, so variables and functions don't have to be defined in the global context.

Thanks to this blog entry by the PowerShell team I got the needed delegation stuff working.

And now the code, happy templating en let me know if it works for you or which features you are missing!!

Save the code below to Template-Expand.ps1. I also added this file as an attachment to this blog post.

---------------cut-----------------cut--------------cut-------------cut------------cut---------cut-------------

# ==============================================================================================
# 
# Microsoft PowerShell Source File -- Created with SAPIEN Technologies PrimalScript 4.1
# 
# NAME: Template-Expand.ps1
# 
# AUTHOR : Serge van den Oever, Macaw
# DATE   : December 30, 2006
# VERSION: 1.0
#
# I needed a MatchEvaluator delegate, and found an example at http://blogs.msdn.com/powershell/archive/2006/07/25/678259.aspx
# ==============================================================================================

# Template-Expand
# Simple templating engine to expand a given template text containing PowerShell expressions.
#
# Arguments:
# $text (optional): The text of the template to do the expansion on (use either $text or $path)
# $path (optional): Path to template to do the expansion on (use either $text or $path)
# $destination (optional): Destination path to write expansion result to. If not specified, the
#                           expansion result is result as text
# $psConfigurationPath (optional) : Path to file containing PowerShell code. File will be 
#                                   sources using ". file", so variables can be declared 
#                                   without global scope
# $leftMarker (optional): Left marker for detecting expand expression in template
# $rightMarker (optional): Right marker for detecting expand expression in template
# $encoding (optional): Encoding to use when reading the template file
#
# Simple usage usage: 
# $message="hello"; ./Template-Delegate -text 'I would like to say [[$message]] to the world'

param
(
    $text = $null,
    $path = $null,
    $destination = $null,
    $psConfigurationPath = $null,
    $leftMarker = "\[\[",
    $rightMarker = "]]",
    $Encoding = "UTF8"
)

# ==============================================================================================
# Code below from http://blogs.msdn.com/powershell/archive/2006/07/25/678259.aspx
# Creates a delegate scriptblock
# ==============================================================================================

# Helper function to emit an IL opcode
function emit
{
    param
    (
        $opcode = $(throw "Missing: opcode")
    )
    
    if ( ! ($op = [System.Reflection.Emit.OpCodes]::($opcode)))
    {
        throw "emit: opcode '$opcode' is undefined"
    }

    if ($args.Length -gt 0)
    {
        $ilg.Emit($op, $args[0])
    }
    else
    {
        $ilg.Emit($op)
    }
}

function GetDelegate
{
    param
    (
        [type]$type, 
        [ScriptBlock]$scriptBlock
    )

    # Get the method info for this delegate invoke...
    $delegateInvoke = $type.GetMethod("Invoke")
    
    # Get the argument type signature for the delegate invoke
    $parameters = @($delegateInvoke.GetParameters())
    $returnType = $delegateInvoke.ReturnParameter.ParameterType
    
    $argList = new-object Collections.ArrayList
    [void] $argList.Add([ScriptBlock])
    foreach ($p in $parameters)
    {
        [void] $argList.Add($p.ParameterType);
    }
    
    $dynMethod = new-object reflection.emit.dynamicmethod ("",
        $returnType, $argList.ToArray(), [object], $false)
    $ilg = $dynMethod.GetILGenerator()
    
    # Place the scriptblock on the stack for the method call
    emit Ldarg_0
    
    emit Ldc_I4 ($argList.Count - 1)  # Create the parameter array
    emit Newarr ([object])
    
    for ($opCount = 1; $opCount -lt $argList.Count; $opCount++)
    {
        emit Dup                    # Dup the array reference
        emit Ldc_I4 ($opCount - 1); # Load the index
        emit Ldarg $opCount         # Load the argument
        if ($argList[$opCount].IsValueType) # Box if necessary
     {
            emit Box $argList[$opCount]
     }
        emit Stelem ([object])  # Store it in the array
    }
    
    # Now emit the call to the ScriptBlock invoke method
    emit Call ([ScriptBlock].GetMethod("InvokeReturnAsIs"))
    
    if ($returnType -eq [void])
    {
        # If the return type is void, pop the returned object
        emit Pop
    }
    else
    {
        # Otherwise emit code to convert the result type which looks
        # like LanguagePrimitives.ConvertTo(value, type)
    
        $signature = [object], [type]
        $convertMethod =
            [Management.Automation.LanguagePrimitives].GetMethod(
                "ConvertTo", $signature);
        $GetTypeFromHandle = [Type].GetMethod("GetTypeFromHandle");
        emit Ldtoken $returnType  # And the return type token...
        emit Call $GetTypeFromHandle
        emit Call $convertMethod
    }
    emit Ret
    
    #
    # Now return a delegate from this dynamic method...
    #
    
    $dynMethod.CreateDelegate($type, $scriptBlock)
}

# ==============================================================================================

Write-Verbose "Template-Expand:"
if ($path -ne $null)
{
    if (!(Test-Path -Path $path))
    {
        throw "Template-Expand: path `'$path`' can't be found"
    }

    # Read text and join the returned Object[] with newlines
    $text = [string]::join([environment]::newline, (Get-Content -Path $path -Encoding $Encoding))
}

if ($text -eq $null)
{
    throw 'Template-Expand: template to expand should be specified through -text or -path option'
}

if ($psConfigurationPath -ne $null)
{
    # Source the powershell configuration, so we don't have to declare variables in the 
    # configuration globally
    if (!(Test-Path -Path $psConfigurationPath))
    {
        throw "Template-Expand: psConfigurationPath `'$psConfigurationPath`' can't be found"
    }
    . $psConfigurationPath
}

$pattern = New-Object -Type System.Text.RegularExpressions.Regex `
                      -ArgumentList "$leftMarker(.*?)$rightMarker",([System.Text.RegularExpressions.RegexOptions]::Singleline)
$matchEvaluatorDelegate = GetDelegate `
    System.Text.RegularExpressions.MatchEvaluator {
           $match = $args[0]
           $expression = $match.get_Groups()[1].Value # content between markers
           Write-Verbose "  -- expanding expression: $expression"
           trap { Write-Error "Expansion on template `'$name`' failed. Can't evaluate expression `'$expression`'. The following error occured: $_"; break }
           Invoke-Expression -command $expression
    }
# Execute the pattern replacements and return the result
$expandedText = $pattern.Replace($text, $matchEvaluatorDelegate)

if ($destination -eq $null)
{
    # Return as string
    $expandedText 
}
else
{
    Set-Content -Path $destination -value $expandedText -encoding $Encoding
}
Published Sunday, December 31, 2006 2:40 AM by svdoever
Filed under: , ,

Comments

No Comments