Category Archives: Quick Learn

Practical examples of PowerShell concepts gathered from specific projects, forum replies, or general PowerShell use.

Top of the Next Hour


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 January 7, 2019.


The Get-Date cmdlet has always been helpful, but just when I thought it had me fully covered, I determined it fell short. That said, it does have enough to get me what I want, even if there isn’t a simple, single built-in method for it.

I’m working on a project that requires me to add an additional trigger to a previously created scheduled task. When the scheduled task was initially deployed, it only had a single trigger. It was to run at midnight the following day (from the day in which the task was first created) and then every hour forever (at the top of the hour), until the end of time. Well, for the next 10,675,199 days at least.*

As a part of updating this project with a new trigger, I’m also going to be overwriting the trigger that begins at midnight. I’m doing that now because, at this point in this project, I don’t want to wait until tomorrow morning (at midnight) to have the task up and running again, I need a better starting time now that the task has been operating successfully for months. While I can do an hour from now — whatever time that may be — for the New-ScheduledTaskTrigger’s At parameter…

PS> (Get-Date).AddHours(1)

Saturday, January 5, 2019 8:14:29 PM

I don’t want that.

What this would mean, is that my fleet of AWS EC2 instances would all have random times in which they execute this task, dependent on when the task was updated. To better complete this picture, this task downloads a PowerShell module from AWS S3 and plops it on the EC2 instance running the task. Historically, or currently, rather, I’ve greatly appreciated knowing that any modifications to the PowerShell module uploaded to S3, are downloaded to all the EC2 instances, at the top of any, and every, hour. At 10:00 a.m., the module is replaced. At 11:00 a.m., the module is replaced, and so on. A collection of random, unknown times would be horrible going forward. I need to know that any modifications to the PowerShell module in S3 will be on all the instances, every hour, at the same time. And even if there are no modifications to the module, it gets downloaded anyway. It’s just easier that way (for now, perhaps).

Therefore, I need to know the top of the next hour. You know, if it’s 7:16 p.m., I need Get-Date to return 8:00 p.m. (on the same day, of course). If it’s 4:30 a.m, I need 5:00 a.m. returned. While I didn’t find a built-in method to accomplish this, as stated, I was able to write something myself, after a short amount of time head down in the console. Take a look at the below commands, and then let’s discuss them.

PS> Get-Date

Saturday, January 5, 2019 7:19:17 PM

PS> (Get-Date).AddMinutes(59 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)

Saturday, January 5, 2019 8:00:00 PM

The first above example returns the current date and time, as we’d expect that it would. The second, above example, indicates how we can ensure the value returned by Get-Date is the same date as today — no changes wanted there — with the time set in the future, at the top of the next hour.

For fun, we’ll pretend the time is 10:43:19 a.m.

Here’s what would happen, if the second command ran against this time. First, it would use Get-Date‘s AddMintues method. That makes sense, as no matter what time it is, we’ll need to add time to the current time, to get to the top of the next hour. Therefore, within the AddMinutes method, we take 59 and subtract the current minute of the current time.

10:43:19

59 – 43 = 16 minutes

Next, we’d add some seconds to our time, as well. We would take the value of 60 and subtract the current second of the current time.

10:43:19

60 – 19 = 41 seconds

Adding 41 seconds to 10:43:19 makes it 10:44:00. Adding in those 16 minutes takes us to 11:00:00. If you didn’t catch it, the command uses 59, not 60, when calculating AddMinutes. This is because the AddSeconds method is going to make up our “missing” minute.

Take a look at the following two examples. I won’t bother to explain them, but perhaps at this point, you can understand why they produce the results they do.

PS> (Get-Date).AddMinutes(60 - (Get-Date).Minute).AddSeconds(59 - (Get-Date).Second)

Saturday, January 5, 2019 8:00:59 PM

PS> (Get-Date).AddMinutes(60 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)

Saturday, January 5, 2019 8:01:00 PM

Now, no matter when my instances have their trigger updated, for the same task across each of them, I can ensure this task is back to updating my PowerShell module on those instances, at the top of every hour.

I took this one step further, and not necessarily because it had anything to do with scheduled tasks. What if I wanted my own method to do this? I quickly wrote a ScriptMethod for my own instance of a Get-Date object. I don’t have an opportunity, or need to do this often, so every little bit of practice is helpful.

$Date = Get-Date
 
Add-Member -InputObject $Date -MemberType ScriptMethod -Name GetNextTopHour -Value {
    $this.AddMinutes(59 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)
}
 
$Date = $Date.GetNextTopHour()

Now we can use our datetime object, and return the top of the next hour.

PS> $Date

Saturday, January 5, 2019 8:00:00

Now back to thinking through the task that started this whole line of thought, anyway.

* For anyone curious, when the scheduled task’s, task trigger was originally created using the New-ScheduledTaskTrigger function, the value used for the RepetitionDuration parameter was set as [System.TimeSpan]::MaxValue. Take a look at the below example, and you’ll see where this 10 million-plus day count is derived.

PS> [System.TimeSpan]::MaxValue
Days              : 10675199
Hours             : 2
Minutes           : 48
Seconds           : 5
Milliseconds      : 477
Ticks             : 9223372036854775807
TotalDays         : 10675199.1167301
TotalHours        : 256204778.801522
TotalMinutes      : 15372286728.0913
TotalSeconds      : 922337203685.478
TotalMilliseconds : 922337203685477

In order that we’re all on the same page, those 10 million-plus days, equate to “indefinitely” in a scheduled task’s Triggers tab, when viewing the task in the GUI. If you’re ever after a scheduled task repetition that never ends, and you’re using PowerShell to piece your task together, then MaxValue is the property to use.

Use PowerShell to Edit a CSV, Revisited

1. Back in January 2019, I started writing on another website. In December of that same year, I modified how I was doing things. Instead of just writing a full post there, I would start a new post there and then finish the post here, on my site. I did that through June 2020. All the posts from January 2019 to November 2019 are gone and all the posts from December 2019 to June 2020 are partially gone (as only a portion was written away from tommymaynard.com).

2. I have recently mentioned that if I hit 416 posts by the end of June 2022, I will have an average of one post per week over an eight-year timeframe. Bringing those posts back to life and publishing them on my site would get me to my goal much quicker. Beyond that, I have long wanted to recapture that work anyway, and realistically, those posts could have just as easily — maybe even more easily — been published here, to begin with.

