PowerShell Modules

Modules are a new feature of PowerShell v2 which were added to CTP2 with very little fanfare and no documentation. After struggling with the demo from the release notes, and searching in vain for help files, I was finally able to figure some of this out with some help from x0n of Nivot Ink and since I still can’t find anything on the web about modules, I figured I’d share what little I know, even though I’m sure the moment I do the PowerShell team will write it up properly on their blog ;) ( [new] Edit I was off by three months, but the PowerShell team has finally started explaining these) ... let’s start with a direct quote from the release notes:

Modules allow script developers and administrators to partition and organize their Windows PowerShell code in self-contained, reusable units. Code from a module executes in its own self-contained context and does not affect the state outside of the module. Modules also enable you to define a restricted runspace environment by using a script.

The point of a script module is that it allows much better control over what portions of a script are exposed in your runspace. Code from a module executes in its own self-contained context and does not have to affect the state outside of the module. Modules have access to the $PsScriptRoot variable which holds the path to the folder the script is running in, which allows you to dot-source additional scripts from the module directory, so you can split up your code into multiple files however you like — perhaps a file per function, or whatever works for you. Modules can even define a restricted runspace environment. If you’re a programmer, the best analogy seems to be to think of a powershell script module as basically a “class” with public and private methods and members. Obviously it can be a bit more than that, since cmdlets are rather more than simple methods, but try to keep that model in mind.

One key feature of modules (for scripters) is that the self-contained state of modules is also persistent, and there can be private and public member functions (or cmdlets). This might be the key feature of script modules: Script-scope variables are visible to all functions loaded inside the module, and their state and values are persistent through multiple calls to cmdlets or functions from the same module. They are invisible to code outside the module, which means that multiple functions that work together don’t need to store settings or state in the global scope, and don’t have to worry about name clashes (other than the publically exported members, we’ll get to that later).

Modules are the new Snapins?

As I’ve been experimenting, it turns out there’s way more to this than simple script modules. Modules appear destined to replace snapins as the main way to extend PowerShell. In fact, in the current CTP2 you can load most existing snapins as a module instead, which means you don’t have to be an administrator to load a new snapin — there’s no need to registery them by running InstallUtil.exe — you can simply place them in a folder and tell PowerShell where to find them.

Package Paths?

You can load any script as a module by specifying the full path as it’s name — that’s not the best way to handle modules. The best way is to create a folder with the module’s name inside a package directory. The environment variable PsPackagePath (which I believe will be changed to PSModulePath or even ModulePath in the next release) contains a list of folders where modules can be placed. The defaults are two folders called “Packages,” one in your Documents\WindowsPowerShell and one in $PSHome … the folders aren’t created by the CTP2 installer, and they’ll probably be changed to “Modules” in a future release.

In one of those path folders (or the default locations), you simply create a folder with the package name, and then put your module in it with the same name as it’s base name and either a .ps1, .psm1, .dll or .psd1 extension PSD1 files don’t actually work in CTP2. Having done that, you can load them by name without specifying the path. The cool thing about this is that you can actually create a folder on a USB or network drive, and stick all your modules in it … and then just add that folder to your PsPackagePath environment variable. It’s just like any other path variable, so you can specify multiple directory paths separated by semi-colons.

The key thing to understand (excuse me for repeating myself) is what PowerShell expects: inside the PsPackagePath there should be a folder for each module, and the name of the folder becomes the name by which you call the module — the module file and the folder should have the same name, so for instance if you want to load a script TestModule.ps1 as a module, you need to put it in ~\Documents\WindowsPowerShell\Packages\TestModule\TestModule.ps1 and then you can just Add-Module TestModule. The name of the folder is the name of the module, and the main module file must have the same name even if it’s a dll module.

Here is my TestModule.psm1 script. This example is fun because it demonstrates quite vividly the difference between a psm1 module and a ps1 script:


