Tag Archives: foreach

A Basic(ish) Active Directory Look-Up Script

It was just Saturday—it’s been a few weeks now, actually—that I wrote a post proclaiming to be back. Back to writing about PowerShell, that is. Why not take the first script I wrote in my new position and share it? It’s possible it has some concepts that might be helpful for readers.

The first thing I want to mention is that I hate writing scripts. Huh!? What I mean by that, is that I prefer to write functions. That didn’t happen my first time out in this position, as you’ll see, and I’m going to be okay with it. A one-off script may have a purpose. I stayed away from a function (and therefore a module [maybe], working with profile scripts [potentially], etc.). I’ll live with it.

Let’s break the script up into sections and explain what’s happening in each. Keep in mind that this isn’t perfect and there are places where I would make changes after having looked over this a couple of times. I’ll be sure to mention those toward the bottom of the post. Okay, let’s do this! I’ll put the script back together into a single, code block further below.

The beginning of the script creates two parameters: Properties and Output. Properties can accept multiple string values,—notice the []—and we’ll see that in the upcoming examples. Output can accept one string value of three predetermined values. Those are Console, GridView, and CsvFile; Console is the default parameter value.

Param (
    [Parameter()]
    [string[]]$Properties,
    [Parameter()]
    [ValidateSet('Console','GridView','CsvFile')]
    [string]$Output = 'Console'
)

Next, we create a path to a flat file called userlist.txt. This file will contain Active Directory Display Names (DisplayName). By using the $PSScriptRoot variable, all we have to do is keep our script and the text file in the same location/folder in order for it to work correctly.

Once the $Path variable is set, we attempt to run a Get-Content command against the values in the file, storing these in the $Userlist variable. If for some reason the file isn’t in place, the script will make use of the catch block of our try-catch to indicate to the user that the file can’t be located.

$Path = "$PSScriptRoot\userlist.txt"
try {
    $Userlist = Get-Content -Path $Path -ErrorAction Stop
} catch {
    Write-Error -Message "Unable to locate $Path."
}

Following that, we set our $TotalProperties variable to three Active Directory user properties we know we want. Then, if any values have been passed in using the Properties parameter, we combine those with the three properties already in the $TotalProperties variable.

$TotalProperties = @('DisplayName','EmployeeNumber','SamAccountName')
if ($Properties) {
    $TotalProperties = $TotalProperties + $Properties
}

Moving forward, we set up foreach language construct in order to loop through each user in the $Userlist variable. Remember, this variable was populated by our Get-Content command earlier. For each loop iteration, we are adding a PSCustomObject to our $Users—plural— variable. Normally, I wouldn’t store each value in a variable and instead just pump it out right there, but the script includes some output options that we’ll see next.

foreach ($User in $Userlist) {
    $Users += [PSCustomObject]@(
    Get-ADUser -Filter "DisplayName -eq '$User'" -Properties $TotalProperties |
        Select-Object -Property $TotalProperties
    )
}

Finally, we consider the output option the user either defaulted to, or didn’t. If they included the Output parameter with GridView, we pipe our $Users variable to OutGridView. If they included the Output parameter with CSVFile, we pipe out $Users variable to Export-Csv, saving them in a CSV file, and then open our saved CSV file. If they didn’t include the Output parameter, or they did with the Console value, then we display the results directly in the console. That’s it.

Switch ($Output) {
    GridView {$Users | Out-GridView -Title Users}
    CsvFile {
        $Users.GetEnumerator() |
        Export-Csv -NoTypeInformation -Path "$PSScriptRoot\$(Get-Date -Format FileDateTime -OutVariable NewFileOutput).csv"
        Invoke-Item -Path "$PSScriptRoot\$NewFileOutput.csv"
    }
    Default {$Users | Format-Table -AutoSize}
}

Although I took the base code from someone’s previously written script, this really is still much of a 1.0.0 version. Knowing that, there are some changes I might make; it’s not perfect, but it’ll get the job done.

  • While it’s not vital, I kind of wish I used a different variable name for $Path
    • It’s a path, sure, but to a specific file
    • Perhaps $FilePath, $UserFile, or $UserFilePath
      • It could’ve been more specific
  • Ensure properties passed in via the Properties parameter are valid for an Active Directory user
    • If someone sends in properties that don’t exist for an Active Directory user, it’s going to cause problems
      • Maybe check a known user, gather all the possible properties, and compare
      • (Or) Maybe wrap some error checking without having to do any property compare operation
  • Don’t use Default in the Switch language construct
    • It’s not necessary, as the Output parameter will only accept three possible values
    • Default could’ve been replaced with Console

Here are a few examples followed by the full PowerShell code in a single, code block.

I’ve redacted information from each of these images. There’s something that’s vital to know about each, however. In front of the full path (all the images but the last one), is the & operator. This is called the invocation or call operator. It informs PowerShell that everything after it should be treated as a command and that it’s not just a long, string value.

This example invokes the script without any parameters, pulling in two users from the userlist.txt file.

This example invokes the script and includes two additional Active Directory properties, which are then also included in the output.

This example does the same as the first one, however, it opens the results using the Out-GridView cmdlet.

This one opens the results using whatever program—Excel in my case—is associated with CSV files. This option is saving the file to disk, so keep that in mind, as it has no cleanup features.

This final example is included to show that it works when you’re inside the same directory as the script and text file. It also includes multiple parameters being included at the same time. You might know you can do it, but my at-work audience may not have—I’m not sure. As we’re in the directory with the script, you can actually see the inclusion of the invocation operator.

And finally, all the code in a single code block.

Param (
    [Parameter()]
    [string[]]$Properties,
    [Parameter()]
    [ValidateSet('Console','GridView','CsvFile')]
    [string]$Output = 'Console'
)

$Path = "$PSScriptRoot\userlist.txt"
try {
    $Userlist = Get-Content -Path $Path -ErrorAction Stop
} catch {
    Write-Error -Message "Unable to locate $Path."
}

$TotalProperties = @('DisplayName','EmployeeNumber','SamAccountName')
if ($Properties) {
    $TotalProperties = $TotalProperties + $Properties
}

foreach ($User in $Userlist) {
    $Users += [PSCustomObject]@(
        Get-ADUser -Filter "DisplayName -eq '$User'" -Properties $TotalProperties |
            Select-Object -Property $TotalProperties
    )
}

Switch ($Output) {
    GridView {$Users | Out-GridView -Title Users}
    CsvFile {
        $Users.GetEnumerator() |
            Export-Csv -NoTypeInformation -Path "$PSScriptRoot\$(Get-Date -Format FileDateTime -OutVariable NewFileOutput).csv"
        Invoke-Item -Path "$PSScriptRoot\$NewFileOutput.csv"
    }
    Default {$Users | Format-Table -AutoSize}
}

Filled-In AD Notes Field


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on May 31, 2019.


