powershellmodulescopetab-completion

Module script-scoped variables not accessable in module function's ArgumentCompleter block


test.psm1:

$script:ProviderItem = [System.Management.Automation.CompletionResultType]::ProviderItem
function Get-Files {Get-ChildItem -Path 'C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_Functions*.txt'}
function Test
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'Name', Position = 0, ValueFromPipeline)]
        [ArgumentCompleter({
            param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)
            Get-Files | Where-Object {$_.Name -like "*$WordToComplete*"} | ForEach-Object {
                $resultName = $_.Name
                $resultFN = $_.FullName
                $toolTip = "File: $resultFN"
                [System.Management.Automation.CompletionResult]::new($resultName, $resultFN, $script:ProviderItem, $toolTip)
            } #ForEach-Object
        })]
        [System.String[]]$Name
    )
    begin {Write-Output $script:ProviderItem}
    process { foreach ($n in $Name) {Write-Output $n} }
}

NOTES:



Questions:


Solution

  • Because the ArgumentCompleter scriptblock has no knowledge about the Module it is being invoked in, thus has no knowledge about variables defined in the module scope. A simple way to prove this is the case is by changing the CompletionResult arguments to:

    [System.Management.Automation.CompletionResult]::new(
        $resultName,
        $resultFN,
        (& (Get-Command Test).Module { $ProviderItem }),
        $toolTip)
    

    Moreover, defining the variable as $script: is not needed, all variables defined in the .psm1 are already scoped to the commands in your module.

    Exactly the same applies for Get-Files if FunctionsToExport = 'Test', then the it is scoped to your module and the completer scriptblock has no knowledge about it, you would've to:

    & (Get-Command Test).Module { Get-Files } | Where-Object { ....
    

    A workaround can be to use a class that implements IArgumentCompleter attribute, classes defined in the module scope can see the scoped variables and functions without issues, same applies to Register-ArgumentCompleter.

    Sharing the class implementation here:

    using namespace System
    using namespace System.Collections
    using namespace System.Collections.Generic
    using namespace System.Management.Automation
    using namespace System.Management.Automation.Language
    
    $ProviderItem = [CompletionResultType]::ProviderItem
    
    function Get-Files {
        Get-ChildItem -Path 'C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_Functions*.txt'
    }
    
    class CustomCompleter : IArgumentCompleter {
        [IEnumerable[CompletionResult]] CompleteArgument(
            [string] $commandName,
            [string] $parameterName,
            [string] $wordToComplete,
            [CommandAst] $commandAst,
            [IDictionary] $fakeBoundParameters
        ) {
            $out = [List[CompletionResult]]::new()
            Get-Files | Where-Object { $_.Name -like "*$WordToComplete*" } | ForEach-Object {
                $resultName = $_.Name
                $resultFN = $_.FullName
                $toolTip = "File: $resultFN"
                $out.Add([CompletionResult]::new($resultName, $resultFN, $ProviderItem, $toolTip))
            }
            return $out.ToArray()
        }
    }
    
    function Test {
        [CmdletBinding()]
        param (
            [Parameter(Mandatory, ParameterSetName = 'Name', Position = 0, ValueFromPipeline)]
            [ArgumentCompleter([CustomCompleter])]
            [string[]] $Name
        )
        begin {
            Write-Output $ProviderItem
        }
        process {
            foreach ($n in $Name) {
                Write-Output $n
            }
        }
    }