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.

PowerShell Painting

I have no idea what you thought you might be getting into when you followed a link to this post. Maybe some painting in the ConsoleHost, because if that is not a thing, it should be. Anyway, that is not what this post is about.

My daughter has come up a few times during my years of writing here. Poor girl. In the first below picture, she is doing an art project when she was probably five. She does not know much about PowerShell, other than it is something her father is passionate about. Hearing me say it a time or two or two-thousand has apparently influenced her. If someone asked her what her dad does for work, she would probably say “PowerShell,” before “computers.”

This picture is from her playing the 1 to 100 Game in 2016. There was also a recent post where she was mentioned called Testing Multiplication Facts. That was a PowerShell tool made for her.

So back to painting. Today we did a painting project together. I wanted to see how she would do painting the PowerShell Girl again after painting her two years ago.

To begin, the first thing we did was trace a printed image on two canvases. Luckily she found the template with the art supplies. I did not want to make that again or attempt to draw it freehand. It may be hard to tell, but even with the light coming in it was not easy to trace.

This next picture is from the table we were going to use, but we had to relocate. Still, good photo.

Here is the second table where we set up shop. Everything was ready to go.

After a solid 45 minutes to maybe an hour, we were done. Here are her two paintings. The one from 2020 and the one from today! I think we would have both wanted the hair to be less black and more purple, but it was the best we were able to do with the paints this time. She was thrilled with her newest painting. Maybe we will do it again in 2024.

There are plenty of passionate dads out there. My passion just happens to be technology, computers, automation, and most of all, PowerShell — so these types of things happen. It was quality time spent making art for the home office. And now that the office is back together — there was a flooring project that just ended — it is time to get the older, and now newer, paintings on the wall. Sorry, that there is not much to learn here this time. I figured why have just a picture on Twitter when can have a post here, too. Back with more PowerShell soon — the way you would expect to find it.

Learning PowerShell

Every few weeks or so, over the last 10 years or so, there is a new post somewhere, where somebody is asking about the best way to learn PowerShell. It has probably been a thing beginning in 2006. While I have not been evaluating this since then, I have noticed it long enough to feel confident in this statement. So confident that I have heard it for a long time, that I will remind everyone that I even wrote about it in 2016. In the time since the initial PowerShell launch, books have been written, videos have been recorded, new domains established, and web hosting purchased too. All of these have been done in the name of teaching other people PowerShell.

For all the posts I see where someone is looking for the best way to learn, I cannot recall hearing anyone mention the official PowerShell documentation. Okay, I saw a link somewhere recently, but it is not often mentioned. I have been spending some time there recently and it is undeniably thorough. It is well-written, explained clearly, and written for the newest of PowerShell users. The examples start with only the basic structures required. Not everyone out that does that, and it can be frustrating at times. I have been there, where I only want the simplest of examples from which to work. But some of the content out there is wildly complex and is obviously intended for an advanced audience. While I have always tried to write intermediate content with basic examples, not everyone has and that is fine.

Here is a link to the PowerShell About topics. Maybe spend a moment there right now. I dare you to challenge yourself to learn one new thing before you continue reading this post. On the linked page, scroll down until you find a topic that is not that familiar to you and read it — maybe even just a paragraph or two is all you will need. Stop when you learn something new. I will do the same and share what I learned…

For me, I scrolled down to about_Debuggers, where I learned that the Get-PSCallStack command is considered one of the Debugger Cmdlets. I know the command and have even written about it myself, but I never considered that it would be a debugging tool, although it makes perfect sense that it would. There is even documentation on all the, Microsoft-written commands. Here is where you can read more about Get-PSCallStack.

Here is my post on that command — I do not even mention debugging, so yes, I learned something new. It took less than five minutes. Hopefully, you really played along and learned something too. You are welcome to Tweet me what you learned and that way we may all learn something from it. Be careful though, learning and teaching people about PowerShell can be addicting.