There’s this thing that might happen. Even as it’s unlikely, I’ve added a protection I’ll probably never even need. I suppose I’d rather be complete and as confident in my code as possible. I don’t want to be the guy, who wrote the code that didn’t protect against the thing. Instead, I’d rather be the guy that wrote the code, that did the thing, even when the protection, was never thought to be needed. It’s like the floor mat at Sears, in front of the escalator, that I saw as a kid that I’ve never forgotten: “Safety First.” It was red, with white writing. There’s plenty of production code out there we’ve written that we’d write differently now, knowing what we’ve already learned.

Just because the code I was working with won’t (likely) ever fill up an Active Directory Group Notes field, doesn’t mean I shouldn’t be sure it never attempts to go over that 1,024 character limit allowed by the field. Knowing that goal, reasonable or otherwise, I set out to use the Notes field and prevent ever exceeding that character limit.

Let’s say, there’s a set of groups in Active Directory — department groups. While we’ll keep their names generic: DEPT 00001, DEPT 00002, etc., we’ll put their true department name in their AD object’s Notes field. In case you don’t know, the attribute name for this field is Info on the back end, and Notes in the UI.

Now, let’s also say that department names change over time. Therefore, it’s bound to happen that DEPT 00001, for example, will need its newest department name added to the Info property. Now, we want to keep all the previous names in the Info property and add the current one to the others. Between each name, we’ll include a pipe character: “|”. This delimiter will assist in helping us determine where one name ends and the next begins. Do consider that department names can change multiple times. If it happens frequently enough, you can see how we might hit our 1,024 character limit in this property. Therefore, it’s a good idea to add in this protection, in order to avoid throwing an error someday.

I’ll include the code I used below, and then we’ll discuss what’s happening further down.

$GroupInfo = $null
$GroupInfo = (Get-ADGroup -Identity $GroupExist.Name -Property Info).Info
If ($GroupInfo) {
	# Edit Notes/Info field (previously populated).
	$GroupInfoSplit = $GroupInfo -split '\|'
	If (-Not($DeptName -in $GroupInfoSplit)) {
		$GroupInfo += "|$DeptName"

	If ($GroupInfo.Length -gt 1024) {
		# Remove previous groups if over 1024 characters.
		Do {
		$GroupInfo = ($GroupInfo -split '\|',2)[1]
		} Until ($GroupInfo.Length -le 1024) # End Do-Until.
	} # End If.

	Set-ADGroup -Identity $GroupExist -Replace @{Info=$GroupInfo}
	} # End If.
} Else {
	# Add to Notes/Info for the first time.
	If ($DeptName.Length -le 1024) {
	        Set-ADGroup -Identity $GroupExist -Add @{Info=$DeptName}
	} # End If.
} # End If-Else

The first thing to know is that this code is inside of a looping construct — a Foreach loop to be exact. Each time we start a new loop, we bring in a different one of our departments, stored in the $GroupExist variable. On line 3, we use its Name property as a part of a Get-ADGroup command and return only the value inside the Info field. Remember, this is the Notes field in the UI. It’s either going to have some data in it, or it’s not. All previously existing departments will already have something in here. It’s new departments that will not.

Once we have, or we don’t have, this data, we go one of two ways. As stated, every department is going to have its department name added to its Notes field. If we don’t return any info from our Get-ADGroup command, we can rest assured, that there’s nothing in this field already and that we only need to add the department name, without any worry about any previously existing data in this field. This If statement begins on line 4, however, in this situation, we’re taking the Else path on line 19. Once there, we check if the length of the department name, stored in $DeptName is longer than 1,024 characters. That will never happen — seriously — but I added the check just in case. I do want to note here that this $DeptName variable changes within our unseen Foreach loop, during each iteration as well.

If there is already a department name or names, then we follow the If path. Let’s deconstruct that, as well. It’s a little more complex.

If our Info/Notes field is already populated, then we need to make some checks before we potentially add a department’s current department name. The first thing we do with the value we’ve returned, as $GroupInfo, is split it at our delimiter — the pipe character on line 6. If there’s only one department name (no delimiter), it will only return that one department name, and that’ll work out just fine. Next, we compare our department name in $DeptName to the value, or values, returned by our split operation. If the current department name is included in our results, we exit — there’s nothing more to do.

Now, if our current department name is not included in our results, we append it to our $GroupInfo variable. At that point, we’ll have this variable with the newest department name appended to the end of this string, where each previous department name is also included with the pipe character in between each. Following this step, our code checks the length of the value stored in $GroupInfo and will only act if its length is greater than those 1,024 characters.

If it is, we run our variable through a Do-Until language construct. For every iteration through the Do-Until, we split our value at the first pipe character from the left of the string and keep everything to the right. We’ll continue to do this, over and over, until our length is less than 1,024 characters.

And that it’s. We can maintain all of a department’s previous department names and ensure they’ll always fit into our Notes field.

There is a Difference: Arrays Versus Hash Tables

After reading a short post on the PowerShell subreddit recently, I was compelled to write a quick post here about arrays and hash tables. The concepts are similar, but the two are different, meaning that — interruption! There have now been two posts where the OP didn’t know one versus the other. It is basic stuff, sure, but it is important stuff.

Anyway, talking about one when you mean the other isn’t going to be okay any longer — it really never was. While someone mentioned the incorrect usage in the thread, writing, for me, still felt necessary and potentially beneficial.

Instead of writing my own definitions, let’s use Microsoft’s. We’ll begin with an array: “Data structures designed to store collections of items.“ If you think of a variable, back when you learned about those the first time, you probably learn how to store a single value in your variable. Okay good. This time we’re going to store multiple values in a single variable. That is what this data structure provides.

We are beginning with a simple variable that contains a simple, single value. Not much to it.

[PS7.2.0][C:\] $SingleValue = 'One thing'
[PS7.2.0][C:\] $SingleValue
One thing
[PS7.2.0][C:\] $SingleValue.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

Now, let’s work with arrays. We’re still going to use a single variable, we’re just going to load it up with a different data structure that’s going to let us store multiple values in it. We actually don’t have to do much to make this happen, so here we go.

In this example, we’re creating a variable named $Array01. We’re storing the numeric values of 1, 2, and 3 inside the variable as an array.

[PS7.2.0][C:\] $Array01 = 1,2,3
[PS7.2.0][C:\] $Array01
1
2
3
[PS7.2.0][C:\] $Array01.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

Well, that was easy. This next example is the same as above, however, it includes @() around our numeric values. This operator — the array sub-expression operator — takes whatever is inside it and makes it into an array.

[PS7.2.0][C:\] $Array02 = @(4,5,6)
[PS7.2.0][C:\] $Array02
4
5
6
[PS7.2.0][C:\] $Array02.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

The array sub-expression operator will even make an array of nothing.

[PS7.2.0][C:\] $Array03 = @()
[PS7.2.0][C:\] $Array03
[PS7.2.0][C:\] $Array03.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

Here is one you probably do not see often. The comma in front of the 7 will indicate to PowerShell to create an array that contains a single value, which we will prove when we start returning the count of our various arrays.

