Search code examples
powershellmodule

What is the purpose of the *.psm1 files in a Powershell module?


So I implemented my first Powershell module with a bunch of .ps1 files (one per function) and the .psd1 manifest file.

I am trying to understand what is the purpose of the .psm1 files - do I need them at all in my module?

What is their added value?

EDIT 1

Here is my .psd1 file:

@{
    ModuleVersion = "0.0.19106.59054"
    GUID = "59bc8fa6-b480-4226-9bcc-ec243102f3cc"
    Author = "..."
    CompanyName = "..."
    Copyright = "..."
    Description = "..."
    ScriptsToProcess = "vsts\config.ps1"
    VariablesToExport = @(
        "TfsInstanceUrl",
        "TfsApiVersion",
        "QANuGetRepoUrl"
    )
    NestedModules = @(
        "db\Backup-Database.ps1",
        ...
        "vsts\work\Get-WorkItems.ps1"
    )
    FunctionsToExport = @(
        "Assert-ExtractionDestFolder",
        ...
        "Write-HostIfNotVerbose"
    )
    PrivateData = @{
        PSData = @{
            ExternalModuleDependencies = "SqlServer"
        }
    }
}

Like I said, each function is in its own file.


Solution

  • what is the purpose of the .psm1 files - do I need them at all in my module?

    • In script modules, i.e., modules authored in PowerShell (as opposed to compiled binary cmdlets), it is only *.psm1 files that provide the module-specific behaviors distinct from regular *.ps1 script files (separate, isolated scope, private commands, control over exported commands).

      • Typically, a script module manifest has a RootModule entry pointing to (the main) *.psm1 file; for smaller modules it is not uncommon for all of the module's functionality to be implemented in this one *.psm1 file.

        • In fact, a stand-alone *.psm1 file can also act as a module, though it doesn't integrate with PowerShell's module auto-discovery and auto-loading feature.

        • Note that if you were to use a regular *.ps1 script directly in RootModule, its definitions would be loaded into the caller's scope, not the module's; that is, you would lose the benefits of a module.

    • Even though you're listing regular *.ps1 scripts in your NestedModules manifest entry, by virtue of using that specific entry these scripts are dot-sourced in the module's context and thereby become part of the module:

      • This is conceptually equivalent to creating and referencing a root *.psm1 script in RootModule, and - instead of defining a NestedModules entry - explicitly dot-sourcing your *.ps1 scripts from there - see bottom section.

      • Note that if you were to reference *.psm1 files in NestedModules, they would truly become nested modules, with their own scopes; a nested module is usable from the enclosing module, but not visible to the outside world (though you can list it among the loaded modules with Get-Module -All).


    Listing *.ps1 files in NesteModules vs. dot-sourcing them from the RootModule

    While there should be no difference in functionality, using a *.psm1 RootModule to dot-source the *.ps1 files containing your module's functions can potentially simplify things, if you simply need to dot-source all *.ps1 files located in your module directory's subtree:

    # Add this to the *.psm1 file specified in 'RootModule'
    
    # Find all *.ps1 files in the module's subtree and dot-source them
    foreach ($script in 
      (Get-ChildItem -File -Recurse -LiteralPath $PSScriptRoot -Filter *.ps1)
    ) { 
      . $script.FullName 
    }
    

    If you need to load the scripts in a specific order, need only a subset of the scripts, or want to speed things up slightly (though I doubt that the speed difference will be noticeable), you can dot-source the files individually from a RootModule *.psm1 file, as an alternative to listing them in the NestedModules entry:

    # Add this to the *.psm1 file specified in 'RootModule'
    
    # Dot-source the *.ps1 files individually.
    . "$PSScriptRoot/db/Backup-Database.ps1"
    # ...
    . "$PSScriptRoot/vsts/work/Get-WorkItems.ps1"
    

    Again, all of the above approaches are functionally equivalent. Given that you explicitly export functions via the ExportedFunctions entry (as is advisable), the use of *.ps1 files that must be dot-sourced is ultimately an implementation detail that is not relevant for the purposes of command discovery and module auto-loading - it only matters at actual import time.