Home > OS >  How to scope a function to a certain module?
How to scope a function to a certain module?

Time:06-15

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 an Export-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 include Read-Host; e.g., if you want to export functions Get-Foo and Set-Foo, place the following at the bottom of module.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 with Remove-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 in main.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)

  • Related