Arild's profileRandom thoughts about An...BlogLists Tools Help

Blog


    September 13

    Powershell MSBuild task

    UPDATE: The source and installer for this task can now be found at Codeplex: 
    http://powershellmsbuild.codeplex.com/ 

    I have written an MSBuild task that hosts a Powershell runspace and allows you to embed Powershell scripts in your MSBuild build files. While MSBuild ships with a large number of useful tasks (and there are also a lot of third party tasks available), there is often something you want to do which doesn't fit into the tasks provided. You can always write your own tasks to achieve what you want, but this can often become tedious to maintain since you will have to recompile and redeploy the custom task every time you need to make a little change. Having small snippets of script embedded in the MSBuild file itself can be a more easily maintainable solution, and what better language for this than Powershell?

    As a custom task, the Powershell task needs to be loaded into your build file with the <UsingTask> element:

    <UsingTask AssemblyFile="PowershellMSBuildTask.dll" TaskName ="Powershell"/>

    Since tasks in MSBuild don't have access to the contents of their tags, the scripts themselves need to be MSBuild properties, declared in a <PropertyGroup>:

    <PropertyGroup> 
    	<HelloWorldScript>Write-Host "Hello world"</HelloWorldScript> 
    </PropertyGroup>

    Now, to invoke this script, you will use the Powershell task and invoke it from within a target. You reference the script property the same way you reference any other MSBuild script property:

    <Target Name="HelloWorld">
      <Powershell Script="$(HelloWorldScript)"/>
    </Target>

    The full MSBuild file will now be something like this:

    <?xml version="1.0" encoding="utf-8" ?>
    <Project DefaultTargets="HelloWorld" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    	<UsingTask AssemblyFile="PowershellMSBuildTask.dll" TaskName ="Powershell"/>
    
    	<PropertyGroup> 
    		<HelloWorldScript>Write-Host "Hello world"</HelloWorldScript> 
    	</PropertyGroup>
    
    	<Target Name="HelloWorld">
    		<Powershell Script="$(HelloWorldScript)"/>
    	</Target>
    </Project>

    Save it as HelloWorld.proj, place PowershellMSBuildTask.dll in the same directory and run it like this:

    msbuild HelloWorld.proj

    The output should be as follows:

    PS D:\tmp> msbuild HelloWorld.proj
    Microsoft (R) Build Engine Version 2.0.50727.42
    [Microsoft .NET Framework, Version 2.0.50727.42]
    Copyright (C) Microsoft Corporation 2005. All rights reserved.
    Build started 13.09.2006 19:02:21.
    __________________________________________________
    Project "D:\tmp\HelloWorld.proj" (default targets):
    Target HelloWorld:
        Hello world
    Build succeeded.
        0 Warning(s)
        0 Error(s)
    Time Elapsed 00:00:05.09

    Not horribly exciting, but it works.

    Of course, such a task would have limited usefullness if there wasn't a way to pass arguments from MSBuild into the custom script. The Powershell task has two ways of doing this.

    The first way is an attribute 'Arguments'. This takes in named arguments on the form 'varname1=value1;varname2=value2'. varname1 and varname2 will then be available as string variables inside the script. For example:

        <PropertyGroup>
            <HelloArgumentsScript><![CDATA[
            Write-Host ("{0}" -f (([int]$value1) + ([int]$value2)))
            ]]></HelloArgumentsScript>
        </PropertyGroup>
    
        <Target Name="HelloArguments">
            <Powershell Script="$(HelloArgumentsScript)" Arguments="value1=40;value2=2"/>
        </Target>
     

    The output is as expected:

    Project "D:\tmp\HelloWorld.proj" (HelloArguments target(s)):
    Target HelloArguments:
        42
    Build succeeded.
    0 Warning(s)
    0 Error(s)
    
    Time Elapsed 00:00:02.78
     

    The other way is through the task attribute 'TaskItems'. As the name implies, this takes in a set of taskitem objects. They are available in the Powershell script as a collection of ITaskItem objects named $TaskItems:  

    <PropertyGroup>
        <TaskItemsScript> <![CDATA[ Write-Host ("The answer is {0}" -f (([int]$taskitems[0].ItemSpec) + ([int]$taskitems[1].ItemSpec))) ]]> </TaskItemsScript>
    </PropertyGroup>
    <PropertyGroup>
        <Argument1>12</Argument1>
        <Argument2>30</Argument2>
    </PropertyGroup>
    <Target Name="TaskItems">
        <Powershell Script="$(TaskItemsScript)" TaskItems="$(Argument1);$(Argument2)">
    </Target>

    It is also possible for a script to return values to the calling task. This happens through the Output taskparameter, and works like this:

    <Powershell Script="$(ValueReturningScript)">
        <Output TaskParameter="Output" PropertyName="ScriptOutput"/>
    </Powershell>
    
    <Message Lines="$(ScriptOutput)"/>
     

    The source code can be found in a Subversion repository here, and a downloadable zip including both source and the prebuilt task can be found here.

    
    
    July 26

    DynamicMethod revisited: Making Powershell handle arbitrary delegates

     

    As I posted on Monday, Powershell doesn’t let you cast a scriptblock to a delegate with a signature other than void (Object, Object). Jeffrey Snover confirmed this the same day. When I learned that this was a limitation in v1.0 of Powershell, I set out to find a way to get around this problem using .NET’s DynamicMethod class.

    I mostly got it done yesterday, but I was feeling a bit tired and didn’t finish it so I could blog about it. Today I see that the Powershell team has posted a workaround of their own, which is remarkably similar to mine. Although, theirs probably gets extra coolness points since it’s written in Powershell itself and not implemented as a cmdlet like mine.

    Anyway, I have now finished it and decided to post it anyway (I spent quite a lot of time on it, mostly silly mistakes on my part). Here goes:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Management.Automation;
    using System.Reflection.Emit;
    using System.Reflection;
    using Fines.Utils.Collections;

    namespace Fines.ConvertScriptBlockSnapin
    {
        [Cmdlet("Convert", "ScriptBlock")]
        public class ConvertScriptBlockCommand : Cmdlet
        {
            [Parameter(Mandatory=true, Position=0)]
            public string DelegateTypeName
            {
                get { return this.delegateTypeName; }
                set { this.delegateTypeName = value; }
            }


            [Parameter(Mandatory=true, Position=1)]
            public ScriptBlock ScriptBlock
            {
                get { return this.scriptBlock; }
                set { this.scriptBlock = value; }
            }

            [Parameter(Mandatory=false, Position=2)]
            public SwitchParameter ReturnPSObject
            {
                get { return returnPSObject; }
                set { returnPSObject = value; }
            }


            protected override void ProcessRecord()
            {
                // Validate the DelegateTypeName parameter
                // It must be a delegate type
                Type delegateType;
                if ( !LanguagePrimitives.TryConvertTo<Type>( this.DelegateTypeName, out delegateType ) )
                {
                    Exception ex = new ArgumentException( "Type not found: " + this.DelegateTypeName, "DelegateTypeName");
                    base.ThrowTerminatingError( new ErrorRecord( ex, "TypeNotFound", ErrorCategory.InvalidType, null ) );         
                }

                if ( ! typeof(Delegate).IsAssignableFrom(delegateType))
                {
                    Exception ex = new ArgumentException( "DelegateTypeName must be of a delegate type. Found " + this.DelegateTypeName,
                        "DelegateTypeName" );
                    base.ThrowTerminatingError( new ErrorRecord( ex, "TypeNotDelegate", ErrorCategory.InvalidType, null ) );
                }

                if ( typeof( Delegate ) == delegateType || typeof(MulticastDelegate) ==  delegateType )
                {
                    Exception ex = new ArgumentException( "DelegateTypeName cannot be of type System.Delegate or System.MulticastDelegate",
                        "DelegateTypeName" );
                    base.ThrowTerminatingError( new ErrorRecord( ex, "TypeSystemDelegate", ErrorCategory.InvalidType, null ) );
                }
                
                // Create our trampoline and write it to the pipeline
                ScriptBlockTrampoline tramp = new ScriptBlockTrampoline( this.ScriptBlock, this.ReturnPSObject );
                this.WriteObject( tramp.CreateDelegate( delegateType ) );
            }

            /// <summary>
            /// This class exists to attach state (the scriptblock and the switch parameter) to the dynamic method
            /// we create.
            /// </summary>
            private class ScriptBlockTrampoline
            {
                public ScriptBlockTrampoline( ScriptBlock scriptBlock, SwitchParameter returnPSObject )
                {
                    this.scriptBlock = scriptBlock;
                    this.returnPSObject = returnPSObject;
                }

                public Delegate CreateDelegate( Type delegateType )
                {
                    MethodInfo invokeMethod = delegateType.GetMethod("Invoke");

                    // get the parameter types of the delegate
                    Type[] baseMethodParmTypes = ListUtils.ToArray(
                        ListUtils.Map<ParameterInfo, Type>( invokeMethod.GetParameters(), delegate( ParameterInfo par )
                        {
                            return par.ParameterType;
                        } )
                    );

                    // the first arg needs to be of type ScriptBlockTrampoline so we can treat it as an instance method
                    Type[] parmTypes = new Type[ baseMethodParmTypes.Length + 1 ];
                    parmTypes[ 0 ] = typeof( ScriptBlockTrampoline );
                    Array.Copy( baseMethodParmTypes, 0, parmTypes, 1, baseMethodParmTypes.Length );

                    // Associate our dynamic method with the ScriptBlockTrampoline class so it can access its privates
                    DynamicMethod dm = new DynamicMethod( "Dele", invokeMethod.ReturnType, parmTypes, typeof( ScriptBlockTrampoline ) );
                    ILGenerator g = dm.GetILGenerator();                

                    // Create a local to hold our params array
                    LocalBuilder arrayLocal = g.DeclareLocal(typeof(object[]));

                    // create the array with our desired length
                    g.Emit( OpCodes.Ldc_I4, baseMethodParmTypes.Length );
                    g.Emit(OpCodes.Newarr, typeof(object));

                    // store it in our local
                    g.Emit(OpCodes.Stloc, arrayLocal);

                    // load our params into it
                    for (int i = 0; i < baseMethodParmTypes.Length; i++)
                    {
                        g.Emit( OpCodes.Ldloc, arrayLocal );

                        // the index in the array in which we want to store the element
                        g.Emit( OpCodes.Ldc_I4, i );

                        // load the arg onto the stack
                        g.Emit( OpCodes.Ldarg, i + 1 ); // 'this' is arg 0

                        // if it's a value type, we need to box
                        if ( baseMethodParmTypes[ i ].IsValueType )
                        {
                            g.Emit( OpCodes.Box, baseMethodParmTypes[ i ] );
                        }

                        // and store it in the array
                        g.Emit( OpCodes.Stelem, typeof( object ) );
                    }

                     //get the scriptblock field
                    g.Emit( OpCodes.Ldarg, 0 );
                    g.Emit( OpCodes.Ldfld, typeof( ScriptBlockTrampoline ).GetField( "scriptBlock",
                        BindingFlags.NonPublic | BindingFlags.Instance ) );

                    //// now emit the call to the scriptblock
                    g.Emit( OpCodes.Ldloc, arrayLocal );
                    g.EmitCall( OpCodes.Callvirt, typeof( ScriptBlock ).GetMethod( "InvokeReturnAsIs" ), null );

                    // can't have an object on the stack if we're returning void
                    if ( invokeMethod.ReturnType == typeof( void ) )
                    {
                        g.Emit( OpCodes.Pop );
                    }
                    else
                    {
                        // does the user want the raw object or the PSObject instance?
                        if ( !this.returnPSObject.IsPresent )
                        {
                            // what's returned from the ScriptBlock is a PSObject, get the underlying object instead
                            MethodInfo getMethod = typeof( PSObject ).GetProperty( "BaseObject" ).GetGetMethod();
                            g.EmitCall( OpCodes.Call, getMethod, null );
                        }
                    }

                    // ret is not optional
                    g.Emit( OpCodes.Ret );

                    return dm.CreateDelegate( delegateType, this );
                }

                private ScriptBlock scriptBlock;
                private SwitchParameter returnPSObject;
            }

            private ScriptBlock scriptBlock;
            private string delegateTypeName;
            private SwitchParameter returnPSObject;

        }

    }
     

    The ScriptBlockTrampoline.CreateDelegate method is where most of the fun stuff happens. I create a DynamicMethod with the same signature as that of the target delegate. Since I also pass in the ScriptBlockTrampoline type to the DynamicMethod constructor, the created method will be treated as a member method on ScriptBlockTrampoline with regards to member access; in other words, it can access private members on this type. The first parameter to the DynamicMethod is a ScriptBlockTrampoline object, which means it can be treated as an instance method and have access to the scriptBlock and returnPSObject private member variables.

    Code is then emitted to create an array of type Object[], which is then loaded with the arguments passed to the delegate at the invocation point. This array is passed to the ScriptBlock’s InvokeReturnAsIs() method, which actually executes the scriptblock.

    One particular thing that caused me a lot of grief (not really sure why, I’ve encountered and solved the same problem before) was that InvokeReturnAsIs() will return an object and place it on the IL evaluation stack even if there is no actual return value and the delegate return type is void. If you don’t pop this object off the stack (the if statement) before the RET instruction, executing the delegate will cause the .NET runtime to bail out and die since the program is invalid. I spent a lot of hours on this one.

    One other thing that bears mentioning is that InvokeReturnAsIs will actually return a PSObject, not the raw underlying object. If the code calling the method expects the actual type (i.e., if it’s called from most managed languages), things will get ugly pretty fast if it gets a PSObject instead. Code is therefore emitted to call the PSObject.BaseMethod property.

    With this cmdlet, implementing a Powershell script for doing an “svn ls” with support for authentication becomes rather simple:

    # get-svndir.ps1
    
    param ([string]$svnurl       = $(read-host "Please specify the SVN url for the directory you want to list")
        )
    
    [System.Reflection.Assembly]::LoadFrom((join-Path "$env:PROGRAMFILES\AnkhSVN" -childPath NSvn.Common.dll)) | Out-Null
    [System.Reflection.Assembly]::LoadFrom((join-Path "$env:PROGRAMFILES\AnkhSVN" -childPath NSvn.Core.dll)) | Out-Null
    
    $PRIVATE:svnclient = new-object NSvn.Core.Client 
    
    $PRIVATE:simplepromptdelegate = convert-scriptblock NSvn.Core.SimplePromptDelegate {
        Write-Host "Authenticating against realm " $args[0]
        $user = Read-Host ("Input username (press Enter to accept default of {0})"  -f $args[1])
        if ( $user.Trim() -eq "" ) 
        {
            $user = $args[1]
        }
        
        $password = Read-Host -AsSecureString "Input password"
        
        # Convert it to a plain string
        $BSTR = [System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password)
        $password = [System.Runtime.InteropServices.marshal]::PtrToStringAuto($BSTR)
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
        
        $cred = New-Object NSvn.Core.SimpleCredential($user, $password, $False)
        $cred
    }
    
    $PRIVATE:svnclient.AuthBaton.Add( [NSvn.Core.AuthenticationProvider]::GetWindowsSimpleProvider() )
    $PRIVATE:svnclient.AuthBaton.Add( [NSvn.Core.AuthenticationProvider]::GetSimplePromptProvider($PRIVATE:simplepromptdelegate, 3) )
      
    
    $items = $PRIVATE:svnclient.List($svnurl, [NSvn.Core.Revision]::Head, [NSvn.Core.Revision]::Head, [NSvn.Common.Recurse]::None)
    $items
     

    Thanks to /\/\o\/\/ for pointing me to his blog post about how to convert a SecureString to a regular .NET string, which is what the SimpleCredential constructor expects.

    A sample run of that script:

    PS D:\mydocs\powershell> D:\mydocs\powershell\get-svndir.ps1 http://ankhsvn.com/svn/draco/ | ft Path,LastAuthor,Time

    Authenticating against realm <http://ankhsvn.com:80> Subversion repository draco

    Input username (press Enter to accept default of Arild):

    Input password: ****

    Path LastAuthor Time

    ---- ---------- ----

    trunk Arild 16.01.2006 03:33:30

    branches Arild 18.10.2005 21:03:32

    tags Arild 18.10.2005 21:03:32

    The snapin can be found in SVN at http://ankhsvn.com/svn/powershell/trunk/ConvertScriptBlockCommand/. It is also downloadable as a .zip here. To install it, run install.ps1 from the root folder in a shell with the .NET environment set. By default, it will add the command “Add-PsSnapin ConvertScriptBlockSnapin” to your $PROFILE file after compiling and installing it if it’s not already there.

    July 24

    Powershell can’t do arbitrary-signature delegates?

    I was asked the other day how to handle authentication in NSvn with Powershell. The scripts I’ve shown up until now([1], [2] have used the NSvn.Core.AuthenticationProvider.GetWindowsSimpleProvider() method to provide authentication when required by Subversion. Subversion uses a concept of “authentication providers” to handle the various forms of authentication it supports (querying the user for the password, SSL etc). GetWindowsSimpleProvider() returns an authentication provider that uses cached authentication info stored in the %APPDATA%\Subversion\auth directory on the user’s computer. It differs from the GetSimpleProvider() method in that the provider returned can handle authentication information that has been encrypted with the Windows Crypto API. The problem with using only GetWindowsSimpleProvider() (or GetSimpleProvider()) is that it won’t work unless there is some authentication information already cached. In other words, you would have to access the repository in question using another Subversion client before you are able to use any of these scripts as written (if the repository actually demands authentication for the operations you perform, that is). To actually be able to query the user for authentication requires the provider returned by GetSimplePromptProvider(). GetSimplePromptProvider() has the following signature: AuthenticationProvider GetSimplePromptProvider( SimplePromptDelegate promptDelegate, int retryLimit ); SimplePromptDelegate again is a callback used by Subversion to actually query the user for username and password. It has the following signature: delegate SimpleCredential SimplePromptDelegate( String realm, String username, bool maySave ); I assumed that calling GetSimplePromptProvider() from Powershell would be a simple task; merely cast a scriptblock to[NSvn.Core.SimplePromptDelegate]. Alas, no such luck: PS D:\mydocs\powershell> $del = [NSvn.Core.SimplePromptDelegate] { param($realm, $username, $maySave) $null } Cannot convert value "param($realm, $username, $maySave) $null " to type "NSvn.Core.SimplePromptDelegate". Error: "Error binding to target method." At line:1 char:40 + $del = [NSvn.Core.SimplePromptDelegate] <<<< { param($realm, $username, $maySave) $null } After trying a couple of variants on this (including trying to cast a named function), I broke out Reflector see how Powershell actually creates this delegate. It turns out that Powershell is calling Delegate.CreateDelegate using a MethodInfo object that points to the following method: private void invokeAsDelegate(object sender, object e) As you can see, this method takes two arguments and has a void return type. It can obviously never match the signature of SimplePromptDelegate, which takes three arguments and returns a SimpleCredential object. This causes Delegate.CreateDelegate to throw an exception which gets passed up through the Powershell call chain and results in the error message shown. Am I missing something here? Is there really no way to create such a delegate from a Powershell script block? Is there a way to do this with a named function? This is not an insurmountable technical problem to solve. An approach similar to what I did with DynamicMethod here would probably work.  
    July 10

    NSvn and Powershell: Log messages

    All NSvn.Core.Client methods that modify the repository directly require a log message. These include MakeDir, Commit, Import, Move, PropDel, PropEdit and PropSet. To provide a log message using NSvn, you need to handle the Client.LogMessage event and set the Message property of the LogMessageEventArgs object to a non-null string. Not setting this property will be interpreted by Subversion as a cancellation of the operation, and it will fail silently, returning CommitInfo::Invalid.

    I have described the way to handle .NET events in Powershell using script blocks here. We can adapt the same approach to handling the LogMessage event:

    # make-svndir.ps1
    
    param ([string]$svnurl       = $(read-host "Please specify the SVN url for the directory you want to create"),
        [string]$logMessage = $null
        )
    
    [System.Reflection.Assembly]::LoadFrom((join-Path "$env:PROGRAMFILES\AnkhSVN" -childPath NSvn.Common.dll)) | Out-Null
    [System.Reflection.Assembly]::LoadFrom((join-Path "$env:PROGRAMFILES\AnkhSVN" -childPath NSvn.Core.dll)) | Out-Null
    
    $PRIVATE:svnclient = new-object NSvn.Core.Client 
    
    # log message entered here
    $PRIVATE:logmessagecallback = [NSvn.Core.LogMessageDelegate] {
        if ( $logMessage )
        {
            $_.Message = $logMessage
        }
        else
        {
            $_.Message = (Read-Host "Enter a log message")
        }
    }
    
    $PRIVATE:svnclient.add_LogMessage($logmessagecallback)
        
    $PRIVATE:svnclient.AuthBaton.Add( [NSvn.Core.AuthenticationProvider]::GetWindowsSimpleProvider() )
    $commitInfo = $PRIVATE:svnclient.MakeDir($svnurl)
    
    # you could potentially do other things with the $commitInfo here
    # I just return it to the caller of the script
    
    $commitInfo
    

    Notice how I pipe the output from Assembly.LoadFrom to Out-Null. This ensures that the only thing returned to the caller of the script is the CommitInfo object returned from the call to MakeDir. This object contains information about the newly created revision, including its revision number, author and date.

    The script can be used like this:

    PS D:\tmp\build> D:\mydocs\powershell\make-svndir.ps1 http://ankhsvn.com/svn/test/test3 "A log message"
                                   Revision Author                                  Date
                                   -------- ------                                  ----
                                        220                                         09.07.2006 23:01:08
    

     

    Or like this:

    PS D:\tmp\build> D:\mydocs\powershell\make-svndir.ps1 http://ankhsvn.com/svn/test/test4
    Enter a log message: A log Message
                                   Revision Author                                  Date
                                   -------- ------                                  ----
                                        221                                         09.07.2006 23:01:40
    
    July 07

    Setting the .NET environment for a Powershell prompt

    VS.NET installs a shortcut that, when invoked, sets the necessary environment variables you need to access the command line tools that comes with VS and the .NET and brings up a command prompt. The shortcut leads to a batch file located in the VS install directory, called vsvars32.bat.

    A lot of developers want this environment set also when working in Powershell. Unfortunately, merely calling this batch file from inside Powershell won't work. The only way to run a batch file from Powershell is by invoking an instance of cmd.exe (or ${env:COMSPEC} and passing it the path of the batch file. This obviously runs in its own process, and when this process exits, its environment will not be propagated to the process that called it (Powershell).

    There are several ways to get around this. You can, if you're not as lazy as I am, rewrite the vsvars32.bat file to a Powershell script and dotsource it on startup. Another way would be to first start up the VS command prompt by clicking on the shortcut, and then invoking Powershell manually from the prompt. I suspect a lot of people do the latter.

    Personally, I've created my own shortcut that targets the following command line:

    cmd.exe /k "D:\Program Files\Microsoft Visual Studio 8\Common7\Tools\vsvars32.bat" && powershell.exe

    The /k argument to cmd.exe tells it to leave the command prompt open after executing the command that follows. && is used to provide more than one command for cmd.exe to run.

    When clicked, this shortcut leaves me with a Powershell prompt that has all the necessary command line utilities in my ${env:PATH}.

    July 03

    Powershell script blocks as .NET event handlers

    Scott Hanselman posts an interesting script where he uses the NSvn library (the .NET wrapper library for Subversion we wrote for AnkhSVN) directly from Powershell to either checkout from a Subversion repository or update an existing working copy. This is a great way to use NSvn and one we definitely didn’t have in mind when writing it.

    He also asks: “How do you do a delegate from within PowerShell if a .NET Assembly needs a delegate with a certain signature? This library will call me back with status updates I'd like to broker to write-progress.”

    NSvn’s NSvn.Core.Client class has an event called Notification, which is raised for every notification callback we get from the underlying Subversion C library. Subversion calls back for various reasons, including whenever a file has been downloaded as part of an update operation (in Subversion, a checkout is really just a special case of an update where the working copy has none of the files). In the Notification event, the NotificationEventArgs object’s Path property contains the path to the file or directory that has been updated, while the .Action property indicates what kind of operation has been performed against the path. will usually contain NotifyAction.UpdateUpdate or NotifyAction.UpdateAdd (in the case of a checkout, all callbacks will be of the NotifyAction.UpdateAdd type). See here for a listing of the various action types.

    Now, the question is, how does one consume this event from a Powershell script? Powershell supports anonymous functions called script blocks. These are roughly equivalent to C#’s anonymous delegates. Script blocks can be stored in a variable and invoked at any given time using the & operator:

    PS D:\tmp> $helloworld = { write-host "Hello world" }

    PS D:\tmp> & $helloworld

    Hello world

    It turns out that a script block can be cast to any arbitrary delegate and used as an event handler:

    PS D:\tmp> $handler = [System.EventHandler]{ write-host "Hello world" }

    PS D:\tmp> $handler.GetType().FullName

    System.EventHandler

    PS D:\tmp> $handler.Invoke($null, [System.EventArgs]::Empty)

    Hello world

     

    In our case, we want a delegate of type NotificationDelegate (ok, not a good name for an event handler delegate, or so FXCop tells me, but anyway…), which has the following signature:

    public delegate void NotificationDelegate( Object sender, NotificationEventArgs args );

    The one thing that’s not obvious about scriptblocks as event handlers is how you get hold of the eventargs instance. The documentation does not go out of its way to tell you about it either, so I started experimenting. My first attempt looked like this:

    PS D:\tmp> $notificationcallback = [NSvn.Core.NotificationDelegate]{ param($sender, $e) write-host $e.Path }

    The param syntax is one of the ways you declare parameters to a regular function or script. If it worked as expected, it would have printed out the path for each item being updated or checked out. Of course, “as expected” being the operative phrase here. $e turned out to be null.

    Another way in Powershell to get hold of arguments to a function is through the special $args array. Unfortunately, this didn’t seem to work either:

    PS D:\tmp> $event = [System.EventHandler]{ write-host "Args: $args" }

    PS D:\tmp> $event.Invoke($null, [EventArgs]::Empty)

    Args:

    PS D:\tmp>

    Another Reading of The Fine Manual (why is this thing in a Word document, anyway?) finally turned up an example, in which they use the $_ special variable to get hold of the EventArgs instance passed as a second parameter to all (most?) .NET events. This seems like a weird choice to me, $_ is generally used to hold the “current” object in a pipeline. Why did they overload it like this? To make things even worse, the sender argument is available in another special variable called $this. Why “$this”? It’s not like the usage in any way resembles the way the this keyword is used in other languages.

    Anyway, armed with this hard-won knowledge, rewriting Scott’s script to “broker” the notification events to Write-Progress proves to be rather simple. This is the revised script:

        
      param ([string]$svnurl       = $(read-host "Please specify the path to SVN"),
            [string]$svnlocalpath = $(read-host "Please specify the local path") 
            )
    
        if ([System.IO.Path]::IsPathRooted($svnlocalpath) -eq $false)
        {
            throw "Please specific a local absolute path"
        }
        
        [System.Reflection.Assembly]::LoadFrom((join-Path "$env:PROGRAMFILES\AnkhSVN" -childPath NSvn.Common.dll))
        [System.Reflection.Assembly]::LoadFrom((join-Path "$env:PROGRAMFILES\AnkhSVN" -childPath NSvn.Core.dll))
        
        
        
        $PRIVATE:svnclient = new-object NSvn.Core.Client
        
        $PRIVATE:notificationcallback = [NSvn.Core.NotificationDelegate]{
            Write-Progress -status ("{0}: {1}" -f ($_.Action, $_.Path)) -activity "Updating Working Copy"
        }
        
        $PRIVATE:svnclient.add_Notification($notificationcallback)    
            
        $PRIVATE:svnclient.AuthBaton.Add( [NSvn.Core.AuthenticationProvider]::GetWindowsSimpleProvider() )
        if ((test-Path $svnlocalpath) -eq $true )
        {
            write-progress -status "Updating from $svnurl" -activity "Updating Working Copy"
            $PRIVATE:svnclient.Update($svnlocalpath, [NSvn.Core.Revision]::Head, $true)
        }
        else
        {
            write-progress -status "Checkout from $svnurl" -activity "Updating Working Copy"
            $PRIVATE:svnclient.Checkout($svnurl, $svnlocalpath, [NSvn.Core.Revision]::Head, $true)
        }
    
    June 12

    SVN Tab Expansion for Windows Powershell

    Windows Powershell allows the user to customize the behavior of TAB expansion/completion on the command line by replacing the default TabExpansion function.

    The following is a Powershell script that adds TAB completion for svn.exe subcommands and their associated options. It generates the subcommand list and the association between subcommands and options by parsing the output from svn help, so it should stay up to date for a while (unless the svn help output format changes drastically). The very first time it's invoked, it will take a while (20-30 seconds) for it to generate the mapping, but subsequent runs will reuse a cached version in ${env:TEMP}\svncompletion.ps1.

    The script contains both the default tab expansion provided by the shell (for paths, functions and cmdlets) as well as my SVN completion, so if you haven't made any changes to the default (and if you have, you're probably capable of figuring out how to merge it with your own anyway) you can install it as follows:

    • Save it somewhere as TabExpansion.ps1
    • Dotsource it at startup by adding the following line to My Documents\PSConfiguration\profile.ps1:

     

    . Somewhere\TabExpansion.ps1

     

    • Ensure svn.exe is somewhere in your ${env:PATH}
    • Start a new Powershell instance
    • Type svn c[TAB] - this should force the mapping cache to be generated and it should eventually complete it as svn cat. Subsequent TABs will cycle through all commands matching c*.

    Completion of options is triggered by the pattern svn command -, where TAB will cycle through the options appropriate for that particular command.

    To refresh the cache (for example, when a new version of SVN introduces new commands), you can either delete the ${env:TEMP}\svncompletion.ps1 file or do "CreateSvnCompletion > ${env:TEMP}\svncompletion.ps1"

    Keep in mind that for this to work, execution of scripts has to be allowed in the shell (it's not allowed by default). This can be done by invoking the command set-executionpolicy remotesigned (see help about_signing for more information).

    Enjoy:

     

    # This function creates a .ps1 file that will generate a hash of all commands and their associated options.

    function CreateSvnCompletion

    {

        # First get all commands with aliases by parsing the output of svn help.

        $help = svn help

        

        $svncmds = $help | where { $_ -match '^\s+(\w+)(?:\s\((.*)\))?' } | `

            foreach {

                $matches[1];

                if ($matches[2])

                {

                    $matches[2].Split(", ", [System.StringSplitOptions]::RemoveEmptyEntries)

                }

            }

        

        # now build a hash of all their options by parsing the output of svn help for that command

        $svncmdoptions = @{}

        foreach( $cmd in $svncmds )

        {

            $svncmdoptions[$cmd] = svn help $cmd | where { $_ -match '^\s+(-+\w[\w\-]*)(\s+\[(.*?)\])?' } |`

                foreach {

                    $matches[1];

                    

                    # More than one option for the same (long option)?

                    if ($matches[3])

                    {

                        $matches[3]

                    }

                }

        }

        

        # Now output a string that will recreate the hash

        '$svncmdhash=@{``'

        $svncmds | foreach {

            $opts = ($svncmdoptions[$_] | foreach{"`"{0}`"" -f $_})

            "`"$_`"" + "=" + [System.String]::Join(",", $opts) +";``"

        }

        "}"

    }

     

    # this is the default TabExpansion function (can be obtained by doing "get-content function:TabCompletion")

    # with the SVN completion stuff added.

    # The begin and process blocks have been added.

    function TabExpansion

    {

        param($line, $lastWord)

        

        begin

        {

            # Has the completion file already been generated?

            $completionfile = "${env:TEMP}\svncompletion.ps1"

            if ( ! (Test-Path $completionfile) )

            {

                # Nope, create it

                CreateSvnCompletion > $completionfile

            }

            

            # And dotsource it (this will bring

            . $completionfile

        }

        

        process

        {

        

            # Expand an SVN command?

            if ( $line -match "^\s*svn\s+(\w+)$" )

            {

                $pat = $matches[1] + "*"

                $svncmdhash.keys | Sort-Object | where { $_ -like $pat }

                return

            }

            # Or an SVN option to an SVN command?

            elseif ( $line -match "^\s*svn\s(\w+).*(-[-\w]*)" )

            {

                $cmd = $matches[1]

                $pat = $matches[2] + "*"

                $svncmdhash[$cmd] | Sort-Object | where { $_ -like $pat }

                return

            }

            

            # This is the default function to use for tab expansion. It handles simple

            # member expansion on variables, variable name expansion and parameter completion

            # on commands. It doesn't understand strings so strings containing ; | ( or { may

            # cause expansion to fail.

          

        

            switch -regex ($lastWord)

            {

                # Handle property and method expansion...

                '\$(\w+)\.(\w*)' {

                    $method = [Management.Automation.PSMemberTypes] 'Method,CodeMethod,ScriptMethod,ParameterizedProperty'

                    $variableName = $matches[1]

                    $val = Get-Variable -value $variableName

                    $pat = $matches[2] + '*'

                    Get-Member -inputobject $val | where {$n = $_.name; $n -like $pat -and $n -notmatch '^[ge]et_'} | foreach {

                        if ($_.MemberType -band $method)

                        {

                            # Return a method...

                            '$' + $variableName + '.' + $_.name + '('

                        }

                        else {

                            # Return a property...

                            '$' + $variableName + '.' + $_.name

                        }

                    }

                    break;

                }

          

                # Handle variable name expansion...

                '(.*^\$)(\w+)$' {

                    $prefix = $matches[1]

                    $varName = $matches[2]

                    foreach ($v in Get-Childitem ('variable:' + $varName + '*'))

                    {

                        $prefix + $v.name

                    }

                    break;

                }

          

                # Do completion on parameters...

                '^-([\w0-9]*)' {

                    $pat = $matches[1] + '*'

        

                    # extract the command name from the string

                    # first split the string into statements and pipeline elements

                    # This doesn't handle strings however.

                    $cmdlet = [regex]::Split($line, '[|;]')[-1]

          

                    # Extract the trailing unclosed block e.g. ls | foreach { cp

                    if ($cmdlet -match '\{([^\{\}]*)$')

                    {

                        $cmdlet = $matches[1]

                    }

          

                    # Extract the longest unclosed parenthetical expression...

                    if ($cmdlet -match '\(([^()]*)$')

                    {

                        $cmdlet = $matches[1]

                    }

          

                    # take the first space separated token of the remaining string

                    # as the command to look up. Trim any leading or trailing spaces

                    # so you don't get leading empty elements.

                    $cmdlet = $cmdlet.Trim().Split()[0]

        

                    # now get the info object for it...

                    $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet)[0]

          

                    # loop resolving aliases...

                    while ($cmdlet.CommandType -eq 'alias') {

                        $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet.Definition)[0]

                    }

          

                    # expand the parameter sets and emit the matching elements

                    foreach ($n in $cmdlet.ParameterSets | Select-Object -expand parameters)

                    {

                        $n = $n.name

                        if ($n -like $pat) { '-' + $n }

                    }

                    break;

                }

            }

        }

            

    }