[PS7.2.0][C:\] $Array04 = ,7
[PS7.2.0][C:\] $Array04
7
[PS7.2.0][C:\] $Array04.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

[PS7.2.0][C:\] $Array04.Count
1
[PS7.2.0][C:\] $Array03.Count
0
[PS7.2.0][C:\] $Array02.Count
3
[PS7.2.0][C:\] $Array01.Count
3

Do you get it? It is a data structure designed to accommodate multiple values. You did not see it here yet, but arrays can hold strings (words or sentences), just as easily as numbers. You can even mix them in the same array and store them in a single variable.

This array, stored in the $Array05 variable contains five different colors.

[PS7.2.0][C:\] $Array05 = 'black','red','green','blue','brown'
[PS7.2.0][C:\] $Array05
black
red
green
blue
brown

Each item in an array can be referenced by an index — a number that represents its location. It is a positional place in line. The first element, or item in the array — black in our case — is in index zero. This is because arrays are always zero-based. This means red would be in index 1, green in index 2, blue in 3, and brown in 4. Zero through four is five, as in five total elements, or values, in our array. Let’s verify.

[PS7.2.0][C:\] $Array05[0]
black
[PS7.2.0][C:\] $Array05[1]
red
[PS7.2.0][C:\] $Array05[2]
green
[PS7.2.0][C:\] $Array05[3]
blue
[PS7.2.0][C:\] $Array05[4]
brown
[PS7.2.0][C:\]

Let’s try some other things! While not often used in my day-to-day, we can perform arithmetic on the indexes. We can even use the range operator (..) and go forward and backward through our elements. What else… Oh, let’s try some negative indexes numbers, too. Those work in reverse from the end of the array to the beginning.

[PS7.2.0][C:\] $Array05[3-3]
black
[PS7.2.0][C:\] $Array05[2+1]
blue
[PS7.2.0][C:\] $Array05[0..4]
black
red
green
blue
brown
[PS7.2.0][C:\] $Array05[4..0]
brown
blue
green
red
black
[PS7.2.0][C:\] $Array05[-1]
brown
[PS7.2.0][C:\] $Array05[-2]
blue
[PS7.2.0][C:\] $Array05[-3]
green
[PS7.2.0][C:\] $Array05[-4]
red
[PS7.2.0][C:\] $Array05[-5]
black

Hash tables have a good number of similarities to arrays, and perhaps that is the reason why there is some confusion. Microsoft even uses the word array to describe them: “A hash table, also known as a dictionary or associative array, is a compact data structure that stores one or more key/value pairs.” I don’t use the words associative array often enough, but I should; I like it.

To the person on Reddit that called hash tables, hashmaps, and just maps: No. I believe that term is a carryover from maybe Java. In my experience, I have not heard it in the PowerShell community.

Anyway, arrays and associative arrays are both data structures. They can both hold multiple values, and they can both be stored in a single variable. The difference is keys and values. Arrays hold single items in each index, while a hash table holds a key-value pair in each.

Before we fill this up, let’s create an empty hash table. Notice the difference between this and the array. If it is used for an array, the operator will always be an instant giveaway: @() is for arrays, and @{} is for hash tables. Notice my liberal use of the GetType() method in this post; it’ll help you make object determinations, so you can always know which you’re working with, without having to view, or see, the data stored in the data structure.

[PS7.2.0][C:\] $Hash01 = @{}
[PS7.2.0][C:\] $Hash01
[PS7.2.0][C:\] $Hash01.Count
0
[PS7.2.0][C:\] $Hash01.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Hashtable                                System.Object

Now, let’s populate a hash table… in VS Code. Somethings I would just rather not demonstrate in the console.

$Hash02 = @{
    Dad = 'David'
    Mom = 'Betty'
    Daughter = 'Janice'
    Son = 'Bryan'
}
$Hash02
Name                           Value
----                           -----
Son                            Bryan
Dad                            David
Mom                            Betty
Daughter                       Janice

This also could have been written in a couple of other ways.

$Hash02 = @{
    Dad = 'David'; Mom = 'Betty'; Daughter = 'Janice'; Son = 'Bryan'
}
$Hash02 = @{
    Dad      = 'David'
    Mom      = 'Betty'
    Daughter = 'Janice'
    Son      = 'Bryan'
}

While hash tables have indexes, they’re non-numeric. Actually, I am not even sure I like the term index when working with hash tables. I suggest we call it what they are: keys. Maybe you noticed, but the order in which we created our hash table was not the order in which it was returned. A numeric value here would not help us anyway. Instead, we’re going to use the keys as our indexes to return our values.

$Hash02['Dad']
David
$Hash02['Daughter']
Janice

There is a good possibility that you are probably going to want to loop through and display all the values in your hash table. It’s a little out of place, but we’ll do that with one of our arrays here, too. Using the GetEnumerator() method allows us to get to the key and value, values out of our hash table. In this first, hash table example we are using the key and associated value in a string. This requires the use of the subexpression operator ($()), so that we can display our values within the string.

foreach ($Person in $Hash02.GetEnumerator()) {
    "The $($Person.Key) is $($Person.Value)."
}

The Son is Bryan
The Dad is David
The Mom is Betty
The Daughter is Janice

We are using the subexpression operator in the string created by looping over our array, too.

$Array05 = 'black','red','green','blue','brown'
foreach ($Color in $Array05) {
	"Is your favorite color $($Color)?"
}
Is your favorite color black?
Is your favorite color red?
Is your favorite color green?
Is your favorite color blue?
Is your favorite color brown?

This has been a lengthy post — who knew this would happen!? There is one last thing I want to mention, and that is about creating an ordered hash table, or dictionary. We tend to use the dictionary term when we order a hash table.

$Hash03 = [ordered]@{
    Monday    = 'Chipotle'
    Tuesday   = 'Pizza'
    Wednesday = 'Mahi Mahi'
    Thursday  = 'Ravioli'
    Friday    = 'Orange Chicken'
}
$Hash03

Name                           Value
----                           -----
Monday                         Chipotle
Tuesday                        Pizza
Wednesday                      Mahi Mahi
Thursday                       Ravioli
Friday                         Orange Chicken

Because this is ordered, we can use numeric indexes again. Keep in mind, however, that doing this is only going to return the values and not the keys, as well. It may even momentarily confuse you into thinking this is an array, not a hash table, or an associative array. Maybe stick to using a foreach at this point. Do not forget the GetType() method to ensure you know your data structures.

$Hash03[0..4]
Chipotle      
Pizza
Mahi Mahi     
Ravioli       
Orange Chicken

$Hash03.GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------     
True     True     OrderedDictionary                        System.Object

Okay, I am stopping here. There’s plenty to know about these two data structures, and at this point, it is more than I’m willing to share. Even so, this should provide a decent breakdown to go along with the Microsoft documentation and other information that’s available to consume on this topic. Know what you are talking about.

Fourth Grade Multiplication Tables

