5

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?

Mike Shepard
  • 17,466
  • 6
  • 51
  • 69
  • I thought it was clear. I'll edit. – Mike Shepard Jan 24 '16 at 21:34
  • So you are able to create the function but the issue is actually scope since you cannot access it outside the current/child scopes? – Matt Jan 24 '16 at 21:50
  • yes. I can call it in the scope I created it in (right after the new-item call) but it's not accessible in the calling scope. – Mike Shepard Jan 24 '16 at 21:51
  • I guess I'm still not being clear. Edited one more time with what I'm trying to achieve. – Mike Shepard Jan 24 '16 at 22:13
  • Yes. I want to be able to create a function (write-greeting) from a function (new-function) in a module. The write-greeting function should be available outside of the module. – Mike Shepard Jan 24 '16 at 22:44

3 Answers3

9

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...

Rohn Edwards
  • 2,499
  • 1
  • 14
  • 19
  • Ok. You're my hero. I didn't think of naming the function with a scope modifier. I had worked out the closure part already but it's a very well thought out point. – Mike Shepard Jan 24 '16 at 23:33
  • 1
    Using global: is usually not recommended for a variety of reasons - it pollutes the global scope, the function can't (easily) access the module scope. – Jason Shirk Jan 25 '16 at 14:00
  • If you don't create a new closure, the scriptblock in Mike's example is bound to the module's scope, allowing easy access. Also, removing the module removes his `write-greeting` command. All that changes once you create a new closure, though... – Rohn Edwards Jan 25 '16 at 14:19
3

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.

Jason Shirk
  • 7,734
  • 2
  • 24
  • 29
  • I read his question as wanting to stomp on the global scope. As I understand it, he wants to be able to load the module without the `write-greeting` command exposed. Then he wants to run `new-function` to expose the greeting command. Also, your example appears to overwrite an existing `write-greeting` function (as I would expect it to), and it isn't removed when the module is removed... – Rohn Edwards Jan 25 '16 at 14:29
  • I updated my sample to correctly define the function in the module scope by using Invoke-Expression (which I usually strongly discourage, but in this case, I think there is no alternative). Importing a module in global scope will overwrite a function in global scope, but you can import a module in a local scope, and in that case, a well behaved module shouldn't overwrite the global scope. – Jason Shirk Jan 26 '16 at 17:06
0

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 )

Chris
  • 527
  • 3
  • 15
  • Why not use the standard &(Join-Path $PSScriptRoot "$Name.ps1") for this case. While I agree, it's a good way to mix code from a file with code written in an editor. – Garric Mar 06 '23 at 20:32
  • well, at that point in time i still tried to keep the function declaration out of the actual file, so i could execute the file as if i would execute the function interchangably for debugging. using your way would basically instantly execute the function – Chris Mar 07 '23 at 23:48