Search code examples
c#powershellpowershell-cmdletrunspace

How does Powershell know where to find modules for import?


I am really at beginner level for working with commandlets and powershell stuff. I am invoking commandlets from C# using PowerShell API. I saw strange behaviours. While on different threads on stackoverfow, people explicitly importing modules using Import-Command or PSImportModule method, I can see if there is an assembly at $env:PSModulePath available already, it is automatically loaded. Does this behaviour is by default or due to criteria configurations which i m overlooking. I am running unit test in ms test environment.

I am using following code.

 System.Management.Automation.PowerShell _powerShellInstance
 _powerShellInstance.AddCommand("Connect-AzureRmAccount", false);
 var output = _powerShellInstance.Invoke();

 // Invoking my commandlets

 _powerShellInstance.AddCommand("Get-LinkParameter", false); 

The above command automatically loads the assembly from C:\Windows\system32\WindowsPowerShellModules\v1.0\Modules\. I have not created any runspace and no configuration sets. Just above automatically loading things. I need to confirm how exactly is the powershell and run space behaves. Because I need to clear how i need to install my commandlets on production machine then. How unit tests on production machine will access my commandlets to perfectly get loaded.


Solution

  • While it is good practice to explicitly load the modules you want using Import-Module, since Powershell 3.0 if a module is available at one of the locations returned by $env:PSModulePath, it will automatically get loaded by default if one of its cmdlets are invoked. Below is a breakdown of the different paths:

    User Modules

    $modulePath = "${env:UserProfile}\Documents\WindowsPowerShell\Modules" Modules installed here are only available to the current user's Powershell session, and by default modules installed using Install-Module are saved here.

    All User Modules

    $modulePath = "${env:ProgramFiles}\WindowsPowerShell\Modules" Modules installed here are available to any users' Powershell session.

    System Modules

    $modulePath = "${env:SystemRoot}\system32\WindowsPowerShell\v1.0\Modules" Modules installed here are available system wide to any Powershell session, but should be kept clean for Windows to manage. Typically, you do not want to install your own modules here.

    Adding Additional Module Paths

    You can add additional paths to $env:PSModulePath similarly to how you would modify the $env:PATH variable for resolving executable paths. It is simply a semicolon ; delimited string of directories where modules reside, and if a module is available at any path in $env:PSModulePath, Powershell will know where to find it. And in fact, you may see that other installed tools may have added their own paths to $env:PSModulePath. A few of examples of programs/toolsets which do this are Microsoft SQL Studio, Microsoft System Center - Operations Manager, and the Chef Development Kit.

    Importing a Module Not On the Path

    You can temporarily edit $env:PSModulePath to contain a directory with a module you want to load. For example, if you wanted to import a module named TestModule from some arbitrary path:

    $env:PSModulePath += ';C:\Path\To\Temporary\ModuleDirectory'
    Import-Module TestModule
    

    where TestModule exists as a sub-folder of C:\Path\To\Temporary\ModuleDirectory

    You do not need to back out the module path change when you are ready to end your Powershell session as the change above is temporary. Consequently, you would need to modify $env:PSModulePath in every session, so if TestModule was something you wanted to have available at all times for use, you can either copy it to one of the other directories in $env:PSModulePath or permanently add C:\Path\To\Temporary\ModuleDirectory to the PSModulePath environment variable.

    Alternatively, you can also directly import a .psm1 file or the directory housing a module with a .psd1 file within it by providing the path to Import-Module. This is useful for importing one-off modules that are not present somewhere in $env:PSModulePath.

    # Must have a valid `.psd1` manifest in the ModuleName folder
    Import-Module C:\SomeFolder\ModuleName
    
    # You can directly import `.psm1` files, but don't do this if a module manifest is present
    Import-Module C:\SomeFolder\OtherModules\SomeModule.psm1
    

    A Note About UNC Paths

    You can also add UNC (network) paths to $env:PSModulePath. However, I believe any remote module scripts will still be subject to the Powershell ExecutionPolicy set on the system.

    And a Bit More About Installing Modules

    By default, Install-Module installs a module to the User Modules directory, but you can control this a bit with the -Scope parameter. For example, the following commands will change the location a module is installed into:

    # Install to the current user location (default behavior if scope unspecified)
    Install-Module -Scope CurrentUser $moduleName
    
    # Install to the all users location (requires elevated permissions)
    Install-Module -Scope AllUsers $moduleName
    

    Unfortunately, those are the only two locations PowerShell will assist you installing your modules to. The System modules are critical to the operation of PowerShell and should not be modified by end users, and additional paths added to $env:PSModulePath are likely managed by software outside of PowerShell, typically by MSIs or other installers.

    Additionally, if you write software that ships with one or more PowerShell modules it's good practice to have your installer add a new directory to the system's %PSModulePath% and drop the module there, rather than installing to the standard AllUsers or CurrentUser path, as these are really meant for the end user to manage at their whim. Have your software update process update the module in this case. This has the benefit of preventing accidental modification or removal of the module to an incompatible version.