Category Archives: Quick Learn

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

Hash Table to CSV, Revisited

I started writing about PowerShell in June 2014 at tommymaynard.com. While I’m not a Microsoft MVP, I’m certainly working to obtain Microsoft’s longevity award. While the distinction doesn’t actually exist, this year will mark six consistent years of reading, researching, spending my time with PowerShell, and giving back to the community in my written word. With the combination of my site and this one, I’ve authored somewhere close to 350+ articles, or posts.

What I’m really out to say here, is that in my time, I’ve noticed which of my writings have been consistently popular. There’s a particular post on my site that sees the largest amount of consistent visitors: It’s Hash Table to CSV. I’ve been looking for a way to take its personal success and provide more to the reader about the topic. That’s what we’re doing today with this post. I put a link to the original post on Twitter recently, and someone provided me an idea for an update — whether they realized it or not. It’s simple, but whatever. As proven, those are sometimes the most popular posts.

Before we get back into this, let’s remember how this all started. It started here: Hash Table to CSV. It’s always been a popular post, but just in case you didn’t follow this over from PowerShell.org, then you need to know this is a forward movement on that 2018 article.

I was sitting around and wondered how to best get a hash table into a CSV file. Maybe it was for configuration; I still don’t recall. Well, my option wasn’t the only option, and so today I’ll pass along something someone on Twitter pointed out that I hadn’t considered myself. We can use [PSCustomObject], introduced in (Windows) PowerShell 3.0.

I’ve long used this to create custom objects for its speed and insertion order preservation (over New-Object), but it didn’t occur to make use of it when getting a hash table into a CSV. It does work, with less work, so let’s try it on. While I won’t show the previous option, revisit the first article on this topic to see the old way; here’s the new way. We’ll start with our previously used hash table.

$HashTable = @{
    Name = 'Tommy'
    StreetName = 'Vista Pl.'
    City = 'Tucson'
}

$HashTable
Name                           Value
----                           ----- 
Name                           Tommy
StreetName                     Vista Pl.
City                           Tucson

Once that’s populated, instead of using the hash table’s GetEnumerator method, we’ll use [PSCustomObject]. This converts each entry in our hash table into an object, and sends the object to the Export-Csv cmdlet. We get what we expect in our CSV file without the additional work scripted into the first hash table to CSV article from 2018.

[PSCustomObject]$HashTable |
    Export-Csv -NoTypeInformation -Path .\Desktop\NewHashToCsv.csv

And, that was it. Quicker, easier, and more likely to be remembered, long term.

Edit: It was pointed out to me that this didn’t produce the exact information in the CSV from 2018. Instead of having a Key and Value properties, it included a property/column for each of the hash table’s keys: Name, StreetName, and City. Honestly, I think I like this better, as it reminds me of what any command that returns objects in PowerShell would do for us. If I were to read it back into the host (via Import-Csv), my output from the operation would be more in line with what PowerShell commands produces anyway: objects with properties. Thank you for the observation, Mike!

PowerShell Function with a Function Full of Functions

Yeah, you read that title correctly. Or did you? You might want to double-check; I had to write it a few times.

I did that thing where I authored some PowerShell code and I don’t know what to do with it. While it was written during some updates to a common function template, I’m not yet sure it’ll be implemented there. Therefore, I figured I’d share it. Additionally, this will give me a place to store it, in case I do use it. Some day you may ask yourself, can I create a function that creates a global function, that contains other functions? Yes. Today, I intend better help answer that question and include an example.

All this said I’m not here to explain why you might want to do this. Again, I’m not even sure if I’ll need this. So Internet and you, it’s yours. Edit: Some time has passed since I wrote this opening, so yes, I’ll be using this in production.

I have a function template that I use when writing any new function(s). Its number one goal is to ensure that all my functions, or tools, include identical logging. It’s identical, whether it’s written to the screen, to a file, or to both. I feel like I’ve written that before.

When the function is complete, and it’s written a log file, I have a global function that is created, too. This allows you to quickly open the log file, open just the location of the log file, or read in the contents of the most recently created log file and create an object from each line. If a single function is created, then the user isn’t required to remember more than a single function name. Sure, they might have to remember the parameter values for the single functi– okay, no they wouldn’t. They can tab through those.

Code Example

