PowerShell Splatting the Argumentlist on Invoke-Command
Asked Answered
C

6

10

How is it possible to use the parameters collected in a hash table for use with ArgumentList on Invoke-Command?

$CopyParams = @{
    Source      = 'E:\DEPARTMENTS\CBR\SHARE\Target'
    Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
    Structure   = 'yyyy-MM-dd'
}
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock ${Function:Copy-FilesHC} -ArgumentList @CopyParams

Whatever I try, it's always complaining about the 'Source':

Cannot validate argument on parameter 'Source'. The "Test-Path $_" validation script for the argument with
 value "System.Collections.Hashtable" did not return true. Determine why the validation script failed

This blog talks about a similar problem, but I can't get it to work.

The same is true for a simple Copy-Item within Invoke-Command, example:

Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {Copy-Item} -ArgumentList @CopyParams

Invoke-Command : Missing an argument for parameter 'ArgumentList'. Specify a parameter of type 'System.Obj
ect[]' and try again.
At line:11 char:89
+ ... ck {Copy-Item} -ArgumentList @CopyParams

Thank you for your help.

Coldshoulder answered 30/1, 2015 at 11:4 Comment(3)
Would help to see what's in the ScriptBlock - assume that's what's pulling args[0] and expecting it to be a hash? Also assume that's where the error message comes from?Lorilee
Thank you for the reply arco444. The ScriptBlock is just a function with in the parameters a Test-path on [String]Source and [String]Destination. After this it just uses Copy-Item to copy stuff. It works fine when passing them as -ArgumentList $Source, $Destination, but not with splatting.Coldshoulder
If you try it with my last example of Copy-Item, you'll see it doesn't work. Not even with the dollar sign.Coldshoulder
C
1

I found a workaround, but you have to make sure that your Advanced function which is located in your module file is loaded up front in the local session. So it can be used in the remote session. I wrote a small helper function for this.

Function Add-FunctionHC {
    [CmdletBinding(SupportsShouldProcess=$True)]
    Param(
        [String]$Name
    )
    Process {
        Try {
            $Module = (Get-Command $Name -EA Stop).ModuleName
        }
        Catch {
            Write-Error "Add-FunctionHC: Function '$Name' doesn't exist in any module"
            $Global:Error.RemoveAt('1')
            Break
        }
        if (-not (Get-Module -Name $Module)) {
            Import-Module -Name $Module
        }
    }
}

# Load funtion for remoting
Add-FunctionHC -Name 'Copy-FilesHC'

$CopyParams = @{
    Source      = 'E:\DEPARTMENTS\CBR\SHARE\Target\De file.txt'
    Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
}

$RemoteFunctions = "function Copy-FilesHC {${function:Copy-FilesHC}}" #';' seperated to add more

Invoke-Command -ArgumentList $RemoteFunctions -ComputerName 'SERVER' -Credential $Cred -ScriptBlock {
    Param (
        $RemoteFunctions
    )
    . ([ScriptBlock]::Create($RemoteFunctions))
    $CopyParams = $using:CopyParams
    Copy-FilesHC @CopyParams
}

The big advantage is that you don't need to copy your complete function in the script and it can stay in the module. So when you change something in the module to the function it will also be available in the remote session, without the need to update your script.

Coldshoulder answered 2/2, 2015 at 7:44 Comment(1)
This part was key for me: $CopyParams = $using:CopyParams. Thanks!Fumigate
K
8

One-liner, to convert a remote script to accept named parameters from a hash.

Given a scriptblock which you wish to call like this:

$Options = @{
    Parameter1 = "foo"
    Parameter2 = "bar"
}

Invoke-Command -ComputerName REMOTESERVER -ArgumentList $Options -ScriptBlock {
    param(
        $Parameter1,
        $Parameter2
    )
    #Script goes here, this is just a sample
    "ComputerName: $ENV:COMPUTERNAME"
    "Parameter1: $Parameter1"
    "Parameter2: $Parameter2"
} 

You can convert it like so

Invoke-Command -Computername REMOTESERVER -ArgumentList $Options -ScriptBlock {param($Options)&{
    param(
        $Parameter1,
        $Parameter2
    )
    #Script goes here, this is just a sample
    "ComputerName: $ENV:COMPUTERNAME"
    "Parameter1: $Parameter1"
    "Parameter2: $Parameter2"
} @Options}

What's going on? Essentially we've wrapped the original script block like so:

{param($Options)& <# Original script block (including {} braces)#> @options }

This makes the original script block an anonymous function, and creates the outer script block which has a parameter $Options, which does nothing but call the inner script block, passing @options to splat the hash.

Kress answered 29/3, 2016 at 11:52 Comment(2)
Great tip! Thanks Ben! :)Coldshoulder
Quite elegant solution :). Wrap your original (unchanged) scriptblock within a simple outer scriptblock that does all the unpacking (splatting). Reference here: about_Script_Blocks (search for call operator).Neutrophil
S
3

