5

For presenting the problem, I have this simple script saved as PowerShell module (test.psm1)

Write-Verbose 'Verbose message'

In real life, it includes command to import additional functions, but that is irrelevant at the moment.

If I run Import-Module .\test.psm1 -Verbose -Force I get only

VERBOSE: Loading module from path 'C:\tmp\test.psm1'.

My Write-Verbose is ignored

I tried adding cmdletbinging but it also did not work.

[cmdletbinding()]
param()

Write-Verbose 'Verbose message'

Any clue how to provide Verbose output while importing the PowerShell module?

P.S. I do not want to display Verbose information always, but only if -Verbose is specified. Here would be my expected output for these two different cases:

PS C:\> Import-Module .\test.psm1 -Verbose -Force # with verbose output
VERBOSE: Loading module from path 'C:\tmp\test.psm1'.
VERBOSE: Verbose message

PS C:\> Import-Module .\test.psm1 -Force # without verbose output

PS C:\>
Igor
  • 1,349
  • 12
  • 25
  • I'm aware of question https://stackoverflow.com/questions/16406682, but I do not want to display verbose messages from commandlet. I want to display verbose information while importing module. – Igor Feb 25 '21 at 12:27
  • 2
    Have you tried using `-Verbose` also in the `Write-Verbose` command? – Marco Luzzara Feb 27 '21 at 17:48
  • @MarcoLuzzara, thanks for the hint. At least in my tests, that displays verbose information **always**, regardless if I specify -Verbose or not while importing. I extended my question to clarify this point. I tried also playing with $PSBoundParameters, but it also did not help. – Igor Feb 28 '21 at 16:27

2 Answers2

5

That is an interesting situation. I have a theory, but if anyone can prove me wrong, I would be more than happy.

The short answer: you probably cannot do what you want by playing with -Verbose only. There may be some workarounds, but the shortest path could be setting $VerbosePreference.


First of all, we need to understand the lifetime of a module when it is imported:

When a module is imported, a new session state is created for the module, and a System.Management.Automation.PSModuleInfo object is created in memory. A session-state is created for each module that is imported (this includes the root module and any nested modules). The members that are exported from the root module, including any members that were exported to the root module by any nested modules, are then imported into the caller's session state. [..] To send output to the host, users should run the Write-Host cmdlet.

The last line is the first hint that pointed me to a solution: when a module is imported, a new session state is created, but only exported elements are attached to the global session state. This means that test.psm1 code is executed in a session different than the one where you run Import-Module, therefore the -Verbose option, related to that single command, is not propagated.

Instead, and this is an assumption of mine, since I did not find it on the documentation, configurations from the global session state are visible to all the child sessions. Why is this important? Because there are two ways to turn on verbosity:

  • -Verbose option, not working in this case because it is local to the command
  • $VerbosePreference, that sets the verbosity for the entire session using a preference variable.

I tried the second approached and it worked, despite not being so elegant.

$VerbosePreference = "Continue" # print all the verbose messages, disabled by default
Import-Module .\test.psm1 -Force
$VerbosePreference = "SilentlyContinue" # restore default value

Now some considerations:

  • Specifying -Verbose on the Import-Module command is redundant

  • You can still override the verbosity configuration inside your module script, by using

    Write-Verbose -Message "Verbose message" -Verbose:$false
    

    As @Vesper pointed out, $false will always suppress the Write-Verbose output. Instead, you may want to parameterized that option with a boolean variable assigned in a previous check, perhaps. Something like:

    if (...) 
    {
        $forceVerbose=$true
    } 
    else 
    { 
        $forceVerbose=$false
    }
    
    Write-Verbose -Message "Verbose message" -Verbose:$forceVerbose
    
  • There might be other less invasive workarounds (for instance centered on Write-Host), or even a real solution. As I said, it is just a theory.