As it is time to fix this, it would not make sense to do things manually unless it was an absolute requirement. Automation is kind of why we are all here. I intend to recover as many of my posts as I can and publish them here with help from archive.org — the Internet Archive — and their API.

I have been down the CSV road before. While not as popular as my Hash Table to CSV post, with greater than 21,000 all-time views, my Use PowerShell to Edit a CSV post has greater than 15,000 all-time views. Hash Table to CSV has part II or revised post, and now Use PowerShell to Edit a CSV does, too.

At first, I started to manually collect data — the data in the below image. I did not want to do that any more than I had already done. That decision was all I needed to save some time that was better spent prepping and preparing for this post. In this first image, you can see the CSV from which I began.

Do notice a few things: One, we’re missing some dates on the left, two, we’re missing all the URLs from the WB_URL column, and three, all the Notes are empty too. Our goal is to add dates on the left where we can, add URLs into the WB_URL column, and Notes when that’s necessary. There’s nothing to add to the TM_URL column. Those empty lines indicate posts that were never written and published on my site.

The first thing to do is ensure I can import the above CSV file. We will use the $CsvBasePath variable to hold the path to the file we will import with the Import-Csv command. The values returned from this command will be stored in the $CsvFileContents variable. Then they will be output to a formatted table, so they are easier to read. This is just a quick check to ensure the CSV file can be imported. As you can see below, the file’s contents could be imported. This is the identical data we saw in the previous image.

$CsvBasePath = 'C:\users\tommymaynard\Desktop\tommymaynard.com'
$CsvFileContents = Import-Csv -Path "$CsvBasePath/Corrections.csv"
$CsvFileContents | Format-Table -AutoSize

If you want to read about it, and it is short, here is information on the API that we will be using: https://archive.org/help/wayback_api.php. It is too important not to show you. The below image is what a response looks like when it is returned from using the API. We will use this API with Invoke-RestMethod. It may not mean much now, but you may end up referring to it as you progress further into this post. As you will see shortly, I will use …status -eq 200, I will use and edit the timestamp, and I will collect the URL. Having seen this image may make understanding the PowerShell much more straightforward.

I now know the CSV file can be imported and I understand the structure of the API response.

In the below PowerShell, I removed outputting the contents of the $CsvFileContents variable to the host, and instead, I set up a ForEach-Object looping construct. The first thing done inside this loop is put the current PS_URL value into the $UriRemainder variable in line 5. In line 6, we concatenate the $UriBase variable and the $UriRemainder variable and use them as the URI supplied to the Invoke-RestMethod command. We will continue working through this code below.

$CsvBasePath = 'C:\users\tommymaynard\Desktop\tommymaynard.com'
$UriBase = 'http://archive.org/wayback/available?url='
$CsvFileContents = Import-Csv -Path "$CsvBasePath\Corrections.csv"
$CsvFileContents | ForEach-Object {
	$UriRemainder = $_.PS_URL
	$WBInfoFull = Invoke-RestMethod -Uri "$UriBase$UriRemainder"
	If ($WBInfoFull.archived_snapshots.closest.status -eq 200) {
		$WBInfoDate = $WBInfoFull.archived_snapshots.closest.timestamp
		$WBInfoDate = -join $WBInfoDate[0..7]
		$WBInfoUrl = $WBInfoFull.archived_snapshots.closest.url
		$WBInfoFull
		$WBInfoDate
		$WBInfoUrl
		<#
		$_.Date = $WBInfoDate
		$_.WB_URL = $WBInfoUrl
		#>
		# Remove-Variable -Name WBInfoFull,WBInfoDate,WBInfoUrl
	} Else {
		'[[[[[NOPE]]]]]'
	} # End If-Else.
	$_
} # | Export-Csv -Path "$CsvBasePath\Corrections-temp.csv" -NoTypeInformation

The Invoke-RestMethod command reaches out to archive.org and stores the result — remember the response object — in the $WBInfoFull variable. For each iteration through the loop, this variable is repeatedly filled with data from each lookup against the Wayback Machine — another name for archive.org if I did not say that already. If the status is 200, we know our Invoke-RestMethod command was a success and so we progress further into the If portion of our nested If-Else construct.

We will then set $WBInfoDate by returning the timestamp property such as 20201022005417 and then joining the first eight “digits” [lines 8 and 9]. We then set, or assign, the $WBInfoURL variable. In the remainder of this code, we just dump our values to the screen, clear the variables, and then move onto the next line in the CSV file. We have yet to actually write to a CSV yet.

The below image shows a portion of the output generated by the above commands. Again, I’m not writing to the CSV yet; I’m only making sure the values in my variables are accurate.

It is here where the working PowerShell will be modified in such a way, that we can begin writing to the CSV file. I should make something clear. I did not edit an existing CSV file as much as I created a new one. I suppose I could have written back to the same file…or maybe I could not. That is probably worth finding out someday, but I do suspect that an open CSV can be written to. In the first iteration of this post, I created a temporary file and then did a remove/rename, so it appeared I actually edited a file.  But this whole time, I have not really been editing anything. Such a fraud, I know.

Moving along though. The below changes include having removed the code that outputs the values in the variables to the screen. Instead, these values are being written to the Date and WB_URL columns. When the status isn’t 200, instead of writing [[[[[NOPE]]]]] to the screen as I did above, something more pleasant and professional is written to the Notes column. In order to write to a new CSV, I uncommented the Export-CSV command, as well. In the post’s final image, you can view the “updated” CSV file. By my count, there are over 40 lines in this CSV that I didn’t have to type or paste in after manually doing the search myself. I’ll take it!

Always search for an API to use. Always.

$CsvBasePath = 'C:\users\tommymaynard\Desktop\tommymaynard.com'
$UriBase = 'http://archive.org/wayback/available?url='
$CsvFileContents = Import-Csv -Path "$CsvBasePath/Corrections.csv"
$CsvFileContents | ForEach-Object {
	$UriRemainder = $_.PS_URL
	$WBInfoFull = Invoke-RestMethod -Uri "$UriBase$UriRemainder"
	If ($WBInfoFull.archived_snapshots.closest.status -eq 200) {
		$WBInfoDate = $WBInfoFull.archived_snapshots.closest.timestamp
		$WBInfoDate = -join $WBInfoDate[0..7]
		$WBInfoUrl = $WBInfoFull.archived_snapshots.closest.url
		$_.Date = $WBInfoDate
		$_.WB_URL = $WBInfoUrl
		Remove-Variable -Name WBInfoFull,WBInfoDate,WBInfoUrl
	} Else { 
		$_.Notes = 'Unable to locate an archived webpage for that URL.'
	} # End If-Else.
	$_
} | Export-Csv -Path "$CsvBasePath\Corrections-temp.csv" -NoTypeInformation

