Variables, arrays, hash tables, and script blocks - PowerShell management - PowerShell in Depth, Second Edition (2015)

PowerShell in Depth, Second Edition (2015)

Part 2. PowerShell management

Chapter 16. Variables, arrays, hash tables, and script blocks

This chapter covers

· Variable names and types

· Strict mode

· Variable drives and cmdlets

· Arrays

· Hash tables

· Script blocks

Variables are a big part of any programming language or operating system shell, and PowerShell is no exception. In this chapter, we’ll explain what they are and how to use them, and we’ll cover some advanced variable-like data structures such as arrays, hash tables, and script blocks.

16.1. Variables

Variables are quite simply a temporary storage area for data. If you have some piece of data you plan to use over and over again, it makes sense to store it in a variable rather than having to retrieve it from where it’s stored each time you need it. Or if you have a command that takes a long time to run and you want to try different things with the results, save the results to a variable so you don’t have to keep executing the same long-running expression.

You can think of variables as a kind of box. The box has its own attributes, such as its size, but what you’re generally interested in is what’s inside the box. Boxes can contain anything: letters, numbers, processes, services, user accounts, you name it. It doesn’t have to be a single value like “Richard.” It could be a collection of job or process objects. But whatever’s in a box remains static: It continues to look the same as it looked when you put it in there. Things in the box don’t update themselves automatically, so it’s possible for their information to be out of date, which isn’t always a bad thing but something to keep in mind.

Note

You’ll see in chapter 39 that the information in a variable created using the CIM cmdlets can be refreshed. The important point to remember is that the original variable isn’t changed but is used to speed up the production of new data.

Think of a variable as holding a point-in-time snapshot.

16.1.1. Variable names

Remember the last time you moved? When you started packing, you were good about writing names on boxes: “Living room,” “Kitchen,” “Kids’ room,” and so on. Later on as you neared the finish you just started throwing random stuff in boxes and skipping the names, didn’t you? But PowerShell always gives variables a name. In fact, variable names are one of the subtle little details that trip people up all the time. In PowerShell, a variable name generally contains a mixture of letters, numbers, and the underscore character. You typically see variable names preceded by a dollar sign:

$var = "Howdy"

But it’s important to remember that the dollar sign isn’t part of the variable name. The dollar sign is a sort of cue to PowerShell, telling it, “I don’t want to work with the box named var, I want to work with the contents of that box.” There are times when PowerShell will need to know the name of a variable so that it knows what box you want to use, and in those cases you must not include the dollar sign! To give you an example, consider the –ErrorVariable common parameter. Supported by all PowerShell cmdlets, this parameter lets you specify the name of a variable that you want any errors to be put into. That way, if an error occurs, you can easily see what it was just by looking in that variable. We constantly see people attempt to use it like this:

Get-Service –ErrorVariable $var

Given the previous example, which set $var = "Howdy", this new example would put the error in a variable named Howdy, because the dollar signed accessed the contents of $var, which were “Howdy.” Instead, the proper syntax is

Get-Service –ErrorVariable var

This little trip-up catches a lot of people, which is one reason we want to point it out nice and early.

Note

The *-Variable cmdlets are another source of confusion when working with variables. Their –Name parameter expects the name of the variable without the $ sign.

There’s another little thing about variable names you should know: They can contain a lot more than letters, numbers, and underscores, provided you wrap the variable’s name in curly brackets. This looks totally weird:

${this is a valid variable name} = 12345

Weird, but it works. We don’t recommend using that technique, because it makes your scripts a lot harder to read and modify. We’re definitely in favor of meaningful variable names, like $computerName instead of $c, especially in a script. When using PowerShell interactively, the emphasis is on command-line efficiency, so using a variable like $c makes sense because you know what it means. But in a script at line 267 if you see $c, it might not be so clear, especially if it’s someone else’s script. In any event we think the curly brackets let you go a bit too far.

16.1.2. Variable types

PowerShell keeps track of the type of object, or objects, contained within a variable. Whenever possible, PowerShell will elect to treat a type of data as a different type if doing so will make a particular operation make more sense. In programming, this is called coercing the variable, and it can lead to some odd results, such as

PS C:\> $a = 5

PS C:\> $b = "5"

PS C:\> $a + $b

10

PS C:\> $b + $a

55

That can freak you out the first time you see it or at least leave you scratching your head. Basically, PowerShell looks at the first operand’s data type and then looks at the operator. Because + can mean addition or string concatenation, PowerShell makes a choice based on what came first: Give it an integer in $a first, and + means addition. So it coerces $b to be an integer (otherwise it’d be treated like a string because it’s enclosed in quotes) and does the math. Give it a string in $b first, and + means concatenation, and so it treats $a like a string character and attaches it to $b.