Function Show-FunctionTemplate {
    'You''re inside the Function Template (1st).'

    Function Global:New-FtFunction {
        Param (
            [Parameter()]
            [ValidateSet('1','2','6')]
            [array]$Function
        ) # End Param.

        Switch ($Function) {
            '1' {Function Test-One {'Test-One'}; Test-One}
            '2' {Function Test-Two {'Test-Two'}; Test-Two}
            '6' {Function Test-Six {'Test-Six'}; Test-Six}
        } # End Switch.
    } # End Function: New-FtFunction.

    'You''re inside the Function Template (2nd).'
} # End Function: Show-FunctionTemplate

Show-FunctionTemplate

New-FtFunction
New-FtFunction -Function 1
New-FtFunction -Function 1,2
New-FtFunction -Function 1,2,6

Discussion

The overall goal in this code is that we’ve created a function named Show-FunctionTemplate, in our local, and in this case, global scope. Done.

Inside this function (when it’s invoked), we echo a couple of statements about being inside the function. Additionally, and in between those two statements, we create a second global function named, New-FtFunction. When Show-FunctionTemplate is done executing, New-FtFunction can then be invoked.

When the New-FtFunction is invoked it won’t do anything unless one of the parameter values that it’s expecting is included. In the above code sample, I’ve included some examples. The below image shows the output returned from executing these.

Output of the New-FtFunction function.

Perhaps it should be noted, but the most nested of functions, both define the function and invoke it — one right after the other. But in obvious news now, I can do it. I can make a (global) function, make a global function responsible for making and invoking other functions. I think this is going into my function template. I really do. Edit: It did.

PowerShell’s Get-Date FileDateTime for Safe Filenames

Since about the beginning of time now, I’ve used Get-Date to create my own safe filenames. It’s a date and time format that is safe to be used as/in a file’s name without including anything that can’t be. Here’s what I’ve long used.

Get-Date -Format 'DyyyyMMddTHHmmss.fffffff'

That’ll produce something such as this.

D20191211T201315.1474900

Okay, maybe I haven’t always used this, but I certainly used a variation. It was after the beginning of time, if you will, that I switched to this version, which includes the dot and the seven Fs. The Fs (in lowercase [it’s important]) display milliseconds. I added these to help ensure that filenames that include the date and time were more likely to be unique. You can imagine. You create more than one file in the same second, and you run up against a file already existing error if you don’t include milliseconds. Before we continue, let’s ensure all the other letters represented in the above code example are included here:

I should mention, that using this format ensures proper file sorting and ordering. That can be important. It’s always been there (or it’s likely, at least), but it turns out that there’s an easier way than what I’ve been doing.

I can still learn something new that I hadn’t known before, such as using Get-Date differently. I can now create safe filenames that include the date and time, even after I’ve spent several dedicated years of using and writing about PowerShell. Maybe it was forgotten. I doubt that in this case. That said, it is possible to overlook easier, and potentially better ways of doing things once you have a solution for something.

Instead of creating safe filenames using Get-Date, the Format parameter, and .NET format specifiers (as they’re called), I recently noticed that someone — likely at Microsoft, as this has probably been around a while — took care of this for us.

The Get-Date cmdlet includes a couple, notable parameter values that can be used with the Format parameter. Two we’ll discuss are FileDate and FileDateTime. Take a look at this; it returns the date, without the time.

PS> Get-Date -Format FileDate
20191211

The date without the time is not exactly what I was after — the time is vital in my mind. Therefore, let’s repeat this example with the FileDateTime Format parameter value.

PS> Get-Date -Format FileDateTime
20191211T2015018186

Oh, look — there it is That’s the built-in way to ensure a unique and safe filename that includes date and time without the need of using my earlier example. It’ll probably be easier to remember that than this value: ‘DyyyyMMddTHHmmss.fffffff’. Do notice that the decision at Microsoft, because again this was probably written before PowerShell was open-sourced, is that four digits for milliseconds is likely suitable. I’m even not sure why I chose to use seven digits, anyway. Meh.

And, if you want to find out about uniqueness for sure — as I did — put it in a loop. The below example creates 25 safe (files and) filenames. It seems to work just fine for me.

PS> 1..25 | ForEach-Object {New-Item -Path '.\Documents\Test' -Name "$(Get-Date -Format FileDateTime).txt"}

    Directory: C:\Users\tommymaynard\Documents\Test

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        12/11/2019  8:30 PM              0 20191211T2030184424.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184471.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184486.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184507.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184521.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184535.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184548.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184561.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184577.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184593.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184614.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184630.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184652.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184669.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184687.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184704.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184721.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184738.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184759.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184784.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184804.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184850.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184866.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184883.txt
-a----        12/11/2019  8:30 PM              0 20191211T2030184899.txt

