How to pass arguments for remote commands

How to pass arguments for remote commands

  • Comments 12

When invoking remote commands, one often wants to use local values as arguments to the remote command. This post explains how to do that.

Hardcoded values

Let's start with a simple example. Below you can see how Get-Process is invoked in a remote session when the Id parameter is set to 0:

PS> $s = New-PSSession -Computer localhost
PS> Invoke-Command -Session $s -Script { Get-Process -Id 0 }
 
Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName     PSComputerName
-------  ------    -----      ----- -----   ------     -- -----------     --------------
      0       0        0         24     0               0 Idle            localhost

Passing local values

What if we want to calculate the value of the Id parameter programmatically, instead of hard-coding it to a particular integer? Well, script blocks can declare parameters and the param block can be used to pass a value to the command that is being invoked. In the following example, we declare the $processId parameter in the script block and then use the Args parameter of Invoke-Command to set the value of $processId to $pid, which is the process ID of the current Windows PowerShell process.

PS> $s = New-PSSession -ComputerName localhost
PS> Invoke-Command -Session $s -Script { param($processId) Get-Process -Id $processId } -Args $pid
 
Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName     PSComputerName
-------  ------    -----      ----- -----   ------     -- -----------     --------------
    533      13    46160      37456   198     6.07   9488 powershell      localhost

The example above demonstrates that Get-Process was invoked with a process id of the local Windows PowerShell console.  Invoke-Command takes whatever you pass in the Args parameter and binds it to script block's parameters in the remote session. Note that above I simply passed in a value of a local $pid variable, but any local expression (not just a simple variable dereference) can be used instead.

Referring to remote variables

A different behavior takes place when you don't declare script block parameters using the "param" keyword:

PS> $s = New-PSSession -ComputerName localhost
PS> Invoke-Command -Session $s -Script { Get-Process -Id $pid }
 
Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName     PSComputerName
-------  ------    -----      ----- -----   ------     -- -----------     --------------
    329      10    21216      33524   154     7.63   6596 wsmprovhost     localhost

In this case $pid is an unbound variable and Windows PowerShell assumes that it refers to the $pid variable in the remote session. Therefore, as you can see Get-Process returns the process that hosts the remote session.

Magic $args variable

What happens when you use the Args parameter of Invoke-Command cmdlet, but don't declare any parameters in the script block? Local values are still passed to the remote session and in this case are bound to the $args variable. This can save some typing:
PS> $s = New-PSSession -ComputerName localhost
PS> Invoke-Command -Session $s -Script { Get-Process -Id $args[0] } -Args $pid

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName                    PSComputerName
-------  ------    -----      ----- -----   ------     -- -----------                    --------------
    808      46    76840      81876   596     5.05   7824 powershell                     localhost

Thanks,

Lukasz Anforowicz [MSFT]
Windows PowerShell Developer
Microsoft Corporation

