Hyper-V and PowerShell: Splatting and Profiles

In the first article of the Hyper-V and PowerShell series, we talked about using tab completion to ease the typing burden. There are other tricks that can reduce it even further. Of those, I’m going to illustrate splatting and profiles. Of the two, I usually get much more mileage out of profiles. However, I’m going to introduce splatting first as it will explain some of the things I do with my profile. If you’re someone who does use PowerShell but does not write script files, splatting may not seem like something that can be of use to you. Nothing could be farther from the truth.

So far we have covered the topics on the following list:

Splatting

At first, I didn’t see a whole lot of value in splatting. It sounds neat, but the usage you see in all the examples doesn’t do very much to sell it. You can see the common reasoning for splatting right in the help (Get-Help about_Splatting): “Splatting makes your commands shorter and easier to read.” Both of these things are debatable. Consider the following:

# Without splatting
New-VM -ComputerName svhv1 -Name newvm -Generation 2 -MemoryStartupBytes 2GB -Path C:LocalVMs -NoVHD

# With splatting
$NewVMParameters = @{
	ComputerName = 'svhv1'
	Name = 'newvm'
	Generation = 2
	MemoryStartupBytes = 2GB
	Path = 'C:LocalVMs'
	NoVHD = $true
}

New-VM @NewVMParameters

With splatting, the actual command line is very short (that’s the New-VM @NewVMParameters part). I don’t know that the total package qualifies as “shorter”, though. You can put all the parameters of the splatting variable on the same line, if you want to. Likewise, you can split out all the parameters without splatting:

# Single-line splat definition
$NewVMParameters = @{ComputerName = 'svhv1'; Name = 'newvm'; Generation=2; MemoryStartupBytes=2GB; Path = 'C:LocalVMs'; NoVHD = $true}
New-VM @NewVMParameters

# Multi-line, no splat
New-VM `
	-ComputerName svhv1 `
	-Name newvm `
	-Generation 2 `
	-MemoryStartupBytes 2GB `
	-Path C:LocalVMs `
	-NoVHD

So, whether or not splatting is shorter than not splatting is really a matter of how you use it.

I also don’t think it’s “easier to read”. When you’re scanning a script for functionality, you get to the parameters before you get to the command that they’re supposed to be used with. So, once you get to that command, you then have to read backward to match up the parameters. It’s not overly troublesome when everything is all together as in the above examples, but there’s nothing requiring that they be anywhere near each other. Scrolling around in the same file to find all the components that go together in a single command does not facilitate comprehension.

But, despite the poor sales pitch (and even though I’m picking on the built-in help here, you’ll find that pretty much all other articles and books give this feature the same short treatment), splatting is very useful. The next part of the help file hints at the real power of splatting: “You can re-use the splatting values in different command calls.” Re-use is a big deal in programming that has equal merits in scripting. Re-use simply means writing a bit of code one time and then referring to it multiple times. Re-use reduces typing, fatigue, and errors. Unfortunately, most of the splatting examples you’ll find don’t really illustrate that.

I found a great use for splatting when I was writing the script to locate orphaned virtual machine files. I wanted to give users the ability to use alternate credentials with the script, but that’s problematic for a number of the commands, such as Invoke-Command. If you use the Credential parameter at all, you must supply something. Otherwise, it fails completely. I couldn’t find any way to just read in the current user’s credentials and pass those. It seemed that my only options would be to force users to always enter a credential, to force users to start PowerShell sessions elevated, to produce near-duplicates of many lines of script, or to generate a lot of overloaded functions to mask existing commands. Splatting saved me from all of that complexity. A simplified illustration follows:

$InvokeCommandParameters = @{
	Session = $RemoteSessionToWorkWith
	ScriptBlock = $RemoteScriptToRun
}

if ($UserSuppliedAlternateCredentials)
{
	$InvokeCommandParameters.Add("Credentials", $Credentials)
}

Invoke-Command @InvokeCommandParameters