Not one naming collision using only four milliseconds. I’m sold. I won’t ever forget you FileDateTime.

A PowerShell Recursive Function

Note: There is a newer post on tommymaynard.com about a recursive function I wrote. When you are done reading this post, read that one.

I was recently in a position where I was asked to provide an example of a PowerShell recursive function. Unfortunately, I didn’t have time to do that as it was a part of a written and timed test. Sadly, I didn’t even really have time to review the other 50-some answers I had already provided. I mostly knew that might happen.

Anyway, the challenge has been eating at me, so why not write an example and share it. It’ll be good for you, as it will be for me too. It’s out there, but it’s not often utilized that I’m aware. A recursive function is a function that calls, or invokes, itself. For real. We’re not referring to a function, or cmdlet, that includes a Recurse parameter. Now that said, it’s possible that some of these commands, with that parameter, do this (call themselves), but I’ve yet to take a look to see for sure.

After you see today’s example, you’ll begin to understand why you might prefer to do things this way. If you can wrap your head around the concept, you’ll understand that it can reduce a portion of code writing on your part.

As stated, “A recursive function is a function that calls, or invokes, itself.” On that note, let’s first set up the folder structure necessary for this example. While you can do this with PowerShell, I simply didn’t bother. As you can see below, there’s a “Test” folder inside my “Documents” folder. That folder contains three files — A, B, and C.ps1. Additionally, it contains a nested, “More” folder, which contains three more files — D, E, and F.ps1.

So there’s that. Now, let’s take a look at and discuss our PowerShell recursive function.