Leave a Comment
  • Please add 4 and 1 and type the answer here:
  • Post
  • This works fine. I can pass [scriptblock] object as a argument.

    Invoke-Command { $args[0].invoke() } -Args {ls}

    But these fail... why?

    Invoke-Command { $args[0].invoke() } -AsJob -Args {ls}

    Start-Job { $args[0].invoke() } -Args {ls} | Wait-Job | Receive-Job

  • Short answer:

    You have to explicitly say that you want to create a script block out of remote data:

    Invoke-Command `

       -Session $s `

       -ScriptBlock { $ExecutionContext.InvokeCommand.NewScriptBlock($args[0]).Invoke() } `

       -Args { ls }

    Your first example worked, because it didn't use remoting (no -Session parameter for Invoke-Command).  Background jobs (i.e. Start-Job or ICm -AsJob) are built on top of remoting.

    Long answer:

    To limit the attack surface, administrators of remote servers can restrict capabilities of remote sessions by setting $ExecutionContext.SessionState.LanguageMode="NoLanguage".  Among other things this disables our parser that knows how script blocks are built.  

    To make sure we do not have any loopholes we always deserialize scriptblocks into strings.  This makes sure that deserializer doesn't ever invoke the parser.  Additionally it also makes code injection attacks more difficult (by making it difficult to go from CliXml to a scriptblock).

    This behavior of deserializer is desirable when working with NoLanguage sessions, but of course the user can create arbitrary script blocks in FullLanguage (default) sessions.  

    We considered making deserializer aware of its location (client vs server), session settings (NoLanguage vs FullLanguage) and operating environment (remoting vs Import-CliXml vs powershell.exe -InputFormat XML).  This feature was cut in Windows 7, because we decided that we need more time to make sure modification of the deserializer do not introduce any security issues.

  • Thank you for your detailed explanation!

    btw, would i be able to read this good sentences by Chapter 17 of Windows PowerShell in Action Second Edition? ;)

  • Excellent!  Thanks for the detailed explanations. Gave me a better perspective on what I can do to solve some of my problems.  

  • Very helpful post! I was really frustrated with the way invoke-command didn't understand my syntax with $variables inside. Now I know why and I like it a lot! :)

  • how would I go about doing this:

    $url = "http://www.whatever.com"

    invoke-command -computername server1 { send-mailmessage .... -body "check $url" }

    How do I pass the $URL into the invoke-command?

    thx

  • I'm trying to do a 'multithreading' script using vmware but start-job has a bug that won't execute Add-PSSnapin VMware.VimAutomation.Core in background jobs.

    To get around this, I'm trying to use invoke command with variables. My test script to launch the tester.ps1 file looks as follows, but I can't seem to figure out the syntax for passing multiple variables into the tester.ps1

    $vm = "Test-VM"

    $dataStore = "DatastoreA"

    $crNumber = "CRxxxxxx"

    $VC = "MyVC1"

    $UN = "User\Domain"

    Invoke-Command -ComputerName localhost -Script { param($VM, $dataStore, $crNumber, $VC, $UN) C:\Scripts\tester.ps1 $VM $Datastore $crNumber $VC $UN } -Args $VM $dataStore $crNumber $VC $UN

    I get:

    Invoke-Command : A positional parameter cannot be found that accepts argument 'DatastoreA'.

    Please help...

  • Hi Mark,

    You have to delimit the arguments with commas, so try:

    Invoke-Command -ComputerName localhost -Script { param($VM, $dataStore, $crNumber, $VC, $UN) C:\Scripts\tester.ps1 $VM $Datastore $crNumber $VC $UN } -Args,$VM,$dataStore,$crNumber,$VC,$UN

  • the text on the right hand side is being cut off by the advertisement

  • Great for automation

  • Hello everyone and thanks a bunch for your help!

    I am connecting two domains using the New-PSSession command.  The remote domain I am connecting to is running Powershell 3.0.  My local domain is running Powershell 2.0.  Not sure if this matters or not.  The connection is working properly as I can see it returns output from the remote domain. My code executes an  "Invoke-Command" which in turn executes another powershell script on the remote domain which contains a simple "Get-AdUser" LDAP query, BUT it needs to pass in the userID ( variable called $UI ) which changes on each execution.

    However, I am not able to get my script to pass the variable $UI into the scriptblock correctly.  It only works if I hard code the value of "NNATEK" as seen here:

    PS C:\Users\Administrator> Invoke-Command -Session $sess -Script{powershell.exe -noprofile -executionpolicy Bypass '& {C:\Users\svc_ad\Desktop\ADMT\ADMTDomain.ps1 "NNATEK"}'}

    And it will display my correct desired Query output:

    CN=Natekar, OU=Employees,DC=JOINT,DC=dwptest,DC=com

    However I need the remote script to accept a dynamically assigned variable.  In my example, $UI is being assigned a different userID earlier in the script, every time it executes.  

    Here is my command:

    PS C:\Users\Administrator> Invoke-Command -Session $sess -Script{ param($UI) powershell.exe -noprofile –executionpolicy Bypass '& {C:\Users\svc_ad\Desktop\ADMT\ADMTDomain.ps1 $UI}'} -ArgumentList $UI

    And output showing that $UI is not defined in the Get-ADUser command:

    Get-ADUser : Variable: 'UI' found in expression: $UI is not defined.

       + CategoryInfo          : NotSpecified: (Get-ADUser : Va...is not defined.:String) [], RemoteException

       + FullyQualifiedErrorId : NativeCommandError

    At C:\Users\svc_ad\Desktop\ADMT\ADMTDomain.ps1:3 char:1

    + Get-ADUser -Filter {samAccountName -eq $UI} | Select-Object -ExpandProperty

    Dist ...

    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

       + CategoryInfo          : InvalidArgument: (:) [Get-ADUser], ArgumentException

       + FullyQualifiedErrorId : Variable: 'UI' found in expression: $UI is not defined.,Microsoft.ActiveDirectory.Management.Commands.GetADUser

    What am I doing wrong?  What do i need to change to get my desired output like in the first command with the hardcoded userId 'NNATEK' ?  Any help will be greatly appreciated.

    Thanks again.

  • Hello everyone and thanks a bunch for your help!

    I am connecting two domains using the New-PSSession command.  The remote domain I am connecting to is running Powershell 3.0.  My local domain is running Powershell 2.0.  Not sure if this matters or not.  The connection is working properly as I can see it returns output from the remote domain. My code executes an  "Invoke-Command" which in turn executes another powershell script on the remote domain which contains a simple "Get-AdUser" LDAP query, BUT it needs to pass in the userID ( variable called $UI ) which changes on each execution.

    However, I am not able to get my script to pass the variable $UI into the scriptblock correctly.  It only works if I hard code the value of "NNATEK" as seen here:

    PS C:\Users\Administrator> Invoke-Command -Session $sess -Script{powershell.exe -noprofile -executionpolicy Bypass '& {C:\Users\svc_ad\Desktop\ADMT\ADMTDomain.ps1 "NNATEK"}'}

    And it will display my correct desired Query output:

    CN=Natekar, OU=Employees,DC=JOINT,DC=dwptest,DC=com

    However I need the remote script to accept a dynamically assigned variable.  In my example, $UI is being assigned a different userID earlier in the script, every time it executes.  

    Here is my command assigning the variable:

    PS C:\Users\Administrator> $UI = 'NNATEK'

    PS C:\Users\Administrator>$UI

    NNATEK

    And executing with variable:

    PS C:\Users\Administrator> Invoke-Command -Session $sess -Script{ param($UI) powershell.exe -noprofile –executionpolicy Bypass '& {C:\Users\svc_ad\Desktop\ADMT\ADMTDomain.ps1 $UI}'} -ArgumentList $UI

    And output showing that $UI is not defined in the Get-ADUser (the remote LDAP query script) command:

    Get-ADUser : Variable: 'UI' found in expression: $UI is not defined.

       + CategoryInfo          : NotSpecified: (Get-ADUser : Va...is not defined.:String) [], RemoteException

       + FullyQualifiedErrorId : NativeCommandError

    At C:\Users\svc_ad\Desktop\ADMT\ADMTDomain.ps1:3 char:1

    + Get-ADUser -Filter {samAccountName -eq $UI} | Select-Object -ExpandProperty

    Dist ...

    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

       + CategoryInfo          : InvalidArgument: (:) [Get-ADUser], ArgumentException

       + FullyQualifiedErrorId : Variable: 'UI' found in expression: $UI is not defined.,Microsoft.ActiveDirectory.Management.Commands.GetADUser

    What am I doing wrong?  What do i need to change to get my desired output like in the first command with the hardcoded userId 'NNATEK' ?  Any help will be greatly appreciated.

    Thanks again.

Page 1 of 1 (12 items)