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
}

11 Comments

  • Alo

    I looks like this template is not supported:

    [[
    $a=10
    for ($i=0; $i -lt 3; $i++)
    {
    ]]
    [[]$a + $i]

    [[
    }
    ]]

  • Hi khuongdp, you are right, the PowerShell code to be executed must stand on its own, so construct like you showed are not possible.

  • Any chance of updating this so that the scenario khuongdp showed would be possible? That would make this so much more kickass ;)

    I'm still going to use this though - Thanks for making this public!

  • Dies ist ein gro�er Ort. Ich m�chte hier noch einmal.

  • hello
    pls how ican save 11 variable
    and apper 24h later ?
    tnx for you help

    arenas

  • I get these errors when running in powershell 2.0

    Cannot convert argument "1", with value: "System.Object[]", for "GetMethod" to type "System.Reflection.BindingFlags": "Cannot convert the "System.Obj
    ect" value of type "System.RuntimeType" to type "System.Reflection.BindingFlags"."
    At Template-Expand.ps1:130 char:62
    + [Management.Automation.LanguagePrimitives].GetMethod <<<< (
    + CategoryInfo : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

    Exception calling "Replace" with "2" argument(s): "Common Language Runtime detected an invalid program."
    At Template-Expand.ps1:188 char:33
    + $expandedText = $pattern.Replace <<<< ($text, $matchEvaluatorDelegate)
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

  • Found a solution. Changing this line and it will work in powershell 2

    From:
    $convertMethod = [Management.Automation.LanguagePrimitives].GetMethod("ConvertTo", $signature);

    To:
    $convertMethod = [Management.Automation.LanguagePrimitives].GetMethod("ConvertTo", [Type[]]$signature);

  • Wonderful post. I learned many interesting things. Thank you)

  • GxTWLW Appreciate you sharing, great post.Thanks Again. Will read on...

  • iBHTOF I appreciate you sharing this blog post.Really looking forward to read more. Keep writing.

  • Appreciate you sharing, great post.Thanks Again. Will read on...

Comments have been disabled for this content.