While the above image includes the updated data, there have been some unexpected changes from the CSV image earlier in this post. This was worrisome for me at first — why were the dates changing!? My PowerShell worked before, so why not now? It turns out that it still is working. The date changes are because newer snapshots of the pages have been taken by the Wayback Machine since I began this post back in October — yeah, it has been a long time coming. Therefore, no worries. What clued me in was the above code and the response. Take a look at this for a brief moment.

$WBInfoFull.archived_snapshots.closest.timestamp

The keyword is “closest,” as in the most recent snapshot. My most recent snapshots changed between the creation of my original CSV file, and the updated one.

There may be a way to use PowerShell and an API to gather the old content, but for now, I am collecting it manually. I want to reclaim this content ASAP in order to line up getting these posts — the ones that are still relevant — republished here on tommymaynard.com. It is a lot of work, but I am after 416 posts by the end of June 2022. It is fair to say that I will be back with some new, old content very soon.

I am TechNet Gallery Years Old, Part III

Well, it is time for Part III of this series of posts. If you did not read the first one, or the second one, I would recommend starting with those. And now, it is time to cover the final four entries.

8. I love how the 1 to 100 game turned out. It is on the PowerShell Gallery and more information can be found on the post I wrote about it here, including what you need to know to get it on your computer. Here is an old screen capture of some gameplay. Just because it is the 1 to 100 game, does not mean you cannot choose your own numbers. This is the 1 to 3 game.

9. “Convert Alphanumeric String to the NATO Phonetic Alphabet Equivalent”

This script does not come with its own post, however, it was mentioned in this post about my Convert-TextToSpeech function. Essentially, it did exactly what this quote says from the post: “It takes a string made up of numbers and letters and writes it out in the Nato phonetic alphabet.” Since it does not have its own post to read, head straight to the GitHub Gist if you are interested. I have opted to take it for a spin this evening and included an image, too. You cannot tell, but including the Speak parameter will ensure the computer says the results out loud.

10. “Check Email Address Domain Against the Top-Level Domain (TLD) List from IANA”

Uh, this one was already covered in Part II. Weird it is on the list twice. That said, there were two different TechNet Gallery URLs. I attempted to use archive.org and the second of the two URLs was gone, gone. That means that what I wrote was 100% accurate: it must have been deleted. Either way, visit Part II if you want to learn more about this script, and you might.

11. “Map Drive to Drive Letter Using the Win32_DiskDrive Interface Type Property”

We have made it; it is the last one. This script does not have its own post here, but you can get the code from its Gist. The most I remember, at the moment, is that it was written for someone that may have needed help finding the drive letter of a USB drive — it has been a while. I may be totally wrong, but the person it was written for was appreciative. And really, it was kind of a monumental moment in my PowerShell community career. It was the first time I wrote something for someone else out there on the Internet. I am thinking that it may have been the beginning of my desire to learn and write about PowerShell here at tommymaynard.com.

I am TechNet Gallery Years Old, Part II

It is time for Part II of this series of posts. If you did not read the first one, I would recommend you start there and cover the first four posts, which are crossed out below. Additionally, you will pick up some additional information about this project. Time to cover the next three.

5.”Specify and Create Multiple Credential Objects”

This was a fun project and like most of these TechNet Gallery scripts/modules, I took the opportunity to write about them on my site, too. I wrote about this one twice (Part I | Part II), as I added a new feature between those. The GitHub Gist link to the newest version is included on both parts — images too!

6. “Check Email Address Domain Against the Top-Level Domain (TLD) List from IANA”

It turns out I did not write a post to go along with this script — that may be a first. First off, you can get a hold of this script by visiting this GitHub Gist. And second, let me tell you a little about this script… right after I reeducate myself about it. Turns out this is very old. Its purpose was to determine whether or not the domain (.com in the below instance), is a valid Top-Level Domain (TLD) or not.

Beyond returning that, it downloads a file from IANA, which it checks the domain against, can provide a bit more information using the MoreInfo parameter, and can pump out the variables it uses in the function using the ShowVars parameter. The Days parameter is used to determine whether the function should use the current file from IANA or download a new one. Neat ideas, but the function is showing its age.

7. “TMConsole Module”

This one was a big deal. It made its way to the PowerShell Gallery and as of today, it is practically 50 downloads away from 3,000. My goal was to add ForeGround and Background colors to Write-Output like Write-Host has always had. And, the best part, it actually worked. You can read more about it here, as well as obtain the information needed to download it. Let’s push it past 3,000 downloads! And, it is really using Write-Output! Written for Windows PowerShell and works in PowerShell. Here is an image from today.

And now, there is a Part III!

I am TechNet Gallery Years Old

I did not know it when I started, but it turns out, this is Part I.

Up until just recently, I had a section on my website called “TechNet Gallery.” It was right up there between “Contents” and “About.” I have an image of what it contained below. There was some pretty great stuff in there, but it was time for that section to be removed. With that, it is now time for the links to be updated, so people can find these scripts and modules again. The TechNet Gallery links no longer work. It has been a while since I have looked these over, but the links will likely be a combination of GitHub and the PowerShell Gallery. Check the content below the image for information about the first four entries. The others will be highlighted in additional, related posts.

1. “Get Synonyms for Approved and Unapproved Verbs”

I have always loved this script. I even used it yesterday, prior to knowing I would start piecing this post together. If there is a verb you want to use for your cmdlet or function, but it is not approved, this function will look for verb synonyms and tell you if they are approved or not. Here’s a quick image (because it is just beautiful).

While the 1.3 version is available in a GitHub Gist, the newest version — 1.4 — was written and uploaded to the PowerShell Gallery. You can either use that link or use PowerShellGet to download it using the PowerShell below. I have written about this script before, so you can read more here: https://tommymaynard.com/get-tmverbsynonym-1-4-2017/.