I remember math class… in fourth grade. It was the year we learned multiplication. Day after day, for so many consecutive days, we’d have to write our “times tables.” Zero though 10, over and over again. Good thing, I suppose, as I still know them today. The combination of knowing PowerShell and my daughter learning her “times tables” now, got me thinking: How quickly could I “write” my “times tables” today? It turns out it takes around 5 milliseconds.

If you’ve been reading along recently you may have seen what we’ll do today, done before. We need multiple loops. One loop, our outer loop, will cycle through the numbers 0 through 10. For each of those, we’ll have an inner loop multiply 0 through 10 to the current outer loop number. You know, we’ll do zero, so zero times zero, zero times one, and so on, until we hit 10, at which point we’ll move on from zero to one (one times zero, one times one, one times two, etc.). Let’s begin with the outer loop.

foreach ($outernumber in 0..10) {
	$outernumber
}
0
1
2
3
4
5
6
7
8
9
10

Now, let’s do the inner loop. Again, this is going to repeat zero through 10 inside each inner loop, for each outer loop. If this is hard to follow, be sure to take a look at the soon-to-be upcoming example where we’ll highlight both outer and inner loops.

foreach ($outernumber in 0..10) {
	foreach ($innernumber in 0..10) {
		$innernumber
	}
}
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10

This is the same example as we saw above; however, we’ve included the value of the outer loop in our output. Each outer loop value has two dashes (--) in front of it.

foreach ($outernumber in 0..10) {
	"-- $outernumber"
	foreach ($innernumber in 0..10) {
		$innernumber
	}
}
-- 0
0
1
2
3
4
5
6
7
8
9
10
-- 1
0
1
2
3
4
5
6
7
8
9
10
-- 2
0
1
2
3
4
5
6
7
8
9
10
-- 3
0
1
2
3
4
5
6
7
8
9
10
-- 4
0
1
2
3
4
5
6
7
8
9
10
-- 5
0
1
2
3
4
5
6
7
8
9
10
-- 6
0
1
2
3
4
5
6
7
8
9
10
-- 7
0
1
2
3
4
5
6
7
8
9
10
-- 8
0
1
2
3
4
5
6
7
8
9
10
-- 9
0
1
2
3
4
5
6
7
8
9
10
-- 10
0
1
2
3
4
5
6
7
8
9
10

And since this post is lengthy in long examples, let’s add another before we actually get to the results we came for. Our outer loop number is on the left side of the multiplication symbol, and our inner loop number is on the right side.

foreach ($outernumber in 0..10) {
	foreach ($innernumber in 0..10) {
		"$outernumber x $innernumber ="
	}
}
0 x 0 =
0 x 1 =
0 x 2 =
0 x 3 =
0 x 4 =
0 x 5 =
0 x 6 = 
0 x 7 = 
0 x 8 = 
0 x 9 = 
0 x 10 =
1 x 0 = 
1 x 1 = 
1 x 2 = 
1 x 3 = 
1 x 4 = 
1 x 5 = 
1 x 6 = 
1 x 7 = 
1 x 8 = 
1 x 9 = 
1 x 10 =
2 x 0 = 
2 x 1 = 
2 x 2 = 
2 x 3 = 
2 x 4 = 
2 x 5 = 
2 x 6 = 
2 x 7 = 
2 x 8 = 
2 x 9 = 
2 x 10 =
3 x 0 = 
3 x 1 = 
3 x 2 = 
3 x 3 = 
3 x 4 = 
3 x 5 = 
3 x 6 = 
3 x 7 = 
3 x 8 =
3 x 9 =
3 x 10 =
4 x 0 =
4 x 1 =
4 x 2 =
4 x 3 =
4 x 4 =
4 x 5 =
4 x 6 =
4 x 7 =
4 x 8 =
4 x 9 =
4 x 10 =
5 x 0 =
5 x 1 =
5 x 2 =
5 x 3 =
5 x 4 =
5 x 5 =
5 x 6 =
5 x 7 =
5 x 8 =
5 x 9 =
5 x 10 =
6 x 0 =
6 x 1 =
6 x 2 =
6 x 3 =
6 x 4 =
6 x 5 =
6 x 6 =
6 x 7 =
6 x 8 =
6 x 9 =
6 x 10 =
7 x 0 =
7 x 1 =
7 x 2 =
7 x 3 =
7 x 4 =
7 x 5 =
7 x 6 =
7 x 7 =
7 x 8 =
7 x 9 =
7 x 10 =
8 x 0 =
8 x 1 =
8 x 2 =
8 x 3 =
8 x 4 =
8 x 5 =
8 x 6 =
8 x 7 =
8 x 8 =
8 x 9 =
8 x 10 =
9 x 0 =
9 x 1 =
9 x 2 =
9 x 3 =
9 x 4 =
9 x 5 =
9 x 6 =
9 x 7 =
9 x 8 =
9 x 9 =
9 x 10 =
10 x 0 =
10 x 1 =
10 x 2 =
10 x 3 =
10 x 4 =
10 x 5 =
10 x 6 =
10 x 7 =
10 x 8 =
10 x 9 =
10 x 10 =

In this example, we’re going to add the multiplication problem to our output. Notice that we are making use of the subexpression operator ($()) in order to embed the results of each expression — or multiplication problem — within our string.

foreach ($outernumber in 0..10) {
	foreach ($innernumber in 0..10) {
		"$outernumber x $innernumber = $($outernumber * $innernumber)"
	}
}
0 x 0 = 0
0 x 1 = 0
0 x 2 = 0
0 x 3 = 0
0 x 4 = 0
0 x 5 = 0
0 x 6 = 0
0 x 7 = 0  
0 x 8 = 0  
0 x 9 = 0  
0 x 10 = 0 
1 x 0 = 0  
1 x 1 = 1  
1 x 2 = 2  
1 x 3 = 3  
1 x 4 = 4  
1 x 5 = 5  
1 x 6 = 6  
1 x 7 = 7  
1 x 8 = 8  
1 x 9 = 9  
1 x 10 = 10
2 x 0 = 0  
2 x 1 = 2  
2 x 2 = 4  
2 x 3 = 6  
2 x 4 = 8  
2 x 5 = 10 
2 x 6 = 12 
2 x 7 = 14 
2 x 8 = 16 
2 x 9 = 18 
2 x 10 = 20
3 x 0 = 0 
3 x 1 = 3 
3 x 2 = 6 
3 x 3 = 9 
3 x 4 = 12
3 x 5 = 15
3 x 6 = 18
3 x 7 = 21
3 x 8 = 24
3 x 9 = 27
3 x 10 = 30
4 x 0 = 0
4 x 1 = 4
4 x 2 = 8
4 x 3 = 12
4 x 4 = 16
4 x 5 = 20
4 x 6 = 24
4 x 7 = 28
4 x 8 = 32
4 x 9 = 36
4 x 10 = 40
5 x 0 = 0
5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25
5 x 6 = 30
5 x 7 = 35
5 x 8 = 40
5 x 9 = 45
5 x 10 = 50
6 x 0 = 0
6 x 1 = 6
6 x 2 = 12
6 x 3 = 18
6 x 4 = 24
6 x 5 = 30
6 x 6 = 36
6 x 7 = 42
6 x 8 = 48
6 x 9 = 54
6 x 10 = 60
7 x 0 = 0
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70
8 x 0 = 0
8 x 1 = 8
8 x 2 = 16
8 x 3 = 24
8 x 4 = 32
8 x 5 = 40
8 x 6 = 48
8 x 7 = 56
8 x 8 = 64
8 x 9 = 72
8 x 10 = 80
9 x 0 = 0
9 x 1 = 9
9 x 2 = 18
9 x 3 = 27
9 x 4 = 36
9 x 5 = 45
9 x 6 = 54
9 x 7 = 63
9 x 8 = 72
9 x 9 = 81
9 x 10 = 90
10 x 0 = 0
10 x 1 = 10
10 x 2 = 20
10 x 3 = 30
10 x 4 = 40
10 x 5 = 50
10 x 6 = 60
10 x 7 = 70
10 x 8 = 80
10 x 9 = 90
10 x 10 = 100