This same behavior can create difficulties for you if you’re not careful. For example, let’s say you have a script, which contains a variable. You fully expect that variable to contain a number—perhaps the number of times a particular task should be performed. Somehow, a string—like a computer name—ends up in that variable instead. Boom, your script breaks. One way to help alleviate that error is to explicitly declare a type for your variable:

[int]$iterations = 5

When you do this, PowerShell will no longer put anything into that variable that isn’t an integer or that PowerShell can’t make into an integer, for example:

PS C:\> [int]$iterations = 5

PS C:\> $iterations+1

6

PS C:\> $iterations = 10

PS C:\> $iterations+1

11

PS C:\> $iterations = "20"

PS C:\> $iterations+1

21

PS C:\> $iterations = "Richard"

Cannot convert value "Richard" to type "System.Int32". Error: "Input

string was not in a correct format."

At line:1 char:12

+ $iterations <<<< = "Richard"

+ CategoryInfo : MetadataError: (:) [], ArgumentTransfo

rmationMetadataException

+ FullyQualifiedErrorId : RuntimeException

Here, everything worked fine even when you tried to put a string into the variable—provided that string consisted of nothing but digits. You always end up with a number. But when you tried to store something that couldn’t be coerced to a number, you got an error. The error is descriptive, and if it occurred in a script it’d tell you the exact line number where things went wrong, making the problem easier to troubleshoot.

You can always re-declare the variable to put a different data type into it. PowerShell won’t do so on its own. Here’s an example:

PS C:\> [string]$iterations = "Richard"

That works fine, because you explicitly changed the type of data that the variable was allowed to contain. Of course, this would be a silly variable name for a value of “Richard”, so we hope that this points out the importance of proper variable naming.

Hungarian notation

In the days of VBScript, scripters often defined their variables using a technique known as Hungarian notation. This involved prepending a short prefix to indicate what type of data was stored in the variable. You’d see variables like strComputer and iCount. Sadly, you still see this in PowerShell with variables like $strComputer. Technically this is a legal name, but it screams that you haven’t grasped PowerShell fundamentals yet. Make your variable names meaningful and the type will follow. If you see a script with a variable $Computername, you’re going to assume it’s a string. A variable of $Count will most likely be an integer. But you’d have no idea what $C might be without some sort of context.

A common use of Hungarian notation is to show that the variable contains an object—$objSomething. All variables in PowerShell are objects, so pointing this out in the variable name is a redundant action that just adds complications and extra typing.

The only valid reason we can see for using Hungarian notation, or any variants, would be if you were performing a series of data type conversions. Putting the type into the name may make it easier to keep track of where you are in the process. In general, though, drop the Hungarian notation and use common sense.

16.1.3. Being strict with variables

PowerShell has another behavior that can make for difficult troubleshooting. For this example, you’re going to create a very small script and name it test.ps1. The following listing shows the script.

Listing 16.1. Initial script—no testing on type

$test = Read-Host "Enter a number"

Write-Host $tset

That typo in the second line is deliberate. This is the exact kind of typo you could easily make in your own scripts. Let’s see what PowerShell does with this by default:

PS C:\> ./test

Enter a number: 5

PS C:\>

Unexpected output and no error. That’s because, by default, PowerShell assumes variables to have a default value of 0, or an empty string, or some other similar value associated with the data type assigned to the variable. If you don’t assign a data type, the variable will contain $null.

PS C:\> [string]$t -eq ""

True

PS C:\> [int]$t -eq 0

True

PS C:\> $t -eq $null

True

This kind of behavior, which doesn’t create an error when you try to access an uninitialized variable, can take hours to debug. The solution is to set a strict mode, which you can do generally in the shell or at the top of each script using the Set-StrictMode cmdlet. The effect of this cmdlet is similar to using Option Explicit in VBScript.

To use the cmdlet, you need to specify a PowerShell version value. The version will dictate how PowerShell handles uninitialized variables and a few other syntax elements that could cause problems. If you use a –version value of 1, PowerShell will complain when you reference an uninitialized variable. An exception is made for uninitialized variables in strings, which could still be difficult to troubleshoot. Let’s add a bit more to our test script, which you should save as test2.ps1.

Listing 16.2. Using strict mode

$test = Read-Host "Enter a number"

Write-host $tset

$a=[system.math]::PI*($tset*$test)

Write-Host "The area is $tset"

Here’s what happens with strict mode off. Go ahead and explicitly set it in the shell before running the script:

PS C:\> Set-StrictMode -off

PS C:\>.\test2.ps1

Enter a number: 5

The area is

PS C:\>

Now set the version value to 1:

PS C:\> Set-StrictMode -Version 1

PS C:\> .\test2.ps1

Enter a number: 5

The variable '$tset' cannot be retrieved because it has not been set.

At c:\test2.ps1:2 char:12

+ Write-Host $tset

+ ~~~~~

+ CategoryInfo : InvalidOperation: (:) [], RuntimeException