So, whether or not the user supplied credentials, the Invoke-Command operation will work as expected and I don’t have to re-write a lot of script to make it happen.

These splatting parameter variables that I’m building are PowerShell hash tables. This means that they can be manipulated quite easily as necessary. Consider the following:

$SomeCommandParameters = @{
	FirstParameter = "One"
	SecondParameter = "Two"
	ThirdParameter = "Three"
}

# Change an existing parameter
$SomeCommandParameters["FirstParameter"] = "One point one"

# Add a new parameter
$SomeCommandParameters.Add("FourthParameter", "Four")

# Remove a parameter
$SomeCommandParameters.Remove("SecondParameter")

You can use these techniques to manipulate a single parameter set to pass to the same command. Or, in the case of cmdlets such as Start-VM and Stop-VM that use similar parameter sets, you can re-use the same set of parameters for entirely different commands.

Splatting is useful, but in its current incarnation, limited in a very unfortunate way. I discovered that when I tried to work them into my PowerShell profile.

Profiles

In PowerShell, a profile is simply a script that runs automatically when a new PowerShell session is started. Any variables or functions created in that script are still available in the session after the script exits. This is potentially one of the greatest time-savers in PowerShell, although it can mean investing some time setting up and maintaining a good profile.

Creating a Profile

The biggest annoyance of a profile is usually creating one. Turns out it’s not as difficult as it might seem. Whether or not the profile script exists, the path to it is always available. So, inside your session, just type $PROFILE and press Enter. Of course, you can also type $P or any other subset of the name and use tab completion. Upon pressing Enter, you’ll be rewarded with the name of the profile file, even if it doesn’t exist:

PS C:Windowssystem32> $PROFILE
C:UsersesadminDocumentsWindowsPowerShellMicrosoft.PowerShell_profile.ps1

We use that to create a profile with very minimal typing:

New-Item -Path $PROFILE -ItemType File -Force

Now you can open the file in your preferred PowerShell editor. On a non-GUI edition, such as Hyper-V Server or Windows Server in core mode, you have access to notepad:

# Open the profile in Notepad:
notepad $PROFILE

# Open the profile in the PowerShell Integrated Scripting Environment (GUI editions only):
ISE $PROFILE

If you want to open the file in other environments that aren’t in the path or that don’t support opening files from the command-line, you can copy the output of $PROFILE and paste it into the application’s Open dialog box. Once you have your new profile file, just enter script into and save it like any other .PS1. Open a new PowerShell session to test it. The following sub-sections will give example uses, followed by a sample profile file.

Profile Functions

Easily the most common use of profiles is to make custom functions available all the time. This is why many scripts that are made public are presented in the following form:

function SomeFunction {
	## function contents
}

When you run the script directly, nothing seems to happen. However, you can then call “SomeFunction” at any time in your session. If you wanted, you could place an entire function, or multiple functions, into your profile. That’s usually not very practical or maintainable, though. It’s easier to use dot-sourcing to load them. We talked about this back in the first post in this series. Dot-sourcing means you type in a period, then a space, then the path to a script file. That loads all the contents of that script into the session:

. .ScriptsRestart-VM.ps1

As you add new functions, it can become tedious to open up the profile and enter a new line for each addition. Since the profile is a complete PowerShell script, you can script the loading of your files so that anything you place in a particular location is automatically loaded. I’ll demonstrate that later in this article.

Profile Splatting

Profiles also present a great use for splatting, especially for those of you that are more likely to use PowerShell in an interactive session than to develop your own scripts. You probably have a few common chores that you use PowerShell for, such as retrieving a particular set of virtual machines to check their status, or something of that nature. Since PowerShell’s tab completion can’t see things like the names of virtual machines, that can mean a lot of typing each time you want to pull a particular virtual machine subset, or from a specific set of hosts, or whatever. One solution is to roll your own function:

