Creating functions dynamically in a module in PowerShell

2.2k Views Asked by At

Suppose I have the following code in a module (called MyModule.psm1, in the proper place for a module):

function new-function{

    $greeting='hello world'
    new-item -path function:\ -name write-greeting -value {write-output $greeting} -Options AllScope
    write-greeting
}

After importing the module and running new-function I can successfully call the write-greeting function (created by new-function).

When I try to call the write-greeting function outside the scope of the new-function call, it fails because the function does not exist.

I've tried dot-sourcing new-function, but that doesn't help. I've supplied the -option Allscope, but apparently that only includes it in child scopes.

I've also tried explicitly following the new-item call with an export-modulemember write-greeting which doesn't give an error, but also doesn't create the function.

I want to be able to create a function dynamically (i.e. via new-item because the contents and name of the function will vary based on input) from a function inside a module and have the newly created function available to call outside of the module.

Specifically, I want to be able to do this:

Import-module MyModule
New-Function
write-greeting

and see "hello world" as output

Any ideas?

3

There are 3 best solutions below

3
On BEST ANSWER

Making the function visible is pretty easy: just change the name of your function in New-Item to have the global: scope modifier:

new-item -path function:\ -name global:write-greeting -value {write-output $greeting} #-Options AllScope

You're going to have a new problem with your example, though, because $greeting will only exist in the new-function scope, which won't exist when you call write-greeting. You're defining the module with an unbound scriptblock, which means it will look for $greeting in its scope (it's not going to find it), then it will look in any parent scopes. It won't see the one from new-function, so the only way you'll get any output is if the module or global scope contain a $greeting variable.

I'm not exactly sure what your real dynamic functions will look like, but the easiest way to work around the new issue is to create a new closure around your scriptblock like this:

new-item -path function:\ -name global:write-greeting -value {write-output $greeting}.GetNewClosure()

That will create a new dynamic module with a copy of the state available at the time. Of course, that creates a new problem in that the function won't go away if you call Remove-Module MyModule. Without more information, I'm not sure if that's a problem for you or not...

2
On

You were close with needing to dot source, but you were missing Export-ModuleMember. Here is a complete example:

function new-function
{
    $greeting='hello world'
    Invoke-Expression "function write-greeting { write-output '$greeting' }"
    write-greeting
}

. new-function

Export-ModuleMember -Function write-greeting

You also did not need or want -Scope AllScope.

Using the global: scope qualifier appears to work, but isn't the ideal solution. First, your function could stomp on another function in the global scope, which modules normally shouldn't do. Second, your global function would not be removed if you remove the module. Last - your global function won't be defined in the scope of the module, so if it needed access to non-exported functions or variables in your module, you can't (easily) get at them.

2
On

Thanks to the other solutions i was able to come up with a little helper that allows me to add plain script-files as functions and export them for the module in one step. I have added the following function to my .psm1

function AddModuleFileAsFunction {
    param (
        [string] $Name, 
        [switch] $Export
    )

    $content = Get-Content (Join-Path $PSScriptRoot "$Name.ps1") -Raw

    # Write-Host $content

    $expression = @"
function $Name {
$content
}
"@

    Invoke-Expression $expression

    if ($Export) {
        Export-ModuleMember -Function $Name
    }
}

this allows me to load scripts as functions:

. AddModuleFileAsFunction "Get-WonderfulThings" -Export

( loads Get-WonderfulThings.ps1 body and exports it as function:Get-WonderfulThings )