v3, AST and dot-sourcing.

How often do you find yourself in situation, when you would like to import functions from one of your scripts? Issue with that is related to the fact, that most of scripts I write have some purpose and they “do stuff”. When I want to import functions from the script I do not want to perform tasks that script would normally do – I just want function(s).

The way I was usually walking around this issue was:

  1. Open up script in ISE.
  2. Select function in question (to make it simpler I would use code folding)
  3. Use F8 to import it into current session.

Dot-sourcing would not do what I want: it would do more – not only import the functions, but also use them (that’s what usually script is created for).

Abstract Syntax Tree for the rescue!

Yesterday I was thinking about this problem when it hit me: AST! Abstract Syntax Tree lets you do crazy things with code stored in files. You can parse them in very smart way, and pick and choose parts of code that you would like to use. The only issue with that approach is that you need v3 to actually use it.

Once I moved pass this point – it became relatively easy to produce something that kind of works like dot-sourcing, but does not run commands. I was also thinking about a ways to keep my code from “dot-sourcing” functions if it’s defined with certain scope prefix. And also – decided that re-creating variables may be handy at times (though this part is very raw and probably can fail in many scenarios). I’ve decided to paste whole module on poshcode.org/ script center, and here only show how it works.

So how it works?

First, I create AST from script in question:

$fullName = (Resolve-Path $Path).ProviderPath            
    $ast = [Parser]::ParseFile(            
        $fullName,            
        [ref]$null,            
        [ref]$null            
    )            

Than, I look for all functions (that are not nested) and depending on the name’s structure – I make decision if I will $define them, or not:

foreach ($function in $ast.FindAll({            
$args[0] -is [FunctionDefinitionAst]            
}, $false)) {            
$define = $false            
switch -Regex ($function.Name) {            
    '^(?!.*:)' {            
        $define = $true            
        $name = $function.Name            
        break            
    }            
    '(Global|Script|Local):' {            
        $define = $true            
        $name = $function.Name -replace '.*:'            
    }            
    default {            
        $define = $false            
    }            
}            
}

First item is for functions without colon in the name. Second – three prefixes that I decided to use. Third option for what left, that won’t be defined. Defining is not perfect in my opinion – I just use $Scope picked by user, by default – it’s global. Dot-sourcing will use “parent” scope, which may or may not be global. I’ve decided that most of use-cases I have require global anyway, so instead of working hard to make it as smart as dot-sourcing, I just left this as one of parameters available on the command. So how definition looks like? It’s very simple:

if ($define) {            
    & ([scriptblock]::Create("
        function $Scope`:$name
        $($function.Body)
        "            
    ))            
}            

With variables it was more complex (at least – for me). First of all, unlike function definition there is no AST that is just for defining variable. Instead, we have to look at all assignments. Assignment that I’m after is the one that uses equal sign. Finally – on the left I need to see variable. Rules of creation are exactly the same (only name of the syntax elements are different). Defining variable itself is also slightly different than the one I’ve used with functions. And last but not least – I wanted this part to be optional, so I’ve added switch parameter that would trigger this option on and off:

if ($IncludeVariables) {            
foreach ($variable in $ast.FindAll({            
    $args[0] -is [AssignmentStatementAst] -and            
    $args[0].Operator -eq [TokenKind]::Equals -and            
    $args[0].Left -is [VariableExpressionAst]            
    }, $false)) {            
    $define = $false            
    switch -Regex ($variable.Left.VariablePath) {            
        '^(?!.*:)' {            
            $define = $true            
            $name = $variable.Left.VariablePath            
            break            
        }            
        '(Global|Script|Local):' {            
            $define = $true            
            $name = $variable.Left.VariablePath -replace '.*:'            
        }            
        default {            
            $define = $false            
        }            
    }            
    if ($define) {            
        & ([scriptblock]::Create(            
            "`$$Scope`:$name = $(
            $variable.Right.Extent.Text
            )"            
        ))            
    }            
}            
}

Finally, two lines that make this seem almost same as usual syntax: alias for function (..) and obviously – instruction for PowerShell to export both functions and aliases (plural not justified obviously, but this is how used syntax works):

New-Alias -Name .. -Value Import-Script -Force            
Export-ModuleMember -Function * -Alias *

Testing… 1, 2, 3…

Finally I wrote very short script that I was testing it on, and how my approach differs from the one used by dot-sourcing. Script:

end {            
    Test-Foo -Bar NotWantIt            
    Missing            
}            
            
begin {            
    function Test-Foo {            
    param (            
        $Bar            
    )            
        function Internal {            
            "Internal modified: $bar"            
        }            
        [PSCustomObject]@{            
            Created = Get-Date            
            Bar = Internal            
            Invocation = $MyInvocation            
        }            
    }            
            
    function Script:Test-WithScope {            
        "I should work too!"            
    }            
            
    $Script:MyVar = 'Test'            
            
    $AnotherVar = Get-Date            
                
    function Private:Missing {            
        "Not there!"            
    }            
            
}

And final result:

PowerShell-Dot-Sourcing-With-Abstract-Syntax-Tree

You can find this simple module on poshcode.org and Script Center. Enjoy! Puszczam oczko

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s