Start-Job - Positional order of arguments - powershell

I'm trying to understand the correct order of arguments when using Start-Job. What is the correct way to supply parameters to a PowerShell job?
I would expect this to print hello world, but it prints world hello.
Is Param() or -ArgumentList the issue here?
$foo = "hello"
$bar = "world"
$job = Start-Job -ScriptBlock {
Param(
$foo,
$bar
)
Write-Host $foo
Write-Host $bar
} -ArgumentList $bar, $foo
Receive-Job $job
Output:
world
hello

The argument of the parameter -ArgumentList is an array, whose values are passed to parameters defined inside the scriptblock in positional order. You're confused about the result you're getting, because you apparently expected your global variables to be mapped to the parameter names you defined in your scriptblock. That is not how this works.
To maybe illustrate a bit better what is happening in your example let's use distinct variable names in the scriptblock and global scope:
$a = "hello"
$b = "world"
$job = Start-Job -ScriptBlock {
Param(
$c,
$d
)
Write-Host $c
Write-Host $d
} -ArgumentList $b, $a
Essentially, the names of the parameters have nothing to do with the names of the variables in the global scope.
You're switching the values when you're passing $b, $a to the scriptblock instead of $a, $b, hence the value of $b is passed to $c and the value of $a is passed to $d.
Normally one would use splatting for mapping values to specific named parameters. However, that won't work here, since -ArgumentList expects an array of values, not a hashtable. If the difference between positional and named parameters is not clear to you please have a look at the documentation.
What you can do if you want to use the same variable names inside and outside the scriptblock is use the using: scope qualifier instead of passing the variables as arguments:
$a = "hello"
$b = "world"
$job = Start-Job -ScriptBlock {
Write-Host $using:a
Write-Host $using:b
}

Related

Invoke-Command Using Local Function With Array As Single Parameter

I have a function that takes a single string array as a parameter in my PowerShell .pm1 that I want to be able to call on a remote server using a second function in my .pm1 (I do not want to rely on the server having a copy of the function). I found this Using Invoke-Command -ScriptBlock on a function with arguments but it only seems to work for 'non-arrays' or for multiple parameters (where array variable is not last)
function Hello_Worlds { param([string[]]$persons)
foreach($person in $persons){
write-host ("hello "+$person)
}
}
$people = "bob","joe"
Invoke-Command -ComputerName "s1" -ScriptBlock ${function:Hello_Worlds} -ArgumentList $people
#output => "hello bob" only
Invoke-Command -ComputerName "s1" -ScriptBlock ${function:Hello_Worlds} -ArgumentList $people, ""
#output => "hello bob hello joe"
I can modify my argument list like -ArgumentList $people, "" (above) to make it work by forcing the function to see the $persons variable as a single parameter and not an array of parameters, but that seems like bad practice and I sure that I am just missing something simple.
EDIT:
I was directed here ArgumentList parameter in Invoke-Command don't send all array and while it works for this exact example, it requires that I KNOW which parameters require an array. Is there a generic way to pass an any arguments that would prevent this issue? I.E. I build my argument list as an array of parameters and there could be 0 or more of them and any number of them could be arrays - or am I stuck with putting this in front of calls?
foreach($parg in $myCustomGeneratedArguments) {
if($parg -is [array]) {$paramArgs += ,$parg}
else {$paramArgs += $parg}
}
Looking at your edit I'm afraid the linked answer doesn't lead you to the easier path, which is to not use -ArgumentList at all, instead, refer to your Hello_Worlds function and to your $people array with the $using: scope modifier:
function Hello_Worlds { param([string[]]$persons)
foreach($person in $persons){
write-host ("hello "+$person)
}
}
# store the function definition locally
$func = ${function:Hello_Worlds}.ToString()
$people = "bob","joe"
Invoke-Command -ComputerName "s1" -ScriptBlock {
# define the function in the remote scope
${function:Hello_Worlds} = $using:func
# now you can use it normally
Hello_Worlds -persons $using:people
}

Looping Register-ArgumentCompleter produces incorrect parameter completions

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

Passing hashtable and args to powershell invoke-command as arguments

