2

I have a module with several files with functions and module loader.

The example function:

Function1.ps1

function Init() {
    echo "I am the module initialization logic"
}

function DoStuff() {
    echo "Me performing important stuff"
}

Module loader file:

Module1.psm1:

$script:Functions = Get-ChildItem $PSScriptRoot\*.ps1 

function LoadModule {
    Param($path)
    foreach ($import in @($path)) {
        . $import.FullName
    }
}

LoadModule script:Functions

Init # function doesn't found 

So I'm trying to load functions from the file Function1.ps1 by procedure LoadModule. Debugging LoadModule shows external functions loaded, but after finishing LoadModule procedure the functions become not accessible so script fails on Init row.

But rewritten module loader with no LoadModule function works fine

Module1.psm1:

Get-ChildItem $PSScriptRoot\*.ps1 | %{
    . $import.FullName
}
Init # In this case - all works fine

So as I understand the file functions loaded from function placed in some isolated scope and to able to access them I need to add some scope flag.

Maybe somebody knows what I should add to make function Init() accessible from the module.pm1 script body but not make it accessible externally (without using Export-ModuleMember)?

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
Stadub Dima
  • 858
  • 10
  • 24

3 Answers3

2

Note: Edit 1, a clarification on what dot sourcing actually does, is included at the end.

First up, you are intermingling terminology and usage for Functions and Modules. Modules, which have the .psm1 extension, should be imported into the terminal using the Import-Module cmdlet. When dot sourcing, such as what you are doing here, you should only be targeting script files which contain functions, which are files with the .ps1 extension.

I too am relatively new to PowerShell, and I ran into the same problem. After spending around an hour reading up on the issue I was unable to find a solution, but a lot of the information I found points to it being an issue of scope. So I created a test, utilising three files.

foo.ps1

function foo {
  Write-Output "foo"
}

bar.psm1

function bar {
  Write-Output "bar"
}

scoping.ps1

function loader {
    echo "dot sourcing file"
    . ".\foo.ps1"
    foo
    echo "Importing module"
    Import-Module -Name ".\bar.psm1"
    bar
}

foo
bar

loader

foo
bar

pause

Lets walk through what this script does.

First we define a dummy loader function. This isn't a practical loader, but it is sufficient for testing scopes and the availability of functions within files that are loaded. This function dot sources the ps1 file containing the function foo, and uses Import-Module for the file containing the function bar.

Next, we call on the functions foo and bar, which will produce errors, in order to establish that neither are within the current scope. While not strictly necessary, this helps to illustrate their absence.

Next, we call the loader function. After dot sourcing foo.ps1, we see foo successfully executed because foo is within the current scope of the loader function. After using Import-Module for bar.psm1, we see bar also successfully executed. Now we exit the scope of the loader function and return to the main script.

Now we see the execution of foo fail with an error. This is because we dot sourced foo.ps1 within the scope of a function. However, because we imported bar.psm1, bar successfully executes. This is because modules are imported into the Global scope by default.


How can we use this to improve your LoadModule function? The main thing for this functionality is that you need to switch to using modules for your imported functions. Note that, from my testing, you cannot Import-Module the loader function; this only works if you dot source the loader.

LoadModule.ps1

function LoadModule($Path) {
    Get-ChildItem -Path "$Path" -Filter "*.psm1" -Recurse -File -Name| ForEach-Object {
        $File = "$Path$_"
        echo "Import-Module -Name $File"
        Import-Module -Name "$File" -Force
    }
}

And now in a terminal:

. ".\LoadModule.ps1"
LoadModule ".\"
foo
bar

Edit 1: A further clarification on dot sourcing

Dot sourcing is equivalent to copy-pasting the contents of the specified file into the file preforming the dot source. The file performing the operation "imports" the contents of the target verbatim, performing no additional actions before proceeding to execute the "imported" code. e.g.

foo.ps1 Write-Output "I am foo"
. ".\bar.ps1"

bar.ps1

Write-Output "I am bar"

is effectively

