Home > OS >  Looping Register-ArgumentCompleter produces incorrect parameter completions
Looping Register-ArgumentCompleter produces incorrect parameter completions

Time:08-31

I have a module with a hashtable of dynamically derived enum values, that I thought would be slick to incorporate into Register-ArgumentCompleter for tab completion.

The motivation here is that I can't directly set the module function's input parameters to autoconvert into the enum type (which would properly enable tab completion), because I wish to dynamically derive the enums to save users from manually managing the enum values, as well as due to limitations with the .NET implementation of enums -- I need to allow for strings with dashes or starting with numbers, and potentially null values, all of which enums sadly don't allow. My idea is to do a workaround by adding tab-completed parameter values via Register-ArgumentCompleter.

Problem: I build this workaround as a script that's loaded in the first position of the ScriptsToProcess member of the module manifest, whereupon I discovered that incorrect values are being set when I loop over the hashtable keys and run Register-ArgumentCompleter.

Sample code to reproduce:

function test {param($a, $b, $c, $d )}

$ht = @{
    '1' = @('a', @('a1','a2'))
    '2' = @('b', @('b1','b2'))
    '3' = @('c', @('c1','c2'))
    '4' = @('d', @('d1','d2'))
}

Foreach ($enum in $ht.Keys){
    $paramName = $ht.$enum[0]
    $paramValue = $ht.$enum[1]

    write-host $paramName
    write-host $paramValue

    Register-ArgumentCompleter -CommandName test2 -ParameterName $paramName -ScriptBlock {$paramValue}
}

PS> test -a <tab>
b1 b2

This is PS 7.2.5. In Windows PowerShell 5.1.19041 I get c1 c2 as suggested values. You can see from the host writes that it's down to whichever key is parsed last in the ht loop.

I also tried $ht.["$enum"][0|1] to cast the key type explicitly to a string, to no avail. When I write-host in the loop, all the values seem correct.

Does this seem like an error from me or a bug?

CodePudding user response:

By the time the loop completes, $enum will have a value of whatever the last key in its sort order is.

Use ScriptBlock.GetNewClosure() to close over the value of $ht and $enum by the time GetNewClosure() is called, making the scriptblock retain the original values of $ht and $enum:


function test {param($a, $b, $c, $d )}

$ht = @{
    '1' = @('a', @('a1','a2'))
    '2' = @('b', @('b1','b2'))
    '3' = @('c', @('c1','c2'))
    '4' = @('d', @('d1','d2'))
}

Foreach ($enum in $ht.Keys){
    Register-ArgumentCompleter -CommandName test -ParameterName $ht.$enum[0] -ScriptBlock { $ht.$enum[1] }.GetNewClosure()
}

FWIW you can simplify the $ht table significantly:

$ht = @{
    'a' = @('a1','a2')
    'b' = @('b1','b2')
    'c' = @('c1','c2')
    'd' = @('d1','d2')
}

Foreach ($enum in $ht.Keys){
    Register-ArgumentCompleter -CommandName test -ParameterName $enum -ScriptBlock { $ht[$enum] }.GetNewClosure()
}
  • Related