I've been writing a few functions that either start as a job or can execute a scriptblock. I blogged about this recently with the post Building Asynchronous PowerShell Functions. As a requirement for one of these functions, I needed to pass all of the parameters in a function directly to a remote computer.

Here's a rough example of my function.

function Do-Something {
	param(
    	[Parameter(Mandatory)]
        [string]$ComputerName,
        
        [Parameter(Mandatory)]
        [string]$Param1,
        
        [Parameter(Mandatory)]
        [string]$Param2
	)
    
    $scriptBlock = {
    	Write-Host "Doing something here to reference $Param1"
        Write-Host "Doing something here to reference $Param2"
        
		## Do some other stuff here
	}
        
        Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock
}

Looks good, right? Nope! It might have worked if I didn't have references to $Param1 and $Param2 in the scriptblock. Since I do, as is, $Param1 and $Param2 would both be $null. I have no way to pass the local values of $Param1 and $Param2 to the remote scriptblock.

No problem. I'll just modify the scriptblock to accept arguments and pass them as arguments using the ArgumentList.

$scriptBlock = {
	Write-Host "Doing something here to reference $args[0]"
    Write-Host "Doing something here to reference $args[1]"
}

Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $Param1,$Param2

Problem solved! ...this time. But wait, I'm now running into a function that has 10 parameters. I'm now going to have to list out every single one of these parameters on the ArgumentList and reference them by position in the scriptblock.

Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $Param1,$Param2,$Param3,$Param4,$Param5,$Param6,$Param7,$Param8,$Param9,$Param10

This is ugly and not sustainable! It would be so much easier to pass all of those parameters directly to the scriptblock by using $PSBoundParameters. All of those parameters are nice and snug inside of that one variable so I'll try passing that dictionary directly to the scriptblock.

Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $PSBoundParameters

Doh! That doesn't work. ArgumentList requires an array; not a dictionary or hashtable. That would definitely be the best way to go since I'm directly passing all parameter values to the scriptblock. How can we make this happen?

Aha!

How about passing the entire hash table as a single argument and then just creating new variables from all of the key/value pairs from within the scriptblock?

$scriptBlock = {
	$args[0].GetEnumerator() | ForEach-Object {
    	New-Variable -Name $_.Key -Value $_.Value
	}
    Write-Host "Doing something here to reference $Param1"
    Write-Host "Doing something here to reference $Param2"
}

Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $PSBoundParameters

Bingo! It now doesn't matter how many parameters I added to my function. Since $PSBoundParameters will always have them all, I will automatically pass all of them to the remote scriptblock! Not only that, I can get rid of using the terrible positional references with $args[$x] and instead use the variable names instead.

But, wait, there's more!

You might be thinking, "That's great Adam, but $PSBoundParameters only has the function parameters that are explicitly passed to the function. I've got some parameters that have default values. Those won't get passed". Let's ensure we also get those default parameters as well. Using my handy Get-FunctionDefaultParameter function, we can grab all of the default parameters in the current function.

We can now get all of those values and combine them with $PSBoundParameters to ensure we get all of the parameter values.

## Find all of the parameters with default values for the currently executing function

$params = Get-FunctionDefaultParameter -FunctionName $MyInvocation.MyCommand.Name

## Ensure no bound parameters are $null that are actually default
## add all bound parameters to the $params hashtable $PSBoundParameters.GetEnumerator() | Where-Object { $_.Key -notin $params.Keys } | foreach {
	$params[$_.Key] = $params[$_.Value]
}

## Pass all bound and default parameters to the remote scriptblock
Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $params

Our function then ends up looking like this which passes all parameters on the Do-Something �function directly to the remote scriptblock.

function Do-Something {
	param(
  		[Parameter(Mandatory)]
        [string]$ComputerName,
        
        [Parameter(Mandatory)]
        [string]$Param1,
        
        [Parameter(Mandatory)]
        [string]$Param2
	)
    
    $scriptBlock = {
    	$args[0].GetEnumerator() | ForEach-Object {
        	New-Variable -Name $_.Key -Value $_.Value
		}
        Write-Host "Doing something here to reference $Param1"
        Write-Host "Doing something here to reference $Param2"
        
        ## Find all of the parameters with default values for the currently executing function
        $params = Get-FunctionDefaultParameter -FunctionName $MyInvocation.MyCommand.Name
        
        ## Ensure no bound parameters are $null that are actually default
        ## add all bound parameters to the $params hashtable
        $PSBoundParameters.GetEnumerator() | Where-Object { $_.Key -notin $params.Keys } | foreach {
        	$params[$_.Key] = $params[$_.Value]
        }
        ## Pass all bound and default parameters to the remote scriptblock
        Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $params
}

Join the Jar Tippers on Patreon

It takes a lot of time to write detailed blog posts like this one. In a single-income family, this blog is one way I depend on to keep the lights on. I'd be eternally grateful if you could become a Patreon patron today!

Become a Patron!