Install-Script -Name Get-TMVerbSynonym

2. “Active Directory User Lookup Form”

This was “my first, hand-coded PowerShell form using Windows Forms.” While it was available on the TechNet Gallery for download, that link no longer works.  Here is the 2015 post, and here is an updated link to the GitHub Gist. I always love doing a forms project and am grateful it made its way into .NET (core).

3. “Find DNS Servers Being Used by DHCP Scopes”

This script was written as a solution to a post on the TechNet Forums. I did not read that whole thread, but I can offer you the link to the Gist if this is helpful or interesting. Oh, one other thing, I apparently wrote about the script here on my own site, as well. All the way back in 2015!

4. “Measure Command with Multiple Commands, Repetitions, Calculated Averages”

There is no way I did not write about this one. I loved this script and to this very day, I still think it should be implemented in PowerShell by the PowerShell Team. Perhaps I will add it as a discussion in GitHub in the future. I do not think that was a thing back in 2017. Here is my post about it now!

Measure-Command can only measure one command and only one time per command invocation. This changed that. It allowed a user to measure multiple commands multiple times. It could even calculate the average time for a command to complete. I think its Command parameter accepted a string, so that should probably be changed to a ScriptBlock.

Here is an old image that accompanies the above link to the article on my site — click it to enlarge it. And here, is a link to the code in a Gist.

There is more to cover, but we will put that on hold for a moment. I will be back with a continuation of these soon. And once I am, the below text will link to Part II of this series!

Part II

Testing Multiplication Facts

Edit: I said I would not, but I added more to the final function at the bottom of this post.

My daughter entered the office right on time — the end of my workday yesterday — and asked me to give her math problems. That means multiplication facts. While she is all but done learning her multiplication tables these days, I guess we are still practicing. It was my job to randomly choose two numbers between 1 and 12 that she would multiply in her head and say the answer out loud. I was sitting an arm’s length away from Windows Terminal and PowerShell, so I was about to give up coming up with the two numbers myself. PowerShell was going to do this for me.

This is not the first time I have discussed multiplication this school year. Here is a related post. This post is not really a part 2.

I didn’t want to spend much of our time writing PowerShell, so I quickly wrote the below PowerShell and we started. Because she could see the problems in the console — I zoomed in like I never zoomed in before — we were able to burn through at least a hundred, maybe two before we wrapped up our  “give me math” session.

Clear-Host; "$(1..12 | Get-Random) x $(1..12 | Get-Random)"

Here is a quick video that is probably worth seeing at this point in the post.

 

As we went through these random multiplication problems, my mind kept going to how I would have written this had I had more time to prepare. Well, that is what this post is for now. I know I can add a ridiculous amount of features, sure, but I have other things to do, so I am going to keep the features to a minimum. It did dawn on me to do something like my 1 to 100 game, but again, I only want to give her enough to play it herself. I did not need to keep score or be as nearly as polished as that one.

$1st = Get-Random -InputObject (1..12)
$2nd = Get-Random -InputObject (1..12)
"$1st x $2nd = $($1st * $2nd)"

3 x 8 = 24

The above example shows some changes to what I had authored previously. Basically, I can create a multiplication problem and include the answer too. This is not very helpful, as my daughter would be able to see the answer. Either way, this was a logical step toward the next example.

In this example, we add a do-until loop, hiding the product from the user. The Read-Host prompt will stop promoting for the answer as soon as it is entered correctly.

$1st = Get-Random -InputObject (1..12)
$2nd = Get-Random -InputObject (1..12)
"$1st x $2nd = ?"
do {$Product = Read-Host -Prompt 'Enter Product'}
until ($Product -eq $1st * $2nd)

1 x 8 = ?
Enter Product: 8

6 x 8 = ?
Enter Product: 45
Enter Product: 50
Enter Product: 48

And that was it. Plenty more could have been added, however, I am not sure she would even appreciate additional features and the extra work. This PowerShell will be good enough. I bet she will think it is great.

Okay fine, I decided to add a little more, but I am done after this, I swear. I put it in a function with a couple of parameters and here it is. As you will see, not much more time was spent doing this.

Function Test-Multiplication {
    [CmdletBinding()]
    Param (
        [Parameter()]
        $FirstRange = (1..12),

        [Parameter()]
        $SecondRange = (1..12)
    )

    $1st = Get-Random -InputObject $FirstRange
    $2nd = Get-Random -InputObject $SecondRange

    "$1st x $2nd = ?"
    do {$Product = Read-Host -Prompt 'Enter Product'}
    until ($Product -eq $1st * $2nd)
}
Test-Multiplication
9 x 9 = ?
Enter Product: 81

Test-Multiplication
4 x 6 = ?
Enter Product: 21
Enter Product: 24

Last note here. Because we have parameters, we can, if we want, send in different ranges for the FirstRange and SecondRange parameters. Ony want to practice your 4’s and you can do this.

Test-Multiplication -FirstRange (4..4)
   
4 x 1 = ?
Enter Product: 4

Test-Multiplication -FirstRange (4..4)
   
4 x 6 = ?      
Enter Product: 24

Okay, I am done now — for real.

Update: Well, it was not for real. I lasted a night. My daughter played the multiplication game and it was clear it needed one more thing. Even though she, at 10, now knows to press the up arrow for the last command, I went ahead and added a little more. Here is the updated function first and then an example of it being executed.

Function Test-Multiplication {
    [CmdletBinding()]
    Param (
        [Parameter()]
        $FirstRange = (1..12),

        [Parameter()]
        $SecondRange = (1..12)
    )

	
	do {
		$1st = Get-Random -InputObject $FirstRange
		$2nd = Get-Random -InputObject $SecondRange

		"$1st x $2nd = ?"
		do {$Product = Read-Host -Prompt 'Enter Product'}
		until ($Product -eq $1st * $2nd)

		do {
			$Replay = Read-Host -Prompt 'Enter = More (Q = quit)' 
		} until ($Replay -eq '' -or $Replay -eq 'Q')
	}
	until ($Replay-eq 'Q')
}
[PS7.2.1][C:\] Test-Multiplication 
4 x 3 = ?      
Enter Product: 12
Enter = More (Q = quit): 
5 x 10 = ?     
Enter Product: 50
Enter = More (Q = quit): 
4 x 5 = ?      
Enter Product: 20
Enter = More (Q = quit): 
9 x 4 = ?      
Enter Product: 35
Enter Product: 39
Enter Product: 36
Enter = More (Q = quit): q
[PS7.2.1][C:\] 

