December 2006 - Posts
[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
}
I had a really strange effect in PowerShell that puzzled me for hours!
I have the following script:
$a = @'
One
Two
'@
$a
$p = [regex]"One"
$p.Replace($a, "OneReplaced")
$b = get-content -path templ.txt
$b
$q = [regex]"One"
$q.Replace($b, "OneReplaced")
And a file templ.txt containing the following text:
One
Two
When I execute the script I get the following output:
One
Two
OneReplaced
Two
One
Two
OneReplaced Two
So what happens:
I initialize a variable $a with two lines of text: line 1: One, line 2: Two. When I display variable $a it shows One and Two on two seperate lines. I now replace One with OneReplaced. Output of the replacement is two lines of text. Line 1: OneReplaced, line 2: Two.
Everything ok so far.
I now read the contents of variable $b from the file templ.txt. This file contains two lines of text: line 1: One, line 2: Two. When I display variable $b it shows One and Two on two seperate lines. I now replace One with OneReplaced. Output of the replacement is ONE LINE of text: OneReplaced Two.
This is not what I expected.
After a lot of debugging I found out why this happened. When you do $b = get-content -path templ.txt you don't get a string back, but an object array. You can see that when you do: (get-content -path templ.txt).GetType(), this displays:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
If you inspect the Object[] variable $b, you see that $b[0] = "One" and $b[1] = "Two".
When the command $q.Replace($b, "OneReplaced") is executed, the variable $b of type Object[] is cast to a string. This cast combines the string objects in the Object[] by appending them with a space in the middle.
So what is the simple solution to all this: when reading the content, join all lines with a newline, as in the following code line:
$b = [string]::join([environment]::newline, (get-content -path templ.txt))
Such a pity that this costed me 4 hours, I thought it was in the regular expression replacement:-(
But the good thing is that I can now solve that nasty bug in my Template Engine using PowerShell expressions.
Sometimes you need a simple thing like calculating the relative path of a file given its full path and a base path. For example you have a file c:\a\b\c\d\e.doc and a base path c:\a\b\c, the relative path is now d\e.doc. I use the following PowerShell function to do this, actually using only .Net framework commands;-)
function global:RelativePath
{
param
(
[string]$path = $(throw "Missing: path"),
[string]$basepath = $(throw "Missing: base path")
)
return [system.io.path]::GetFullPath($path).SubString([system.io.path]::GetFullPath($basepath).Length + 1)
}
Note that I use GetFullPath to get rid of things like .. in a path, like in c:\a\b..\c\d\e.
I still did not found a good debugging environment for my PowerShell development. The thing I use most is a command I found in this blog post, part of a great blog post series on PowerShell debugging on the Windows PowerShell blog by the PowerShell team. The command allows you to set a breakpoint at any location in your script, and enter an interactive shell where you can do whatever you need to do. I made a minimal modification to the command so I can see at which breakpoint I am:
# Start-Debug (alias: bp)
# Stop running current script and go into interactive mode so values of variables can be inspected
function global:Start-Debug
{
param
(
$name = ""
)
$scriptName = $MyInvocation.ScriptName
function prompt
{
"Debugging [{0}]>" -f $(if ([String]::IsNullOrEmpty($scriptName)) { "globalscope:$name" } else { "$scriptName:$name" } )
}
$host.EnterNestedPrompt()
}
Set-Alias bp Start-Debug -Scope "global"
You can now set a breakpoint in your code by adding just the command bp, or by adding a parameter like in bp "new piece of code" so you get a prompt indicating at which breakpoint you are.
If you enter the nested prompt you can do things like listing all variables with the command ls variable:*, or show the values of all ther currently defined environment variables with ls env:*.
When you construct a path in powershell with for example Join-Path, you can get things like ".."in your path.
For example:
Join-Path -Path "c:\program files" -ChildPath "..\Temp"
results in:
"c:\program files\..\Temp"
Instead of:
"c:\Temp"
I had to solve this problem, and now came up with the below dirty solution. Any cleaner solutions are appreciated;-)
# CleanPathName
# Clean a given path from elements like .. in the path and trailing '\',
# so c:\program files\..\temp\ becomes c:\temp.
# The given Path must exist.
# Should be rewritten if a cleaner approach is found
function global:CleanPathName
{
param
(
[string]$path = $(throw "Missing: path")
)
$orgLocation = Get-Location
Set-Location -Path $path
$cleanPath = Get-Location
# restore original location
Set-Location -Path $orgLocation
return $cleanPath.Path
}
[NOTE: Because this page is the first hit in Google when you search on Powershell + enum, and I landed on this page too often myself, I decided to expand the page with some additional information]
Scripting is heaven when you can utilize the complete .Net framework. One thing that was not directly clear for me was how to use enum values when calling .Net functions. It happened to be really easy, just cast the string representative of the enum value.
$myString = "/A/B/C//D/E//F/G"
$myParts = $myString.Split("/", [System.StringSplitOptions]"RemoveEmptyEntries")
results in an array of A,B,C,D,E,F,G
UPDATE: it happens to be even easier, you can say:
[System.Text.RegularExpressions.RegexOptions]::Singleline
And you can even binary-or them together:
[System.Text.RegularExpressions.RegexOptions]::Singleline -bor [System.Text.RegularExpressions.RegexOptions]::ExplicitCapture
It is also possible that an enum is defined within an enclosed type, in this case use [<namespace>.<enclosing type>+<nested type>]::EnumValue (thanks Alex)
For example:
[Microsoft.SharePoint.SPViewCollection+SPViewType]::Gantt
It is also possible to create a new real .net enum from PowerShell script. See http://blogs.msdn.com/powershell/archive/2007/01/23/how-to-create-enum-in-powershell.aspx
And in PowerShell 2.0 you can do it even cleaner: http://thepowershellguy.com/blogs/posh/archive/2008/06/02/powershell-v2-ctp2-making-custom-enums-using-add-type.aspx
PowerShell does .Net objects. And for a web service you can create a .Net object wrapper using wsdl.exe. Combine those two and you are in business. Great way to write utilities against the extensive set of SharePoint web services.
See the following weblog entries for all the details you need to get started:
Keith Hill released the PowerShell Community Extensions version 1.0 at http://www.codeplex.com/PowerShellCX. This well documented set of extra CmdLets, aliases and scripts provides you with a lot of goodies that make life in both PowerShell as a Shell and PowerShell as a scripting language way easier.
One of the nice goodies is the Get-CmdletMaml cmdlet which reflects over a snapin assembly and produces PowerShell MAML help. It uses a number of attributes defined by PowerShell and .NET as well as some defined by PSCX. Maml is an XML format to describe help on PowerShell cmdlets and their arguments.
It also contains the latest version of TabExpansion functionality I wrote about in this blog post.
See the Documentation for details on all the new commands. Check this out if you live your life in PowerShell.
And by the way: CodePlex provides source code to the goodies, so there is a lot of code here to learn from how to do PowerShell development!
One of those things that can take you ages to find out: I create a function where a parameters that can either have a string value or can be $null:
function doit
{
param
(
[string]$a = $( throw "Missing: parameter a"),
$b = $( throw "Missing: parameter b")
)
if ($a -eq $null) { Write-Host "a is null" } else { write-Host "a is not null" }
if ($b -eq $null) { Write-Host "b is null" } else { Write-Host "b is not null" }
}
If I call this function with: doit $null $null
I get the following result:
a is not null
b is null
What happened: because I specified a type [string] for parameter a, the $null value gets casted to a string with value "".
When working in script languages where declaration of variables is not used there is always the problem of typos in names. PowerShell has a possibility to use 'strict" mode: when a variable is used without an initial assignment you get an error:
set-psdebug -strict -trace 0
But now I have problems with third-party scripts that check for the existance of variables by comparing them to $null like this:
if ($var -eq $null) { ... }
This throws an error in strict mode.
To solve this problem use the variable provider. The variable provider gives you access to all variables. There is also a provider for functions, the environment variables, etc. You can even write your own providers.
Check this out:
ls variable:*
ls function:*
ls env:*
To prevent the error in the variable existance check do the following:
if (!(Test-Path variable:var)) { ... }
More Posts
Next page »