+ FullyQualifiedErrorId : VariableIsUndefined

The variable '$tset' cannot be retrieved because it has not been set.

At c:\test2.ps1:3 char:23

+ $a=[system.math]::PI*($tset*$test)

+ ~~~~~

+ CategoryInfo : InvalidOperation: (:) [], RuntimeException

+ FullyQualifiedErrorId : VariableIsUndefined

The area is

PS C:\>

PowerShell complains that $tset hasn’t been set on lines 2 and 3. The script fails but now you know what to fix. Notice PowerShell didn’t complain about the last line that also had a variable typo because it’s part of a string. Let’s fix all typos but the last one and run it again (save the script as test3.ps1).

Listing 16.3. Removing most typos

$test = Read-Host "Enter a number"

Write-host $test

$a=[system.math]::PI*($test*$test)

Write-Host "The area is $tset"

This version (test3.ps1) behaves better:

PS C:\> Set-StrictMode -Version 1

PS C:\> .\test.ps1

Enter a number: 5

5

The area is

PS C:\>

Even though you didn’t get an error, at least you recognize that there’s a problem with the last line.

Using a –version value of 2 will do everything in version 1 as well as prohibit references to nonexistent properties of an object, prohibit function calls that use the syntax for calling methods, and not allow you to use a variable without a name, such as ${}.

PS C:\> Set-StrictMode -Version 2

PS C:\> .\test3.ps1

Enter a number: 5

5

The variable '$tset' cannot be retrieved because it has not been set.

At C:\test\test3.ps1:4 char:25

+ Write-Host "The area is $tset"

+ ~~~~~

+ CategoryInfo : InvalidOperation: (tset:String) [], RuntimeException

+ FullyQualifiedErrorId : VariableIsUndefined

Using:

Set-StrictMode –Version 3

or

Set-StrictMode –Version 4

will give the same results. Alternatively, you can use a –Version value of Latest. PowerShell will use the strictest version available—this is our recommended practice.

Tip

When a new version of PowerShell becomes available, we recommend that you read the release notes to determine if there are any changes to strict mode.

This is a great way to make your script future proof. When you use strict mode, set it at the beginning of your script to make it obvious it’s on, as shown in the next listing.

Listing 16.4. Removing all typos

Set-Strictmode –Version Latest

$test = Read-Host "Enter a number"

Write-host $test

$a=[system.math]::PI*($test*$test)

Write-Host "The area is $test"

Be aware that if you have multiple errors like these, PowerShell will only throw an exception at the first one. If you have other errors, you won’t see them until you rerun the script. We suggest that if you discover a variable typo, use your script editor’s find-and-replace feature to look for other instances.

One thing missing in PowerShell, even version 4, is the ability to determine the current StrictMode setting. It’s possible using a number of .NET programming techniques, but that’s not something we want to get into. We recommend that you be aware of StrictMode and be explicit in your code as to when you use it.

Tip

Many scripts you obtain from the internet will fail if you turn StrictMode on. If you use it, be prepared to spend time rewriting the script.

The whole strict mode thing plays into something called scope in PowerShell, which we’re not quite ready to talk about yet. We’ll revisit strict mode in chapter 22.

16.2. Built-in variables and the Variable: drive

PowerShell starts up with a number of variables already created and ready to go. Most of these variables control various aspects of PowerShell’s behavior, and you can change them in order to modify its behavior. Any changes you make will be lost when you exit the shell, and they won’t be reflected in any other shell instances you may have open unless you put the changes in your profile. These variables load up with the same values in each new shell session, and they’re specific to each session rather than being global for the entire PowerShell engine. You can get a look at these by getting a directory listing for the Variable: drive, which is where PowerShell stores all variables:

PS C:\> dir variable:

Name Value

---- -----

$

? True

^

args {}

ConfirmPreference High

ConsoleFileName

currentUser System.Security.Principal.WindowsIdentity

DebugPreference SilentlyContinue

Error {}

ErrorActionPreference Continue

ErrorView NormalView

ExecutionContext System.Management.Automation.EngineIntrin...

false False

FormatEnumerationLimit 4

HOME C:\Users\Richard

Host System.Management.Automation.Internal.Hos...

input System.Collections.ArrayList+ArrayListEnu...

MaximumAliasCount 4096

MaximumDriveCount 4096

MaximumErrorCount 256

MaximumFunctionCount 4096

MaximumHistoryCount 4096

MaximumVariableCount 4096

MyInvocation System.Management.Automation.InvocationInfo

NestedPromptLevel 0

null

OutputEncoding System.Text.ASCIIEncoding

PID 2516

principal System.Security.Principal.WindowsPrincipal

PROFILE C:\Users\Richard\Documents\WindowsPowerSh...

ProgressPreference Continue

PSBoundParameters {}

PSCommandPath C:\Users\Richard\Documents\WindowsPowerSh...