function Get-WebVMs
{
	<#
	.SYNOPSIS
	Retrieve all the company's web servers.
	#>
	Get-VM -ComputerName svhv1, svhv2, svhv3 -Name sviis1, sviis2, sviis3, sviis4, sviis5 -ErrorAction SilentlyContinue
}

There’s nothing directly wrong with that approach, but it can be quite a bit of maintenance to build very many special-case functions like that. Splatting gives you another option that can, with a little work, be more flexible. Imagine if the following were loaded into your profile:

$GetVMHostsWithIISGuests = @{
	ComputerName = @(
		"svhv1"
		"svhv2"
		"svhv3"
	)
}

$GetVMIISGuests = @{
	Name = @(
		"sviis1"
		"sviis2"
		"sviis3"
		"sviis4"
		"sviis5"
	)
}

With that, all you’d need to do to pull the IIS guests is this (note the use of @ where you might have wanted to use $):

Get-VM @GetVMHostsWithIISGuests @GetVMIISGuests -ErrorAction SilentlyContinue

That’s still a lot of typing though, right? Well, there are a couple of good answers for that. First, as you’ll find out pretty quickly, the splat operator (the @ that might have seemed out of place) doesn’t accept tab completion at all. But, the variables that hold the parameters to be splatted do support tab completion. So, what I do is start by typing “$GetVM” and then press [TAB] until I have what I want. Then I go back and replace the $ with an @. Until the splat operator gets some love, that’s your only option for that part. But, we could still do a little bit more work to make that command a whole lot shorter.

$VMHostsWithIISGuests = @{
	ComputerName = @(
		"svhv1"
		"svhv2"
		"svhv3"
	)
}

$VMIISGuests = @{
	Name = @(
		"sviis1"
		"sviis2"
		"sviis3"
		"sviis4"
		"sviis5"
	)
}
$GetVMIISGuestsParameters = $VMHostsWithIISGuests + $VMIISGuests
$GetVMIISGuestsParameters.Add("ErrorAction", "SilentlyContinue")

The first thing I did was shorten the names of the two groups of computers to remove the GetVM prefix. That’s to show that I’m going to decouple them from that cmdlet.

The next thing I did, immediately after creating the two groups, was create a third group called “GetVMIISGuestsParameters”. It adds the other two together to make a unique parameter set. The purist in me wants you to be aware that this isn’t terribly efficient because we’re making copies of the hash tables and that will take up more memory than the previous example. However, it’s really only a few bytes and your system will reclaim them once you exit from your session, so I think it will be OK.

The final thing is the addition of the ErrorAction override. Since we’re providing Get-VM with multiple VMs to look for on multiple hosts, it will want to error each time it searches a host for a virtual machine that’s not there. This suppresses that error.

Now all you have to do is run the following:

Get-VM @GetVMIISGuestsParameters

Don’t forget to lead off with the $ so you can use tab completion, then replace it with an @ before submitting.

By building on these concepts, you can design a profile that reduces your typing for common tasks down to nearly nothing.

Limitations of Splatting

As I read the material on splatting, I wonder if the designers actually planned for this type of usage. I’d thought of one great use for it that, unfortunately, the basic design of splatting completely prohibits.

In the profile examples above, I showed you how to store hash tables for use with splatting. But, you could put any kind of variable into your profile that you want. If you have a server with an obnoxiously long name, you could just make an entry like “$MyServer = superobnoxiouslylongservername.unbelievablylongouterinternaldomainname.areyouserious.youdoknowthatnetbiosonlyaccepts15charsinahostnameright.internal” and use that anywhere the server’s name is needed. Because of that capability, I thought that I’d design a nice splatting object like this:

throw("Non-functional script; do not attempt to operate")

$Splats = @{
	VMHostsWithIISGuests = @{
		ComputerName = @(
			"svhv1"
			"svhv2"
			"svhv3"
		)
	}
	VMIISGuests = @{
		Name = @(
			"sviis1"
			"sviis2"
			"sviis3"
			"sviis4"
			"sviis5"
		)
	}
}

