Advanced PowerShell syntax - PowerShell management - PowerShell in Depth, Second Edition (2015)

PowerShell in Depth, Second Edition (2015)

Part 2. PowerShell management

Chapter 18. Advanced PowerShell syntax

This chapter covers

· Splatting

· Defining default parameter values

· Running external utilities

· Using subexpressions

· Using hash tables as objects

This chapter is a kind of catchall—an opportunity to share some advanced tips and tricks that you’ll see other folks using. Almost everything in this chapter can be accomplished in one or more other ways (and we’ll be sure to show you those as well), but it’s nice to know these shorter, more concise PowerShell expert techniques. These techniques save you time by enabling you to complete your tasks quicker and more easily.

18.1. Splatting

“Splatting” sounds like something a newborn baby does, right? In reality, it’s a way of wrapping up several parameters for a command and passing them to the command all at once.

For example, let’s say you want to run the following command:

Get-WmiObject -Class Win32_LogicalDisk -ComputerName SERVER2

-Filter "DriveType=3" -Credential $cred

Notice that in this command, you pass a variable, $cred, to the –Credential parameter (for this example, assume that you’ve already put a valid credential into $cred). Now, if you were doing this from the command line, splatting wouldn’t save you any time. In a script, stringing all of those parameters together can make things a little hard to read. One advantage of splatting is making that command a little prettier:

$params = @{class='Win32_LogicalDisk'

computername='SERVER2'

Filter="DriveType=3"

Credential=$cred

}

Get-WmiObject @params

This code creates a variable, $params, and loads it with a hash table. In the hash table, you create a key for each parameter name and assign the desired value to each key. To run the command, you don’t have to type individual parameters; instead, use the splat operator (the @ sign—being a splat operator is one of its many duties) and the name of your variable. Note that you shouldn’t add the dollar sign to the variable in this instance, which is a common mistake.

Note

We’re in the habit of using single quotation marks around strings in most cases, but notice that the –Filter parameter value was enclosed in double quotation marks. That’s because in WMI, the filter criteria will often contain single quotes. By wrapping it in double quotes, you can include single quotes within it without any problems. When it comes to that particular parameter value, it’s best to use double quotes—even when using single quotes would work fine.

There’s no reason whatsoever that the hash table has to be so nicely formatted. It’s obviously easier to read when it is nicely formatted, but PowerShell doesn’t care. This code is also perfectly valid:

$params = @{class='Win32_LogicalDisk';computername='SERVER2';

filter="DriveType=3";credential=$cred}

Get-WmiObject @params

When you’re working interactively with PowerShell you can use positional parameters. Okay, technically you can use them in scripts as well, but we discourage that practice because it makes the scripts harder to read and maintain. Positional parameters take the values you pass to the cmdlet and assume that you want to apply them to the parameters that are positional in nature.

Get-WmiObject has two positional parameters: Class and Property. You’d do this if using the full parameter names:

Get-WmiObject -Class Win32_LogicalDisk -Property Size,FreeSpace

If you wanted to make use of PowerShell’s ability to work with positional parameters, the code would become:

Get-WmiObject Win32_LogicalDisk Size,FreeSpace

Win32_LogicalDisk is automatically assigned to the -Class parameter and Size, FreeSpace to the –Property parameter. If you pass the data to the cmdlet in the wrong order, you’ll get an error:

PS C:\> Get-WmiObject Size,FreeSpace Win32_LogicalDisk

Get-WmiObject : A positional parameter cannot be found

that accepts argument 'System.Object[]'.

At line:1 char:1

+ Get-WmiObject Size, FreeSpace Win32_LogicalDisk

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

+ CategoryInfo : InvalidArgument: (:)

[Get-WmiObject], ParameterBindingException

+ FullyQualifiedErrorId : PositionalParameterNotFound,

Microsoft.PowerShell.Commands.GetWmiObjectCommand

You can use an array of values to splat against positional parameters:

PS C:\> $params = 'Win32_LogicalDisk', @('Size','FreeSpace')

PS C:\> Get-WmiObject @params

You have to define the multiple values for –Property as an array; otherwise, you’ll get an error:

PS C:\> $params = 'Win32_LogicalDisk','Size','FreeSpace'

PS C:\> Get-WmiObject @params

Get-WmiObject : A positional parameter cannot be found

that accepts argument 'FreeSpace'.

At line:1 char:1

+ Get-WmiObject @params

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

+ CategoryInfo : InvalidArgument: (:)

[Get-WmiObject], ParameterBindingException

+ FullyQualifiedErrorId : PositionalParameterNotFound,