Another challenge is that if you think you know a PowerShell topic, take a minute and read top to bottom on one of the About pages. You are probably going to learn something new. I was reading about the -replace operator recently and learned a few things I had never considered or known, simply because I have never needed to use it that way. Good to have read before, as someday I may be out there searching for a solution.

If you want to learn PowerShell, do not forget about the product’s own documentation; it has been written for you, by the people intimately involved in PowerShell. Like, it is their job. They create the resources that you can use to completely learn PowerShell — and it is all free. Maybe read two a day. Your job may provide some room for personal development, so read one to start the day and one to end it.

Changing direction a bit, the next thing I want to discuss is a script I wrote years ago. It is available on the PowerShell Gallery and it is called the Start-1to100Game3.0. If you are wondering, yes, it still pains me that I included the version of the script in the script’s name. Let that be a lesson.

This project was awesome. PowerShell chooses a number and then you select higher or lower until you guess the number. Simple concept, but well written. It kept totals, allowed continuous gameplay. I wrote about it many years ago, so read about it here if you want to know more.

You can download the poorly-named script using the below command.

Install-Script -Name Start-1to100Game3.0

By default, the script is going to be downloaded to the Scripts directory. On my computer, that is C:\Users\tommymaynard\Documents\PowerShell\Scripts. As you may know, you will have to dot source the script (as it is written as a function). This will allow you to run the function in the current scope. There is more information about this in about_Scripts. Here is the information you might be after on that page:

The dot sourcing feature lets you run a script in the current scope instead of in the script scope. When you run a script that is dot sourced, the commands in the script run as though you had typed them at the command prompt. The functions, variables, aliases, and drives that the script creates are created in the scope in which you are working. After the script runs, you can use the created items and access their values in your session.”

I could not have said it any better myself. So, keep in mind the About pages for learning PowerShell and enjoy a quick game of the 1 to 100 game — why not?

But, before we wrap up, what do these two things have in common!? Why have I placed these two topics — learning PowerShell using the About topics and my 1 to 100 script in the same post? Hopefully, I will be telling you soon. I am doing that thing where I am writing posts here before I write the PowerShell to back them up. So…, as long as I complete that project, some of this may actually make sense. And if it does not, at least we all got a reminder about the PowerShell About Topics and learned about an interactive game you, or your little one(s) — if you have those — can play. At this point, perhaps I will be back with more!

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

It is All About the PowerShell Users

There was a recent series of Twitter posts from Jeffery Snover, the inventor of PowerShell himself. It started with the 15th birthday of PowerShell, which happened recently. I think I have captured it all here; have a read. You may have missed it and … I do not think that anyone should.

Jeffery said, “The thing I am most proud of is our unwavering and complete focus on our users – it is about them not us.” It goes on, “Notice that I used the word “user” and not “customer”. Why? Because PowerShell is free. It is a benefit of being a Microsoft customer. That distinction is critical.”

That was what allowed us to backport to previous OSes instead of using it to force people to the latest version of Windows. That was what allowed us to ship V2, V3…V7 when some thought we should stop after V1. That was what allowed us to open source our code.

There was more, That was what allowed us to partner with VMWare, Amazon, Google and many other customers that our users also used. That was what allowed us to port to Linux. And so many other things. I honestly cannot think of a more USER-centered project in the entirety of MSFT history. PowerShell is about our USERS not about us THAT is what I am most proud of.

I get that PowerShell has long been an investment in users. Back in 2013, Jeffery wrote this Tweet: “The team made a promise so important, we called it our sacred vow: Learn PowerShell and we’ll do everything we can to make it the best investment you’ve ever made. It’s 13 years later and I can confidently say that we have kept faith with that vow.

This was obviously a couple of years ago.

I bring all this up because, for me, it has been about the users too. I am into PowerShell so much, that I do what I can to bring others along with me. Microsoft has given us a tool to automate, and in doing so, given us a way to become highly efficient, and to ensure accuracy. Those are the words to live by when it comes to PowerShell: efficiency and accuracy.

So, if you didn’t get to read these comments from Jeffery Snover before, I am glad you have now! Hopefully, they will stick in your brain, as they do mine.

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.