Okay, I am done again, for now. Ugh.

CSV Break Down

I wrote a post recently, but a while back—it is kind of both now. It was about arrays and hash tables. I highlighted the differences between them and who knew, it turned out to be a success. There was a good amount of sharing of the post on Twitter and my same-day visitor count jumped up a fair amount above my average, which is typically around 175 visits per weekday. I do not know who these people are that do not use PowerShell on the weekends but get your priorities straight. Fine, maybe it is me.

The above image indicates I brought in nearly 400 visitors the same day it was published—not bad. There were people in the PowerShell subreddit using the words “array” and “hash table” incorrectly and whatever just took over in me and did what it does to help clarify things for people in the PowerShell community. I have said it before: I have no problem revisiting the things I have already learned about PowerShell. There is a good deal to know, and so any opportunity to learn something you have already learned—we will call that review, right—is okay. I review these concepts all the time; I can only know if I know it by checking! And usually, I have forgotten something. The whole, “If you don’t use it, you lose it” saying, applies to PowerShell, too.

There was another post I did more recently, and while it was not as popular, I liked it. It used a CSV file of links—think, URLs—to create a menu system that could open links as they were chosen inside PowerShell. Good enough for me to push my post count up by one. I will have been writing about PowerShell for eight years in June 2022. Just maybe I can get to 416 posts by then making my average one post per week for eight straight years. Even if I do not make it, to even be this close is a feat. I must like PowerShell.

As I was working with a CSV for that most recent post, I read another PowerShell Reddit post where someone was talking about arrays in conjunction with CSVs. I do not have a link for that post, and I am not sure how much it would apply, but it did get me wondering about the type of variable you end up with when you import a CSV. Do you know by heart!? Care to guess? Let’s bring back the CSV we used in the last post. It will work fine to get us the answers we need.

Title,Link,Note
PowerShell GitHub,https://github.com/PowerShell/PowerShell,
PowerShell GitHub Issues,https://github.com/PowerShell/PowerShell/issues,
PowerShell Docs GitHub,https://github.com/MicrosoftDocs/PowerShell-Docs,
Powershell Docs GitHub Issues,https://github.com/MicrosoftDocs/PowerShell-Docs/issues,
PowerShell Docs,https://docs.microsoft.com/en-us/powershell,
PowerShell Gallery,https://www.powershellgallery.com,
PowerShell Reddit,https://www.reddit.com/r/PowerShell,
Twitter Legends @jeffhicks,https://twitter.com/JeffHicks,
Twitter Legends @jsnover,https://twitter.com/jsnover,
Twitter Legends @concentrateddon, https://twitter.com/concentrateddon,
TechCrunch.com,https://techcrunch.com/,Tech News
Cnet.com,https://www.cnet.com/,Tech News
Gizmodo.com,https://gizmodo.com/,Tech News
9to5mac.com,https://9to5mac.com/,Tech News
Engadget.com,https://www.engadget.com/,Tech News
Wired.com,https://www.wired.com/,Tech News
TechRadar.com,https://www.techradar.com/,News
Axios.com,https://www.axios.com/,News

If we assume the above CSV file is stored in the below path, then the Import-CSV command will supply us with the below results. Further below in the example, we will import it a second time and store that in the $CSVFile variable. From there, we will take a closer look at it. What do we have once it is imported and stored in our variable?

[PS7.2.1][C:\] Import-Csv -Path 'C:\users\tommymaynard\Desktop\Links.csv'

Title                            Link                                                    Note
-----                            ----                                                    ----
PowerShell GitHub                https://github.com/PowerShell/PowerShell
PowerShell GitHub Issues         https://github.com/PowerShell/PowerShell/issues
PowerShell Docs GitHub           https://github.com/MicrosoftDocs/PowerShell-Docs
Powershell Docs GitHub Issues    https://github.com/MicrosoftDocs/PowerShell-Docs/issues
PowerShell Docs                  https://docs.microsoft.com/en-us/powershell
PowerShell Gallery               https://www.powershellgallery.com
PowerShell Reddit                https://www.reddit.com/r/PowerShell
Twitter Legends @jeffhicks       https://twitter.com/JeffHicks
Twitter Legends @jsnover         https://twitter.com/jsnover
Twitter Legends @concentrateddon https://twitter.com/concentrateddon
TechCrunch.com                   https://techcrunch.com/                                 Tech News
Cnet.com                         https://www.cnet.com/                                   Tech News
Gizmodo.com                      https://gizmodo.com/                                    Tech News
9to5mac.com                      https://9to5mac.com/                                    Tech News
Engadget.com                     https://www.engadget.com/                               Tech News
Wired.com                        https://www.wired.com/                                  Tech News
TechRadar.com                    https://www.techradar.com/                              News
Axios.com                        https://www.axios.com/                                  News

[PS7.2.1][C:\] $CSVFile = Import-Csv -Path 'C:\users\tommymaynard\Desktop\Links.csv'
[PS7.2.1][C:\] $CSVFile.GetType()

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

It is an array! That makes perfect sense, right!? It is a list of values. Each row in the CSV file is a single value, in the array. Sure, each row has the potential of containing multiple values (Title, Link, Note), but each containing row is one single element in the array. The two commands below do the same thing. They return the first row, or element, in the CSV file, as it is stored in the $CSVFile variable.

[PS7.2.1][C:\] $CSVFile[0]

Title             Link                                     Note
-----             ----                                     ----
PowerShell GitHub https://github.com/PowerShell/PowerShell

[PS7.2.1][C:\] $CSVFile | Select-Object -First 1

Title             Link                                     Note
-----             ----                                     ----
PowerShell GitHub https://github.com/PowerShell/PowerShell

Just putting this out there, but we did not even have to use the .GetType() method earlier, to know this was an array. The minute we were able to use a numeric index ([0]), we should have known. I do not know about you, but this got me wondering about two things: One, where did the header row go? As we saw, it had nothing to do with index [0], and two, if each row is an element in an array, what is inside each element? How are the previous columns represented? Let’s answer these.