Get-VM @Splats.VMIISGuests

Unfortunately, if you try that, this is what you’ll get for your efforts:

[svhv1]: PS C:LocalVMs> Get-VM @Splats["VMIISGuests"]
At line:1 char:8
+ Get-VM @Splats["VMIISGuests"]
+        ~~~~~~~
The splatting operator '@' cannot be used to reference variables in an expression. '@Splats' can be used only as an
argument to a command. To reference variables in an expression use '$Splats'.
    + CategoryInfo          : ParserError: (:) [], ParseException
    + FullyQualifiedErrorId : SplattingNotPermitted

There is no workaround. I first tried to encase it in parenthesis, sort of the way that we do with strings in a quote: “Get-VM @($Splats[“VMIISGuests”])”. The problem here is that @() is a completely different operator that tells PowerShell to create an array. So, instead of treating it like a splat, it treats it like an array and Get-VM becomes really confused. So, kids, if you ever decide to design your own language, let this be an object lesson for you that you should never overload an operator for completely unrelated uses that might ever be used at the same time. Hopefully the PowerShell designers will see this as a problem and revisit it in a future version.

For now, I just name all of my profile splats with a common prefix that ties them to the cmdlet I intend to use them with. For instance, $GetVMParameterGroupName for parameters to be used with Get-VM, etc.

The other “limitation” with splatting is that the splat variable can’t contain any parameter that the target cmdlet doesn’t have, or it will completely fail. So even though Start-VM and Stop-VM have similar inputs, you have to be careful. For instance, if you pass a “Save” parameter to “Start-VM” through a splat, it will not function. I don’t know if I can call this a true limitation since you can’t successfully pass non-existent parameters to a cmdlet when you’re not splatting, either. But, attempts to reuse a splat set are more likely to run you afoul of the problem. What I do is use my splats with the Get- cmdlets and pipe them to the other types whenever possible.

Other Profiles

The profile examples above show only the profile that you get when you open a standard PowerShell prompt on the local computer. There are, of course, other ways to start PowerShell.

ISE Profile

One graphical PowerShell tool that many people use is the Integrated Scripting Environment (ISE). I don’t use this IDE often because I don’t think it’s well-developed or offers sufficient functionality in comparison to other available solutions. However, for those of you that do use it, the ISE has its own profile. The easiest way to get to it is to start the ISE and run $PROFILE from its built-in shell and follow the same directions with it as above. What you cannot do is reference one profile file from another. If you attempt to dot-source your primary profile into your ISE profile, it will error. You’ll have to duplicate content if you want the same profile to load in both places. I suspect that Get-Content | Set-Content would work.

System Profiles

We’ve only seen the current user’s profile so far, but $PROFILE will show you all the profiles available, which you can see with the following:

PS C:Windowssystem32> $PROFILE.AllUsersAllHosts
C:WindowsSystem32WindowsPowerShellv1.0profile.ps1
PS C:Windowssystem32> $PROFILE.AllUsersCurrentHost
C:WindowsSystem32WindowsPowerShellv1.0Microsoft.PowerShell_profile.ps1
PS C:Windowssystem32> $PROFILE.CurrentUserAllHosts
C:UsersesadminDocumentsWindowsPowerShellprofile.ps1
PS C:Windowssystem32> $PROFILE.CurrentUserCurrentHost
C:UsersesadminDocumentsWindowsPowerShellMicrosoft.PowerShell_profile.ps1

Use extreme caution when performing any operation on the other profiles.

Remote Profiles

When you connect to a PowerShell session on a remote machine (such as through Enter-PSSession), no profiles are loaded. The $PROFILE variable is empty. You could switch into the profile folder and dot-source your profile, but for all that typing, I’d probably just RDP to the target system.

Instead of that, you can create a custom PowerShell session. On the target system while logged in as the same user account you would connect with remotely, you can run something like the following:

Register-PSSessionConfiguration -Name Eric -StartupScript $PROFILE

