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