Here's one way to approach passing named parameters:

function Copy-FilesHC 
{
  param ($Source,$Destination,$Structure)
  "Source is $Source"
  "Desintation is $Destination"
  "Structure is $Structure"
  }


$CopyParams = @{
    Source      = 'E:\DEPARTMENTS\CBR\SHARE\Target'
    Destination = "'E:\DEPARTMENTS\CBR\SHARE\Target 2'" #Nested quotes required due to embedded space in value.
    Structure   = 'yyyy-MM-dd'
}

$SB = [scriptblock]::Create(".{${Function:Copy-FilesHC}} $(&{$args}@CopyParams)")

Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock $SB

Basically, you create a new script block from your invoked script, with the parameters splatted to that from the hash table. Everything is already in the script block with the values expanded, so there's no argument list to pass.

Silas answered 30/1, 2015 at 14:38 Comment(2)
Thank you mjolinor, it does work when there are no spaces in the Source path. When there are, it fails (Ex. Source = 'E:\DEPARTMENTS\CBR\SHARE\Target\De file.txt').Coldshoulder
If the path contains embedded spaces, then you have to pass an explicitly quoted string, just the same as if you were passing it as a parameter at the command line. This is true in any situation where you're using a hash table to splat parameters, not just for this application. I simply missed that one of the values had an embedded space in the answer. You use the same hash table that you would use if you wanted to splat the parameters to the command if it was being run in the local session. I updated the answer with an example.Silas
C
1

I found a workaround, but you have to make sure that your Advanced function which is located in your module file is loaded up front in the local session. So it can be used in the remote session. I wrote a small helper function for this.

Function Add-FunctionHC {
    [CmdletBinding(SupportsShouldProcess=$True)]
    Param(
        [String]$Name
    )
    Process {
        Try {
            $Module = (Get-Command $Name -EA Stop).ModuleName
        }
        Catch {
            Write-Error "Add-FunctionHC: Function '$Name' doesn't exist in any module"
            $Global:Error.RemoveAt('1')
            Break
        }
        if (-not (Get-Module -Name $Module)) {
            Import-Module -Name $Module
        }
    }
}

# Load funtion for remoting
Add-FunctionHC -Name 'Copy-FilesHC'

$CopyParams = @{
    Source      = 'E:\DEPARTMENTS\CBR\SHARE\Target\De file.txt'
    Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
}

$RemoteFunctions = "function Copy-FilesHC {${function:Copy-FilesHC}}" #';' seperated to add more

Invoke-Command -ArgumentList $RemoteFunctions -ComputerName 'SERVER' -Credential $Cred -ScriptBlock {
    Param (
        $RemoteFunctions
    )
    . ([ScriptBlock]::Create($RemoteFunctions))
    $CopyParams = $using:CopyParams
    Copy-FilesHC @CopyParams
}

The big advantage is that you don't need to copy your complete function in the script and it can stay in the module. So when you change something in the module to the function it will also be available in the remote session, without the need to update your script.

Coldshoulder answered 2/2, 2015 at 7:44 Comment(1)
This part was key for me: $CopyParams = $using:CopyParams. Thanks!Fumigate
D
0

I recently experienced a similar problem and solved it by building the hash (or rebuilding the hash) inside the invoke by leveraging the $using variable scope (more on that here)

it looks something like this:

$Source      = 'E:\DEPARTMENTS\CBR\SHARE\Target'
$Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
$Structure   = 'yyyy-MM-dd'

Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {
    $CopyParms= @{ 
        'Source'=$Using:Source
        'Destination'=$Using:Destination
        'Structure'=$Using:Structure
    }
    Function:Copy-FilesHC @CopyParms
}
Dunt answered 20/12, 2017 at 22:10 Comment(0)
T
0

This is what works for me:

$hash = @{
    PARAM1="meaning of life"
    PARAM2=42
    PARAM3=$true
}
$params = foreach($x in $hash.GetEnumerator()) {"$($x.Name)=""$($x.Value)"""}
Thach answered 15/11, 2018 at 8:43 Comment(1)
Does it work with switch parameters?Simmers
E
-1

I know this is late, but I ran into the same problem and found a solution that worked for me. Assigning it to a variable within the scriptblock and then using that variable to splat didn't show any problems.

Here's an example:

$param=@{"parameter","value"}
invoke-command -asjob -session $session -ScriptBlock {$a=$args[0];cmdlet @a } -ArgumentList $param
Egide answered 6/10, 2015 at 16:51 Comment(1)
@{"parameter","value"} is invalid syntax of hashtable, have you realy tested your posted script?Conceivable

© 2022 - 2024 — McMap. All rights reserved.