function fooize {
   if(!(Test-Path variable:script:count)) { $script:count = 0 }
   $script:count++
   "He spoke 'foo' and then there were {0}!" -f $script:count
}
function barize {
   if(!(Test-Path variable:script:count)) { $script:count = 0 }
   $script:count--
   "She spoke 'bar' and then there were {0}!" -f $script:count
}
function foobar($value) {
   $script:count = $value
}
Export-ModuleMember fooize, barize

If you comment out the last line of that script and name it TestModule.ps1 (after putting it in an appropriate folder), you can Add-Module TestModule and run fooize and barize … they work fine. The problem is that the $count variable leaks into your runspace, and since it isn’t initialized as part of the script, you can’t reset it by re-running Add-Module TestModule, even with the -Force parameter, and Remove-Module TestModule won’t work. You can also arbitrarily access the count variable: "The count is $count" will print the current value, and $count = 42 will change it without running the associated code.

However, if you rename it to TestModule.psm1 and restore the Export-ModuleMember call … then when you run it, it has it’s own internal $count variable. In fact, if you do that without resetting your runspace from the previous example, you’ll see that even though your $count is still at 42, the fooize and barize functions are using a separate counter which you can’t modify externally. If you run Add-Module TestModule -Force now, the $count variable will reset!

Cmdlets for Modules

There are so far four cmdlets which are available for dealing with modules, but there isn’t any help or documentation for any of them, short of the release notes, so apart from what I’ve said so far, let me give you their parameter set definitions, since that’s the best we’ve got, and I’ll add my explanations, as I understand them.

Get-Module (returns PSModuleInfo objects)

  • Get-Module [[-Name] <String[]>]

Get-Module lets you get all, or some, of the loaded Modules. It returns PSModuleInfo objects for them which can be used in the Add and Remove cmdlets.

Remove-Module

  • Remove-Module [-Name] <String[]>
  • Remove-Module -ModuleInfo <PSModuleInfo[]>

Oddly enough, in the current CTP Remove-Module doesn’t really do what you expect. As far as I can tell, all that it does, is removes the PSmoduleInfo from the list of loaded modules. This means you can run add-module against it again (otherwise, running Add-Module a second time on a module that’s already loaded does nothing unless you use the -Force parameter) but it doesn’t really do anything else. In fact, you can continue to use cmdlets and functions from these “removed” modules without any problems that I can see so far. I’m not sure if this is considered a bug, or if the team expects to leave it like this. I dearly hope that in the next CTP we will at least see the cmdlets and functions removed so they can’t be executed, especially since I can already remove the functions by hand by simply deleting them from the provider.

Add-Module

  • Add-Module [-Name] <String[]> [[-ImportList] <String[]>] [-Force] [-PassThru] [-ScriptBlock <ScriptBlock>]
  • Add-Module [-Name] <String[]> [-ScriptBlock] <ScriptBlock> [[-ImportList] <String[]>] [-Force] [-PassThru]
  • Add-Module [-Force] [-PassThru] [-ScriptBlock <ScriptBlock>] [-ModuleInfo <PSModuleInfo[]>]

The Add-Module cmdlet is the main cmdlet that provides the functionality of the modules. It loads modules into your runspace, and it can load various types of modules:

  1. The first, and most obvious from the release notes, is the script module — a PowerShell script with the extension PSM1 — I’ll come back to these in a bit.
  2. The second type is what we now call “Snapins” in v1 — as far as I can tell, you can load nearly any snapin dll as a module if you configure it carefully — although I’m not sure that Providers can be loaded this way, and some of the snapin initialization code isn’t run if you do this. The current PSCX snapin, for instance, intializes many things during its “install” and doesn’t work right if you don’t use InstalUtil and load it as a Snapin.
  3. You can also load PSD1 files, which are like metadata files about modules (which as far as I can tell, is how you would load a multi-assembly snapin-type module, or modules which needed type files, etc. like the PSCX snapin).
  1. And finally, you can load plain old ps1 script files, which are loaded just as though you had dot-sourced them (which incidentally means they can’t be “Remove“d at all).

I’ll fill in the details of how the ImportList works when I figure it out, I think it allows you to (further) restrict what is exported into your runspace from the module. You can use the -Force parameter to reload scripts you’ve already loaded, and you can specify -PassThru to have the PSModuleInfo object output from Add-Module (the same as running Get-Module)...