Marco Luzzara
  • 5,540
  • 3
  • 16
  • 42
  • 1
    Won't `Write-Verbose -verbose:$false` do nothing in any case, environment or not? Maybe use a different cmdlet as an example? – Vesper Mar 03 '21 at 14:44
  • @Vesper Sure, it won't do anything if `Verbose` is set to `$false`. I just wanted to show him that he can always override the `$VerbosePreference` just be setting the `Verbose` option. This means that it can be parameterized and instead of `$false` he could use a boolean variable assigned in a previous check for example. Anyway, thanks for pointing it out, I am editing. – Marco Luzzara Mar 03 '21 at 23:18
  • 1
    Thanks @MarcoLuzzara, very insightful. Especially this link about module lifetime. – Igor Mar 05 '21 at 10:52
  • Bounty is awarded, but I still have not accepted the answer. In case someone else have another idea, they are welcome to share it. – Igor Mar 05 '21 at 20:23
  • @Igor actually I still hope for someone to show up with an acceptable solution to that. Thanks anyway for the bounty, I also would not accept my answer, even though I doubt this post will receive many visits now that the bounty has been awarded. – Marco Luzzara Mar 05 '21 at 20:33
3

Marco Luzzara's answer is spot on (and deserves the bounty in my opinion) in regards to the module being run in its own session state, and that by design you can't access those variables.

An alternative solution to setting $VerbosePreference and restoring it, is to have your module take a parameter specifically for this purpose. You touched on this a little bit by trying to add [CmdletBinding()] to your module; the problem is you have no way to pass in named parameters, only unnamed arguments, via Import-Module -ArgumentList, so you can't specifically pass in a $true for -Verbose.

Instead you can specify your own parameter and use it.

(psm1)

[CmdletBinding()]param([bool]$myverbose)

Write-Verbose "Message" -Verbose:$myverbose

followed with:

Import-Module test.psm1 -Force -ArgumentList $true

In the above example, it would apply only to a specific command, where you were setting -Verbose:$myverbose every time.

But you could apply it to the module's $VerbosePreference:

[CmdletBinding()]param([bool]$myverbose)
$VerbosePreference = if ($myverbose) { 'Continue' } else { 'SilentlyContinue' }

Write-Verbose "Message"

That way it applies throughout.


At this point I should mention the drawback of what I'm showing: you might notice I didn't include -Verbose in the Import-Module call, and that's because, it doesn't change the behavior inside the module. The verbose messages from inside will be shown purely based on the argument you passed in, regardless of the -Verbose setting on Import-Module.


An all-in-one solution then goes back to Marco's answer: manipulating $VerbosePreference on the caller's side. I think it's the only way to get both behaviors aligned, but only if you don't use -Verbose switch on Import-Module to override.

On the other hand, within a scope, like within an advanced function that can take -Verbose, setting the switch changes the local value of $VerbosePreference. That can lead us to wrap Import-Module in our own function:

function Import-ModuleVerbosely { 
    [CmdletBinding()]
    param($Name, [Switch]$Force) 

    Import-Module $Name -Force:$Force
}

Great! Now we can call Import-ModuleVerbosely test.psm1 -Force -Verbose. But... it didn't work. Import-Module did recognize the verbose setting but it didn't make it down into the module this time.

Although I haven't been able to find a way to see it, I suspect it's because the variable is set to Private (even though Get-Variable seems to say otherwise) and so that value doesn't make it this time. Whatever the reason.. we could go back to making our module accept a value. This time let's make it the same type for ease of use:

(psm1)

[CmdletBinding()]param([System.Management.Automation.ActionPreference]$myverbose)

if ($myverbose) { $VerbosePreference = $myverbose }

Write-Verbose "message"

Then let's change the function:

function Import-ModuleVerbosely { 
    [CmdletBinding()]
    param($Name, [Switch]$Force) 

    Import-Module $Name -Force:$Force -ArgumentList $VerbosePreference 
}

Hey now we're getting somewhere! But.. it's kind of clunky isn't it?

You could go farther with it, making a full on proxy function for Import-Module, then making an alias to it called Import-Module to replace the real one.


Ultimately you're trying to do something not really supported, so it depends how far you want to go.

briantist
  • 45,546
  • 6
  • 82
  • 127
  • 1
    Thanks for the efforts on those tests! +1 definitely deserved. The `$VerbosePreference` set inside the local function seemed to be promising, I am going to look into it when I have time. – Marco Luzzara Mar 01 '21 at 21:56