Function Get-Ps1File {
    Param ($Path= '.\')

    Get-ChildItem -Path $Path | ForEach-Object {
        If ($_.Name -like "*.ps1") {
            $_.Name
        } ElseIf ($_.PSIsContainer) {
            Get-Ps1File -Path $_.FullName
        } # End If-ElseIf.
    } # End ForEach-Object.
} # End Function: Get-Ps1File.

This function loops its way through a path and returns the file(s) that are .ps1 files. If there’s a nested folder, it then goes into that folder and runs itself again (against the content of that folder). Knowing what we know about this folder structure and its files, the below results returned from running this function should make perfect sense.

Get-Ps1File
D.ps1
E.ps1
F.ps1
A.ps1
B.ps1
C.ps1

Let’s recap on this a bit with a little more detail. When the function is first invoked, it uses the current path, as none was supplied. It then runs the ForEach-Object cmdlet against the files in the base folder. Using the If portion of the If-ElseIf statement inside the loop, it determines it should return the A, B, and C .ps1 files. Then, on the fourth iteration through the loop, it hits our “More” folder and determines it’s a folder. There are other ways to determine if it’s a folder, but I stuck with the tried and tested, PSIsContainer NoteProperty.

At this point, it invokes itself against the path of the “More” folder. It runs or executes itself, again. This time, however, it uses the folder’s FullName property (its full path). Inside that second invocation of the function, it iterates over the files inside the “More” folder, echos them to the screen as output, and then ends the function’s second invocation. At that point, it returns back to the first invocation, where it then ends that invocation, as well, as its work is completed.

If you’d prefer to see this sorted, pipe the function name (Get-Ps1File) to the Sort-Object cmdlet.

Get-Ps1File | Sort-Object
A.ps1
B.ps1
C.ps1
D.ps1
E.ps1
F.ps1

And there you go, an example of a PowerShell recursive function. You may not need it often, but you may need it someday.

 

Countdown Options

In a somewhat recent post here — that I can’t remember off the top of my head — I put something together much like the below example. It completed a countdown from 10 to 1 seconds (or did it count up?), paying attention to whether it should use the word “second” or “seconds.” I’m not going to chase down that post, but do take a look at the example.

10..1 | ForEach-Object {
    If ($_ -eq 1) {
        "$_ second"
    } Else {
        "$_ seconds"
    }
    Start-Sleep -Seconds 1
}

Here’s what that looks like once it’s done executing. Do notice that each string includes seconds up until we get down to one second. It works as expected!

10 seconds
9 seconds
8 seconds
7 seconds
6 seconds
5 seconds
4 seconds
3 seconds
2 seconds
1 second

Before we move on, if you find this in my code, you’ll likely see it use substantially less amount of lines. Like one. While I pride on myself on readable code, something this simplistic and of little consequence need not be on full display at times — it’s up to your discretion.

10..1 | ForEach-Object {If ($_ -eq 1) {"$_ second"} Else {"$_ seconds"}; Start-Sleep -Seconds 1}

I got to thinking (due a project — which is typical), what if we didn’t move downward in the console as we did the count down? Is it possible to overwrite the last countdown indication? What I mean is, is it possible to overwrite 10 seconds, with 9 seconds, and so on? if you’re reading this, then yes, it’s possible.

10..1 | Foreach-Object {
    If ($_ -eq 10) {
        $Cursor = [System.Console]::CursorTop
    }
    [System.Console]::CursorTop = $Cursor
    '{0:d2}' -f $_
    Start-Sleep -Seconds 1
}

The following gif indicates how the above code executes. You can see that the countdown occurs right over the top of itself. There’s no moving downward in the console, and for me, that’s preferred. Do notice that we’re using the the -f formatting operator. I bring this up, because it’s helping me deal with a bit of a problem. If we didn’t use it, and used single digits for the values 9 through 1, we’d have the left over zero from 10 to the right of each single digit. That would make 9, 90, it would make 8, 80, etc. There’s probably a better way to handle this obnoxiousness, but this is the option I went with this evening. I’ll keep this in mind, and perhaps update this post if I determine a better way to avoid double digit, single digits.

In this example, we’ve combined the previous two. We’re including the string values of “seconds,” and “second” in our countdown. I’ve also included a gif for this example further below.

10..1 | Foreach-Object {
    If ($_ -eq 10) {
        $Cursor = [System.Console]::CursorTop
    }
    [System.Console]::CursorTop = $Cursor
    If ($_ -eq 1) {
        "$('{0:d2}' -f $_) second"
    } Else {
        "$('{0:d2}' -f $_) seconds"
    }
    Start-Sleep -Seconds 1
}

Take a look at this gif, and then we’ll discuss it. Ugh.

Did you see it? The problem I mentioned previously is back. It needs to be dealt with now, not some time in the future, which was what was preferred. When it goes to 1 second remaining, our final “s,” in the plural form of seconds, sticks around and gives the appearance the code is failing in a manner different than one might expect at first.

I wanted to get this post published, so let me show you what I did. I’ll do my best to get back to this post later, but for now, here’s the “fix.”

        10..1 | Foreach-Object {
            If ($_ -eq 10) {
            $Cursor = [System.Console]::CursorTop
            }
            [System.Console]::CursorTop = $Cursor
            "Seconds remaining: $('{0:d2}' -f $_)"
            Start-Sleep -Seconds 1
        }

Yeah, you saw that correctly. I’ve moved the numeric values to the end, or right-most, portion of our string. This means that we’re not going to leave the final “s” in seconds. Even so, we do have to continue to use double digits for single digits (the whole leading-zero thing). I can live with that for now. Have a look. Now to add this to my project, so the user can have something to do while these seconds pass before a restart.

Run One from the Other

It wasn’t but a few days ago that I saw something that piqued my interest. It was an example of running a command against PowerShell Core (pwsh.exe) from Windows PowerShell (powershell.exe), or maybe it was the other way around? Whichever, I’ve included an image that shows this working in both directions. That’s to say it’s an example of running the same command in both versions of PowerShell, from each version of PowerShell.

I do suspect the picture will help explain things, if that last paragraph didn’t.

In the above image, we can see that we’ve run a command against both Windows PowerShell (5.1) and PowerShell Core (6.1). In both consoles, we’ve run the same command; it’s included below. Its goal was to return the version of PowerShell, for both versions of PowerShell.

'powershell.exe','pwsh.exe' | ForEach-Object {
    & $_ -Command {$PSVersionTable.PSVersion}
}

Our results are identical; we knew, we were running 5.1 and 6.1. Neat, right!? Keep this trick in your back pocket, as I do suspect it may be helpful for one of us, one day. Maybe it won’t have anything to do with obtaining the version, of a version of PowerShell, and instead someone will find another use.

Here’s a start. The thought I had was, can I run an Active Directory command in Windows PowerShell (powershell.exe) from PowerShell Core (pwsh.exe)? You bet I can.

In the below example, I’ve returned my GivenName from Active Directory using Windows PowerShell, from PowerShell Core. That could be potentially helpful for someone in some yet-to-be-thought-of project.

Especially in the case of Active Directory, it’s important to remember that each time a command is run, it must import the Active Directory module. Consider that each command is spinning up a new powershell.exe process. For all we know, this may be the reason why the WindowsCompatibility module uses PowerShell sessions, and not PowerShell processes.

In this next example, we issue a Windows PowerShell only command that’s tied to a builtin Microsoft Windows PowerShell model. We issue the Get-LocalUser cmdlet out of the Microsoft.PowerShell.LocalAccounts module. While it’s still importing a module into a powershell.exe process, it loads quicker than the ActiveDirectory PowerShell module did.

I’m going to need to call this a night here soon, but I keep finding more things to try. Like this example. There’s two commands. The first one runs Windows PowerShell, which runs PowerShell Core to determine the version of PowerShell Core.

The second one runs Windows PowerShell, which runs PowerShell Core, which runs Windows PowerShell to determine the version of Windows PowerShell. The image for these examples might be easier to understand, too.

Alright, that’s it for now. Perhaps I’ll come up with some other ideas to try another day. I need to put this post, and me, to bed.

Command Type and Name

Sometimes I just need to write something down somewhere. That way, I might be able to find what I need when I need it. Have you been to my website? If so, then just maybe you already know this. Over the last many years, I’ve done this repeatedly. It’s basically why this place exists. Best part, it’s not just for me. It’s for me and the 196.5 people that have visited daily over the last 20 days, and those before them. That excludes weekends, of course, as only some of us PowerShell on the weekend.

As you may know, we have way to determine a command type and command name (think function and function name), from within a command itself, as it executes. I’m tired of looking for this code inside something I’ve already written, or figuring out again, and so here we are. I’ve given myself another chance to “remember” it sooner. Here’s how it’s done.

Function Get-CommandInfo {
    $CmdType = "$($MyInvocation.MyCommand.CommandType)"
    $CmdName = "$($MyInvocation.MyCommand.Name)"
    "The name of this $($CmdType.ToLower()) is $CmdName."
} # End Get-CommandInfo.

In the above function, we create two variables — $CmdType and $CmdName. These hold, as you might expect, the type of command we’re executing and the name of it, as well. These variables are made possible due to the $MyInvocation automatic variable. This variable holds a great deal of information. As you can tell, we’ve used the CommandType and Name nested properties that reside inside the MyCommand property. We then echo a string to include the two values we’ve derived from this variable.

PS > Get-CommandInfo
The name of this function is Get-CommandInfo.

And, there it is; the type and name returned. Now to remember that I’ve written about it here, for that next time I don’t feel like exploring the $MyInvocation variable again, or tracking down the use of these properties somewhere else. Enjoy your Wednesday.

Making Dates: Good and Better

I have a new task. Review some old code written in the time of (Windows) PowerShell 2.0 ensuring I can support it, in case that’s ever needed.

Well, I started last week. While I haven’t been able to get back to it just yet, as some other work came up, I did code a potential change. First off, before you see this code, I didn’t write the original myself. Second, the person that did, would probably do if differently now, too. I’m not here to dis on anyone’s five-year-old code. It’s quite old, in this industry.

Here’s the code as I found it:

$Today = Get-Date
$DateArray = (Get-Date $Today.AddDays(-59) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-58) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-57) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-56) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-55) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-54) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-53) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-46) -Format "M-d-yyyy")

