5

Say I have MyScript.ps1:

[cmdletbinding()]
param ( 
    [Parameter(Mandatory=$true)]
        [string] $MyInput
)

function Show-Input {
    param ([string] $Incoming)
    Write-Output $Incoming
}

function Save-TheWorld {
    #ToDo
}

Write-Host (Show-Input $MyInput)

Is it possible to dot source the functions only somehow? The problem is that if the script above is dot sourced, it executes the whole thing...

Is my best option to use Get-Content and parse out the functions and use Invoke-Expression...? Or is there a way to access PowerShell's parser programmatically? I see this might be possible with PSv3 using [System.Management.Automation.Language.Parser]::ParseInput but this isn't an option because it has to work on PSv2.

The reason why I'm asking is that i'm trying out the Pester PowerShell unit testing framework and the way it runs tests on functions is by dot sourcing the file with the functions in the test fixture. The test fixture looks like this:

MyScript.Tests.ps1

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"

Describe "Show-Input" {

    It "Verifies input 'Hello' is equal to output 'Hello'" {
        $output = Show-Input "Hello"
        $output.should.be("Hello")
    }
}
Andy Arismendi
  • 50,577
  • 16
  • 107
  • 124

2 Answers2

4

Using Doug's Get-Function function you could include the functions this way:

$script = get-item .\myscript.ps1
foreach ($function in (get-function $script))
{
  $startline = $function.line - 1
  $endline = $startline
  $successful = $false
  while (! $successful)
  {
    try {
      $partialfunction = ((get-content $script)[$startline..$endline]) -join [environment]::newline
      invoke-expression $partialfunction
      $successful = $true
    }
    catch [Exception] { $endline++ }
  }
}

Edit: [System.Management.Automation.IncompleteParseException] can be used instead of [Exception] in Powershell V2.

jon Z
  • 15,838
  • 1
  • 33
  • 35
2

Note -- if you find this answer helpful please upvote jonZ's answer as I wouldn't of been able to come up with this if it weren't for his helpful answer.

I created this function extractor function based on the script @jonZ linked to. This uses [System.Management.Automation.PsParser]::Tokenize to traverse all tokens in the input script and parses out functions into function info objects and returns all function info objects as an array. Each object looks like this:

Start       : 99
Stop        : 182
StartLine   : 7
Name        : Show-Input
StopLine    : 10
StartColumn : 5
StopColumn  : 1
Text        : {function Show-Input {,     param ([string] $Incoming),     Write-Output $Incoming, }}

The text property is a string array and can be written to temporary file and dot sourced in or combined into a string using a newline and imported using Invoke-Expression.

Only the function text is extracted so if a line has multiple statements such as: Get-Process ; function foo () { only the part relevant to the function will be extracted.

function Get-Functions {
    param (
        [Parameter(Mandatory=$true)]
        [System.IO.FileInfo] $File
    )

    try {
        $content = Get-Content $File
        $PSTokens = [System.Management.Automation.PsParser]::Tokenize($content, [ref] $null)

        $functions = @()

        #Traverse tokens.
        for ($i = 0; $i -lt $PSTokens.Count; $i++) {
            if($PSTokens[$i].Type -eq  'Keyword' -and $PSTokens[$i].Content -eq 'Function' ) {
                $fxStart = $PSTokens[$i].Start
                $fxStartLine = $PSTokens[$i].StartLine
                $fxStartCol = $PSTokens[$i].StartColumn

                #Skip to the function name.
                while (-not ($PSTokens[$i].Type -eq  'CommandArgument')) {$i++}
                $functionName = $PSTokens[$i].Content

                #Skip to the start of the function body.
                while (-not ($PSTokens[$i].Type -eq 'GroupStart') -and -not ($PSTokens[$i].Content -eq '{')) {$i++ }

                #Skip to the closing brace.
                $startCount = 1 
                while ($startCount -gt 0) { $i++ 
                    if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++}
                    if ($PSTokens[$i].Type -eq 'GroupEnd'   -and $PSTokens[$i].Content -eq '}') {$startCount--}
                }

                $fxStop = $PSTokens[$i].Start
                $fxStopLine = $PSTokens[$i].StartLine
                $fxStopCol = $PSTokens[$i].StartColumn

                #Extract function text. Handle 1 line functions.
                $fxText = $content[($fxStartLine -1)..($fxStopLine -1)]
                $origLine = $fxText[0]
                $fxText[0] = $fxText[0].Substring(($fxStartCol -1), $fxText[0].Length - ($fxStartCol -1))
                if ($fxText[0] -eq $fxText[-1]) {
                    $fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol - ($origLine.Length - $fxText[0].Length)))
                } else {
                    $fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol))
                }

                $fxInfo = New-Object -TypeName PsObject -Property @{
                    Name = $functionName
                    Start = $fxStart
                    StartLine = $fxStartLine
                    StartColumn = $fxStartCol
                    Stop = $fxStop
                    StopLine = $fxStopLine
                    StopColumn = $fxStopCol
                    Text = $fxText
                }
                $functions += $fxInfo
            }
        }
        return $functions
    } catch {
        throw "Failed in parse file '{0}'. The error was '{1}'." -f $File, $_
    }
}

# Dumping to file and dot sourcing:
Get-Functions -File C:\MyScript.ps1 | Select -ExpandProperty Text | Out-File C:\fxs.ps1
. C:\fxs.ps1
Show-Input "hi"

#Or import without dumping to file:

Get-Functions -File  C:\MyScript.ps1 | % { 
    $_.Text -join [Environment]::NewLine | Invoke-Expression
}
Show-Input "hi"
Andy Arismendi
  • 50,577
  • 16
  • 107
  • 124
  • 1
    I think this is not true. I've just tried you code with `Write-Host blah` - 'blah' is printed to the host. – Roman Kuzmin May 11 '12 at 06:48
  • @RomanKuzmin You might be right, I only tested with the script above. It did ignore the mandatory parameter, but that must of made $MyInput null so I may of received a blank new line without realizing it. So maybe it only gets half of what I need. I really want to avoid creating my own script parse tree thing! – Andy Arismendi May 11 '12 at 06:55
  • I changed the answer completely, thanks for trying out the original @RomanKuzmin :-) – Andy Arismendi May 11 '12 at 19:20
  • ```if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++}``` should be ```if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -in @('{', '@{')) {``` to parse correctly such code as ```$hashmapFiltered = @{}``` – sebbrochet Dec 05 '19 at 08:54