Write-Output "I am foo"
Write-Output "I am bar"
Jekotia
  • 71
  • 1
  • 3
  • The `Import-Module` syntax doesn't fit for me because the `Module1.psm1` it is actually a module which aim is to load other functions into the module. And if the module will use other functions importing with `Import-Module` the module won't be able to export them to environment. – Stadub Dima Jul 30 '19 at 16:05
  • By the way, thanks a lot for the "modules load" and "a dot syntax" detailed explanation – Stadub Dima Jul 30 '19 at 16:07
  • If you use it as I have described, it should work for your use-case. Source a .ps1 file containing your module loader, and then invoke the module loader to import your remaining functions as modules. Importing a module which then imports more modules does not seem to work. Sourcing a function which then imports modules does work. – Jekotia Jul 30 '19 at 18:28
  • I should also note that .ps1 and .psm1 files are technically the same. You're not missing anything there. The distinction is a semantic one, which encourages better code practices. Your reusable code should be in modules which you import, and your use-case-specific code should be in .ps1 files which you execute. These .ps1 files then have access to your reusable modules. – Jekotia Jul 30 '19 at 19:41
1

Edit: You don't actually need to use Import-Module. So long as you have the modules in your $env:PSModulePath PowerShell will autoload any exported functions when they are first called. Source.

Depending on the specifics of your use case, there's another method you can use. This method addresses when you want to mass-import modules into a PowerShell session.

When you start PowerShell it looks at the values of the environment variable $PSModulePath in order to determine where it should look for modules. It then looks under this directory for directories containing psm1 and psd1 files. You can modify this variable during the session, and then import modules by name. Here's an example, using what I've added to my PowerShell profile.ps1 file:

$MyPSPath = [Environment]::GetFolderPath("MyDocuments") + "\WindowsPowerShell"

$env:PSModulePath = $env:PSModulePath + ";$MyPSPath\Custom\Modules"

Import-Module `
    -Name Confirm-Directory, `
    Confirm-File, `
    Get-FileFromURL, `
    Get-RedirectedURL, `
    Get-RemoteFileName, `
    Get-ReparseTarget, `
    Get-ReparseType, `
    Get-SpecialPath, `
    Test-ReparsePoint

In the event that you're new to PowerShell profiles (they're pretty much the same as Unix's ~/.profile file), you can find:

  1. more information about PowerShell profiles here.
  2. a summary of what profile files are used and when here.

While this may not seem as convenient as an auto-loader, installing & importing modules is the intended and accepted approach for this. Unless you have a specific reason not to, you should try to follow the established standards so that you aren't later fighting your way out of bad habits.

You can also modify the registry to achieve this.

Jekotia
  • 71
  • 1
  • 3
0

After some research, I found: During the execution of the LoadModule function, all registered functions will be added to Functions Provider

So from the LoadModule function body they can be enumerated via Get-ChildItem -Path Function:

[DBG]: PS > Get-ChildItem -Path Function:
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        C:
Function        Close-VSCodeHtmlContentView                        0.2.0      PowerShellEditorServices.VSCode
Function        Init                                               0.0        Module1
Function        ConvertFrom-ScriptExtent                           0.2.0    
Function        Module1                                            0.0        Module1

So we can store functions list to variable in the beginning of the invocation of the LoadModule

$loadedFunctions =  Get-ChildItem -Path Function:

and after dot load notation retrieve the added function list

Get-ChildItem -Path Function: |  where { $loadedFunctions -notcontains $_ } 

So the modified LoadModule function will look like:

function LoadModule {
    param ($path)
    $loadRef = Get-PSCallStack

    $loadedFunctions =  Get-ChildItem -Path Function:
    foreach ($import in @($path)) {
        . $import.FullName
    }
    $functions= Get-ChildItem -Path Function: | `
        Where-Object { $loadedFunctions -notcontains $_ } | `
        ForEach-Object{ Get-Item function:$_ }

    return $functions
}

the next step it just assigns the functions to list More about this

$script:functions = LoadModule $script:Private ##Function1.ps1
$script:functions += LoadModule $script:PublicFolder

After this step, we can

  • Invoke initalizer:
    $initScripts = $script:functions| #here{ $_.Name -eq 'Initalize'} #filter
    $initScripts | ForEach-Object{ & $_ } ##execute
  • and export Public functions:
    $script:functions| `
    where { $_.Name -notlike '_*'  } |  ` # do not extport _Name functions
    %{ Export-ModuleMember -Function $_.Name}

Full code of the module load function I moved to the ModuleLoader.ps1 file. And it can be found in the GitHub repo PowershellScripts

And the complete version of the Moudule.psm1 file is

if($ModuleDevelopment){
    . $PSScriptRoot\..\Shared-Functions\ModuleLoader.ps1 "$PSScriptRoot"
}
else {
    . $PSScriptRoot\Shared\ModuleLoader.ps1 "$PSScriptRoot"
}
Stadub Dima
  • 858
  • 10
  • 24