PSCulture en-GB

PSDefaultParameterValues {}

PSEmailServer

PSHOME C:\Windows\System32\WindowsPowerShell\v1.0

PSScriptRoot C:\Users\Richard\Documents\WindowsPowerShell

PSSessionApplicationName wsman

PSSessionConfigurationName http://schemas.microsoft.com/powershell/...

PSSessionOption System.Management.Automation.Remoting.PSS...

PSUICulture en-GB

PSVersionTable {PSVersion, WSManStackVersion,

SerializationVersion, CLRVersion...}

PWD C:\MyData\SkyDrive\Data\scripts

role Administrator

ShellId Microsoft.PowerShell

StackTrace

true True

VerbosePreference SilentlyContinue

WarningPreference Continue

WhatIfPreference False

The list shows the state of the variables in a console session that has just been opened. You can even find a variable that controls the maximum number of variables PowerShell can keep track of! Any variables that you create are also stored in this drive—so can you think of how you might completely delete a variable? The same way you’d delete a file: the Del (or Remove-Item) command! And yes, you can absolutely delete the built-in variables, but they’ll come right back when you open a new shell instance. As a practical rule, though, be careful about deleting automatic variables because many PowerShell commands rely on them. A number of help files are available that deal with variables (get-help about*variable*).

16.3. Variable commands

PowerShell includes a dedicated set of commands for variable management:

PS C:\> Get-Command -noun Variable

CommandType Name ModuleName

----------- ---- ----------

Cmdlet Clear-Variable Microsoft.PowerShell.Utility

Cmdlet Get-Variable Microsoft.PowerShell.Utility

Cmdlet New-Variable Microsoft.PowerShell.Utility

Cmdlet Remove-Variable Microsoft.PowerShell.Utility

Cmdlet Set-Variable Microsoft.PowerShell.Utility

For the most part, you never need to use these. For example, to create a new variable you just use it for the first time and assign a value to it:

$x = 5

To assign a new value to it, you don’t need to use Set-Variable; you can just do this:

$x = 10

The variable cmdlets are there if you decide to use them. One advantage to using them is that they let you modify variables in scopes other than your own. Again, scope is something we’re going to come to later, so you may see these cmdlets in use then. Remember that when working with variables using the variable cmdlets the name of the variable is used without the $ prefix, so:

PS C:\> New-Variable -Name newvar -Value 10

not:

PS C:\> New-Variable -Name $newvar -Value 10

New-Variable : Cannot bind argument to parameter 'Name' because it is null.

At line:1 char:20

+ New-Variable -Name $newvar -Value 10

+ ~~~~~~~

+ CategoryInfo : InvalidData: (:) [New-Variable],

ParameterBindingValidationException

+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,

Microsoft.PowerShell.Commands.NewVariable Command

Note

Folks with a programming background will ask if there’s a way to make PowerShell require variable declaration, rather than letting you make up new variables on the fly. They’ll often look at strict mode, and the New-Variable cmdlet, to see if they can create some kind of “declaration required” setting. They can’t. PowerShell doesn’t require you to announce your intention to use a variable, and there’s no way to make it a requirement.

The other possibility for using New-Variable to create your variables is to make the variables read-only (which can be changed using –Force or deleted) or a constant (which can’t be deleted or changed). You’d use New-Variable if you wanted to ensure that particular variables couldn’t be modified once created.

16.4. Arrays

In many programming languages, there’s a definite difference between an array of values and a collection of objects. In PowerShell, not so much. There’s technically a kind of difference, but PowerShell does a lot of voodoo that makes the differences hard to see. So we’ll tend to use the terms array and collection interchangeably. If you have a software development background, that might bug you. Sorry. It’s just how PowerShell is.

Simply put, an array is a variable that contains more than one value. In PowerShell, all values—like integers or strings—are technically objects. So it’s more precise to say that an array can contain multiple objects. One way to get multiple objects into a variable is to run a command that returns more than one object:

$services = Get-Service

In PowerShell, the equals sign is the assignment operator. Whatever’s on the right side of the operator gets put into whatever’s on the left side. In this case, the right side contains a pipeline command—albeit a short pipeline, with only one command. PowerShell runs the pipeline, and the result goes into $services. You can have more complex pipelines, too:

$services = Get-Service | Where Status –eq 'Running'

You can access individual elements in an array by using a special notation:

PS C:\> $services = Get-Service

PS C:\> $services[0]

Status Name DisplayName

------ ---- -----------

Running ADWS Active Directory Web Services

PS C:\> $services[1]

Status Name DisplayName

------ ---- -----------

Stopped AeLookupSvc Application Experience

PS C:\> $services[-1]

Status Name DisplayName

------ ---- -----------

Stopped wudfsvc Windows Driver Foundation - User-mo...

PS C:\> $services[-2]

Status Name DisplayName