That was it. Well, there is one thing. Let’s get back to seeing how long this command takes to run.

Measure-Command -Expression {
	foreach ($outernumber in 0..10) {
		foreach ($innernumber in 0..10) {
			"$outernumber x $innernumber = $($outernumber * $innernumber)"
		}
	}
} | Select-Object -Property Milliseconds

Milliseconds
------------
           5

We could put this entire command in a loop too, and test it across ten iterations. This example includes ticks, as well as milliseconds because when this command is run back-to-back, the speed per iteration drastically increases. Have a look. So it has been said, there are 10,000 ticks per millisecond and 1,000 milliseconds per second.

1..10 | ForEach-Object {
	Measure-Command -Expression {
		foreach ($outernumber in 0..10) {
			foreach ($innernumber in 0..10) {
				"$outernumber x $innernumber = $($outernumber * $innernumber)"
			}
		}
	} | Select-Object -Property Milliseconds,Ticks
}

Milliseconds Ticks
------------ -----
           3 39181
           0  7081
           1 14917
           1 10313
           0  2318
           0  2289
           0  2235
           0  2149
           0  2099
           0  2083

Setting the Rounding Scale

Sometimes sharing one concept in a post, allows you to share another concept in another post. That brings us to today. This is that other post.

Originally, I had a great title for this post: It was Rounding Precision Decision. Then, for whatever reason, I went and made sure that precision was the word I was after. It wasn’t. Precision is the total number of digits in a number. Scale is the number of digits after the decimal.

Last night I decided I should add a Round parameter to a function I’m writing. This would allow the user to determine how rounding should be applied to a given number. The parameter values, as I see them now, will be 0 through 4. This is to say a numerical value may have zero decimal places up to four, depending on the person/process running the function. Neat, right? Like I sometimes do, I opened Window Terminal to start some one-off testing.

Let’s start with an ordered hash table of numeric values. As you’ll see, the values will vary in the number of decimal places from zero to five.

[PS7.1.5][C:\] $Hash = [ordered]@{'Num0' = 42; 'Num1' = 42.1; 'Num2' = 42.12; 'Num3' = 42.123; 'Num4' = 42.1234; 'Num5' = 42.12345}
[PS7.1.5][C:\] $Hash

Name               Value
----               -----
Num0               42
Num1               42.1
Num2               42.12
Num3               42.123
Num4               42.1234
Num5               42.12345
[PS7.1.5][C:\] 

While we’re here, let’s loop through the keys and values in this hash table. We’ll need this construct soon enough anyway.

[PS7.1.5][C:\] Foreach ($Key in $Hash.Keys) {"$Key`: $($Hash[$Key])"}
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.12345
[PS7.1.5][C:\] 

That works. Now what we need to consider is that we’re going to need to iterate over $Hash multiple times, so we can apply each of the decimal values, 0 – 5 for this post. This may be confusing, but the next example should help explain how we get there.

[PS7.1.5][C:\] Foreach ($Value in 0..5) {$Value}
0
1
2
3
4
5
[PS7.1.5][C:\] Foreach ($Value in 0..5) {Foreach ($Key in $Hash.Keys) {"$Key`: $($Hash[$Key])"}}
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.12345
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.12345
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.12345
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.12345
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.12345
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.12345
[PS7.1.5][C:\]

Now that we have this working, let’s apply some rounding to each of the sets of numbers. Before we do that, however, I’m heading back to VSCode. Sometimes the one-off code I write gets a bit unwieldy. For reference, we’re going to use the Round method in the System.Math class, such as [math]::Round(<number>,<scale>).

$Hash = [ordered]@{
	'Num0' = 42
	'Num1' = 42.1
	'Num2' = 42.12
	'Num3' = 42.123
	'Num4' = 42.1234
	'Num5' = 42.12345
}
Foreach ($Value in 0..5) {
	"Rounding to $Value"
	Foreach ($Key in $Hash.Keys) {
		"$Key`: $([math]::Round($Hash[$Key],$Value))"
	} # End Foreach.
'--separator--'
} # End Foreach.

Rounding to 0
Num0: 42
Num1: 42
Num2: 42
Num3: 42
Num4: 42
Num5: 42
--separator--
Rounding to 1
Num0: 42
Num1: 42.1
Num2: 42.1
Num3: 42.1
Num4: 42.1
Num5: 42.1
--separator--
Rounding to 2
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.12
Num4: 42.12
Num5: 42.12
--separator--
Rounding to 3
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.123
Num5: 42.123
--separator--
Rounding to 4
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.1234
--separator--
Rounding to 5
Num0: 42
Num1: 42.1
Num2: 42.12
Num3: 42.123
Num4: 42.1234
Num5: 42.12345
--separator--

At first glance, this PowerShell code may look like it’s failing. Look at the “Rounding to 3” section, for instance. There are values where it appeared to round to zero, one, and two decimal places. Remember though, these numbers didn’t start with three decimals places or more. It did exactly what we asked it to do to the best of its ability. I don’t know of a way that the Round method can add zeros if we actually wanted that — let me know if you — but there’s another way!

This time, we’ll use the -f format operator and force zeros into the decimals places. I probably won’t do this in my upcoming post, but I think it’s worth seeing, as there’s value here.

Foreach ($Value in 0..5) {
	"Rounding to $Value"
	Foreach ($Key in $Hash.Keys) {
		"$Key`: $("{0:N$Value}" -f $Hash[$Key])"
	} # End Foreach.
'--separator--'
} # End Foreach.

Rounding to 0
Num0: 42
Num1: 42
Num2: 42
Num3: 42
Num4: 42
Num5: 42
--separator--
Rounding to 1
Num0: 42.0
Num1: 42.1
Num2: 42.1
Num3: 42.1
Num4: 42.1
Num5: 42.1
--separator--
Rounding to 2
Num0: 42.00
Num1: 42.10
Num2: 42.12
Num3: 42.12
Num4: 42.12
Num5: 42.12
--separator--
Rounding to 3
Num0: 42.000
Num1: 42.100
Num2: 42.120
Num3: 42.123
Num4: 42.123
Num5: 42.123
--separator--
Rounding to 4
Num0: 42.0000
Num1: 42.1000
Num2: 42.1200
Num3: 42.1230
Num4: 42.1234
Num5: 42.1234
--separator--
Rounding to 5
Num0: 42.00000
Num1: 42.10000
Num2: 42.12000
Num3: 42.12300
Num4: 42.12340
Num5: 42.12345
--separator--