When you run Add-Module -Force, it reinitializes the script scope for the module. Take for example the previous script module named TestModule.psm1. If you Add-Module TestModule and then run “fooize” ... each time you run it, the number at the end of the string will increment. When you re-run Add-Module TestModule with the -Force parameter, the “count” variable is reset, but otherwise it remains, and even after you Remove-Module Test you can still run fooize and barize!

Forwarned is Forarmed

If you’re working on a compiled PowerShell snapin, you need to start working right now on what it would take to get it to load as a module, because unlike Snapins, Modules can be loaded by path, and more importantly, do not need installation by an administrator. This is going to mean that when PowerShell 2 becomes final, end users are going to have a clear preference for modules which are easier to trust (since they can be run entirely non-elevated), and easier to work with (since they can be carried on a memory stick or disk and loaded without installing on any computer).

Export-ModuleMember

  • Export-ModuleMember [-ExportList] <String[]> [-Update]

The final module cmdlet is actually the second most important one — without it, nothing in your PowerShell script modules would be visible after you load them. Generally speaking, you run Export-ModuleMember and pass it a list of function or cmdlet names which you wish to export — to make them visible from outside the script.

Exporting a module member is analogous to marking a C# member method public, in that it marks it for use by external scripts, etc. You shouldn’t use this cmdlet in a plain .ps1 script (or on the command-line) because it won’t work, and it actually does weird things (like killing my profile) that I can’t explain :-) .

And for my final trick …

In terms of “protecting” your scripts from malicious users you should know that it’s possible to access the “private” variables and methods in a cmdlet, but it’s not possible to “change” them from outside. So for instance, if we take the example TestModule again — you’ll notice there’s a function at the bottom that we haven’t exported and that isn’t used at all — but which would allow us to set the count value. Let me demonstrate why:


[1]: Add-Module TestModule
[2]: fooize
He spoke 'foo' and then there were 1!
[3]: fooize
He spoke 'foo' and then there were 2!
[4]: barize
She spoke 'bar' and then there were 1!
[5]: $count
[6]: foobar 45
The term 'foobar' is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try again.
[7]: $tm = Get-Module TestModule
[8]: $tm
Name                          Path                                         IsCompiledCode Exports
----                          ----                                         -------------- -------
TestModule                    C:\Users\JBennett\Document...                         False {fooize, barize}

[9]: & $tm { $count }
1
[10]: & $tm { foobar 41 }
[11]: fooize
He spoke 'foo' and then there were 42!

Similar Posts:

5 thoughts on “PowerShell Modules”

  1. Hi Jay

    Here are some quick notes on your post.

    Yes, modules are the new snapins (though the snapin mechanism will remain for backwards compatibility.) We intend to support xcopy install of PowerShell extensions, either script or dlls.

    BTW – the default package path also includes $PSHOME/packages/ (i.e. there are user and system package directories)

    The module implementation is not complete in CTP2. This is why remove doesn’t do what you expect. Also, we’re planning to just use the term “module” in the final product instead of modules and packages.)

    If Export-ModuleMember is not used in a module, then by default all functions are exported from the script.

    If Export-ModuleMember is used at the top level of a session, then it will constrain the top level. In other words, after running Export-Module member, the only functions you can use are the ones you exported. (Cmdlets are currently not constrained by this mechanism.) We will be adding additional support to allow you to also control aliases and variable exports.

    -bruce

  2. If you’re interested in more information about Microsoft’s intentions for modules … Bruce made a presentation at the Virtual PowerShell User Group about modules, and uploaded his slide deck … which was very interesting, and focused on “post-CTP2 functionality” (meaning a lot of it doesn’t work in the build you have now).

    He did say plainly that Modules are intended to replace Snapins, and gave some details about using PowerShell data files (.psd1) for module loading (although I still can’t get that to work in CTP2) ... I could really dig getting some help files for these cmdlets and functionality, or maybe a new release with some of the bug fixes they’ve already put in ;-)

Comments are closed.