It’s a bit tough to follow. Let me use the included commas as line breaks. Do keep in mind that we can always move to the next line after a comma, and not interrupt our command. It already looks better; it’s much easier on the eyes, at minimum.

$Today = Get-Date
$DateArray = (Get-Date $Today.AddDays(-59) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-58) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-57) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-56) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-55) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-54) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-53) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-46) -Format "M-d-yyyy")

Even though the readability is better, we can still clean this up some more. Before we do that however, let’s discuss what this code does. It essentially creates eight, string dates. Yesterday’s date was October 15, 2018. Based on that date, this code is designed to create string dates from dates in the past. It creates a date string from 58 days ago, 57 days ago, 56 days ago, 55, 54, 53, and even 46 days ago. We’ll run the code now, and return the $DateArray variable.

PS > $DateArray
8-17-2018
8-18-2018
8-19-2018
8-20-2018
8-21-2018
8-22-2018
8-23-2018
8-30-2018

Before I show you what I came up with to change this code, I do want to remind everyone that just because you can simplify something, doesn’t mean you always should. This is especially true if it’s going to make the next person to see your code wonder what the hell you did. I don’t think I’ve done that in this case.

In the below code, we set things up in a ForEach-Object loop. We iteratively send in the numeric values of -59 though -53, and -46. You’ll see that these same values were used in the above example. Inside our loop, we’ll run the Get-Date command subtracting the number of days, based on the number submitted to the loop on that iteration. Additionally, we format the date such as we did originally.

