IT professionals rarely work just on our local computer. Using the PowerShell Invoke-Command cmdlet, we don’t have to! This cmdlet allows us to seamlessly write code as if we were working on our local computer.
By using the PowerShell Remoting feature, The Invoke-Command
cmdlet is a commonly used PowerShell cmdlet that allows the user to execute code inside of a PSSession. This PSSession can either be one created previously with the New-PSSession
cmdlet or it can quickly create and tear down a temporary session as well.
Related: PowerShell Remoting: The Ultimate Guide
Think of Invoke-Command as the PowerShell psexec. Though they are implemented differently, the concept is the same. Take a bit code or command and run it “locally” on the remote computer.
For Invoke-Command
to work though, you must have PowerShell Remoting enabled and available on the remote computer. By default, all Windows Server 2012 R2 or later machines do have it enabled along with the appropriate firewall exceptions. If you’re unfortunate enough to still have Server 2008 machines, there are multiple ways to set up Remoting but an easy way is by running winrm quickconfig
or Enable-PSRemoting
on the remote machine.
To demonstrate how Invoke-Command works with an “ad-hoc command” meaning one that doesn’t require a new PSSession to be created, let’s say you’ve got a remote Windows Server 2012 R2 or later domain-joined computer. Things get a little messy when working on workgroup computers. I’ll open up my PowerShell console, type Invoke-Command
and hit Enter.
PS> Invoke-Command
cmdlet Invoke-Command at command pipeline position 1
Supply values for the following parameters:
ScriptBlock:
I’m immediately asked to provide a scriptblock. The scriptblock is the code that we’re going to run on the remote computer.
So that we can prove that the code inside of the scriptblock is executed on the remote computer, let’s just run the hostname
command. This command will return the hostname of the computer it is running on. Running hostname
on my local computer yields, it’s name.
PS> hostname
MACWINVM
Let’s now pass a scriptblock with that same code inside of a scriptblock to Invoke-Command
. Before we do that though, we’re forgetting a required parameter: ComputerName
. We have to tell Invoke-Command
what remote computer to run this command on.
PS> Invoke-Command -ScriptBlock { hostname } -ComputerName WEBSRV1 WEBSRV1
Notice that the output of hostname
is now the name of the remote computer WEBSRV1
. You’ve run some code on WEBSRV1. Running simple code inside of a scriptblock and passing to a single remote machine is the easiest application of Invoke-Command
but it can do so much more.
Passing Local Variables to Remote Scriptblocks
You’re not going to have a single Invoke-Command reference inside of a script. Your script is probably going to be dozens of lines long, have variables defined places, functions defined in modules and so on. Even though just enclosing some code in a couple curly braces may look innocent, you’re in fact changing the entire scope that code is running in. After all, you’re sending that code to a remote computer. That remote computer has no idea of all of the local code on your machine other than what’s in the scriptblock.
For example, perhaps you’ve got a function with computer name and a file path parameter. This function’s purpose is to run some software installer on the remote computer. You are able to pass the computer name and the “local” file path to the installer that’s already located on the remote computer.
The function below seems reasonable, right? Let’s run it.
function Install-Stuff {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$InstallerFilePath
)
Invoke-Command -ComputerName $ComputerName -ScriptBlock { & $InstallerFilePath }
}
PS> Install-Stuff -ComputerName websrv1 -InstallerFilePath 'C:\install.exe'
The expression after '&' in a pipeline element produced an object that was not valid. It must result in a command name, a script block, or a CommandInfo object.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : BadExpression
+ PSComputerName : websrv1
It fails with an obscure error error message due to my use of the ampersand operator. The code wasn’t wrong but it failed because $InstallerFilePath
was empty even though you passed a value in with the function parameter. We can test this by replacing the ampersand with Write-Host
.
New function line: Invoke-Command -ComputerName $ComputerName -ScriptBlock { Write-Host "Installer path is: $InstallerFilePath" }
PS> Install-Stuff -ComputerName websrv1 -InstallerFilePath 'C:\install.exe'
Installer path is:
PS>
Notice that the value of $InstallerFilePath
is nothing. The variable hasn’t expanded because it wasn’t passed to the remote machine. To pass locally defined variables to the remote scriptblock, we’ve got two options; we can preface the variable name with $using:
inside of the scriptblock or we can use Invoke-Command
parameter ArgumentList
. Let’s look at both.
The ArgumentList Parameter
One way to pass local variables to a remote scriptblock is to use the Invoke-Command
ArgumentList
parameter. This parameter allows you to pass local variables to the parameter and replace local variable references in the scriptblock with placeholders.
Passing the local variables to the ArgumentList
parameter is easy.
Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { & $InstallerFilePath } -ArgumentList $InstallerFilePath
The part that trips up some people is how to structure the variables inside of the scriptblock. Instead of using { & $InstallerPath }
, we need to change it to either be { & $args[0] }
or {param($foo) & $foo }
. Either way works the same but which one should you use?
The ArgumentList
parameter is an object collection. Object collections allow you to pass one or more objects at a time. In this instance, I’m just passing one.
When executed, the Invoke-Command cmdlet takes that collection and then injects it into the scriptblock essentially transforming it into an array called $args
. Remember that $args -eq ArgumentList
. At this point, you’d reference each index of the collection just like you would an array. In our case, we only had one element in the collection ($InstallerFilePath
) which “translated” to $args[0]
meaning the first index in that collection. However, if you had more, you’d reference with them $args[1]
, $args[2]
and so on.
Additionally, if you’d rather assign better variable names to scriptblock variables, you can also add parameters to the scriptblock just like a function. After all, a scriptblock is just an anonymous function. To create scriptblock parameters, create a param block with the name of the parameter. Once created, then reference that parameter in the scriptblock like below.
Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { param($foo) & $foo } -ArgumentList $InstallerFilePath
In this instance, the elements in the ArgumentList
collection are “mapped” to the defined parameters in order. The parameter names don’t matter; it’s the order that’s important. Invoke-Command
will take the first element in the ArgumentList
collection, look for the first parameter and map those values, do the same for the second, the third and so on.
The $Using Construct
The $using
construct is another popular way to pass local variables to a remote scriptblock. This construct allows you to reuse the existing local variables but simply prefacing the variable name with $using:
. No need to worry about an $args
collection nor adding a parameter block.
Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { & $using:InstallerFilePath }
The PowerShell $using
construct is a lot simpler but if you ever get into learning Pester, you’ll see that ArgumentList
will be your friend.
Invoke-Command and New-PSSession
Technically, this post is only about Invoke-Command but to demonstrate it’s usefulness, we need to briefly touch on the New-PSSession
command as well. Recall earlier that I mentioned Invoke-Command
can use “ad-hoc” commands or use existing sessions.
Throughout this post, we’ve just been running “ad-hoc” commands on remote computers. We’ve been bringing up a new session, running code and tearing it down. This is fine for one-off situations but not so much for a time when you’re performing dozens of commands on the same computer. In this case, it’s better to reuse an existing PSSession by creating one with New-PSSession
ahead of time.
Before running any commands, you’ll first need to create a PSSession with New-PSSession
. We can do this by simply running $session = New-PSSession -ComputerName WEBSRV1
. This creates a remote session on the server as well as a reference to that session on my local machine. At this point, I can replace my ComputerName
references with Session
and point Session
to my saved $session
variable.
Invoke-Command -Session $session -ScriptBlock { & $using:InstallerFilePath }
When ran, you’ll notice performance is faster because the session has already been built. When complete though, it’s important to remove the open session with Remove-PSSession
.
Summary
The Invoke-Command PowerShell cmdlet is one of the most common and powerful cmdlets there is. It is one I personally use the most out of nearly all of them. It’s ease of use and ability to run any code on remote computers is extremely powerful and is one command I recommend learning top to bottom!