Before we do, though, a quick reminder: There is really no need to know the things we are discussing to use CSV files and the Import-CSV command. This happens whether we care to know it or not. It is just that I wanted to know what was happening and so I brought you with me. Back to the discussion.

As we have seen, the header row, as we may want to picture it, is gone. It found a place though. Several even. It is stored once per array element, or line or row, if you will, within the $CSVFile variable. What does that even mean!? That is answered by viewing the type of object returned when we view a single element in the array. The below examples use the .GetType() method to return the type of just index [0]. It is a PSCustomObject! We saw the output of this in the last two examples, however, we may have not put it together from just that. Let’s pipe index [0] to the Get-Member command and see what we recognize.

[PS7.2.1][C:\] $CSVFile[0].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    PSCustomObject                           System.Object

[PS7.2.1][C:\] $CSVFile[0] | Get-Member

   TypeName: System.Management.Automation.PSCustomObject

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
Link        NoteProperty string Link=https://github.com/PowerShell/PowerShell
Note        NoteProperty string Note=
Title       NoteProperty string Title=PowerShell GitHub

Each column of the CSV has become a NoteProperty in the PSCustomObject. Each name is a column header and each corresponding value has become a Definition. Each definition is a string that includes an entry such as <header>=<value>. Knowing that it makes sense that each of the following examples indicates that their type is a string.

[PS7.2.1][C:\] $CSVFile[0].Title
PowerShell GitHub
[PS7.2.1][C:\] $CSVFile[0].Title.GetType()

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

[PS7.2.1][C:\] $CSVFile[0].Link.GetType()

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

[PS7.2.1][C:\] $CSVFile[0].Note.GetType()

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

While we do not need to know these things to import a CSV file and use it as a part of an assignment or project, there is value in understanding what the command does with each row and the values in each row of a CSV file.

CSV Browser Links

Edit: At some point between publishing this post and now — 11:43 p.m. on Christmas Eve, Eve — I thought people consuming this post would benefit from seeing the menu open a CSV-based bookmark. A short gif has been added to the bottom of this post.

I do not know what my problem is, but for the last few years, I’ve been an anti-browser-bookmarks person. I don’t really have a good reason as to why, but I look forward to getting over it; I do. But until then, I have been saving all my links inside of a CSV file with the assumption that at some point I will write some PowerShell that will allow me to easily search my CSV file and then open links programmatically. Well, guess what, I finally wrote that.

The first thing sharing this project is going to require is a properly formatted CSV file we can both work from. No need to put mine in here; it would be full of links that are worthless to anyone but me. Copy and paste the comma-separated data below and save that off to a CSV file called Links.csv Be sure to note the path where you chose to save it, as we will work with it as a part of this post.

Title,Link,Note
PowerShell GitHub,https://github.com/PowerShell/PowerShell,
PowerShell GitHub Issues,https://github.com/PowerShell/PowerShell/issues,
PowerShell Docs GitHub,https://github.com/MicrosoftDocs/PowerShell-Docs,
Powershell Docs GitHub Issues,https://github.com/MicrosoftDocs/PowerShell-Docs/issues,
PowerShell Docs,https://docs.microsoft.com/en-us/powershell,
PowerShell Gallery,https://www.powershellgallery.com,
PowerShell Reddit,https://www.reddit.com/r/PowerShell,
Twitter Legends @jeffhicks,https://twitter.com/JeffHicks,
Twitter Legends @jsnover,https://twitter.com/jsnover,
Twitter Legends @concentrateddon, https://twitter.com/concentrateddon,
TechCrunch.com,https://techcrunch.com/,Tech News
Cnet.com,https://www.cnet.com/,Tech News
Gizmodo.com,https://gizmodo.com/,Tech News
9to5mac.com,https://9to5mac.com/,Tech News
Engadget.com,https://www.engadget.com/,Tech News
Wired.com,https://www.wired.com/,Tech News
TechRadar.com,https://www.techradar.com/,News
Axios.com,https://www.axios.com/,News

With our CSV in place, we can work through the below PowerShell and then take it for a test drive. Have a look and meet me below for a quick discussion, before we try it out.

function Find-Link {
    [CmdletBinding()]
    Param (
        [Parameter()]
        $Path = 'C:\Users\tommymaynard\Documents\CSVs\Links.csv',
        [Parameter()]$Title,[Parameter()]$Link,[Parameter()]$Note
    )

    #region Import CSV/filter.
    $AllLinks = Import-Csv -Path $Path
    $ FilterLinks = $AllLinks | Where-Object -FilterScript {
        $_.Title -like "*$Title*" -and $_.Link -like "*$Link*" -and $_.Note -like "*$Note*"
    }
    #endregion
    #region Create link menu.
    for ($i = 0; $i -lt $FilterLinks.Count; $i++) {
        "[$($i + 1)] $(($FilterLinks[$i]).Title)"
    } # for
    #endregion
    #region Prompt user.
    do {
        $Option = Read-Host -Prompt 'Link Number'
    } # do
    until ($Option -in (1..$FilterLinks.Count))
    Start-Process -FilePath "$($FilterLinks[$($Option - 1)].Link)"
    #endregion
}

The function is named Find-Link, and it includes a -Path parameter. This is the location of the CSV file. While the function contains a default path, it can be changed when the function is invoked by passing in a different value. The static entry in the function can also be permanently modified, as well — it is up to you.

Find-Link -Path '/users/landrews/Documents/bookmarks.csv'

When the file is imported, the entire CSV is assigned to the $AllLinks variable. Then, it runs a command against that variable, creating a new variable, to filter down the results using the value(s) potentially passed to three other parameters: -Title, -Link, and -Note. There is more than just the -Path parameter.  It does not check if any of these parameters were actually included, but it could have using $PSBoundParameters. Once we have a filtered list of links to display, we cycle through them using a for loop, which creates a menu of options. Here’s an example of one of the outputs created by invoking this command with the -Title parameter.

Find-Link -Title Twitter
[1] Twitter Legends @jeffhicks
[2] Twitter Legends @jsnover        
[3] Twitter Legends @concentrateddon
Link Number:

The final portion of the function is a do-until loop. This invokes Read-Host prompt until one of the available menu numbers is entered. When a number that is not included is entered, it will prompt the user again for a different value. Here is an example of that.

