I have main.ps1
and module.psm1
. module.psm1
is a class module and a function. The function is Read-Host
. I had to override it to return 'y'
when called within an instance of module.psm1
.
Now, in main.ps1
, I'd like to use Read-Host
with its default behavior (prompt the user.) Is there a way to do that? Or a way to confine Read-Host
override function to the scope of that module only?
Thank you.
CodePudding user response:
You might address the original Read-Host
function with a module-qualified function path:
Without the custom Read-Host
function:
Read-Host 'Native Read-Host'
Native Read-Host:
Get-Command Read-Host
CommandType Name Version Source
----------- ---- ------- ------
Cmdlet Read-Host 7.0.0.0 Microsoft.PowerShell.Utility
Creating your custom function:
function Read-Host { Write-Host 'Custom Read-Host' }
Read-Host
Custom Read-Host
Get-Command Read-Host
CommandType Name Version Source
----------- ---- ------- ------
Cmdlet Read-Host
Now using the module-qualified function path (Source
property of the native Read-Host
command):
Microsoft.PowerShell.Utility\Read-Host 'Native Read-Host'
Native Read-Host:
CodePudding user response:
If you have control over the
.psm1
file:Make the overridden
Read-Host
function private to your module, by not exporting it; that way, only functions in the same module see the overridden definition, unlike importers of your module.Since all functions defined in a
.psm1
file (that doesn't have an associated module manifest,.psd1
) are exported by default, you need anExport-ModuleMember
call to limit what functions are exported; in this call, enumerate the names of all functions you do want to export (to be visible an importer) and do not includeRead-Host
; e.g., if you want to export functionsGet-Foo
andSet-Foo
, place the following at the bottom ofmodule.psm1
:Export-ModuleMember -Function Get-Foo, Set-Foo
You can also use wildcard expression:
Export-ModuleMember -Function *-Foo
Otherwise:
Remove your module once you no longer need it (you can re-import it later; you could even remove the
Read-Host
function withRemove-Item Function:Read-Host
, though modifying a loaded module's state that way seems tricky):Remove-Module module # After this, Read-Host has its original meaning
Alternatively, if modifying the
Read-Host
call inmain.ps1
is an option, use a module-qualified invocation (Microsoft.PowerShell.Utility\Read-Host
), as shown in iRon's helpful answer
CodePudding user response:
As noted by commenter, this is not recommended. The module function should be refactored by adding an optional parameter that allows callers to provide a choice programmatically.
If you really can't modify the source of the module, then here are two ways to temporarily override Read-Host
.
For testing both solutions I have created the following module file:
module.psm1
Function ModuleFun {
$choice = Read-Host 'Enter (Y)es or (N)o'
"Output from module: $choice"
}
1st solution
The easiest solution is possible when you have Pester installed. I've tested it with Pester 5 but I believe it should work with older Pester versions too. It uses the InModuleScope command of Pester to "hack" a module.
main.ps1
Import-Module $PSScriptRoot\module.psm1
# InModuleScope is provided by Pester.
# Override Read-Host within module scope only.
InModuleScope -ModuleName module -ScriptBlock {
Function Script:Read-Host { $global:ReadHostOverride }
}
# Use Read-Host normally
Read-Host 'Enter something'
# Call module function with overridden Read-Host
$ReadHostOverride = 'Y'
ModuleFun
# Use Read-Host normally again
Read-Host 'Enter something'
# Call module function with different Read-Host override value
$ReadHostOverride = 'N'
ModuleFun
Output:
Enter something: 23
23
Output from module: Y
Enter something: 42
42
Output from module: N
2nd solution
If you don't have Pester available, you may use my function Invoke-WithReadHostOverride
to override Read-Host
during the execution of a script block that calls a module function.
main.ps1
Import-Module $PSScriptRoot\module.psm1
#------------------------------------------------------------------------------------
# Create an in-memory module which has the advantage that the dot sourcing operator "."
# allows us to run a scriptblock in the caller's scope (outside of the module).
# You may put this code into a separate module file instead.
$null = New-Module {
# Helper function to call the given script block while Read-Host is overridden
Function Invoke-WithReadHostOverride {
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $ReadHostOutput,
[Parameter(Mandatory)] [scriptblock] $ScriptBlock
)
# Temporary override of Read-Host command, returns value given by parameter.
Function Global:Read-Host { $ReadHostOutput }
try {
# Everything in ScriptBlock calls our Read-Host override
. $ScriptBlock
}
finally {
# Remove our Read-Host override again
Remove-Item Function:Read-Host
}
}
}
#------------ DEMO CODE --------------------------------------------------------------
# Use Read-Host normally
Read-Host 'Enter something'
# Call module function with overridden Read-Host
Invoke-WithReadHostOverride -ReadHostOutput Y -ScriptBlock {
$choice = ModuleFun
}
$choice # Thanks to the in-memory module trick, $choice has been set in current scope!
# Use Read-Host normally again
Read-Host 'Enter something'
# Call module function with different Read-Host override value
Invoke-WithReadHostOverride -ReadHostOutput N -ScriptBlock {
ModuleFun
}
Output:
Enter something: 23
23
Output from module: Y
Enter something: 42
42
Output from module: N
As usual with such temporary things, it's a good idea to wrap them in try
/finally
blocks to ensure the cleanup runs even in case of an exception.
The override works, because functions have higher precedence than cmdlets defined in the same session. From the docs:
If you do not specify a path, PowerShell uses the following precedence order when it runs commands for all items loaded in the current session:
Alias Function Cmdlet External executable files (programs and non-PowerShell scripts)