EN VI

Powershell - Module script-scoped variables not accessable in module function is ArgumentCompleter block?

2024-03-15 05:30:06
Powershell - Module script-scoped variables not accessable in module function is 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:

  • 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?

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

Login


Forgot Your Password?

Create Account


Lost your password? Please enter your email address. You will receive a link to create a new password.

Reset Password

Back to login