Find-Link -Title Twitter
[1] Twitter Legends @jeffhicks
[2] Twitter Legends @jsnover        
[3] Twitter Legends @concentrateddon
Link Number: 8
Link Number: 15
Link Number: 4
Link Number: 

When a value is selected that is included from the list, Start-Process invokes the corresponding link. Before we close out, here are a few more examples.

Find-Link -Link powershell
[1] PowerShell GitHub
[2] PowerShell GitHub Issues     
[3] PowerShell Docs GitHub       
[4] Powershell Docs GitHub Issues
[5] PowerShell Docs
[6] PowerShell Gallery
[7] PowerShell Reddit
Link Number:
Find-Link -Note News
[1] TechCrunch.com
[2] Cnet.com
[3] Gizmodo.com
[4] 9to5mac.com
[5] Engadget.com
[6] Wired.com
[7] TechRadar.com
[8] Axios.com
Link Number:
Find-Link -Note 'Tech News' 
[1] TechCrunch.com
[2] Cnet.com      
[3] Gizmodo.com   
[4] 9to5mac.com 
[5] Engadget.com
[6] Wired.com   
Link Number:   

We can combine the parameters too, to further filter the results.

Find-Link -Title GitHub -Link powershell
[1] PowerShell GitHub
[2] PowerShell GitHub Issues
[3] PowerShell Docs GitHub
[4] Powershell Docs GitHub Issues
Link Number: 

There may be a few things to add over time, but for now, this gives me what I wanted. It is better than navigating to the document and copying out a link — never. again.

To Rename a PowerShell Variable

I had one of those thoughts…  you know, a dumb one. As dumb as it might be, it gave some brief inspiration to try something new. It seemed possible and easy enough and so I set out to prove, that as dumb (and useless), as it is, that it could be mine.

First off, on my machine, as it sits here today, I have 40 commands that include the “rename” verb. Additionally, I have five commands that include the “Variable” noun. Do you know what I do not have, though? I will tell you in a second if you have not figured it out yet. If you have figured it out, I will also tell you.

[PS7.2.1][C:\] Get-Command -Verb Rename | Measure-Object | Select-Object -Property Count

Count
-----
   40

[PS7.2.1][C:\] Get-Command -Noun Variable | Measure-Object | Select-Object -Property Count

Count
-----
    5

A Rename-Variable command. And you know why!? Because it is dumb and mostly useless. Regardless, I made it a thing and we will discuss it during this post. In the end, I think it turned out to be a success even though I will never use it again after today. That is what I say now, anyway.

Let’s do this in reverse. We will start with the commands I will invoke and then we will look at my function. My first below command Rename-Variable will send in the variable named TestVariableZero and the name to which we want to rename it, TestVariableZeroZero. This will not work, as we cannot rename a variable that does not yet exist. The function will use Write-Warning to inform us that it cannot be renamed. I could have used this section of the if-else to create the variable, but that is not today’s assignment. The next two lines will one, create the variable TestVariableOne with the value ValueOne and two, return information about the variable, so we know it has been created.

We are using the Force parameter in the first of these two commands in case the variable already exists. New-Variable cannot create a variable that already exists by default.

Rename-Variable -Name TestVariableZero -NewName TestVariableZeroZero

New-Variable -Name TestVariableOne -Value ValueOne -Force
Get-Variable -Name TestVariableOne

Rename-Variable -Name TestVariableOne -NewName TestVariableTwo

Get-Variable -Name TestVariableTwo
Get-Variable -Name TestVariableOne

Looking upward, we have three more commands to discuss. This Rename-Variable command will attempt to rename the TestVariableOne variable to TestVariableTwo. After that operation, we have two Get-Variable commands. The first will prove that we now have a TestVariableTwo variable and the second, that we no longer have a TestVariableOne variable. How fun right,… even though renaming a variable should not require its own command. With that, let’s take a look at the function.

function Rename-Variable {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        $Name,
        [Parameter(Mandatory)]
        $NewName
    )

    if (Get-Variable -Name $Name -Scope Global -ErrorAction SilentlyContinue) {
        Set-Variable -Name $NewName -Value (Get-Variable -Name $Name -ValueOnly -Scope Global) -Scope Global
        Remove-Variable -Name $Name -Scope Global
    }
    else {
        Write-Warning -Message "Unable to locate a variable with the name of $Name."
    }
}

You will be glad to know that not a lot of work went into writing this; it is very simple and very straightforward. Our function accepts two parameters: Name and NewName. Simple. We use Name to search for our existing variable and NewName to change its name. Notice the use of the -Scope Global parameter and parameter value. Without it, the variables that we would be working with inside the function would only be scoped to the function; they would not exist outside of the function. Providing the variable exists, we use Set-Variable inside the function to create a new/set an existing variable with the new name and take the value from the old variable and assign it as the value to the new variable. Once the new variable has been created and the old value assigned to it, we remove the previously named variable. Let’s try out the commands we saw at the beginning of this post.

Here is what happens when we try and rename a variable that does not exist.

[PS7.2.1][C:\] Rename-Variable -Name TestVariableZero -NewName TestVariableZeroZero
WARNING: Unable to locate a variable with the name of TestVariableZero.

So we can test out our function, let’s create our TestVariableOne variable and prove to ourselves that it has been created and that it now exists.

[PS7.2.1][C:\] New-Variable -Name TestVariableOne -Value ValueOne -Force
[PS7.2.1][C:\] Get-Variable -Name TestVariableOne

Name                           Value
----                           -----
TestVariableOne                ValueOne

Now, it is time to rename our variable from TestVariableOne to TestVariableTwo. The first command does just that. The next couple of commands proves that TestVariableTwo now exists with the proper value and TestVariableOne does not exist.

[PS7.2.1][C:\] Rename-Variable -Name TestVariableOne -NewName TestVariableTwo
[PS7.2.1][C:\] Get-Variable -Name TestVariableTwo

Name                           Value   
----                           -----   
TestVariableTwo                ValueOne

[PS7.2.1][C:\] Get-Variable -Name TestVariableOne
Get-Variable: Cannot find a variable with the name 'TestVariableOne'.

And that is it. If you are ever looking to rename a variable, using a built-in -Variable command, you are not going to find it. You can roll your own, however, if you really want to do that.

Variables, and More About Note Properties