$DateArray = -59..-53 + -46..-46 | ForEach-Object {
    Get-Date -Date (Get-Date).AddDays($_) -Format 'M-d-yyyy'
}

As you can see below, we get the exact same output as we did above. We, simplified the code in such as way that it’s easier to follow visually and intuitively. There’s no speed improvement, as best I could tell, but I’ll still take what I’ve written over what was written years and years ago.

PS > $DateArray
8-17-2018
8-18-2018
8-19-2018
8-20-2018
8-21-2018
8-22-2018
8-23-2018
8-30-2018

Until next time, and for me at least, back to these two PowerShell projects!

Those AWS Region Commands

More and more, Amazon Web Services has become a significant part of my day. Luckily for me, PowerShell and AWS, work well together. There’s long been the AWSPowerShell module, which much like the AWS CLI, allows us to issue API calls to AWS from a command line.

As a part of continuing my journey into AWS, and maintaining my mild obsession with PowerShell, I’ve decided to better learn a few PowerShell cmdlets, from the AWSPowerShell module throughout at least a couple of posts. As of version 3.3.365, the module only contains a few thousand cmdlets. It seems like AWS has gone ahead and made an investment here in PowerShell. Especially, when it wasn’t even terribly long ago that there were only 2,000.

(Get-Command -Module AWSPowershell | Measure-Object).Count
4499

Oh yeah, that reminds me, Lambda supports PowerShell (Core) now, too. As I read somewhere recently, “It’s not Bash, it’s not Ruby. It’s PowerShell.”

In a few previous, AWS-specific posts I was able to point out some things I thought should be changed. And somehow, AWS paid close enough attention, that some changes were actually made. It’s still hard to believe; it’s a bit surreal.

Hashtag AWS Tweet Prompts Fix to AWSPowerShell Module
AWS Stop-EC2Instance Needs Complimentary Cmdlet
More AWS PowerShell Changes Due to Twitter (and Me)

I’m mostly writing this evening to help solidify a few commands for my own education, and anyone else who is reading along. But… as I experimented with some of these AWS PowerShell cmdlets, I couldn’t help but feel that some changes were in order. So, with that knowledge, let’s review a few Region-specific cmdlets and touch on some potential changes, as they make the most sense to me.

Get-AWSRegion: “Returns the set of available AWS regions.”

When the Get-AWSRegion cmdlet is invoked, it returns the Region Name (the full name), the Region (abbreviated Region Name), and whether or not the Region is the default in the shell. In this first example, you can see that the default output returns all the Regions.

Get-AWSRegion

Region         Name                      IsShellDefault
------         ----                      --------------
ap-northeast-1 Asia Pacific (Tokyo)      False
ap-northeast-2 Asia Pacific (Seoul)      False
ap-south-1     Asia Pacific (Mumbai)     False
ap-southeast-1 Asia Pacific (Singapore)  False
ap-southeast-2 Asia Pacific (Sydney)     False
ca-central-1   Canada (Central)          False
eu-central-1   EU Central (Frankfurt)    False
eu-west-1      EU West (Ireland)         False
eu-west-2      EU West (London)          False
eu-west-3      EU West (Paris)           False
sa-east-1      South America (Sao Paulo) False
us-east-1      US East (Virginia)        False
us-east-2      US East (Ohio)            False
us-west-1      US West (N. California)   False
us-west-2      US West (Oregon)          False

Wrong. Be sure to take a look at the examples further below. As you’ll see there are a couple of parameters—IncludeChina and IncludeGovCloud—that add some Regions that aren’t there by default. I’m not suggesting a change here, mostly, but Get-AWSRegion should return all the Regions, right?

Based on the cmdlet name alone, I suspected that all Regions were going to be listed in the output that’s returned. Good thing I looked into this cmdlet’s parameters, instead of assuming that the Regions were actually all included. And why China? I get why we might make an exception for GovCloud—we shouldn’t—but what was the thought in regard to China? You’ll see what I mean in the following examples.

(Get-AWSRegion | Measure-Object).Count
15
(Get-AWSRegion -IncludeChina | Measure-Object).Count
17
(Get-AWSRegion -IncludeChina -IncludeGovCloud | Measure-Object).Count
18

