Get 30% discount
DotNetSlacker readers can get 30% off the full print book or ebook at
www.manning.com using the promo code dns30 at checkout.
Converting a PowerShell Script into a Module Series
Introduction
In this article, we'll look at some of the more sophisticated things we can do with modules. These features are not intended for typical day-to-day use but they do allow for
some very sophisticated scripting. As always, if you aren't just scripting for yourself, have pity on the person who will have to maintain your code and avoid "stunt-
scripting".
The PSModuleInfo Object
PowerShell modules, like everything in PowerShell, are objects we can work with directly. The type of the object used to reference modules is System.Management.Automation.PSModuleInfo.
This is what Get-Module returns. PSModuleObject can be used to get basic information about a module and
for a lot of other things. Let's take a look at what can be done (and try to explain why you'd do those things).
Invocation in the module context
A module-level scope is used to isolate the private variables and functions. When we execute code where function and variable lookup is done in a module scope, we call this
"executing in the module context". This is, of course, what happens any time we execute a function that has been exported from a module. However, we can also cause arbitrary
code to be executed in the module context even though it wasn't defined in that context. In effect, we're pushing code into the module context. This is done with a PSModuleInfo
object using the call operator &.
Author's NoteYes, this ability to inject code into a module context violates all principles of isolation and information hiding. And, from a language
perspective, this is a bit terrifying. Regardless, people do it all the time when debugging. One of nice things about dynamic languages is that you are effectively running the
debugger attached all the time.
To try this out, we'll need a module object to play with. Let's load a counter module. First, we'll use the Select-Object cmdlet to limit what gets output to the first
8 lines, as that is all that we're concerned with here.
This module has private state in the form of the two variables $count and $increment and one public function Get-Count.
Now import it
and use Get-Module to get the module reference:
We could have done this in one step with the -PassThru parameterr but we're using two steps here to illustrate that these techniques can be done with
any in-memory module. Now run the Get-Count function, and it returns 1, as it should right after the module is first loaded.
Now set a global variable $count using the Set-Variable command. (Again we're using the command instead of assignment to set the
variable, for illustration purposes.)
When we run Get-Count again, of course it returns 2, since the $count it uses exists in the module context.
So far, nothing much to see. Now let's do something a bit fancier. Let's see what the current value of $count in the module context is. We can do
this by invoking Get-Variable in the module context with the call operator:
The call to Get-Count returns 34; so, we have successfully changed the value of the variable it uses in its operation.
Ok. We know
how to get and set state in the module; let's try altering the code. First we'll look at the body of the Get-Count function:
Although we've redefined the function in the module, we have to re-import the module in order to get the new definition into our function table.
Now that we've done that, we can call the function again to make sure we're getting what we expected.
and yes, Get-Count is now incrementing by 2 instead of 1.
All of the tweaks that we've been making on the module only affect the module in
memory. The module file on disk hasn't changed:
If we use the -Force parameter on Import-Module, we'll force the system to reload the file from disk, reinitializing everything to the way it
was:
This is one of the characteristics of dynamic languages - the ability of programs to modify themselves in a profound way at runtime and then restore the original
state. Next, we'll look at how to we can use properties on the PSModuleInfo to access the members of a module without importing them.
Accessing modules exports using the
PSModuleInfo object
The exported members of a module are discoverable through properties on the PSModuleInfo object that represents the module. This gives us a way to look
at the exported members without having to import them into our environment. For example, the list of exported functions is available in the ExportedFunctions member. These
properties are hashtables, indexed by the name of the exported member. Let's look at some examples of what we can do using these properties.
As always, we need a module to
work with. In this case, we'll use a dynamic module. Dynamic modules don't require a file on disk which makes them easy to use for experiments. We'll create a dynamic module and
save the PSModuleInfo object in a variable $m.
and now we can use the export lists on the PSModuleInfo to see what was exported.
In the output, we see that one function and one variable are exported. We also see the function turns up in the ExportedCommands member. Modules can export more
than one type of command - functions, aliases, or cmdlets - and this property exists to provide a convenient way to see all commands regardless of type.
Author's NoteBy implementing the exported member properties as hashtables, we allow you to access and manipulate the state of the module in a fairly convenient
way. The downside is that the default output for the exported members is a bit strange, especially for functions where we see things like [foo, foo]. These tables
map the name of a command to the CommandInfo object for that command. When the contents of the table are displayed, both the key and the value are displayed as strings. And,
since the presentation of a CommandInfo object as a string is the name of the object, we see the name twice.
Let's use the ExportedFunctions property to see how the function foo is defined. We'll use the property syntax instead of explicit indexing to access the
member because it's easier to write:
The value returned from the expression is a CommandInfo object. This means that we can use the call operator '&' to invoke this function:
We can also use the PSModuleInfo object to change the value of the exported variable $x:
Call the function again to validate this change.
and the return value from the call is the updated value as expected. Next, we'll look at some of the methods on PSModuleInfo objects.
Using the PSModuleInfo methods
The call operator is not the only way to use the module info object. The object itself has a number of methods that can be useful. Let's take a look at some of these
methods:
We'll cover the first two listed Invoke() and NewBoundScriptBlock(). (Check out my book for a detailed coverage of AsCustomObject().)
The Invoke() method
This method is essentially a .NET programmer's way of doing what we did earlier with the call operator. Assuming we still have our counter module loaded, let's use this
method to reset the count and change the increment to 5. First get the module info object:
Now invoke a scriptblock in the module context using the method:
The corresponding invocation using the call operator would b
which is more scripter friendly. Either way, let's try to verify the result.
And the count was reset and Get-Count now increments by 5 instead of 1. Next we'll look at a way to attach modules to a scriptblock.
The NewBoundScriptBlock() method
A module-bound scriptblock is a piece of code - a scriptblock - that has the module context to use attached to it. Normally an unbound scriptblock is executed in the caller's
context but, once a scriptblock is bound to a module, it always executes in the module context. In fact that's how exported functions work - they are implicitly bound to the
module that defined them.
Let's use this mechanism to define a scriptblock that will execute in the context of the counter module. First we need to get the module (again).
We could use Get-Module as before but now that we know that exported functions are bound to a module, we can just use the Module property on an
exported command to get the module info object. Let's do this with Get-Count.
Now we can get the module for this command
Next we need to define the scriptblock we're going to bind. We'll do this and place the scriptblock into a variable.
This scriptblock takes a single parameter which it uses to set the module level $increment variable. Now let's bind it to the target module. Note
that this doesn't bind the module to the original scriptblock. Instead is creates a new scriptblock with the module attached.
Now test using the scriptblock to set the increment. Invoke the scriptblock with the call operator passing in an increment of 10
And verify that the increment has been changed.
OK, good! However, if we want to use this mechanism frequently, it would be useful to actually have a named function. We can do this by assigning to the function
drive.
And now the increment is 100 per the argument to the Set-CountIncrement. Now let's use Get-Command to look at the function we've
defined:
and, like Get-Count, it's listed as being associated with the counter module. Now that we've introduced the idea of a function being dynamically
attached to a module, we really should have a more in-depth discussion about the context where a function gets evaluated. We'll do just that now.
The Defining Module vs. the Calling Module
Here, we'll go into more detail about how the execution context for a module is established.
Commands always have two module contexts - the context where they were
defined and the context they were called from. This is a somewhat subtle concept. Before PowerShell had modules, this wasn't terribly interesting except for getting filename and
line number information for the location where the function was called and where it was defined. With modules, this distinction becomes more significant. Among other things, the
module where the command was defined contains the module specific resources like the manifest PrivateData. For functions, the ability to access the two contexts allows the
function to access the caller's variables instead of the module variables.
Accessing the defining module
The module in which a function was defined can be retrieved by using the expression $MyInvocation.MyCommand.Module. Similarly, the module in which a cmdlet was
defined is available through the instance property this.MyInvocation.MyCommand.Module. If the function is defined in the global scope (or 'top level'), the module
field will be $null. Let's try this. First define a function at the top level.
then run it, formatting the result as a list showing the module name and PrivateData fields.
Nothing was output because the defining module at the top level is always null. Now let's define the function inside a module. We'll use a here-document to create
a .psm1 file.
Now load the file and run the same test command as we did previously.
This time the result of the function was not null - we see the module name and, of course, the PrivateData field is empty because there was no module
manifest to provide this data. We can remedy this by creating a module manifest to go along with the .psm1 file. This abbreviated manifest defines the minimum - the module
version, then module to process - and specifies a hash table for PrivateData.
Now load the module using the manifest and -force to make sure everything gets updated.
Run the test command:
and we see that the PrivateData field is now also filled in.
Accessing the calling module
The module from which a function was called can be retrieved using the expression $PSCmdlet.SessionState.Module. Similarly, the module a cmdlet was called from
is available through this.SessionState.Module. In either case, if the command is being invoked from the top level, this value will be null because there is no
"global module".
Author's NoteIt's unfortunate that we didn't get a chance to wrap the global session state in a module before we shipped. This means that this kind of code has
to be special cased for the module being $null some of the time.
Working with both contexts
Now let's look at a very tricky scenario where we access both contexts at once. This is something that is rarely necessary but, when needed, absolutely required.
In
functions and script modules, accessing the module session is trivial since unqualified variables are resolved in the module context by default. To access the caller's context
we need to use the caller's session state which is available as a property on $PSCmdlet. Let's update the Test-ModuleContext module to access a variable "testv"
both in the caller's context and the module context. Here's the module definition.
This defines our test function, specifying the cmdlet binding to be used so we can access $PSCmdlet. The module body also defines a module-scoped
variable $testv. The test function will emit the value of this variable and then use the expression
to get the value of the caller's $testv variable. Let's load the module
Now define a global $testv
and run the command.
And we see the module $testv was correctly displayed as "123" and the caller's variable is the global value "456". Now, wait a minute, you say! We
could have done this much more easily by simply specifying $global:testv. This is true if we were only interested in accessing variables at the global level. But
sometimes we want to get the local variable in the caller's dynamic scope. Let's try this. We'll define a new function "nested" that will set a local
$testv.
This function-scoped $testv variable is the "caller's variable" we want to access so we should get "789" instead of the global value "456". Let's try
this:
and it works. The module $testv was returned as "123" and the caller's testv returned the value of the function-scoped variable instead of the global
variable.
So when would we need this functionality? If we want to write a function that manipulates the caller's scope - say, something like the Set-Variable
cmdlet implemented as a function, then we'd need this capability. Another time we might need to do this is when we want to access the value of locally scoped configuration
variables like $OFS.
Setting module properties from inside a script module
Manifests are required to set metadata on a module but it turns out that there is a way for the script module to do some of this itself during the module load operation. In
order to do this it needs to have access to its own PSModuleInfo object during the load. This can be retrieved using the rather awkward expression
$MyInvocation.MyCommand.ScriptBlock.Module
However, once we have the PSModuleInfo object, the rest is easy. Let's try it out by setting the Description
property on our own module.
Setting the module description
In this example, we're going to set the Description property for a module from within the module itself. We'll create a module file in the current directory
called setdescription.psm1. Let's look at the contents of this file:
On the first line of the module, we copy the reference to the PSModuleInfo object into a variable $mInfo. On the second line, we assign a value to the
Description property on that object. Let's try it out. We'll import the module
and then call Get-Module, piping into Format-List so we can just see the module name and its description.
and there we go. We've dynamically set the Description property on our module.
As well as being able to set this type of metadata entry on the
PSModuleInfo object, there are a couple of behaviors we can control as well. We'll look at how this works right now.
Controlling when modules can be unloaded
The module AccessMode feature allows us to restrict when a module can be unloaded. There are two flavors of restriction: static and constant. A static module is a module that
can't be removed unless the -Force option is used on the Remove-Module cmdlet. A constant module can never be unloaded and will remain in memory
until the session that loaded it ends. This model parallels the pattern for making variables and functions constant.
To make a module either static or constant, we need to
set the AccessMode property on the module's PSModuleInfo object to the appropriate setting. Set it to ReadOnly for static modules and Constant
for constant modules. Let's look at how this is done. Here's an example script module called readonly.psm1 that makes itself ReadOnly.
The first line of the module is the same as in the previous example. It retrieves the PSModuleInfo object. The next line sets the AccessMode to
"readonly". We'll load this module and verify the behavior.
We've verified that it's been loaded so let's try and remove it:
When we try and remove the module, we get an error stating that -Force must be used to remove it. Let's do that.
This time we don't get an error. We verify that the module has been removed by calling Get-Module:
Nothing was returned confirming that the module has been removed. The same approach is used to mark a module as constant.
And now, the final feature we're
going to cover: how to run an action when a module is unloaded.
Running an action when a module is removed
Sometimes we need to do some to clean-up when a module is unloaded. For example, if the module establishes a persistent connection to a server, when the module is unloaded,
we'll want that connection to be closed. An example of this pattern can be seen in implicit remoting. The PSModuleInfo object provides a way to do this through its
OnRemove property.
To set up an action to execute when a module is unloaded, assign a scriptblock defining the action to the OnRemove property
on the module's PSModuleInfo object. Here is an example that shows how this is done:
then remove it
and the message from the scriptblock was printed confirming that the OnRemove action was executed.
Summary
Here are some of the key topics we covered:
- Modules in memory are represented by a PSModuleInfo object. This object allows us to perform a number of advanced scenarios with modules.
- The PSModuleInfo object for a module can be retrieved using
Get-Module. Alternatively, the module object for a function can be retrieved using the
Module property on scriptblock for that function.
- If we have access to the PSModuleInfo object for a module, we can inject code into the module where it will be executed in the module context. This allows us to
manipulate the state of a module without having to reload it. This feature is primarily intended for diagnostic and debugging purposes.
- From within a script module, we can use the PSModuleInfo object to directly set some metadata elements, like the module description.
- PSModuleInfo objects have an
AccessMode field that controls the ability to update or remove a module from the session. This field is set to
ReadWrite by default but can be set to Static, requiring the use of the -Force parameter to update it or Constant where it cannot be removed from the
session. A Constant module remains in the session until the session ends.
- To set up an action to be taken when a module is removed, a scriptblock can be assigned to the
OnRemove property on the PSModuleInfo object for that
module.
Get 30% discount
DotNetSlacker readers can get 30% off the full print book or ebook at
www.manning.com using the promo code dns30 at checkout.
Converting a PowerShell Script into a Module Series
About Manning Publications
 |
Manning Publication publishes computer books for professionals--programmers, system administrators, designers, architects, managers and others. Our focus is on computing titles at professional levels. We care about the quality of our books. We work with our authors to coax out of them the best writi...
This author has published 33 articles on DotNetSlackers. View other articles or the complete profile here.
|
You might also be interested in the following related blog posts
Why is ASP.NET encoding &s in script URLs? A tale of looking at entirely the wrong place for a cause to a non-existing bug.
read more
Twilight: A Silverlight Twitter Badge
read more
Copy Pictures To Folders By Date Taken with Powershell
read more
New Release: BGI SCORM LMS Portal v2.5.0 - New Certification Manager with WYSIWYG Certificate Designer - DotNetNuke Integrated
read more
How my team does agile
read more
Anatomy of a Subtle JSON Vulnerability
read more
Utilizing the Microsoft AJAX Framework and ClientAPI to Develop Rich Modules: Part III
read more
Utilizing the Microsoft AJAX Framework and ClientAPI to Develop Rich Modules: Part II
read more
URL Rewrite for IIS - SEO Friendly URLs love it !
read more
EOAST - Evolution of a software thingy - Part 1
read more
|
|
Please login to rate or to leave a comment.