------ ---- -----------

Running wuauserv Windows Update

The first index in an array is 0 (zero), which points to the first item in the array. Index 1 is the second item, and so on. Negative numbers start at the end of the array, so -1 is the last item, -2 the second-to-last, and so on.

Note

Be careful of the array indices if you’re used to starting at 1. PowerShell is .NET based and follows the .NET convention that the first element in an array is index 0. This is sometimes awkward but it’s something we’re stuck with.

Arrays can be created from simple values by using the array operator (the @ symbol) and a comma-separated list:

PS C:\> $names = @('one','two','three')

PS C:\> $names[1]

two

PowerShell will tend to treat any comma-separated list as an array, so you can generally skip the array operator and the parentheses:

PS C:\> $names = 'one','two','three'

PS C:\> $names[2]

three

This is exactly why some cmdlet parameters can accept multiple values in a comma-separated list. For example, look at the help for Get-Service and you’ll see the following:

Get-Service [[-Name] <string[]>] [-ComputerName <string[]>]

[-DependentServices] [-Exclude <string[]>][-Include <string[]>]

[-RequiredServices] [<CommonParameters>]

Back in chapter 3, on interpreting the help files, we pointed out that the <string[]> notation’s double square brackets indicated that it could accept multiple values; technically, it’s an array. Because PowerShell interprets comma-separated lists as arrays, this is legal:

PS C:\> get-service -name a*,b*

Status Name DisplayName

------ ---- -----------

Running ADWS Active Directory Web Services

Stopped AeLookupSvc Application Experience

Stopped ALG Application Layer Gateway Service

Stopped AppIDSvc Application Identity

Stopped Appinfo Application Information

Stopped AppMgmt Application Management

Stopped AudioEndpointBu... Windows Audio Endpoint Builder

Stopped AudioSrv Windows Audio

Running BFE Base Filtering Engine

Running BITS Background Intelligent Transfer Ser...

Stopped Browser Computer Browser

Note

PowerShell is picky about parameter input. In this case, the –Name parameter not only can accept an array, it must accept only an array. If you provide only a single value, PowerShell converts that to an array of one object behind the scenes.

Arrays can hold different types of objects as well:

PS C:\> $a=42,"Jeff",(Get-Date).Month,(get-process -id $pid)

PS C:\> $a

42

Jeff

1

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName

------- ------ ----- ----- ----- ------ -- -----------

1111 42 107184 73588 609 11.89 6608 powershell

Each item is a complete object, so assuming you know the index number you can do things with it:

PS C:\> $a[0]*2

84

PS C:\> $a[1].Length

4

PS C:\> $a[-1].path

C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe

Thus the reference to $a[-1] is a process object that allows you to retrieve the path property.

Measuring the number of items in an array is usually simple using the Count or Length property. Technically Length is the property of the .NET array object and Count is an alias created by PowerShell. Count is usually easier to remember. In PowerShell 3 and 4, an array with zero or one element will return a value for Count. Earlier versions didn’t.

PS C:\> $a.count

4

Sometimes, though, you want to start with an empty array and add items to it. First, define the empty array:

PS C:\> $myarray=@()

To add an item to the array, use the += operator:

PS C:\> $myarray+="Don"

PS C:\> $myarray+="Jeff"

PS C:\> $myarray+="Richard"

PS C:\> $myarray.count

3

PS C:\> $myarray

Don

Jeff

Richard

Unfortunately, removing an item isn’t as simple:

PS C:\> $myarray-="Jeff"

Method invocation failed because [System.Object[]] doesn't contain a method

named 'op_Subtraction'.

At line:1 char:1

+ $myarray-="Jeff"

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

+ CategoryInfo : InvalidOperation: (op_Subtraction:String) [],

RuntimeException

+ FullyQualifiedErrorId : MethodNotFound

Instead you need to re-create the array using only the items you wish to keep:

PS C:\> $myarray=$myarray | where {$_ -notmatch "Jeff"}

PS C:\> $myarray

Don

Richard

So far the arrays you’ve seen have all been a single list (think of a column or data). You can create arrays of any objects, including other arrays. It’s also possible to have arrays with multiple, or even variable numbers of, columns, although this is a technique we haven’t seen used by many administrators. Arrays can be a powerful tool, and you’ll use them more than you realize.

16.5. Hash tables and ordered hash tables

Hash tables (which you’ll also see called hash tables, associative arrays, or dictionaries) are a special kind of array. These must be created using the @ operator, although they’re created within curly brackets rather than parentheses—and those brackets are also mandatory. Within the brackets, you create one or more key-value pairs, separated by semicolons. The keys and values can be anything you like:

PS C:\> @{name='DonJ';

>> samAccountName='DonJ';

>> department='IT'

>> title='CTO';

>> city='Las Vegas'}

>>

Name Value

---- -----

samAccountName DonJ

name DonJ