Now, let’s take a look at the SystemName parameter included in Get-AWSRegion. This is where it becomes quickly evident to me that we can definitely do better. Why does the Region property use a parameter called SystemName? I think maybe that parameter needs a Region parameter alias, at minimum.

Get-AWSRegion -SystemName us-west-1

Region    Name                    IsShellDefault
------    ----                    --------------
us-west-1 US West (N. California) False

Get-AWSRegion -SystemName us-west-2

Region    Name             IsShellDefault
------    ----             --------------
us-west-2 US West (Oregon) False

Get-AWSRegion -SystemName us-west-*

Region    Name    IsShellDefault
------    ----    --------------
us-west-* Unknown False

I didn’t spend any time reading the help for Get-AWSRegion, but as you can see directly above, the wildcard character isn’t supported with the SystemName parameter. That’s too bad. That would’ve been a great addition to this cmdlet (and one that can still be added). To use a wildcard character against this value, you’re required to pipe your output to the Where-Object cmdlet and therefore, filter it further down the pipeline. Yes, this does mean that all results are piped to Where-Object, whereas a wildcard character built in to Get-AWSRegion, would filter immediately and avoid the need for the pipeline. The pipeline is crucial to the language, but when it’s not needed, we’re better off.

Get-AWSRegion | Where-Object Region -like 'us-west-*'

Region    Name                    IsShellDefault
------    ----                    --------------
us-west-1 US West (N. California) False
us-west-2 US West (Oregon)        False

And if you’re wondering, Get-AWSRegion doesn’t include a Name parameter, so there’s no checking there for the ability to use wildcards.

Get-DefaultAWSRegion: “Returns the current default AWS region for this shell, if any, as held in the shell variable $StoredAWSRegion.”

This command’s purpose, as indicated, returns the Region the AWSPowerShell module’s commands should use by default. If the command returns nothing, then a default hasn’t been set. If it does, then someone has likely already used the command we’ll discuss after Get-DefaultAWSRegion: Set-DefaultAWSRegion.

Get-DefaultAWSRegion
# Nothing here yet...

Before we move on, the commands that include the string “Default” before “AWS,” should have instead maintained the same noun prefix as Get-AWSRegion. That’s right, each of these three cmdlets should’ve included the verb, the dash, and then the string AWS, before the remaining portion of the included nouns. Why in the world would we stuff “AWS” into the middle of some command names and at the beginning of others? Amazon should have maintained a consistent prefix. Every Microsoft Active Directory command uses an AD prefix, right after the dash. You’ll never find it anywhere else. Even in the office, the prefix we use on our self-written functions is right where we’ve been trained to expect it:

<ApprovedVerb>-<Dept><SingularNoun(s)InCamelCase>

In my experience, AWS isn’t afraid of using command aliases—so one command can resolve to another—discontinuing the use of parameter names at times, and changing cmdlet names altogether. Therefore, I suspect someone needs to revisit these three. It’s not like Get-AWSRegionDefault, Set-AWSRegionDefault, and Clear-AWSRegionDefault don’t make sense and are already being used. The current commands should be aliases to these three, keeping the prefixes in the proper place.

Get-DefaultAWSRegion -> Get-AWSRegionDefault
Set-DefaultAWSRegion -> Set-AWSRegionDefault
Clear-DefaultAWSRegion -> Clear-AWSRegionDefault

While we’re here, we need to stop using the plural form of any nouns. That said, I do recognize this move is happening, such that Get-AWSCredentials is an alias that resolves to Get-AWSCredential. Oh look at that, the AWS prefix is in the right place on those two!

Set-DefaultAWSRegion: “Sets a default AWS region system name (e.g. us-west-2, eu-west-1 etc) into the shell variable $StoredAWSRegion. AWS cmdlets will use the value of this variable to satisfy their -Region parameter if the parameter is not specified.”

The first thing to notice here is that we have a Region parameter. That’s what Get-AWSRegion should’ve been included (in addition to SystemName [as not to create a breaking change]). Maybe make Region the actual parameter, and SystemName an alias to the Region parameter. That sounds like the best way to phase out that parameter.

Set-DefaultAWSRegion -Region ca-central-1
Get-DefaultAWSRegion

Region       Name             IsShellDefault
------       ----             --------------
ca-central-1 Canada (Central) True 

Clear-DefaultAWSRegion: “Clears any default AWS region set in the shell variable $StoredAWSRegion.”

Get-DefaultAWSRegion
Region       Name             IsShellDefault
------       ----             --------------
ca-central-1 Canada (Central) True
Clear-DefaultAWSRegion
Get-DefaultAWSRegion
# Nothing here again.