And that’s, that. Back to the other post that inspired this one. See you there in time!

Prestaging Modules for PowerShell, Windows PowerShell

I recently wrote this to a colleague: “Finally, if you’re going to peer into the module manifest file (.psd1), you might also check ‘CompatiblePSEditions.’ If it includes both the ‘Core’ and ‘Desktop’ values, then you might consider installing modules in both $env:USERPROFILE\Documents\WindowsPowerShell\Modules and $env:USERPROFILE\Documents\PowerShell\Modules.”

He’s using SCCM, Software Center, and a Windows PowerShell form to install modules. My thought was to install it for both PowerShell and Windows PowerShell, regardless of which they were using and in reference to the manifest file. This would ensure a module was already in place provided the user switched between PowerShell and Windows PowerShell or moved from one to another one day. Not a bad idea, I suppose. If the duplicated module is never used, it’s okay as the disk space used would likely never really be noticed. Maybe one day, PowerShell will include this option itself. You install a module in PowerShell and include some yet-to-be-determined switch parameter, and boom, it’s ready in both locations.

This got me thinking. How would one write the code to do the conditional logic here? While it wasn’t my project, I couldn’t help myself from determining how I might write it. And, when it was done — and this has happened before — I wasn’t sure what to do with it. Enter, this blog post; its new home, or final resting place.

I wrote this to check four different modules on my computer. They were: (1) AWS.Tools.Common, (2) AWSLambdaPSCore, (3) ISE, and (4) ExchangeOnlineManagement. Each of these four contained a different value in the psd1’s CompatiblePSEditions. In the same order as above, there were (1) Core and Desktop, (2) Only Core, (3) Only Desktop, and (4) Neither Core nor Desktop.

Here’s what I wrote; we’ll discuss it further below. It was saved into a file named ModuleDeploy.ps1.

# Both Core AND Desktop.
$Path01 = "$env:USERPROFILE\Documents\PowerShell\Modules\AWS.Tools.Common\4.0.5.0\AWS.Tools.Common.psd1"

# ONLY Core.
$Path02 = "$env:USERPROFILE\Documents\PowerShell\Modules\AWSLambdaPSCore\2.0.0.0\AWSLambdaPSCore.psd1"

# ONLY Desktop.
$Path03 = "$env:SystemRoot\system32\WindowsPowerShell\v1.0\Modules\ISE\ISE.psd1"

# Neither Core NOR Desktop.
$Path04 = "$env:USERPROFILE\Documents\PowerShell\Modules\ExchangeOnlineManagement\1.0.1\ExchangeOnlineManagement.psd1" 

$Paths = $Path01,$Path02,$Path03,$Path04
Foreach ($Path in $Paths) {
	"`r`n||| Module: $(Split-Path -Path $Path -Leaf) |||"
	$Psd1File = Import-PowerShellDataFile -Path $Path

	Switch ($Psd1File.CompatiblePSEditions) {
		{$_ -contains 'Desktop'} {
			Write-Output -InputObject 'Copy to WindowsPowerShell directory.'
		} # End Condition.

		{$_ -contains 'Core'} {
			Write-Output -InputObject 'Copy to PowerShell directory.'
		} # End Condition.

		default {
			If ($PSVersionTable.PSEdition -eq 'Desktop' -or $null -eq $PSVersionTable.PSEdition) {'Copy to WindowsPowerShell directory (default).'
			} ElseIf ($PSVersionTable.PSEdition -eq 'Core') {'Copy to PowerShell directory (default).'} # End If-ElseIf.
		} # End Condition.
	} # End Switch.
} # End Foreach.

Let’s quickly cover what’s happening in the above code. Then we’ll view the below results for the execution in both PowerShell and Windows PowerShell. Showing both versions may clarify the reason for the If statement inside the above final/default Switch condition. We’re already jumping ahead.

First, we created four different path variables for four specific module manifest files (.psd1 files). The first one is to a module that includes both “Core” and “Desktop” in CompatiblePSEdition. The second only has “Core,” the third only has “Desktop,” and the final one has neither/nothing. This was mentioned earlier. These four variables and their assigned values are combined into an array, which we then iterate over inside of a Foreach loop.

During each iteration, we run through a Switch statement that indicates where each module would be copied (if we were actually copying the modules). Based on the value(s) in CompatiblePSEdition for each of the files, the first module would be copied to both the PowerShell and WindowsPowerShell modules, the second, only to PowerShell modules, and the third, only to WindowsPowerShell modules. The final copy is dependent on whether or not we’re using PowerShell or Windows PowerShell. Take a look at the results.

PowerShell 7.0.1

[PS7.0.1] C:\&gt; . "$env:TEMP\ModuleDeploy.ps1"

||| Module: AWS.Tools.Common.psd1 |||
Copy to PowerShell directory.
Copy to WindowsPowerShell directory.

||| Module: AWSLambdaPSCore.psd1 |||
Copy to PowerShell directory.

||| Module: ISE.psd1 |||
Copy to WindowsPowerShell directory.

||| Module: ExchangeOnlineManagement.psd1 |||
Copy to PowerShell directory (default).

WindowsPowerShell 5.1

[PS5.1.18362.752] C:\&gt; . "$env:TEMP\ModuleDeploy.ps1"

||| Module: AWS.Tools.Common.psd1 |||
Copy to PowerShell directory.
Copy to WindowsPowerShell directory.

||| Module: AWSLambdaPSCore.psd1 |||
Copy to PowerShell directory.

||| Module: ISE.psd1 |||
Copy to WindowsPowerShell directory.

||| Module: ExchangeOnlineManagement.psd1 |||
Copy to WindowsPowerShell directory (default).

That’s it. Just some code that needed a home. I’m fully aware that there’s better ways this could be done, and that there would be some copy duplication if this were dropped into production the way it is. Additionally, if a directory didn’t yet exist, it might have to be created. Still, it needed a home (as is), and I didn’t think about much more than the conditional logic involved to determine where a module would need to be deployed.

Fake the Data when Missing the CSV

Earlier today, I had an opportunity to help a coworker run ICMP requests against a series of IP addresses. They were being pulled in from a CSV file, which contained hostnames with matching IP addresses. If for some reason the import failed, such as the file wasn’t where it was supposed to be, he wanted to supply a single IP address he knew would fail in place of the successful CSV import. I suspect this was for testing purposes. Whatever the reason, I obliged him in his quest and request. I leaned heavily on a recently written and published article of mine: Hash Table to CSV, Revisited. It lined up perfectly with what I was working toward in this instance.