You’ll get a few prompts, which I recommend that you read closely. Custom PowerShell sessions have a lot of power that I’m not explaining here.

Once you’ve done this, you have a permanent custom session available on that host that will run your profile when anyone connects to it by name (they need credentials, though, but this is why I want you to read the prompts). By permanent, I mean that its availability survives reboots, etc. If you haven’t actively opened the session, it’s not running in the background.

To connect to that custom session from a remote computer, while logged on as the same user you created the session as:

Enter-PSSession -ComputerName svpsmanager -ConfigurationName Eric

You can, of course, use the -Credential parameter with Enter-PSSession so that you don’t have to be logged on as that user. Once this command executes, you’ll connect in and run your profile.

A Sample Profile with Automatically Loading Functions

To wrap up this article, I leave you with a gift: a sample profile that will automatically load any custom function files that you want. This script calls them from C:ScriptsAutoLoad, so modify that line (3) as necessary to match your system.

## Constants that will be permanently loaded into each session ##
New-Variable -Name PROFILE -Value $PSCommandPath -Option Constant -ErrorAction SilentlyContinue
New-Variable -Name ProfileAutoLoadScriptsFolder -Value "C:ScriptsAutoLoad" -Option Constant -ErrorAction SilentlyContinue

## Variables that will be loaded into each session ##
# ComputerName Splat Components
$CNHVTestHosts = @{ComputerName = @("svhv1t", "svhv2t")}
$CNHVGeneralProductionHosts = @{ComputerName = @("svhv1", "svhv2")}
$CNHVLyncHosts = @{ComputerName = @("svhvlync1", "svhvlync2", "svhvlync3", "svhvlync4", "svhvlync5")}

# VMName Splat Components
$VMIIS = @{Name=@("svweb1", "svweb2", "svweb3", "svweb4", "svweb5")
$VMLyncFE = @{Name=@("svlyncfe1", "svlyncfe2", "svlyncfe3")}
$VMLyncEdge = @{Name=@("svlyncedge1", "svlyncedge2")}
$VMDC = @{Name=@("svdc1", "svdc2")}

# Generic splat components
$EASilent = @{ErrorAction="SilentlyContinue"}

# Splats
$GetVMAll = $CNHVTestHosts + $CNHVGeneralProductionHosts + $CNHVLyncHosts + $EASilent
$GetVMProduction = $CNHVGeneralProductionHosts + $EASilent
$GetVMTest = $CNHVTestHosts + $EASilent
$GetVMDC = $CNHVGeneralProductionHosts + $VMDC + $EASilent
$GetVMLync = $CNHVLyncHosts + $VMLyncFE + $VMLyncEdge + $EASilent

## Load function scripts
$AutoLoadScripts = Get-ChildItem -Path $ProfileAutoLoadScriptsFolder -Filter "*.ps1" -File -ErrorAction SilentlyContinue
if($AutoLoadScripts.Count)
{
    0..($AutoLoadScripts.Count - 1) | foreach `
    {
        Write-Progress -Activity "Loading functions" -Status "$($AutoLoadScripts[$_])" -PercentComplete (($_ + 1) / $AutoLoadScripts.Count)
        . $AutoLoadScripts[$_].FullName
    }
}

So, with the above, you could run Get-VM @GetVMLync | Restart-VM to quickly and easily perform a graceful restart all the Lync guests by running them through our override for the built-in Restart-VM cmdlet, all without a custom function or a complex select statement. I’d say that’s some dramatically reduced typing, wouldn’t you?

That’s Not All

This is really only the beginning for what you can preset in a profile. I used a lot of “hardcoded” entries. If you added another domain controller or Lync system, you’d have to manually change the items in the above profile. Those lists are probably duplicates of other lists. To reduce typing and maintenance even further, you could use Get-Content and Import-CSV and other PowerShell tools to read from those lists and dynamically keep your profile entries up to date. That’s a subject for another article.

 

Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published. Required fields are marked *

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.