Normally one can do the following:
function example{
param(
$Parameter1,
$Parameter2
)
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
$PSBoundParameters
$args
}
and pass parameters explicitly or by splatting like so:
$options = #{parameter1='bounded parameter'}
example #options -Parameter2 'will be bounded too' 'this will go into args' -Parameter3 'this will too','and this one also'
Parameter1: bounded parameter
Parameter2: will be bounded too
Key Value
--- -----
Parameter1 bounded parameters
Parameter2 will be bounded too
this will go into args
-Parameter3
this will too
and this one also
I would like to replicate that syntax's behavior somehow when using invoke command.
Ideally somethings like this syntax:
$options = #{parameter1='bounded parameter';parameter3='this will too'}
Invoke-Command -Computername REMOTESERVER -ArgumentList #options 'will be bounded to parameter2' 'this will go into args', 'and this one also' -ScriptBlock {'block'}
I have seen this answer:
https://stackoverflow.com/a/36283506/12603110 suggesting to wrap the script block with 👇 which allows splatting of hashtables
{param($Options)& <# Original script block (including {} braces)#> #options }
I'm unsure how to deal with the $args and explicitly specified parameters(e.g. Parameter2).
Nothing stops you from deconstructing, modifying and then proxying the arguments received by Invoke-Command:
$options = #{parameter1='bounded parameter'}
Invoke-Command -ArgumentList $options, "some other param", "unbound stuff" -ScriptBlock {
# extract splatting table from original args list
$splat,$args = $args
& {
param($Parameter1, $Parameter2)
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
$PSBoundParameters
$args
} #splat #args
}

Passing parameters to a PowerShell job [duplicate]

This question already has an answer here:
Parenthesis Powershell functions
(1 answer)
Closed 7 years ago.
I've been toying around with this dang parameter passing to powershell jobs.
I need to get two variables in the script calling the job, into the job. First I tried using -ArgumentList, and then using $args[0] and $args[1] in the -ScriptBlock that I provided.
function Job-Test([string]$foo, [string]$bar){
Start-Job -ScriptBlock {#need to use the two args in here
} -Name "Test" -ArgumentList $foo, $bar
}
However I realized that -ArgumentList gives these as parameters to -FilePath, so I moved the code in the scriptblock into its own script that required two parameters, and then pointed -FilePath at this script.
function Job-Test([string]$foo, [string]$bar){
$myArray = #($foo,$bar)
Start-Job -FilePath .\Prog\august\jobScript.ps1 -Name 'Test' -ArgumentList $myArray
}
#\Prog\august\jobScript.ps1 :
Param(
[array]$foo
)
#use $foo[0] and $foo[1] here
Still not working. I tried putting the info into an array and then passing only one parameter but still to know avail.
When I say no avail, I am getting the data that I need however it all seems to be compressed into the first element.
For example say I passed in the name of a file as $foo and it's path as $bar, for each method I tried, I would get args[0] as "filename path" and args[1] would be empty.
ie:
function Job-Test([string]$foo, [string]$bar){
$myArray = #($foo,$bar)
Start-Job -FilePath .\Prog\august\jobScript.ps1 -Name 'Test' -ArgumentList $myArray
}
Then I called:
$foo = "hello.txt"
$bar = "c:\users\world"
Job-Test($foo,$bar)
I had jobScript.ps1 simply Out-File the two variables to a log on separate lines and it looked like this:
log.txt:
hello.txt c:\users\world
#(empty line)
where it should have been:
hello.txt
c:\users\world
you don't need to call the function like you would in java. just append the two variables to the end of the function call Job-Test $foo $bar

powershell: how to expose some parameters defined in the main script to all the modules to be called

The main calling script defines 3 parameters, and I'd like all the module can use them, one way is to use global script, but looks bad.
I hope we can use something like the following to pass the parameters, but doesn't work
import-module "$currentPath\ETLLib.psm1" $a $b $c
my main script is like:
$a
$b
$c
import-module "$currentPath\ETLLib.psm1" $a $b $c
import-module "$currentPath\Tranform.psm1" $a $b $c
ETLLib.psm1
param($a $b $c)
Tranform.psm1
param($a $b $c)
The ArgumentList parameter of Import-Module should be used.
Test.psm1:
param($a, $b, $c)
Write-Host $a
Write-Host $b
Write-Host $c
Import using ArgumentList:
Import-Module Test -ArgumentList arg1, arg2, arg3
Output:
arg1
arg2
arg3