This evening we covered some Region cmdlets from the AWSPowerShell module. They all do what they should in the end, but in my mind, there’s some room for some changes for consistency’s sake and overall improvement. Perhaps we’ll do this again… there are some credential-related AWS cmdlets I’m going to need to learn once. and. for. all.

Change Prompt on Module Import

To me, my PowerShell prompt is quite important. I’ve written about it nine times already. While I’m not going to write about it again, so much, I am going to focus on an updated prompt I’ve created for a recent project. I couldn’t help but take a few things into this project from my prompt, and that’s why that’s been mentioned.

I recently received a screen capture from someone running into a problem using one of the tools I’ve written. Sure, it needs some error checking, I won’t deny that, but it was a very obscure and unforeseen problem. You know, how we often find out about error conditions.

This tool, or function, can be run on an Amazon Web Services EC2 instance within a project, to determine the status of its partner EC2 instance. When it works, it returns the status of the other instance, to include things like running, stopping, stopped, etc. There’s also a couple other companion functions that can start and stop the partner instance. The second of the two machines has very high specifications, and so we ask that our users shut down those secondary instances when they’re not running experiments. They pricey.

The problem is that when I look at this error, the prompt doesn’t tell me enough. I can only tell it’s PowerShell — the PS in the prompt — and the current path — some of which has been hidden in this first image. I want more information without having to ask the user, and so I’ve added that in.  Here’s the default prompt up close.

The new prompt includes the username (to the left of the @), the computer name (to the right of the @), the project name (while it’s not in this example, it’s normally a part of the computer name), the path, and whether the user is an admin (#) or not ($). Now, when I receive a PowerShell screen capture, I already have a few of my first questions answered.

All of the functions, such as the one that was run that generated this error, are a part of the same PowerShell module. There’s somewhere near 20 of them so far, and I keep finding reasons to add new ones. If you don’t know, creating a PowerShell script module that includes a module manifest file (a .psd1), allows one to include scripts that execute just before a module is imported. This, whether the module is imported manually using Import-Module, or by invoking a function from within the module, when the module hasn’t yet been imported.

Let’s take a look at my SetPrompt.ps1 script file that executes when my PowerShell module is imported.

Function prompt {
    # Determine Admin; set Symbol variable.
    If ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups -match 'S-1-5-32-544')) {
        $Symbol = '#'} Else {$Symbol = '$'
    }

    # Create prompt.
    "[$($env:USERNAME.ToLower())@$($env:COMPUTERNAME.ToLower()) $($executionContext.SessionState.Path.CurrentLocation)]$Symbol "
} # End Function: prompt.

With this script in place and a ScriptsToProcess entry in the module’s manifest file, pointing to this file, I can be ensured that the user’s prompt will change the moment my module is imported. From here on out, I can rest assured that if a user — of these machines at least — sends us a screen capture that it’ll include relevant pieces of information I would have had to ask for, had this prompt not been in place.

There’s a final thought here. When the user is done with this PowerShell module, they’re still going to have this prompt. In my case, it’s perfectly suitable because my users won’t be in PowerShell, unless they’re issuing commands from the module. At least, I can’t imagine they will be. The only other thoughts I’ve had about this “problem” would be to (1) teach users to remove the module, and have code in the prompt monitor whether the module is loaded or not, and revert the prompt if the module is removed, or (2) revert the prompt if a command outside of my module is invoked.

That’s it for today. I don’t have it shown here, but it’s neat to see the prompt alter itself when the module is loaded. Something to keep in mind, if you find yourself in a similar situation.

Update: I was annoyed that $HOME, or C:\Users\tommymaynard, was being displayed as the full path, so I made some additional modifications to the prompt that’s being used for this project. It’ll now look like this, when at C:\Users\tommymaynard (or another user’s home directory).

Here’s the new prompt function, to include a better layout.

Function prompt {
    # Determine Admin; set Symbol variable.
    If ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups -match 'S-1-5-32-544')) {
        $Symbol = '#'
    } Else {
        $Symbol = '$'
    }

    If ((Get-Location).Path -eq $env:USERPROFILE) {
        $Path = '~'
    } Else {
        $Path = (Get-Location).Path
    }

    # Create prompt.
    "[$($env:USERNAME.ToLower())@$($env:COMPUTERNAME.ToLower()) $Path]$Symbol "
} # End Function: prompt.