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

30 Views Asked by At

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:

  • This is for illustration; You could easily use 'ProviderItem' instead of a constant in the [System.Management.Automation.CompletionResult] constructor.
  • In this example the Get-Files function is intended to be a private (non-exported) function.


  • I'm wondering why autocompletion works when $ProviderItem is scoped as $global:ProviderItem but not $script:ProviderItem
  • In the module manifest, even if $ProviderItem is scoped globally I still have to export all functions, rather than just Test, in order to get tab-completion to work properly.
    • Doesn't Work: FunctionsToExport = 'Test'
      • Tab completion falls back on TabExpansion2 and lists child items in the current directory.
    • Works: FunctionsToExport = '*'
      • Performs tab-completion as I expect.
  • I thought this might have to do with scoping and PSReadLine, but ISE behaves the same way, so I'm obviously missing something critical.


Questions:

  • How can I use Get-Files inside an ArgumentCompleter block of a different function's parameter block(s), export only Test and still retain tab-completion?
  • Can I avoid using the global scope for module-wide constants that are used in ArgumentCompleter function param blocks?
1

There are 1 best solutions below

2
Santiago Squarzon On BEST ANSWER

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
        }
    }
}