Package Management and a PowerShell Bug

UPDATE: I have worked out how the behavior described at the end of this post is not a bug, but in fact just PowerShell doing what it’s told. Don’t have time to explain right now, but I’ll write something up later today. I also worked out how to “fix” the behavior.

For a long time now, I’ve been dissatisfied with what I call “package management” in PowerShell.  Those of you who know me will be shocked that anything in PowerShell is less than perfect in my eyes, but this is one place that I feel let down.  Modules in 2.0 remedy the situation somewhat, but it still isn’t quite what I want or am used to in other languages.

Let me give an example.  In VB.NET, if you need to use the functions in an assembly, you put “Imports AssemblyName” at the top of your script.  In C#, you would have “Using AssemblyName”.  In Python, there would be “Import Something”.

In PowerShell 1.0, you had nothing.  In 2.0, you could create a module manifest which would specify either RequiredModules or ScriptsToProcess (or several other things to do upon loading the module).  The problems I see  with using the module manifest are:

  • What if I’m not writing a module?  There’s no such thing as a “script manifest”
  • What if the script or module that is required performs some initialization that should only be done once per session?
  • What if the script or module that is required performs expensive initialization?

Because of these reasons (and because I only started using 2.0 when it went RTM) I wrote a couple of quick functions to do what I thought made sense.

$global:loaded_scripts=@{pkg_utils='INITIAL'}

function require($filename){
	if (!$global:loaded_scripts[$filename]){
	   . scripts:$filename.ps1
	   $global:loaded_scripts[$filename]=get-date
	}
}
function reload($filename){
	. scripts:$filename.ps1
	$global:loaded_scripts[$filename]=get-date
}

To use these you need to create a psdrive called scripts: with code like this (probably in your profile):

New-PSdrive -name scripts -PSprovider filesystem -root PathToYourLibraries | Out-Null

Then, also in your profile, you’ll want to dot-source the file you put these functions in (for example, package_tools.ps1):

. scripts:package_tools.ps1

Once you have those set up, you can dot-source the require function to make sure that a script has been loaded as such:

. require somelibrary

I have the functions I use divided by “subject” into several library scripts, and make sure that at the top of each script, I use “. require” to ensure that any prerequisites are already loaded.

Now for the PowerShell bug (which took me a long time to track down).
Create 2 files, a.ps1 and b.ps1 in your scripts: directory.

# a.ps1
write-host "this is script a"
#b.ps1
write-host "this is script b"
write-host "this script loads a"
. require a

After dot-sourcing package_tools, run the following commands:

. require b

You should get output that looks something like this:

this is script b
this script loads a
this is script a

Everything looks good until you inspect the $global:loaded_scripts variable:

ps> $loaded_scripts

Name                           Value
----                           -----
a                              1/19/2010 11:23:09 PM
package_tools                  INITIAL

Although b.ps1 was indeed dot-sourced (you can see the output), and the only code-path through the require function that would dot-source it would also add an entry to $loaded_scripts, there is no such entry. The problem is that when b.ps1 called the require function (to load a.ps1), the $filename variable in the calling context (where it should have been “b”) was overwritten by the call with “a” as a parameter. Walking through the code in a debugger confirms the problem.

Have you ever seen problems with recursion and dot-sourcing in PowerShell? Can you see any way around the problem I’ve described? For instance, saving the $filename in a variable and restoring it after the dot-source call (line 5 above) doesn’t help, because the same code-path is followed in the recursive call, and that variable is overwritten as well.

Even with this bug, I find the require function (and reload, which I didn’t discuss, but always loads the script in question) to be very helpful. I also have extended them to include importing modules, if they exist. I’ll discuss them in my next post, coming soon.

-Mike

P.S. Here‘s a question I posted to StackOverflow.com about these functions back in November of 2008.