Microsoft.PowerShell.Commands.GetWmiObjectCommand

Remember that an array is only for positional parameters, so properties of Get-WmiObject such as –Filter or –Credential can’t have values passed. You can use other parameters, as you saw with hash table splatting earlier:

PS C:\> $params = 'Win32_LogicalDisk', @('Size','FreeSpace')

PS C:\> Get-WmiObject @params -ComputerName server02

We recommend that you don’t use positional parameters in scripts, but they’re useful when you’re working interactively. Splatting’s sole purpose in life isn’t necessarily to make your scripts easier—but that’s one thing you can use it for. Another use is to minimize typing. For example, let’s say that you wanted to run that same command against a number of computers, one at a time, all from the command line (not from within a script). You’re going to be retyping the same parameters over and over, so why not bundle them into a hash table for splatting?

$params = @{class='Win32_LogicalDisk'

Filter="DriveType=3"

Credential=$cred}

Get-WmiObject @params –ComputerName SERVER1

Get-WmiObject @params —ComputerName SERVER2

Get-WmiObject @params —ComputerName SERVER3

As you can see, it’s legal to mix splatted and regular parameters, so you can bundle up a bunch of parameters that you plan to reuse into a hash table and splat them along with manually typed parameters to get whatever effect you’re after. You can take this approach a step further if you remember that the –ComputerName parameter can accept multiple machine names:

$computers = "Server02", "Win7", "WebR201"

$params = @{class='Win32_LogicalDisk'

filter="DriveType=3"

credential=$cred}

Get-WmiObject @params -ComputerName $computers

Run Get-WmiObject three times, once for each machine using the same class, filter, and credential values that were splatted. And that’s a good lead-in to defining default values!

18.2. Defining default parameter values

When cmdlet authors create a new cmdlet, they often define default values for some of the cmdlet’s parameters. For example, when running Dir, you don’t have to provide the –Path parameter because the cmdlet internally defaults to “the current path.” Before PowerShell v3, the only way to override those internal defaults was to manually provide the parameter when running the command.

PowerShell v3 introduced a new technique that lets you define default values for one or more parameters of a specific command, which creates a kind of hierarchy of parameter values:

· If you manually provide a parameter and value when running the command, then whatever information you provide takes precedence.

· If you don’t manually provide a value but you’ve defined a default value in the current shell session, then that default value kicks in.

· If you haven’t manually specified a parameter or defined a default value in the current session, then any internal defaults created by the command’s author will take effect.

As with splatting, you don’t have to define default values for every parameter. You can define defaults for the parameters that you want and then continue to provide other parameters manually when you run the command. And as stated in the previous list, you can override your own defaults at any time by manually specifying them when you run a command. One cool trick is specifying a default –Credential parameter so that it’ll kick in every time you run a command and allow you to avoid having to retype it every single time. Keep in mind that such a definition is active only for the current shell session.

Default parameter values are stored in the $PSDefaultParameterValues variable using a hash table. This variable is empty until you add something to it. The variable is also scope- and session-specific. You could define the default value in your PowerShell profile script if you wanted it to take effect every time you opened a new shell window. The hash table key is the cmdlet and parameter name separated by a colon. The value is whatever you want to use for the default parameter value. You can define a script block, which will be evaluated to produce the default value.

Let’s say that you’ve defined a credential object for WMI connections. Create the default parameter:

PS C:\> $PSDefaultParameterValues=@{"Get-WmiObject:credential"=$cred}

Now when you run a Get-WmiObject command, this default parameter will automatically be used:

PS C:\> get-wmiobject win32_operatingsystem -comp coredc01

SystemDirectory : C:\Windows\system32

Organization : MyCompany

BuildNumber : 7600

RegisteredUser : Administrator

SerialNumber : 00477-001-0000421-84776

Version : 6.1.7600

But you have to be careful. This default value will apply to all uses of Get-WmiObject, which means that local queries will fail because you can’t use alternate credentials:

PS C:\> get-wmiobject win32_operatingsystem

Get-WmiObject : User credentials cannot be used for local connections

At line:1 char:1

+ gwmi win32_operatingsystem

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

+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject],

ManagementException

+ FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell

.Commands.GetWmiObjectCommand

Perhaps a more likely scenario is a hash table, like this:

$PSDefaultParameterValues=@{"Get-WmiObject:class"="Win32_OperatingSystem";

"Get-WmiObject:enableAllPrivileges"=$True;

"Get-WmiObject:Authentication"="PacketPrivacy"}

Now whenever you run Get-WmiObject, unless you specify otherwise, the default parameter values are automatically included in the command:

PS C:\> Get-WmiObject -comp coredc01

