Frederik
Frederik

Reputation: 2971

Dot-sourcing a script without executing it

Is it possible in Powershell to dot-source or re-use somehow script functions without it being executed? I'm trying to reuse the functions of a script, without executing the script itself. I could factor out the functions into a functions only file but I'm trying to avoid doing that.


Example dot-sourced file:

function doA
{
    Write-Host "DoAMethod"
}

Write-Host "reuseme.ps1 main."

Example consuming file:

. ".\reuseme.ps1"

Write-Host "consume.ps1 main."
doA

Execution results:

reuseme.ps1 main.
consume.ps1 main.
DoAMethod

Desired result:

consume.ps1 main.
DoAMethod

Upvotes: 11

Views: 7425

Answers (5)

Nova
Nova

Reputation: 108

Here is a simple way import functions from another file into the current scope without executing the file. Pretty useful when unit testing with Pester.

function Get-Functions($filePath)
{
    $script = Get-Command $filePath
    return $script.ScriptBlock.AST.FindAll({ $args[0] -is [Management.Automation.Language.FunctionDefinitionAst] }, $false)
}

Get-Functions ScriptPath.ps1 | Invoke-Expression

This works by invoking the Powershell parser directly, like in previous answers.

Answer was inspired by this thread: Is there a way to show all functions in a PowerShell script?

Upvotes: 3

Alex
Alex

Reputation: 18546

While looking a bit further for solutions for this issue, I came across a solution which is pretty much a followup to the hints in Aaron's answer. The intention is a bit different, but it can be used to achieve the same result.

This is what I found: https://virtualengine.co.uk/2015/testing-private-functions-with-pester/

It needed it for some testing with Pester where I wanted to avoid changing the structure of the file before having written any tests for the logic.

It works quite well, and gives me the confidence to write some tests for the logic first, before refactoring the structure of the files so I no longer have to dot-source the functions.

Describe "SomeFunction" {
  # Import the ‘SomeFunction’ function into the current scope
  . (Get-FunctionDefinition –Path $scriptPath –Function SomeFunction)

  It "executes the function without executing the script" {
     SomeFunction | Should Be "fooBar"
 }
}

And the code for Get-FunctionDefinition

#Requires -Version 3

<#
.SYNOPSIS
    Retrieves a function's definition from a .ps1 file or ScriptBlock.
.DESCRIPTION
    Returns a function's source definition as a Powershell ScriptBlock from an
    external .ps1 file or existing ScriptBlock. This module is primarily
    intended to be used to test private/nested/internal functions with Pester
    by dot-sourcsing the internal function into Pester's scope.
.PARAMETER Function
    The source function's name to return as a [ScriptBlock].
.PARAMETER Path
    Path to a Powershell script file that contains the source function's
    definition.
.PARAMETER LiteralPath
    Literal path to a Powershell script file that contains the source
    function's definition.
.PARAMETER ScriptBlock
    A Powershell [ScriptBlock] that contains the function's definition.
.EXAMPLE
    If the following functions are defined in a file named 'PrivateFunction.ps1'

    function PublicFunction {
        param ()

        function PrivateFunction {
            param ()
            Write-Output 'InnerPrivate'
        }

        Write-Output (PrivateFunction)
    }

    The 'PrivateFunction' function can be tested with Pester by dot-sourcing
    the required function in the either the 'Describe', 'Context' or 'It'
    scopes.

    Describe "PrivateFunction" {
        It "tests private function" {
            ## Import the 'PrivateFunction' definition into the current scope.
            . (Get-FunctionDefinition -Path "$here\$sut" -Function PrivateFunction)
            PrivateFunction | Should BeExactly 'InnerPrivate'
        }
    }
.LINK
    https://virtualengine.co.uk/2015/testing-private-functions-with-pester/
