More Tied Variables in PowerShell

Ibrahim just posted something to the PowerShell blog about how to create tied variables in PowerShell. If you extend this approach with script blocks, you have a very powerful dynamic scripting technique.

PS C:\temp> cd \temp
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:lee { $myTestVariable } { $GLOBAL:myTestVariable = 2 * $args[0] }
PS C:\temp> $lee
PS C:\temp> $lee = 10
PS C:\temp> $lee
20
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:today { (Get-Date).DayOfWeek }
PS C:\temp> $today
Wednesday
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:random -Get { Get-Random } -Set { Get-Random -SetSeed $args[0] }
PS C:\temp> $random
1740776676
PS C:\temp> $random
1507521897
PS C:\temp> $random = 10
PS C:\temp> $random
1613858733
PS C:\temp> $random = 10
PS C:\temp> $random
1613858733

He alluded to it in the post – here is the full text of the script:

(Edit 05/17: Updated to make the getters more like PowerShell pipelines: return a single object, or collection of PSObject)

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
## New-ScriptVariable.ps1
param($name, [ScriptBlock] $getter, [ScriptBlock] $setter)

Add-Type @"
using System;
using System.Collections.ObjectModel;
using System.Management.Automation;

namespace Lee.Holmes
{
    public class PSScriptVariable : PSVariable
    {
        public PSScriptVariable(string name,
            ScriptBlock scriptGetter, ScriptBlock scriptSetter)
            : base(name, null, ScopedItemOptions.AllScope)
        {
            getter = scriptGetter;
            setter = scriptSetter;
        }
        private ScriptBlock getter;
        private ScriptBlock setter;

        public override object Value
        {
            get
            {
                if(getter != null)
                {
                    Collection<PSObject> results = getter.Invoke();
                    if(results.Count == 1)
                    {
                        return results[0];
                    }
                    else
                    {
                        PSObject[] returnResults = new PSObject[results.Count];
                        results.CopyTo(returnResults, 0);
                        return returnResults;
                    }
                }
                else { return null; }
            }
            set
            {
                if(setter != null) { setter.Invoke(value); }
            }
        }
    }
}
"@

if(Test-Path variable:\$name)
{
    Remove-Item variable:\$name -Force
}
$executioncontext.SessionState.PSVariable.Set(
    (New-Object Lee.Holmes.PSScriptVariable $name,$getter,$setter))

 

6 Responses to “More Tied Variables in PowerShell”

  1. Doug writes:

    This is really interesting. I am wondering when to prefer this approach.

    Looking forward to your 2nd Edition.

  2. David writes:

    I get the following error when I try to use this. This seems like a very useful script if I can get it to work. I am using CTP3 if that matters.

    Unrecognized token in source text.
    At C:\Users\dmonder\Documents\WindowsPowerShell\Scripts\New-ScriptVariable.ps1:4 char:10
    + Add-Type <<<< @"
    + CategoryInfo : ParserError: (:) [], ParseException
    + FullyQualifiedErrorId : UnrecognizedToken

  3. Lee Holmes writes:

    David: It looks like IE added an extra space at the end of line 4. Remove that space, and you’re good to go.

  4. Per Østergaard writes:

    Hi Lee.

    Great stuff. But have two problems:

    1) Calling it with global:something results in a variable having a name including the global. Works, but ugly.
    2) The objects returned are of type System.Collections.ObjectModel.Collection`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]. This means that useful stuff (see example below) is not that useful

    New-ScriptVariable global:Jan1st {get-date -hour 0 -min 0 -sec 0 -mon 1 -day 1}
    dir | where {$_.lastwritetime -gt $jan1st}
    Bad argument to operator ‘-gt’: Could not compare "13-02-2009 10:07:58" to "System.Collections.ObjectModel.Collection`1
    [System.Management.Automation.PSObject]". Error: "Cannot convert the "System.Collections.ObjectModel.Collection`1[Syste
    m.Management.Automation.PSObject]" value of type "System.Collections.ObjectModel.Collection`1[[System.Management.Automa
    tion.PSObject, System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]" to ty
    pe "System.DateTime".".
    At line:1 char:34
    + dir | where {$_.lastwritetime -gt <<<< $jan1st}
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : BadOperatorArgument

    Any fixes for these things?

  5. Lee Holmes writes:

    Thanks, Per — updated the script to act more like you would expect!

  6. Tied Variables in PowerShell - Shay Levy writes:

    […] posted a question to the PowerShell MVP private list and soon enough Lee Holmes, pointed me to this blog post. Here's the code I used to create […]

Leave a Reply