The goal here was to fake out an upcoming Foreach loop in such a way that no coding changes needed to be made, whether or not the CSV file existed, etc. That was the end goal for this work: leave the later code doing what it had been doing previously.

We’re, mostly, going to start with the code. In this first example, you’ll have to believe me that the file IPs.csv does in fact exist, and it contains two columns. One is the hostname, which we don’t actually even use in this example, and the other is the IP address. As expected, this column contains a properly formatted IP address. As everything is in place, the CSV file is imported and saved in $IPAddresses. Following that, the code loops through the contents of our new variable. For every entry, it returns True or False depending on whether it’s able to return a successful ping request or not, against that IP address.

try {
    $IPAddresses = Import-Csv -Path 'C:\IPs.csv'
} catch {
    $IPAddresses = [PSCustomObject]@{Host = 'Test';IP = '8.8.8.9'}
} # End try-catch

Foreach ($IP in $IPAddresses) {
    If (Test-Connection $IP.IP -Count 1 -Quiet) {
        $true
    } Else {
        $false
    } # End If-Else.
} # End Foreach.

Pinging 8.8.8.8 [8.8.8.8] with 32 bytes of data:
Reply from 8.8.8.8: bytes=32 time=13ms TTL=50
Ping complete.
True
Pinging 216.58.193.206 [216.58.193.206] with 32 bytes of data:
Reply from 216.58.193.206: bytes=32 time=12ms TTL=53
Ping complete.
True

In this next example, we’ve altered the CSV file extension. This attempted CSV import will fail, and will therefore execute our code in the catch block. This will make it appear as though we’ve imported the CSV; however, because we recognize this IP address and its guaranteed to fail, we’ll know there was a problem with the CSV import.

try {
    $IPAddresses = Import-Csv -Path 'C:\IPs.csvvvvvv'
} catch {
    $IPAddresses = [PSCustomObject]@{Host = 'Test';IP = '8.8.8.9'}
} # End try-catch

Foreach ($IP in $IPAddresses) {
    If (Test-Connection $IP.IP -Count 1 -Quiet) {
        $true
    } Else {
        $false
    } # End If-Else.
} # End Foreach.

Pinging 8.8.8.9 [8.8.8.9] with 32 bytes of data:
Request timed out.
Ping complete.
False

There are other ways to handle this, yes. But remember, the co-worker needed data to be produced just as it would be if it worked (the import part), even though it didn’t. It had something to do with JSON, schemas, and Azure Log Analytics. It was good enough for him, so it was good enough for me, too.

ValidateSet Default Parameter Values

The example code I’m going to include below, I’ve used before. I really like it and so I’m going to give it place here on my website, in case it may ever be helpful for you, and maybe even me again.

The first time I used something like this code example was for a function that created random passwords. By default, that function’s CharacterType parameter would include the four values Lowercase, Number, Symbol, and Uppercase. By using the parameter, you can specify which of the values you actually use, if you didn’t want to use all four. By default, the parameter included them all.

We are defining an advanced function called Test-Function with a single parameter called Type. This parameter uses ValidateSet in order that it’ll only ever accept four different parameter values, for the Type parameter. Additionally, the Type parameter actually includes a default value that includes all four of the values: FullAccess, SendAs, SendOnBehalf, and Calendar. If you ever find yourself needing an All parameter value, just use this option instead; you don’t actually need an All parameter value, you just need to include all the possible values as the default.

After the parameter inclusion, the function begins with a Foreach language construct that will evaluate each Type that been included, whether it’s all four by default, all four because someone use the parameter and entered all four possibilities (not necessary, obviously), or something less than the four options.

Inside each iteration thought the Foreach there’s a Switch statement that will be evaluated. Based on the current type value, its value will be displayed in a string that includes a hard coded value to ensure it’s correct.

Function Test-Function {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateSet('FullAccess','SendAs','SendOnBehalf','Calendar')]
        [string[]]$Type = ('FullAccess','SendAs','SendOnBehalf','Calendar')
    )

    Foreach ($T in $Type) {
        Switch ($T) {
            'FullAccess' {
                "Doing $T stuff (should match FullAccess)."
            }
            'SendAs' {
                "Doing $T stuff (should match SendAs)."
            }
            'SendOnBehalf' {
                "Doing $T stuff (should match SendOnBehalf)."
            }
            'Calendar' {
                "Doing $T stuff (should match Calendar)."
            }
        } # End Switch.
    } # End Foreach.
}

PS > Test-Function
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).
Doing SendOnBehalf stuff (should match SendOnBehalf).
Doing Calendar stuff (should match Calendar).

PS > Test-Function -Type 'FullAccess','SendAs','SendOnBehalf','Calendar' # Same as above.
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).
Doing SendOnBehalf stuff (should match SendOnBehalf).
Doing Calendar stuff (should match Calendar).

PS > Test-Function -Type 'FullAccess','SendAs','SendOnBehalf'
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).
Doing SendOnBehalf stuff (should match SendOnBehalf).

PS > Test-Function -Type 'FullAccess','SendAs'
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).

PS > Test-Function -Type 'Calendar','FullAccess','SendAs'
Doing Calendar stuff (should match Calendar).
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).

PS > Test-Function -Type 'SendOnBehalf','FullAccess','Calendar'
Doing SendOnBehalf stuff (should match SendOnBehalf).
Doing FullAccess stuff (should match FullAccess).
Doing Calendar stuff (should match Calendar).

Nothing down here, but thanks for reading all the way! Actually, here’s a bonus if you didn’t already know it. Those hard coded statements inside the Switch statement, could’ve been written a little differently.

This:

...
            'FullAccess' {
                "Doing $T stuff (should match FullAccess)."
            }
            'SendAs' {
                "Doing $T stuff (should match SendAs)."
            }
            'SendOnBehalf' {
                "Doing $T stuff (should match SendOnBehalf)."
            }
            'Calendar' {
                "Doing $T stuff (should match Calendar)."
            }
...

could’ve actually been this:

...
            'FullAccess' {
                "Doing $T stuff (should match $_)."
            }
            'SendAs' {
                "Doing $T stuff (should match $_)."
            }
            'SendOnBehalf' {
                "Doing $T stuff (should match $_)."
            }
            'Calendar' {
                "Doing $T stuff (should match $_)."
            }
...

Use Foreach When it’s Really Needed

We’re not all the same, but if we were, and you were like me, you’d have a huge number of tabs open in your browser of choice. Each of them, would have some relation to PowerShell and each would be sitting by idle, and waiting to be read. No idea where it came from, but in one of them last week, I saw this:

Get-Date -Format o | foreach {$_ -replace ":", "."}

I stared at it for a moment, and thought, why is someone piping Get-Date to foreach? Get-Date only returns a single value. Why would that need to be handed off to a looping construct? The point here is, it wouldn’t need to be, even though it works. I’ve decided I should bring this up, in case someone else is potentially going to make this same mistake. Even if you’re a Systems Administrator, you’re still going to want to write efficient, and well thought out code. Here’s how I would have expected to see this written:

