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 }