SystemDirectory : C:\Windows\system32

Organization : MyCompany

BuildNumber : 7600

RegisteredUser : Administrator

SerialNumber : 00477-001-0000421-84776

Version : 6.1.7600

PS C:\> Get-WmiObject -comp coredc01 -Class Win32_LogicalDisk `

>> -Filter "Drivetype=3"

>>

DeviceID : C:

DriveType : 3

ProviderName :

FreeSpace : 5384888320

Size : 12777943040

VolumeName :

The $PSDefaultParameterValues variable exists for as long as your PowerShell session is running. You can check it at any time:

PS C:\> $PSDefaultParameterValues

Name Value

---- -----

Get-WmiObject:class Win32_OperatingSystem

Get-WmiObject:Authentication PacketPrivacy

Get-WmiObject:enableAllPriv... True

You can add definitions:

PS C:\> $PSDefaultParameterValues.Add("Get-ChildItem:Force",$True)

PS C:\> $PSDefaultParameterValues

Name Value

---- -----

Get-WmiObject:class Win32_OperatingSystem

Get-WmiObject:Authentication PacketPrivacy

Get-WmiObject:enableAllPriv... True

Get-ChildItem:Force True

A new default parameter has been added for Get-ChildItem that sets the –Force parameter to True, which will now display all hidden and system files by default:

PS C:\> dir

Directory: C:\

Mode LastWriteTime Length Name

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

d--hs 2/16/2013 3:54 PM $Recycle.Bin

d--hs 7/14/2009 1:08 AM Documents and Settings

d---- 12/7/2013 2:02 PM Help

d---- 7/13/2009 11:20 PM PerfLogs

d-r-- 3/22/2013 10:08 PM Program Files

d-r-- 12/2/2013 3:19 PM Program Files (x86)

d--h- 12/2/2013 3:07 PM ProgramData

d--hs 8/21/2009 1:08 PM Recovery

d---- 1/12/2012 10:42 AM scripts

d--hs 12/1/2013 8:31 PM System Volume Information

d---- 3/24/2013 12:03 PM Temp

d---- 12/12/2013 2:22 PM test

d-r-- 12/2/2013 9:09 AM Users

d---- 12/29/2013 3:31 PM Windows

-a--- 12/12/2013 8:51 AM 3688 myprocs.csv

-a--- 12/2/2013 6:00 AM 4168 temp.txt

Notice that this command works even though it contained an alias, DIR.

Here’s another useful example. The Format-Wide cmdlet displays output in columns, usually based on the object’s name or some other key value. This setup usually results in two columns. The cmdlet has a –Columns parameter, so let’s give it a default value of 3. Add it to the existing variable:

PS C:\> $PSDefaultParameterValues.Add("Format-Wide:Column",3)

Running the command will automatically use the default parameter value:

PS C:\> dir | fw

Directory: C:\

[$Recycle.Bin] [Documents and Settings] [Help]

[PerfLogs] [Program Files] [Program Files (x86)]

[ProgramData] [Recovery] [scripts]

[System Volume Informat... [Temp] [test]

[Users] [Windows] myprocs.csv

temp.txt

If you want to modify this value, you can do so as you would for any other hash table value. The trick is including quotes around the key name because of the colon. Here’s what you have now:

PS C:\> $PSDefaultParameterValues."Format-Wide:Column"

3

Let’s assign a new value and test it out:

PS C:\> $PSDefaultParameterValues."Format-Wide:Column"=4

PS C:\> Get-Process | where {$_.ws -gt 10mb} | Format-Wide

explorer powershell powershell_ise svchost

svchost

Don’t forget that you can specify a different value to override the default preference:

PS C:\> Get-Process | where {$_.ws -gt 10mb} | Format-Wide -col 5

explorer powershell powershell_ise svchost svchost

Removing a default value is just as easy:

PS> $PSDefaultParameterValues.Remove("Format-Wide:Column")

PS> $PSDefaultParameterValues

Name Value

---- -----

Get-WmiObject:enableAllPriv... True

Get-WmiObject:class Win32_OperatingSystem

Get-WmiObject:Authentication PacketPrivacy

Get-ChildItem:Force True

One thing to be aware of is that the $PSDefaultParameterValues variable contents can be overwritten with a command such as the following:

PS> $PSDefaultParameterValues=@{

>> "Format-Wide:Column"=4

>> "Get-WmiObject:Filter"="DriveType=3"

>> }

>>

PS> $PSDefaultParameterValues

Name Value

---- -----

Format-Wide:Column 4

Get-WmiObject:Filter DriveType=3

Be sure to use the Add and Remove methods to modify your default values; otherwise, your results may not be quite what you expected. $PSDefaultParameterValues is a great feature, but beware: It’s easy to get in the habit of assuming that certain parameters will always be set. If you begin writing scripts with those same assumptions, you must include or define the $PSDefaultParameterValues variable; if you don’t, your script might fail or produce incomplete results.

18.3. Running external utilities

As you work with PowerShell, you’ll doubtless run into situations in which you need to accomplish something that you know can be done with an old-fashioned command-line utility but that might not be directly possible using a native PowerShell cmdlet. Mapping a network drive is a good example: The old NET USE command can do it, but there’s nothing immediately obvious in PowerShell v2.

Note

The –Persist parameter introduced in PowerShell v3 enables you to map persistent network drives. As with any mapping of drives, just because you can doesn’t mean you should.

That’s fine—use the old-style command! In many cases, Microsoft has assigned an extremely low priority to creating PowerShell cmdlets when there’s an existing method that already works and can be used from within PowerShell, and for the most part, PowerShell is good at running external or legacy commands.

Under the hood, PowerShell opens an instance of Cmd.exe, passes it the command you’ve entered, lets the command run there, and then captures the result as text. Each line of text is placed into PowerShell’s pipeline as a String object. If necessary, you could pipe those String objects to another cmdlet, such as Select-String, to parse the strings or take advantage of other PowerShell scripting techniques.

Ideally, you should take results from external tools and turn them into objects that you can pass on to other cmdlets in the PowerShell pipeline. Several techniques might work, depending on the command you’re running.

First, see whether the command produces CSV text. For example, the Driverquery.exe command-line tool has a parameter that formats the output as CSV, which is great because PowerShell happens to have a cmdlet that will convert CSV input to objects:

PS C:\> $d=driverquery /fo csv | ConvertFrom-Csv

PS C:\> $d

Module Name Display Name Driver Type Link Date

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

1394ohci 1394 OHCI Compli... Kernel 11/20/2010 5:44:...

ACPI Microsoft ACPI D... Kernel 11/20/2010 4:19:...

AcpiPmi ACPI Power Meter... Kernel 11/20/2010 4:30:...

adp94xx adp94xx Kernel 12/5/2008 6:54:4...

...

Because you have objects written to the pipeline, you can use other PowerShell cmdlets:

PS C:\> $d | Where {$_."Driver Type" -notmatch "Kernel"} |

sort @{expression={$_."Link date" -as [datetime]}} -desc |

Select -first 5 -prop "Display Name","Driver Type","Link Date"

Display Name Driver Type Link Date

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

VirtualBox Shared Folders File System 12/19/2011 7:53:53 AM

SMB 1.x MiniRedirector File System 7/8/2011 10:46:28 PM

Server SMB 1.xxx Driver File System 4/28/2011 11:06:06 PM

Server SMB 2.xxx Driver File System 4/28/2011 11:05:46 PM

srvnet File System 4/28/2011 11:05:35 PM

You should watch out for a few things. First, you might end up with property names that have spaces, which is why you must enclose them in quotes:

Where {$_."Driver Type" -notmatch "Kernel"}

Sometimes the values have extra spaces, in which case using an operator such as -eq might not work—hence the –NotMatch regular expression operator. Finally, everything is treated as a string, so if you want to use a particular value as a particular type, you may need to use a hash table, as shown earlier, to sort on the Link Date property:

sort @{expression={$_."Link date" -as [datetime]}} -desc

Otherwise, the property would’ve been sorted as a string, which wouldn’t produce the correct results.

You’ll often need to parse output using a combination of PowerShell techniques such as the Split operator and regular expressions. Here’s how to take the results of the nbtstat command and turn them into PowerShell objects. Here’s the original command:

PS C:\> nbtstat –n

Local Area Connection 2:

Node IpAddress: [172.16.10.129] Scope Id: []

NetBIOS Local Name Table

Name Type Status

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

CLIENT2 <00> UNIQUE Registered

MYCOMPANY <00> GROUP Registered

CLIENT2 <20> UNIQUE Registered

MYCOMPANY <1E> GROUP Registered

MYCOMPANY <1D> UNIQUE Registered

..__MSBROWSE__. <01> GROUP Registered

You might want to turn the name table results into objects but also to ignore the MSBROWSE entry. The first step is to parse out all the irrelevant lines:

$data=nbtstat /n | Select-String "<" | where {$_ -notmatch "__MSBROWSE__"}

This command should leave only the lines that have a <> in the text. Next, take each line and clean it up:

$lines=$data | foreach {$_.Line.Trim()}

When writing this example, we took the extra step of trimming empty spaces from the beginning and end of each line. Now it’s time to split each line into an array using whitespace as the delimiter. One approach is to use a regular expression pattern to indicate one or more spaces. After each line is turned into an array, you can define a “property” name for each array element in a hash table:

$lines | foreach {

$temp=$_ -split "\s+"

$phash=@{

Name=$temp[0]

NbtCode=$temp[1]

Type=$temp[2]

Status=$temp[3]

}

As each line is written to the pipeline, all that remains is to write a new object to the pipeline:

New-Object -TypeName PSObject -Property $phash

Alternatively, you can use a hash table as an object. We’ll discuss this topic a bit more later in the chapter. The following listing shows all of this code wrapped into a simple function.

Listing 18.1. Get-NBTName.ps1

#requires -version 3.0

Function Get-NBTName {

$data=nbtstat /n | Select-String "<" | where {$_ -notmatch "__MSBROWSE__"}

#trim each line

$lines=$data | foreach { $_.Line.Trim()}

#split each line at the space into an array and add

#each element to a hash table

$lines | foreach {

$temp=$_ -split "\s+"

#create an object from the hash table

[PSCustomObject]@{

Name=$temp[0]

NbtCode=$temp[1]

Type=$temp[2]

Status=$temp[3]

}

}

} #end function

Here’s the result in a PowerShell expression:

PS C:\> Get-NBTName | sort type | Format-Table –Autosize

Name NbtCode Type Status

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

MYCOMPANY <1E> GROUP Registered

MYCOMPANY <00> GROUP Registered

MYCOMPANY <1D> UNIQUE Registered

CLIENT2 <00> UNIQUE Registered

CLIENT2 <20> UNIQUE Registered

The final technique to be demonstrated here is how to handle output that’s grouped. For example, you might run a command such as the following:

PS C:\> whoami /groups /fo list

GROUP INFORMATION

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

Group Name: Everyone

Type: Well-known group

SID: S-1-1-0

Attributes: Mandatory group, Enabled by default, Enabled group

Group Name: BUILTIN\Users

Type: Alias

SID: S-1-5-32-545

Attributes: Mandatory group, Enabled by default, Enabled group

...

To convert this command into PowerShell, turn each group of four lines into an object with properties of GroupName, Type, SID, and Attributes. We recommend using property names without spaces. The first step is to save only the text that you want to work with, so skip the first few lines and strip out any empty lines:

whoami /groups /fo list | Select -Skip 4 | Where {$_}

Next, take what’s left and pipe it to the ForEach-Object cmdlet. Use a Begin script block to initialize a few variables:

foreach-object -Begin {$i=0; $hash=@{}}

In the Process script block, keep track of the number of lines that have been processed. When $i is equal to 4, you can write a new object to the pipeline and reset the counter:

-Process {

if ($i -ge 4) {

#turn the hash table into an object

[PSCustomObject]$hash

$hash.Clear()

$i=0

}

If the counter is less than 4, split each line into an array using the colon as the delimiter. The first element of the array is added as the key to the hash table, replacing any spaces with nothing. The second array element is added as the value, trimmed of extra leading or trailing spaces:

$data=$_ -split ":"

$hash.Add($data[0].Replace(" ",""),$data[1].Trim())

$i++

This process repeats until $i equals 4, at which point a new object is written to the pipeline. The next listing provides the finished script.

Listing 18.2. Get-WhoamiGroups.ps1

#Requires -version 3.0

whoami /groups /fo list | Select -Skip 4 | Where {$_} |

foreach-object -Begin {$i=0; $hash=@{}} -Process {

if ($i -ge 4) {

#turn the hash table into an object

[PSCustomObject]$hash

$hash.Clear()

$i=0

}

else {

$data=$_ -split ":"

$hash.Add($data[0].Replace(" ",""),$data[1].Trim())

$i++

}

}

Here’s a sample of the final result in a PowerShell expression:

PS C:\> S:\Get-WhoamiGroups.ps1 | where {$_.type -eq "Group"} |

>> Select GroupName | sort GroupName

GroupName

---------

MYCOMPANY\AlphaGroup

MYCOMPANY\Denied RODC Password Replication Group

MYCOMPANY\Domain Admins

MYCOMPANY\Exchange Organization Administrators

MYCOMPANY\Exchange Public Folder Administrators

MYCOMPANY\Exchange Recipient Administrators

MYCOMPANY\Group Policy Creator Owners

MYCOMPANY\LocalAdmins

MYCOMPANY\SalesUsers

MYCOMPANY\Schema Admins

MYCOMPANY\SCOM Ops Manager Admins

MYCOMPANY\Test Rollup

These examples are by no means the only way you could accomplish these tasks, but they demonstrate some of the techniques you might use.

We hope you caught our little caveat at the beginning of this section: “For the most part, PowerShell is good at running external commands.” Sometimes it isn’t so good—usually when the external command has its own complicated set of command-line parameters. In such cases, PowerShell sometimes hiccups and gets confused about what it’s supposed to be passing to Cmd.exe and what it’s supposed to be handling itself.

Tip

PowerShell v3 introduced a new command-line parser feature. You can add the --% sequence anywhere in the command line, and PowerShell won’t try to parse the remainder of that line. But be aware that it’s not infallible, which is why we’re showing you this approach.

The result is usually a screenful of error messages. There are some tricks you can use, though, to see what PowerShell is trying to do under the hood and to help it do the right thing.

At one time or another, you’ve probably done something like this:

PS C:\> ping 127.0.0.1 -n 1

Pinging 127.0.0.1 with 32 bytes of data:

Reply from 127.0.0.1: bytes=32 time<1ms TTL=128

What if you want to use some variables with that?

PS C:\> $pn = "-n 1"

PS C:\> $addr = "127.0.0.1"

PS C:\> ping $addr $pn

Value must be supplied for option -n 1.

You can’t combine the variables, either:

PS C:\> ping "$addr $pn"

Ping request could not find host 127.0.0.1 -n 1. Please check the name and try again.

Though awkward, this approach will work:

PS C:\> ping $addr

As will this approach:

PS C:\> cmd /c "ping $addr $pn"

You need to investigate what’s happening with the arguments being passed to ping. PowerShell has a tokenizer (it reads the command you type, or run in your script, and works out what to do with them). You can feed this problem child to the tokenizer and see what it tells you. There’s a lot of output, so it’s been truncated here using Format-Table to control the display:

PS C:\> [management.automation.psparser]::Tokenize("ping $addr $pn",

[ref]$null) | ft Content, Type –a

Content Type

------- ----

ping Command

127.0.0.1 CommandArgument

-n CommandParameter

1 Number

Four arguments are returned. First is ping itself, then the IP address, and finally the -n parameter and its argument. This output implies that PowerShell isn’t interpreting the argument (1) for the parameter (-n) correctly; this result agrees with the error messages. So how can you get this approach to work?

You need to be able to pass the variables to ping and have the whole string run as a single expression. One way to achieve this is to use the Invoke-Expression cmdlet as follows:

PS C:\> Invoke-Expression "ping $addr $pn"

Pinging 127.0.0.1 with 32 bytes of data:

Reply from 127.0.0.1: bytes=32 time<1ms TTL=128

Ping statistics for 127.0.0.1:

Packets: Sent = 1, Received = 1, Lost = 0 (0% loss),

Approximate round trip times in milli-seconds:

Minimum = 0ms, Maximum = 0ms, Average = 0ms

18.4. Expressions in quotes: $($cool)

This example is a handy little bit of syntax that you’ll see all over people’s blogs, in books, and so on—but if they don’t explain what it’s doing, it can be downright confusing.

One thing not covered elsewhere in this book is the trick you can do with variables that are placed inside double quotes. Check out this example:

PS C:\> $a = 'World'

PS C:\> $b1 = 'Hello, $a'

PS C:\> $b2 = "Hello, $a"

PS C:\> $b1

Hello, $a

PS C:\> $b2

Hello, World

As you can see, inside double quotes PowerShell scans for the $ symbol (the dollar sign). PowerShell assumes that what follows the dollar sign is a variable name and replaces the variable with its contents. So there’s rarely a need to concatenate strings—you can stick variables directly inside double quotes.

Here’s a more practical example. Let’s keep it simple by loading numeric values into variables and then performing some math on them so that you can display the result in a human-readable phrase:

$freespace = 560

$size = 1000

$freepct = 100 - (($freespace / $size) * 100)

Write-Host "There is $freepct% free space"

There’s nothing wrong with this approach, except perhaps that a third variable was created to hold the result. That’s a variable PowerShell now has to keep track of, set aside memory for, and so on. It’s no big deal, but it isn’t strictly necessary, either. Please note the percent sign used here: When PowerShell is doing its variable-replacement trick inside double quotes, it looks for the dollar sign. It then scans until it finds a character that isn’t legal inside a variable name, such as whitespace or—in this case—the percent sign. So, in this example, PowerShell knows that$freepct is the variable name. That means you couldn’t stick the math formula into the double quotes—you wouldn’t get the intended output at all. What you can do, though, is this:

$freespace = 560

$size = 1000

Write-Host "There is $(100 - (($freespace / $size) * 100))% free space"

Here, the entire mathematical expression is placed inside a $() construct, shown in boldface here for emphasis. PowerShell knows that when it sees a dollar sign immediately followed by an open parenthesis it isn’t looking at a variable name but instead at a complete expression. Inside those parentheses, you can put almost anything that PowerShell can execute. It’ll evaluate the contents of the parentheses and replace the entire $() expression with its results. You avoid creating a new variable, and you still get the intended result.

You’ll most often see this approach used with object properties. For example, suppose you have a variable $service that contains a single service. If you want to try this out on your own, run the following command:

$service = Get-Service | Select -first 1

This command won’t work:

$service = Get-Service | Select -first 1

Write-Host "Service name is $service.name" –ForegroundColor Green

A period isn’t a valid character in a variable name, so PowerShell will treat $service as a variable and treat .name as a literal string. The result will be something like “Service name is System.ServiceProcess.ServiceController.name,” which isn’t what was intended. But using the $() trick works:

$service = Get-Service | Select -first 1

Write-Host "Service name is $($service.name)" –foregroundcolor Green

In this code, the expression $service.name is enclosed in the special $() construct so that PowerShell will evaluate the entire expression and replace it with the result. You should see something like “Service name is ADWS” or whatever the name of the first service on your system happens to be. These subexpressions are handy when you’re working in the console, where the emphasis is on efficiency and brevity.

18.5. Parentheticals as objects

This trick relies on the same basic premise as the previous one: When PowerShell sees a parenthetical expression, it executes the contents and then effectively replaces the entire expression with its results. This is an incredibly useful behavior, but you may see folks using it in ways that are, at first, a bit confusing to read. For example, can you make sense of this example?

(Get-Process -name conhost | Select -first 1).id

The result, on our system, is 1132—the ID of the first process named conhost. As in algebra, you start by executing the parentheses first. So, inside parentheses is this command:

Get-Process -name conhost | Select -first 1

Running that command entirely on its own, you can see that it returns a single process at most. That process has properties such as Name, ID, and so on. In your head, you should read the entire parenthetical expression as representing a single process. To avoid displaying the entire process, follow the process with a period, which tells PowerShell that you want to access a piece of the object (remember: in math a period comes before the fractional portion of a number, so you can think of the period as coming before a portion of the object). Follow the period with the piece of the object you want, which in this example is its ID property, so that’s what was displayed.

A longer form of this same command might look like this:

$proc = Get-Process -name conhost | Select -first 1

$proc.id

That form has exactly the same effect, although an intermediate variable was created to hold the original result. Using the parenthetical expression eliminates the need for the middleman variable that’s helpful at the command line, but in a script there’s no penalty for taking the extra step to create the variable. It’ll make your code easier to read, debug, and maintain.

18.6. Increasing the format enumeration limit

Here’s something you may have come across and been a little frustrated by. Consider a command like this:

PS C:\> Get-Module Microsoft.PowerShell.Utility

ModuleType Name ExportedCommands

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

Manifest Microsoft.PowerShell.Utility {Add-Member, Add-Type, Cl...

The braces under ExportedCommands indicate a collection. You might try this command next:

PS C:\> Get-Module Microsoft.PowerShell.Utility | select ExportedCommands

ExportedCommands

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

{[Add-Member, Add-Member], [Add-Type, Add-Type], [Clear-Variable, Clear-...

One way around this issue to see more entries is to use one of our earlier parentheticals as objects tips in section 18.5:

PS C:\> (Get-Module Microsoft.PowerShell.Utility).ExportedCommands

Alternatively, you could try

Get-Module Microsoft.PowerShell.Utility | select -ExpandProperty

ExportedCommands

Sure, these commands work if that’s all you want to see. Here’s one more variation on the problem:

PS C:\> Get-Module Microsoft.PowerShell.Utility | Select Name,

ExportedCommands | Format-List

Name : Microsoft.PowerShell.Utility

ExportedCommands : {[Add-Member, Add-Member], [Add-Type, Add-Type],

[Clear-Variable, Clear-Variable], [Compare-Object,

Compare-Object]...}

Clearly, there are more than four commands. Wouldn’t you like to see a bit more? You need to modify the $FormatEnumerationLimit variable, which has a default value of 4, to accomplish this:

PS C:\> $FormatEnumerationLimit

4

PS C:\> $FormatEnumerationLimit=8

See what happens:

PS C:\> get-module Microsoft.PowerShell.Utility | Select

Name,ExportedCommands | format-list

Name : Microsoft.PowerShell.Utility

ExportedCommands : {[Add-Member, Add-Member], [Add-Type, Add-Type],

[Clear-Variable, Clear-Variable], [Compare-Object,

Compare-Object], [ConvertFrom-Csv, ConvertFrom-Csv],

[ConvertFrom-Json, ConvertFrom-Json],

[ConvertFrom-StringData, ConvertFrom-StringData],

[ConvertTo-Csv, ConvertTo-Csv]...}

Now you’ll get more enumerated items. If you set the variable to a value of –1, PowerShell will return all enumerated values. If you want to take advantage of this, add a line in your PowerShell profile to modify this variable. If you do modify this variable in your profile, be aware that it’ll apply to all format enumerations. Some of these can get quite large and could swamp your display, making it difficult to pick out other information.

18.7. Hash tables as objects

A feature introduced in PowerShell v3 gives you the ability to turn hash tables into objects. In v2, you could use a hash table of property values with the New-Object cmdlet:

$obj=New-Object psobject -Property @{

Name="PowerShell"

Computername=$env:computername

Memory=(Get-WmiObject Win32_OperatingSystem).TotalVisibleMemorySize/1kb

}

In PowerShell v3 and later, you can create an object of a known type:

PS C:\>[Microsoft.Management.Infrastructure.Options.

DComSessionOptions]$co= @{PacketPrivacy=$True;

PacketIntegrity=$True;Impersonation=

"Impersonate"}

PS C:\> $co

PacketPrivacy : True

PacketIntegrity : True

Impersonation : Impersonate

Timeout : 00:00:00

Culture : en-US

UICulture : en-US

You can use this object in an expression as follows:

PS C:\scripts> $cs=New-CIMsession coredc01 -SessionOption $co

The type you use must have a default Null constructor (no arguments are needed, or the arguments have default values). In this example, you could as easily have created the $co variable using the New-CIMSessionOption cmdlet, so perhaps this example doesn’t help that much.

You can also use this technique to create your own custom objects:

$obj=[PSCustomObject]@{

Name="PowerShell"

Computername=$env:computername

Memory=(Get-WmiObject Win32_OperatingSystem).TotalVisibleMemorySize/1kb

}

Looking at this in the shell yields a typical object:

PS C:\> $obj

Name Computername Memory

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

PowerShell CLIENT2 1023.5546875

This is the same result you’d get with New-Object and a hash table of property values. But using the hash table as an object does have a few advantages:

· It might save a little bit of typing.

· It’s better with regard to performance.

· Properties are written to the pipeline in the order defined—basically the same as an [ordered] hash table.

As a final example, consider the following code:

[System.Management.ManagementScope]$scope = @{

Path = "\\webr201\root\WebAdministration"

Options = [System.Management.ConnectionOptions]@{

Authentication = [System.Management.AuthenticationLevel]::PacketPrivacy

}

}

[System.Management.ManagementClass]$website = @{

Scope = $scope

Path = [System.Management.ManagementPath]@{

ClassName = "Site"

}

Options = [System.Management.ObjectGetOptions]@{}

}

[System.Management.ManagementClass]$bind = @{

Scope = $scope

Path = [System.Management.ManagementPath]@{

ClassName = "BindingElement"

}

Options = [System.Management.ObjectGetOptions]@{}

}

$BInstance = $bind.CreateInstance()

$Binstance.BindingInformation = "*:80:HTasO.manticore.org"

$BInstance.Protocol = "http"

$website.Create("HTasO", $Binstance, "c:\HTasO", $true)

This example uses the System.Management .NET classes to wrap WMI calls to a web server, \\webr201. The IIS WMI provider requires the Packet Privacy level of DCOM authentication. In PowerShell v2, use .NET code or Remoting to use the IIS provider remotely. This requirement changed in PowerShell v3, as is discussed in chapter 39. For now, you can simplify (yes, this is simplified) the code using hash tables as objects.

The code starts by defining the WMI scope, which includes the namespace and the authentication level; notice that the hash tables are nested. The Site and BindingElement objects are created. The scope information is used in both objects, which is why it’s defined first. The site binding information is set and then the site is created.

Using hash tables as objects does make working directly with .NET classes easier but is also an advanced technique that you should probably wait to use until you’ve gained some experience with PowerShell.

18.8. Summary

Our goal in this chapter was to introduce you to some of the advanced tricks and techniques that you’ll often see people using—and that we expect you’ll want to use yourself, now that you’ve seen them. These tricks are ones that didn’t fit in neatly elsewhere in the book and that tend to confuse newcomers to PowerShell at first. Now they won’t trip you up!