MadBoy
MadBoy

Reputation: 11104

Loading additional files (ps1) for use in PowerShell modules

I've PowerShell Module (lets call it PSModule) in which I've defined as follows in .psd1

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
ScriptsToProcess = @('Enums\MessageType.ps1')

This is added there so that the file MessageType.ps1 that holds the just Enums can be reused across multiple files.

This file looks like:

enum MessageType {
   None
   Something
}

An example of this is PSWriteWord - https://github.com/EvotecIT/PSWriteWord module. This is supposed to keep Enums in it's own folder and still load them when module is started.

This works fine most of the time. I've run scripts without issues. Now I've a weird situation where I've created: script1.ps1 - that I call.

Content of script1.ps1

Import-Module PSModule -Force

Import-Module PSOtherModule -Force

Call-Me -Parameters <params> # part of PSOtherModule

Now within Call-Me function from PSOtherModule I call

Do-Something
do-Something

Call-OtherFunction # function from PSModule

It will work when running in ISE or VSCode...

Now if I rerun the same script from Task Scheduler it won't load the MessageType which essentially will fail at some point. It seems that it simply skips the processing of ScriptsToProcess.

Now if you do:

Do-Something
do-Something
Import-Module PSModule
Call-OtherFunction # function from PSModule

It still won't work ... But this will...

Do-Something
do-Something
Import-Module PSModule -Force
Call-OtherFunction # function from PSModule

So now I'm struggling to find a way to properly add ENUMS as separate files to my modules and keep this running without weird workaround with IMport-Module just before calling PSModule.

I've found this: https://d-fens.ch/2014/11/26/bug-powershell-scripts-in-scriptstoprocess-attribute-appear-as-loaded-modules/ which does what it says it does well but it doesn't solve my problem.

Anyone knows a way to workaround it? I tried putting fullpath to ScriptsToProcess thinking that Enums path in ScriptsToProcess may be somehow overwritten ... but no..

My .psm1 file looks like this:

#Get public and private function definition files.
$Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue )
$Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue )
$Enums = @( Get-ChildItem -Path $PSScriptRoot\Enums\*.ps1 -ErrorAction SilentlyContinue )

#Dot source the files
Foreach ($import in @($Public + $Private + $Enums)) {
    Try {
        . $import.fullname
    } Catch {
        Write-Error -Message "Failed to import function $($import.fullname): $_"

    }
}
Export-ModuleMember -Function '*'
[string] $ManifestFile = '{0}.psd1' -f (Get-Item $PSCommandPath).BaseName;
$ManifestPathAndFile = Join-Path -Path $PSScriptRoot -ChildPath $ManifestFile;
if ( Test-Path -Path $ManifestPathAndFile) {
    $Manifest = (Get-Content -raw $ManifestPathAndFile) | iex;
    foreach ( $ScriptToProcess in $Manifest.ScriptsToProcess) {
        $ModuleToRemove = (Get-Item (Join-Path -Path $PSScriptRoot -ChildPath $ScriptToProcess)).BaseName;
        if (Get-Module $ModuleToRemove) {
            Remove-Module $ModuleToRemove;
        }
    }
}

I added Enums just now but it doesn't change anything... The thing after Export-Module is for removing the Script from Modules (as in the link). It doesn't matter for my problem.

Can be seen here: enter image description here

Upvotes: 1

Views: 2406

Answers (2)

MadBoy
MadBoy

Reputation: 11104

To work around this issue I've opted to use Add-Type with C# code that is compiled on demand instead of PowerShell's own enum construct. It works.

<#
enum MessageType {
    Alert
    Cancel
    Disable
    Download
    Minus
    Check
    Add
    None
}

#>


Add-Type -TypeDefinition @"
   public enum MessageType
   {
    Alert,
    Cancel,
    Disable,
    Download,
    Minus,
    Check,
    Add,
    None
   }
"@

Upvotes: 1

mklement0
mklement0

Reputation: 438208

Note:

  • If you're just looking for an effective solution based on on-demand compilation of C# code via Add-Type, see MadBoy's own answer.

  • If you'd like to understand the issue, including why an Add-Type-based solution helps, read on.


The behavior is as expected:

Your Import-Module PSModule -Force call in script.ps1 dot-sources Enums\MessageType.ps1 only in script.ps1's scope.

Therefore, when Call-Me from module PSOtherModule runs, it knows nothing about the enums you've loaded into script.ps1, because PSOtherModule has its own scope (that doesn't inherit from the caller).

The reason that Import-Module PSModule -Force in Call-Me works is that you're forcing reloading of the module in the context of PSOtherModule, which therefore then dot-sources Enums\MessageType.ps1, making the enum available (whereas omitting -Force would be a no-op, since PowerShell has already loaded the module (which happens session-globally) and therefore doesn't process the module manifest and its ScriptsToProcess entry again).

Yes, using Add-Type with a string containing C# code that is compiled on demand solves the problem (as shown in the answer you've since posted), but for an unrelated reason: Add-Type-added types are invariably session-global, irrespective of in what scope Add-Type is called.

To achieve the same effect with PowerShell enum (or class) definitions - and thereby solve your problem - you need to:

  • include the enum definitions directly in your PSModule.psm1 file.

  • use the using module PSModule directive instead of an Import-Module PSModule cmdlet call in order to import your module - only using module makes PowerShell-based enum and class definitions available outside the module, and then also session-globally.


Optional background information:

  • enum and class definitions are a recent addition to PowerShell (v5), and they're not part of the module-export mechanism.

    • Outside of modules, enum and class definitions are local to the scope they're defined in / dot-sourced into.

    • If you make enum and class definitions part of a module (*.psm1) and you import that module with using module, these definitions do become available, but invariably all of them, and invariably session-globally (as do Add-Type-based types, although they do so even with Import-Module).

    • If you use Import-Module instead of using module, the class and enum definitions that are part of the module are effectively private.
  • Many fixes and enhancements to enum and class definitions are pending as of Windows PowerShell v5.1 / PowerShell Core v6.1.0:

    • This meta GitHub issue tracks all of them.

    • Making Import-Module see a module's enum and class definitions too is discussed in this issue; while fundamentally, possible, the fact that Import-Module executes at runtime whereas using module executes at parse time precludes certain use cases with Import-Module (in short: class definitions referencing other class definitions).

    • Separately, being able to explicitly control which enum and class definitions are to be exported - analogous to how you can selectively export functions, aliases, and variables - is being discussed in this issue.

Upvotes: 4

Related Questions