I faced a specific behavior while using verbose output in class.
This works like a charm:
# SomeModule.psm1
class SomeClass {
SomeClass() {
Write-Verbose "It works!"
}
}
function Test-SomeModule {
[CmdletBinding()]
param()
[SomeClass]::new()
}
And console output looks as expected:
PS C:\> Test-SomeModule -Verbose
VERBOSE: It works!
PS C:\>
But! if we move the class to a separate file and import it to root module:
# SomeClass.psm1
class SomeClass {
SomeClass() {
Write-Verbose "It works!"
}
}
# SomeModule.psm1
using module .\SomeClass.psm1
function Test-SomeModule {
[CmdletBinding()]
param()
[SomeClass]::new()
}
There will be no verbose out, even -Verbose parameter is specified:
PS C:\> Test-SomeModule -Verbose
PS C:\>
Can anybody explain me this? Is there any way to passthrough verbose preference to the class, imported with "using module"?
The root cause of your problem is a long-standing, known problem:
In advanced functions, common parameters such as -Verbose are translated to function-local instances of preference variables with generally equal effect, such as $VerbosePreference = 'Continue'
However, preference variables are not seen across scope-domain ("session state") boundaries, so that a class definition from a (different) module does not respect them - which explains your symptom.
Workarounds:
PowerShell (Core) 7 (v7.4+) only:
Decorate your class definition with the [NoRunspaceAffinity()] attribute, which prevents it from binding to the scope domain of the module of origin and instead binds it to the caller's scope domain on instantiation of the class:
# SomeClass.psm1 - PowerShell (Core) 7.4+ only
[NoRunspaceAffinity()]
class SomeClass {
SomeClass() {
Write-Verbose "It works!"
}
}
The caller's code can remain the same as in your question.
Alternative that also works in Windows PowerShell (the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and final version is 5.1):
Define your class in a *.ps1 (regular script file) rather than a *.psm1 file (script-module file).
# SomeClass.ps1 - NOTE: .ps1 rather than .psm1
class SomeClass {
SomeClass() {
Write-Verbose "It works!"
}
}
Then, in the calling code, use dot-sourcing rather than using module in order to import your class definition from the .ps1 file (dot-sourced *.ps1 scripts implicitly run in the same scope domain as the caller):
# Caller code
# Dot-source the *.ps1 file with the class definition(s)
. .\SomeClass.ps1
function Test-SomeModule {
[CmdletBinding()]
param()
[SomeClass]::new()
}
Another workaround could be to pass the cmdlet as parameter of your constructor and use it instead of Write-Verbose to stream verbose output, this wouldn't require any change to the file structure (both can be .psm1) but would require adding the parameter in your class .ctor and passing $PSCmdlet when creating the instance of your class.
Additionally, this approach will respond correctly to -Verbose and $VerbosePreference.
# SomeClass.psm1
class SomeClass {
SomeClass([System.Management.Automation.PSCmdlet] $cmdlet) {
# $cmdlet could be stored in a field to use in other methods
$cmdlet.WriteVerbose('It works!')
}
}
# SomeModule.psm1
using module .\SomeClass.psm1
function Test-SomeModule {
[CmdletBinding()]
param()
[SomeClass]::new($PSCmdlet)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With