PowerShell Invoke-Command: A Comprehensive Guide

Published:16 June 2019 - 6 min. read

Azure Cloud Labs: these FREE, on‑demand Azure Cloud Labs will get you into a real‑world environment and account, walking you through step‑by‑step how to best protect, secure, and recover Azure data.

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!

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!