#>
function Get-FunctionDefinition {
    [CmdletBinding(DefaultParameterSetName='Path')]
    [OutputType([System.Management.Automation.ScriptBlock])]
    param (
        [Parameter(Position = 0,
          ValueFromPipeline = $true,
          ValueFromPipelineByPropertyName = $true,
          ParameterSetName='Path')]
        [ValidateNotNullOrEmpty()]
        [Alias('PSPath','FullName')]
        [System.String] $Path = (Get-Location -PSProvider FileSystem),

        [Parameter(Position = 0,
          ValueFromPipelineByPropertyName = $true,
          ParameterSetName = 'LiteralPath')]
        [ValidateNotNullOrEmpty()]
        [System.String] $LiteralPath,

        [Parameter(Position = 0,
          ValueFromPipeline = $true,
          ParameterSetName = 'ScriptBlock')]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.ScriptBlock] $ScriptBlock,

        [Parameter(Mandatory = $true,
          Position =1,
          ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [System.String] $Function        
    )

    begin {
        if ($PSCmdlet.ParameterSetName -eq 'Path') {
            $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path);
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') {
            ## Set $Path reference to the literal path(s)
            $Path = $LiteralPath;          
        }
    } # end begin

    process {
        $errors = @();
        $tokens = @();
        if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
            $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock.ToString(), [ref] $tokens, [ref] $errors);
        } 
        else {
            $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors);
        }

        [System.Boolean] $isFunctionFound = $false;
        $functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true);
        foreach ($f in $functions) {
            if ($f.Name -eq $Function) {
                Write-Output ([System.Management.Automation.ScriptBlock]::Create($f.Extent.Text));
                $isFunctionFound = $true;
            }
        } # end foreach function

        if (-not $isFunctionFound) {
            if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
                $errorMessage = 'Function "{0}" not defined in script block.' -f $Function;
            }
            else {
               $errorMessage = 'Function "{0}" not defined in "{1}".' -f $Function, $Path;
            }
            Write-Error -Message $errorMessage;
        }
    } # end process
} #end function Get-Function

Upvotes: 2

SultanShell
SultanShell

Reputation: 121

After your function, the line Write-Host "reuseme.ps1 main." is known as "procedure code" (i.e., it is not within the function). You can tell the script not to run this procedure code by wrapping it in an IF statement that evaluates $MyInvocation.InvocationName -ne "."

$MyInvocation.InvocationName looks at how the script was invoked and if you used the dot (.) to dot-source the script, it will ignore the procedure code. If you run/invoke the script without the dot (.) then it will execute the procedure code. Example below:

function doA
{
    Write-Host "DoAMethod"
}

If ($MyInvocation.InvocationName -ne ".")
{
    Write-Host "reuseme.ps1 main."
}

Thus, when you run the script normally, you will see the output. When you dot-source the script, you will NOT see the output; however, the function (but not the procedure code) will be added to the current scope.

Upvotes: 12

Aaron Jensen
Aaron Jensen

Reputation: 26749

The best way to re-use code is to put your functions in a PowerShell module. Simply create a file with all your functions and give it a .psm1 extension. You then import the module to make all your functions available. For example, reuseme.psm1:

function doA
{
    Write-Host "DoAMethod"
}

Write-Host "reuseme.ps1 main."

Then, in whatever script you want to use your module of functions,

# If you're using PowerShell 2, you have to set $PSScriptRoot yourself:
# $PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Import-Module -Name (Join-Path $PSScriptRoot reuseme.psm1 -Resolve)

doA

Upvotes: 4

Joey
Joey

Reputation: 354586

You have to execute the function definitions to make them available. There is no way around it.

You could try throwing the PowerShell parser at the file and only executing function definitions and nothing else, but I guess the far easier approach would be to structure your reusable portions as modules or simply as scripts that don't do anything besides declaring functions.

For the record, a rough test script that would do exactly that:

$file = 'foo.ps1'

$tokens = @()
$errors = @()
$result = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$tokens, [ref]$errors)

$tokens | %{$s=''; $braces = 0}{
    if ($_.TokenFlags -eq 'Keyword' -and $_.Kind -eq 'Function') {
        $inFunction = $true
    }
    if ($inFunction) { $s += $_.Text + ' ' }
    if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'LCurly') {
        $braces++
    }
    if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'RCurly') {
        $braces--
        if ($braces -eq 0) {
            $inFunction = $false;
        }
    }
    if (!$inFunction -and $s -ne '') {
        $s
        $s = ''
    }
} | iex

You will have problems if functions declared in the script reference script parameters (as the parameter block of the script isn't included). And there are probably a whole host of other problems that can occur that I cannot think of right now. My best advice is still to distinguish between reusable library scripts and scripts intended to be invoked.

Upvotes: 8

Related Questions