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
}
Posted by svdoever | 2 comment(s)
Filed under: , ,

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.

Posted by svdoever | 5 comment(s)
Filed under: ,

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.

Posted by svdoever | 3 comment(s)
Filed under: ,

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:*.

Posted by svdoever | with no comments
Filed under: ,

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
}

Posted by svdoever | 4 comment(s)
Filed under: ,

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, and you can even binary-or them together:

[System.Text.RegularExpressions.RegexOptions]::Singleline -bor [System.Text.RegularExpressions.RegexOptions]::ExplicitCapture

Posted by svdoever | 3 comment(s)
Filed under: ,

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:

Posted by svdoever | with no comments
Filed under: ,

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!

Posted by svdoever | with no comments
Filed under: ,

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

Posted by svdoever | 5 comment(s)
Filed under: ,

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)) { ... }

Posted by svdoever | with no comments
Filed under: ,
More Posts Next page »