department IT

city Las Vegas

title CTO

Note

As you can see in that example, the semicolon is one of the characters that PowerShell knows must be followed by something else. By pressing Enter after one, you made PowerShell enter a multiline prompt mode. Technically, PowerShell will recognize that the command is incomplete and provide the nested prompts even without the semicolon. You could’ve easily typed the entire hash table on a single line, but doing it this way makes it a bit easier to read in the book. (If we’d elected to use a single line, then the semicolon would be required between hash table entries. For the sake of consistency, you may wish to always use the semicolon.) Finally, you ended that by completing the structure’s closing curly bracket, pressing Enter, and pressing Enter on a blank line.

The key is usually a string (or integer, though we don’t see that used much), and we recommend avoiding spaces if you can. You’ll see why in a bit. The next thing about hash tables is that they’re distinct objects themselves. Simple arrays like the ones we looked at earlier don’t have a type per se; their contents do. But hash tables are different. For example, if you’d assigned that hash table to a variable, you could’ve accessed its individual elements easily:

PS C:\> $user = @{name='DonJ';

>> samAccountName='DonJ';

>> department='IT';

>> title='CTO';

>> city='Las Vegas'}

>>

PS C:\> $user.department

IT

PS C:\> $user.title

CTO

This is why we recommend no spaces in the key name. If you pipe $user to Get-Member, you can see that this is a new type of object, a System.Collections.Hashtable:

PS C:\> $user | get-member

TypeName: System.Collections.Hashtable

Name MemberType Definition

---- ---------- ----------