(Get-Date -Format o) -replace ":", "."

Again, Get-Date is only going to provide a single returned value, and therefore, we can trust that we don’t need to loop through a set of results. It’s cleaner code, it’s tighter code, and it gives the rest of us some confidence that you’ve thought things through.

One of the things I enjoying doing is testing the speed of various ways to do the same thing. Don’t think for a minute that I didn’t do that here. This next example indicates the time to run the foreach version of this Get-Date command. The second example, indicates the time to run when foreach is not used.

Milliseconds Ticks
------------ -----
           1 19094
           1 10543
           0  7044
           0  4212
           0  3944
           0  3849
           0  4133
           0  4349
           0  4099
           0  3948
Milliseconds Ticks
------------ -----
           1 12386
           0  5277
           0  4438
           0  2479
           0  2345
           0  2325
           0  4003
           0  2352
           0  2335
           0  2311

The times are close; they’re indistinguishable to us humans, but the numbers don’t lie. There’s a better way based both on time to complete, and competency. If you wanted to see it, here’s what I used to measure these two different commands. Keep these things in mind, and keep on learning!

1..10 | ForEach-Object {
    Measure-Command -Expression {
        Get-Date -Format o | foreach {$_ -replace ":", "."}
    } | Select-Object -Property Milliseconds,Ticks
}

1..10 | ForEach-Object {
    Measure-Command -Expression {
        (Get-Date -Format o) -replace ":", "."
    } | Select-Object -Property Milliseconds,Ticks
}

Keep PowerShell Cmdlets Powerful

I’ve seen what I’m about to show you done repeatedly in Windows PowerShell. With that thought in my mind, I’ve decided it’s time to officially write about it. The first time I noticed someone doing it, well, it was me. I did, however, write a correction to my team so they knew I was aware of my error, and as a hope they wouldn’t repeat it.

Those doing this may be new to PowerShell, but if you aren’t careful and don’t fully understand the capabilities of the cmdlets you use, then it’s possible you might make this mistake, too. What am I talking about? I’m talking about Invoke-Command and an improper way that it’s sometimes used.

Invoke-Command‘s purpose is to run a command, or commands, on remote computers and return the results back to the local computer. That said, it can be used on the local computer, as well; however, this is usually only necessary to check if PowerShell Remoting is working on the local computer. While I don’t normally do this in my examples, I’m piping my result of this example to Format-Table -AutoSize once they’re returned from the remote computer. This is so it’ll display better on this webpage.

Invoke-Command -Computer DC01 -ScriptBlock {Get-PSDrive -PSProvider FileSystem} | Format-Table -AutoSize
Name Used (GB) Free (GB) Provider Root              CurrentLocation PSComputerName
---- --------- --------- -------- ----              --------------- --------------
A                                 A:\                               DC01
C        12.91     26.75          C:\  Users\tommymaynard\Documents DC01
D                                 D:\                               DC01

This example of Invoke-Command connected to the remote computer DC01, ran a filtered Get-PSDrive command, returned the results to my computer, and then were formatted by the Format-Table cmdlet.

Now, let’s say we have… 23 computer names stored in the variable $Computers and we want to run the command we used above, against each computer. What so many people seem to do is wrap Invoke-Command in a Foreach construct (think: loop). I’m guessing, because I’ve made the error myself, that it’s because someone learned about Foreach first. Like I said, I discovered my error on my own, and only moments later. It didn’t wake me at 2 a.m. in a cold sweat after two months, which could’ve been possible.

Here’s how not to do this, and again, $Computers is holding the names of 23 computers.

Foreach ($Computer in $Computers) {
    Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
}
Name           Used (GB)     Free (GB) Provider      Root                                CurrentLocation PSComputerName
----           ---------     --------- --------      ----                                --------------- --------------
C                  20.47         59.18               C:\                                 ...rd\Documents DC02
D                                                    D:\                                                 DC02
A                                                    A:\                                                 WEB01
C                  50.36         29.30               C:\                                 ...rd\Documents WEB01
D                                                    D:\                                                 WEB01
A                                                    A:\                                                 SQL01
C                  53.59         26.06               C:\                                 ...rd\Documents SQL01
D                                                    D:\                                                 SQL01
... # Not all results included.

What we’ve done here is force Invoke-Command to run against only one computer at a time. During each iteration of the Foreach loop, Invoke-Command runs against the computer name currently stored in $Computer. This variable is updated to the next computer name in the variable at the start of each loop. It’s like this: connect to DC02 and run the command. Now, connect to WEB01 and run the command. Are you done, Invoke-Command? Okay then, now Foreach says to run the same command against the computer SQL01. That’s not how this cmdlet was designed to be used.

Before we go any further, I wrapped this command inside the Measure-Command cmdlet to determine how long it took my system to run this against the computers in $Computers. It took nearly 30 seconds. While that’s not an eternity, we’re only talking about 23 computers, and we’re only talking about a single command.

Measure-Command -Expression {
    Foreach ($Computer in $Computers) {
        Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
    }
}
Days : 0
Hours : 0
Minutes : 0
Seconds : 27
Milliseconds : 513
Ticks : 275132689
TotalDays : 0.000318440612268519
TotalHours : 0.00764257469444444
TotalMinutes : 0.458554481666667
TotalSeconds : 27.5132689
TotalMilliseconds : 27513.2689

Now, let’s set this command up the proper way. The ComputerName parameter of Invoke-Command can take a collection of computers. It should be said that many cmdlets can take more than one computer as a value to their ComputerName parameter. The difference is that Invoke-Command can run against all of the 23 computers at the same time. In fact, the default throttle limit—the number of computers in which the command will run against simultaneously—is 32. It can be changed by including the ThrottleLimit parameter, such as -ThrottleLimit 50 or -ThrottleLimit 15.

Now, here’s how this command should’ve been written.

Invoke-Command -ComputerName $Computers -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
Name           Used (GB)     Free (GB) Provider      Root                                CurrentLocation PSComputerName
----           ---------     -------u-- --------      ----                                --------------- --------------
A                                                    A:\                                                 DC02
A                                                    A:\                                                 WEB01
C                  41.72         37.93               C:\                                 ...rd\Documents WEB01
C                  41.13         38.52               C:\                                 ...rd\Documents DC02
D                                                    D:\                                                 DC02
D                                                    D:\                                                 WEB01
A                                                    A:\                                                 SQL01
C                  46.75         32.91               C:\                                 ...rd\Documents SQL01
D                                                    D:\                                                 SQL01
... # Not all results included.

I measured this command five times, and the average of those five runs was only 2.8 seconds. Remember, the Foreach loop took almost 30 seconds to get the same results. This is due to the fact that, again, Invoke-Command will work with the remote computers simultaneously (when it’s not inside a Foreach). There’s never any waiting to run the command on any additional computers unless there’s more computers than the value of ThrottleLimit.

Please keep the topic in this post in mind, as it’s possible to momentarily forget this feature just long enough to wrap the Invoke-Command cmdlet in a Foreach, or other looping construct.