I have been thinking a lot about variables and note properties recently. You know that, if you read A Better Way to Solve the Same Problem. It sets the stage for what we are going to see today. The question that started that post was, whether or not I could add note properties to a single variable, such as PowerShell does with the $PROFILE variable. I could. I ended up creating a variable that held one URL when you returned the variable but also held four other URLs, one in each additional note property.

I want to take this further, and the best part is that all the things I was worried about trying to do myself, PowerShell just does for me without any thinking about it. Follow along and you will see.

Let’s set up the variable I was using last time to help this recap. Then we will create another variable and work with it to explore some new things I learned. Building this variable is being done a little differently than it was previously. It incorporates some concepts from another recent post, There is a Difference: Arrays Versus Hash Tables. First, we will assign our initial value to the variable.

$PSSites = 'https://docs.microsoft.com/en-us/powershell/scripting/overview'

Next, we will create a hash table that includes four key-value pairs. Each pair includes a website name and an associated URL, because as we learned in that post, a hash table is an associative array.

$Sites = @{
    GitHub  = 'https://github.com/PowerShell/PowerShell'
    Docs    = 'https://docs.microsoft.com/en-us/powershell/'
    Gallery = 'https://www.powershellgallery.com/'
    Reddit  = 'https://www.reddit.com/r/PowerShell/'
}

Following that, we will use the foreach statement to loop through the key-value pairs in the hash table, running Add-Member against each of them, and thusly adding each as a note property on our $PSSites variable.

foreach ($Site in $Sites.GetEnumerator()) {
    $PSSites = $PSSites | Add-Member -NotePropertyName $Site.Key -NotePropertyValue $Site.Value -PassThru
}

We can use Get-Member to ensure the previous command added the additional URLs as note properties.

$PSSites | Get-Member -MemberType NoteProperty

   TypeName: System.String

Name    MemberType   Definition
----    ----------   ----------
Docs    NoteProperty string Docs=https://docs.microsoft.com/en-us/powershell/
Gallery NoteProperty string Gallery=https://www.powershellgallery.com/
GitHub  NoteProperty string GitHub=https://github.com/PowerShell/PowerShell
Reddit  NoteProperty string Reddit=https://www.reddit.com/r/PowerShell/

At this point, we have our own variable like the $PROFILE variable that contains multiple string values. We can access the note properties of $PSSites using dot notation.

$PSSites.GitHub
$PSSites.docs
$PSSites.Gallery
$PSSites.Reddit

https://github.com/PowerShell/PowerShell
https://docs.microsoft.com/en-us/powershell/
https://www.powershellgallery.com/
https://www.reddit.com/r/PowerShell/

Now, let’s create another variable and add some members to it, as well. These will be a little different and you will see that momentarily. We will start by assigning a string value to the variable, as we did above.

$TestingVariable = 'A string value'
$TestingVariable
A string value

Before we add the rest, I suppose I should remind everyone about my end goal written at the bottom of the first post linked above: Save the previous value of a variable to a note property when assigning a new value to the variable itself.

This would be much easier if we could guarantee that every new value assigned to the variable was a string, and therefore, every old value property would also be a string. My worries, as mentioned earlier, were about arrays, hash tables, ordered dictionaries, and other data structures I may have to save to a note property. Can other data structures like those be put in a note property? Let’s work through these data structures and see if we can get them into note properties, to begin with. In this example, we will add two arrays to two separate note properties, as Array01 and Array02. One of these will and one will not use the array, sub-expression operator (@()).

$TestingVariable = $TestingVariable | Add-Member -NotePropertyName 'Array01' -NotePropertyValue 1,2,3 -PassThru
$TestingVariable = $TestingVariable | Add-Member -NotePropertyName 'Array02' -NotePropertyValue @(4,5,6) -PassThru

Here, we will create a hash table and store it in the $HashTable variable. We will then use that variable as a part of another Add-Member command.

$HashTable = @{Dad = 'David'; Mom = 'Betty'; Daughter = 'Janice'; Son = 'Bryan'}
$TestingVariable = $TestingVariable | Add-Member -NotePropertyName 'HashTable' -NotePropertyValue $HashTable -PassThru

Finally, we’ll create an ordered dictionary and add it as a note property, as well. If the hash table takes, then it is probably safe to assume this will take, too.

$OrderedDictionary = [ordered]@{
    Monday    = 'Chipotle'
    Tuesday   = 'Pizza'
    Wednesday = 'Mahi Mahi';
    Thursday  = 'Ravioli'
    Friday    = 'Orange Chicken'
}
$TestingVariable = $TestingVariable | Add-Member -NotePropertyName 'OrderedDictionary' -NotePropertyValue $OrderedDictionary -PassThru

At this point, we should probably see what Get-Member returns about the note properties we have assigned to our $TestingVariable. We have yet to test that anything has actually been added so far, although we have not received any errors stating otherwise. In the below example, we can see the four, note properties that we have added. The best part, the definition property indicates it knew the data structure of these when they were added as note properties. The arrays show object[], the hash table, hashtable, and the ordered dictionary, OrderedDictionary.

$TestingVariable | Get-Member -MemberType NoteProperty

   TypeName: System.String

Name               MemberType   Definition
----               ----------   ----------
Array01            NoteProperty Object[] Array01=System.Object[]
Array02            NoteProperty Object[] Array02=System.Object[]
HashTable          NoteProperty hashtable Hash Table=System.Collections.Hashtable
OrderedDictionary  NoteProperty OrderedDictionary Ordered Dictionary=System.Collections.Specialized.OrderedDictionary

Let’s take a closer look at each of our new note properties. For each of them, we will run GetType() against the note property and then return the value it has stored. When we return a note property that contains an array, it returns that. Same for the others; the data structure was recognized and retained when it was added as a member. And it is respected on the way out, too. I cannot tell you how grateful I am that I do not have to be the one to figure out the underlying data structure of a value going in and then coming back out. This is great news for some of the other things I have planned.

$TestingVariable.Array01.GetType()

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

$TestingVariable.Array01
1
2
3

$TestingVariable.Array02.GetType()

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

$TestingVariable.Array02
4
5
6

$TestingVariable.HashTable.GetType()       

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

$TestingVariable.HashTable

Name                           Value
----                           -----
Daughter                       Janice
Son                            Bryan
Dad                            David 
Mom                            Betty

$TestingVariable.OrderedDictionary.GetType()

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

$TestingVariable.OrderedDictionary

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