Add Method System.Void Add(System.Object ke...

Clear Method System.Void Clear()

Clone Method System.Object Clone()

Contains Method bool Contains(System.Object key)

ContainsKey Method bool ContainsKey(System.Object key)

ContainsValue Method bool ContainsValue(System.Object...

CopyTo Method System.Void CopyTo(array array, ...

Equals Method bool Equals(System.Object obj)

GetEnumerator Method System.Collections.IDictionaryEn...

GetHashCode Method int GetHashCode()

GetObjectData Method System.Void GetObjectData(System...

GetType Method type GetType()

OnDeserialization Method System.Void OnDeserialization(Sy...

Remove Method System.Void Remove(System.Object...

ToString Method string ToString()

Item ParameterizedProperty System.Object Item(System.Object...

Count Property int Count {get;}

IsFixedSize Property bool IsFixedSize {get;}

IsReadOnly Property bool IsReadOnly {get;}

IsSynchronized Property bool IsSynchronized {get;}

Keys Property System.Collections.ICollection K...

SyncRoot Property System.Object SyncRoot {get;}

Values Property System.Collections.ICollection V...

Each value is its own type:

PS C:\> $user.title.getType().Name

String

Because the hash table is its own object, there’s a bit more you can do with it. You might want to list all the keys:

PS C:\> $user.keys

title

department

name

city

samAccountName

The Count property returns the number of items in the hash table. Just to be inconsistent, hash tables don’t respond to using Length:

PS C:\> $user.count

5

Or perhaps you might want to list all the values:

PS C:\> $user.values

CTO

IT

DonJ

Las Vegas

DonJ

Managing the hash table members is also considerably easier. The object has methods for adding and removing members. Be aware that each key must be unique, so you can’t add another key called Name with a different value. You could use the Contains-Key() method to test before invoking the Add() method:

PS C:\> if (-Not $user.containsKey("EmployeeNumber")) {

>> $user.Add("EmployeeNumber",11805)

>> }

>>

In this command you use the –Not operator to reverse the result of the ContainsKey() method so that if the expression is true, you’ll add a new entry. As you can see, it worked:

PS C:\> $user.EmployeeNumber

11805

The Add() method needs the name of the key and the value, separated by a comma. It’s even easier to remove an item:

PS C:\> $user.Remove("employeenumber")

The effect is immediate. And as with arrays, you can create an empty hash table and add elements to it as needed. The items don’t even have to be all of the same type. For example, you might start like this:

PS C:\> $hash=@{}

PS C:\> $hash.Add("Computername",$env:computername)

Later, you gather additional data and add to the hash table:

PS C:\> $running=Get-Service | where Status -eq "running" | measure

PS C:\> $hash.Add("Running",$running.count)

PS C:\> $os=Get-WmiObject –Class Win32_operatingsystem

PS C:\> $hash.Add("OS",$os)

PS C:\> $time=Get-Date -DisplayHint time

PS C:\> $hash.Add("Time",$time)

Here’s what the hash table looks like now:

PS C:\> $hash

Name Value

---- -----

Time 12/24/2013 12:07:40 PM

Computername CLIENT2

Running 65

OS \\CLIENT2\root\cimv2:Win32_OperatingSystem=@

You have different types of objects that might even be nested objects. This can lead to some handy results:

PS C:\> $hash.os

SystemDirectory : C:\Windows\system32

Organization :

BuildNumber : 7601

RegisteredUser : LocalAdmin

SerialNumber : 00426-065-0389393-86517

Version : 6.1.7601

PS C:\> $hash.os.caption

Microsoft Windows 7 Ultimate

Because hash tables are a convenient way to organize data, you might want to try nesting hash tables:

PS C:\> "coredc01","client2" | foreach -begin {

>> $comphash=@{}

>> } -process {

>> $svc=Get-Service -ComputerName $_

>> $proc=get-process -comp $_

>> $cs=Get-WmiObject –Class Win32_computersystem ComputerName $psitem

>> $nest=@{Computername=$cs.Name;

>> Services=$svc;Processes=$proc;

>> ComputerSystem=$cs

>> }

>> $comphash.Add($($cs.Name),$nest)

>> }

>>

This block of code takes a few names and pipes them to ForEach-Object.

Tip

Remember that you can interchange $_ and $psitem to represent the object on the pipeline.

In the begin script block, you define an empty hash table. In the process script block, a variety of system information is gathered from each computer and put into its own hash table, $nest. At the end, each nested hash table is added to the master hash table. Confused? Here’s what you end up with:

PS C:\> $comphash

Name Value

---- -----

CLIENT2 {ComputerSystem, Computername, Services, Proc...

COREDC01 {ComputerSystem, Computername, Services, Proc...

This offers some intriguing possibilities:

PS C:\> $comphash.COREDC01

Name Value

---- -----

ComputerSystem \\COREDC01\root\cimv2:Win32_ComputerSyste...

Computername COREDC01

Services {AdtAgent, ADWS, AeLookupSvc, AppHostSvc...}

Processes {System.Diagnostics.Process (conhost), System...

PS C:\> $comphash.COREDC01.processes | select -first 3

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName

------- ------ ----- ----- ----- ------ -- -----------

32 5 828 2668 22 1480 conhost

181 21 7544 13960 79 3584 cscript

545 13 2280 1888 45 300 csrss

PS C:\> ($comphash.COREDC01.ComputerSystem).TotalPhysicalMemory

536403968

By using a hash table, you can explore a lot of information without having to rerun commands.

16.5.1. Ordered hash tables

One problem with hash tables is that the order of the elements isn’t preserved. Consider a simple hash table:

$hash1 = @{

first = 1;

second = 2;

third = 3

}

$hash1

This code produces the following output:

Name Value

---- -----

second 2

first 1

third 3

The order of the elements appears to be random. If you’re using the hash table as a lookup device, for instance, this won’t matter. But if you’re using the hash table to create a new object, it may. A standard technique to create a new object looks like this:

$hash1 = @{

first = 1;

second = 2;

third = 3

}

$test = New-Object -TypeName PSObject -Property $hash1

$test | Format-Table -AutoSize

But the order of the properties as you defined them isn’t preserved:

second first third

------ ----- -----

2 1 3

In most cases, this isn’t a real issue, but we know of PowerShell users who object to the property order not being preserved. Okay, we’ll be honest: They moan a lot!

With PowerShell v3 and v4, you can create a hash table and preserve the order of the elements:

$hash2 = [ordered]@{

first = 1;

second = 2;

third = 3

}

$hash2

All you’ve done here is add the [ordered] attribute to the hash table definition. A standard hash table is a System.Collections.Hashtable object, but using [ordered] creates a System.Collections.Specialized.OrderedDictionary object.

Now when you create an object, you can use an ordered hash table:

$hash2 = [ordered]@{

first = 1;

second = 2;

third = 3

}

$test2 = New-Object -TypeName PSObject -Property $hash2

$test2 | Format-Table –AutoSize

This results in the order of the defined properties being preserved:

first second third

----- ------ -----

1 2 3

16.5.2. Common uses for hash tables

We’ve shown you how the Select-Object, Format-Table, and Format-List cmdlets use hash tables to attach custom properties, table columns, and list entries to objects. In the case of those cmdlets, the hash tables must follow a specific form that the cmdlets have been programmed to look for: The keys must be “l” or “label” or “n” or “name,” along with “e” or “expression”, and so forth. But these are requirements of those particular cmdlets, not of hash tables in general. In other words, we as humans have to construct the hash tables in a specific way, because those cmdlets have been designed to look for specific keys.

16.5.3. Defining default parameter values

Hash tables find another use in PowerShell v3 and v4 with the ability to define default parameter values. For example, let’s say you commonly run cmdlets like Invoke-Command that have a –Credential parameter, and you want to always specify a particular credential. Rather than having to type the parameter and provide a value every single time you run the cmdlet, you can define your credential as a default:

PS C:\> $cred = Get-Credential COMPANY\Administrator

PS C:\> $PSDefaultParameterValues.Add("Invoke-Command:Credential",$cred)

$PSDefaultParameterValues is a built-in PowerShell variable, and it’s a specialized hash table. In this example, you use its Add() method to add a new key-value pair. Doing so lets you continually add more items to it, without overwriting what was already there. You can see that the key added here takes a special form, cmdlet: parameter, where cmdlet is the cmdlet or advanced function you want to define a default for and parameter is the parameter you’re defining a default for. The value of the hash table item is whatever you want the default parameter value to be—in this case, the credential you created and stored in $cred.

You could even use a wildcard to create a default for all cmdlets that use the -Credential parameter. This time, you’ll completely redefine $PSDefaultParameterValues, overwriting whatever else you’ve put in there with this new setting:

PS C:\> $PSDefaultParameterValues = @{"*:Credential"=$cred}

This is a great feature, although it can be a bit cumbersome to use—you’ll see more on default parameters in chapter 18. $PSDefaultParameterValues starts out empty each time you open a new shell window; if you want to define a “persistent” default, the only way to do so is to put the definition into a PowerShell profile script. That way, your definition is re-created each time you open a new shell. You can read more about default parameter values by running help about_parameters_default_values in the shell.

16.6. Script blocks

They might seem like a funny thing to lump into this chapter, but like variables, arrays, and hash tables, script blocks are a fundamental element in PowerShell scripting. They’re key to several common commands, too, and you’ve been using them already.

A script block is essentially any PowerShell command, or series of commands, contained within curly brackets, or {}. Anything in curly brackets is usually a script block of some kind, with the sole exception of hash tables (which also use curly brackets in their structure). You’ve used a script block already:

PS C:\> Get-Service | Where { $_.Status –eq 'Running' }

In that example, you used a special kind of script block called a filter script, providing it to the –FilterScript parameter of the Where-Object cmdlet. The only thing that makes it special is the fact that the cmdlet expects the entire block to result in True or False, thus making it a filter script instead of a plain-old script block. You also used script blocks with Invoke-Command, in chapter 10, and with the ForEach-Object cmdlet, and in several other cases.

You can create a script block right from the command line and store the entire thing in a variable. In this example, notice how PowerShell’s prompt changes after you press Enter for the first time. It does this because you’re still “inside” the script block, and it’ll continue to use that prompt until you close the script block and press Enter on a blank line.

PS C:\> $block = {

>> Import-Module ServerManager

>> Get-WindowsFeature | Where { $_.Installed } |

>> Select Name,DisplayName,Description |

>> ConvertTo-HTML

>> }

>>

Now you have the script block stored in the variable $block and you can execute it by using PowerShell’s invocation operator and the variable:

PS C:\> &$block | Out-File installed.html

In the script block you defined, notice that it ends in ConvertTo-HTML, meaning the result of the script block is a bunch of HTML being placed into the pipeline. When you invoke the block, you pipe that output to Out-File, thus saving the HTML into a file. You could also use the variable $block anywhere a script block is required, such as with Invoke-Command:

PS C:\> Invoke-Command -ScriptBlock $block -ComputerName win8 |

Out-File InstalledFeatures.html

Here, you’re asking a remote machine to execute the script block. The resulting HTML is transmitted back to your computer and placed into the pipeline; you pipe it to Out-File to save the HTML into a file.

Script blocks can be parameterized, too. For example, create another script block that displays all processes whose names start with a particular character or characters:

PS C:\> $procbloc = {

>> param([string]$name)

>> Get-Process -Name $name

>> }

>>

The param() section defines a comma-delimited list of parameters; in this case, you’ve included only a single parameter. It’s just a variable that you create. When you run the script block, pass a value to the parameter as follows:

PS C:\> &$procbloc svc*

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName

------- ------ ----- ----- ----- ------ -- -----------

481 36 9048 11984 60 1.00 348 svchost

301 12 2124 7512 36 1.17 600 svchost

295 14 2572 5656 27 1.89 636 svchost

392 15 18992 21248 56 3.02 728 svchost

1289 43 19312 33964 129 41.09 764 svchost

420 24 5768 11488 98 1.20 788 svchost

712 45 19932 24076 1394 10.41 924 svchost

45 4 508 2340 13 0.02 1248 svchost

213 18 10076 9104 1375 0.13 1296 svchost

71 6 804 3560 28 0.00 1728 svchost

This passed the value svc* into the parameter $name, which you then pass to the -Name parameter of Get-Process. You can see that script blocks are flexible; you’ll see a lot more of them, and more of what they can do, as you read about other topics in this book.

16.7. Summary

Variables are one of the core elements of PowerShell that you’ll find yourself using all of the time. They’re easy to work with, although some of the specific details and behaviors that we covered in this chapter represent some of the biggest “gotchas” that newcomers stumble into when they first start using the shell. Hopefully, by knowing a bit more about them, you’ll avoid those pitfalls and be able to make better use of variables.