Dude, Where’s my Command?

The Dilemma

This post was inspired by a recent occurrence at work.  I have built a framework which constructs documents based on a lists of functions in a module specific to that kind of document.

I found myself running into an issue where even though I knew there was a command named a certain thing, and that the function was correctly exported from the module, PowerShell wasn’t finding the command.

My code looked something like this:

$module='Module1'
$Steps='Get-CustomerData','Get-MarketingData','New-CustomerMarketingDocument'
# each function takes a hashtable as a parameter and outputs a hashtable
$state=@{}
foreach($Step in $Steps){
    $stepCmd=Get-Command -Name $step -module $module
    $state=& $StepCmd -state @State
}

So why was PowerShell not working nicely for me? The problems has to do with how PowerShell loads functions. Let’s take a step back

PowerShell’s Command-Loading Procedure

For now, let’s assume we have a simple script module called Module1.

For a script module to be “discoverable” by PowerShell, it must exist in one of the folders listed in the PSModulePath environment variable.

This PowerShell will show you the folders that PowwerShell will look in:

$env:PSModulePath.Split(';')

Here are the results from a PowerShell 7 instance:

When you try to run a command (or use Get-Command), PowerShell will first look to see if the command is already loaded. IF so, then it just runs (or outputs) the command you asked for.

If it isn’t already present, PowerShell looks through valid modules for the function. As a reminder, valid modules:

  • Exist in one of the folders in PSModulePath
  • Have a “module file” named the same as the folder it’s in (for instance, module1\module1.psm1)
  • Module Files extensions are psm1, psd1, dll, or cdxml.
  • File is syntactically valid and not blocked

If the command is present, and exported (via Export-ModuleMember and Functions/CmdletsToExport in the manifest), then the module will be loaded silently and the command (now present) will be executed or output.

Pretty simple, right?

In my case, I had a valid module, in a valid folder, in the PSModulePath, with the named function exported from it.  What could have gone wrong?

PowerShell Command Hiding

The problem was that I had gotten used to naming functions somewhat generically, and had duplicated one.

I had two modules which contained a function called Get-DocumentData.  The command discovery procedure worked fine, it just found the wrong command.

Let’s see an example of what I’m talking about.

Consider two modules, module1 and module2.

Module1 has a single command, Get-Stuff

function Get-Stuff{
    'in module1'
}

Similarly, Module2 has a Get-Stuff command which outputs ‘in module2’.

If I were to open a new PowerShell session and type Get-Stuff, PowerShell would find module1’s copy of Get-Stuff first and load it (that is, module1).

If that were the only Get-Stuff, the story would be over.

In this case, I have two different modules with Get-Stuff functions.

To get to the second, I can import the module explicitly:

Import-Module module2

You can verify the output below:

Now, we’ve imported both modules and executed both functions. What does Get-Command tell us?

First, it only shows us the command from module2, that is, the most recently loaded.

If we ask for the command from module1, we won’t find it:

You might notice that the error is misleading.  There sure is a Get-Stuff command available, just not from module1 (or at least not available from module1).

But if we ask for “all” Get-Stuff commands, there it is:

So, why didn’t Get-Command show it?

The answer (which you can read in more detail here) is that the module1 version of the command has been hidden, and Get-Command (without -All) shows commands that aren’t hidden.

Adding -All to our earlier search for the command works like we had expected:

Note that there’s another way to get a module-specific command, and that’s by using a fully-qualified command reference like module1\get-stuff:

I beat my head against this one for quite a while.  I was even considering writing a blog post about it when I read the about_command_precedence help topic (for research) and figured out the “issue”.

 

Thanks for reading,

Mike