Prep Command for Immediate Use

Edit: I’ve updated this post by adding a really quick video at the bottom — check it out. It briefly shows the results of the concepts in this post.

It was just yesterday when I wrote the Windows Terminal Fun Surprise post. This was a short post about executing the Alt+Shift+D keyboard key combination inside Windows Terminal. I got myself to a point where I could programmatically send in this key combination using the below commands.

Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('%+D')

This got me thinking: Can I use this to prep a command for immediate use? I know, what does that even mean? My profile script $PROFILE, contains a good deal of helpful tools. And then it has something like the PowerShell ASCII art below. It’s actually a work logo, but you get the idea. This laptop with the PowerShell logo will stand in just fine, however.

Function Show-PowerShellAsciiArt {
@"
   ._________________.
   |.---------------.|
   ||   PowerShell  ||
   ||     \         ||
   ||      \        ||
   ||      /        ||
   ||     / ------  ||
   ||_______________||
   /.-.-.-.-.-.-.-.-.\
  /.-.-.-.-.-.-.-.-.-.\
 /.-.-.-.-.-.-.-.-.-.-.\
/______/__________\___o_\
\_______________________/

"@
} # End Function: Show-PowerShellAsciiArt.
Show-PowerShellAsciiArt

As cool as I think this logo is — the one I actually use — and as certain as I am about it appearing every time I open a new terminal, I feel like it’s in the way when I want to start to use my terminal. Like you, I suspect, I want my prompt and cursor in the top-leftish corner and ready to go pretty quickly. The difference is, I’m willing a momentary slowdown. I can do better, however.

The thought was: Can I place a command at the prompt as a part of my profile script, that will allow me to just press Enter to clear the screen? You see, I usually type cls or clear, press Enter and then start with whatever I’m doing. I want the logo, but I also want to get started as soon as I can. The answer to that most recent thought though is yes.

In addition to the above Show-PowerShellAsciiArt function, is one more brief function and a modification to the code that started off this post.

Function Clear-Logo {Clear-Host}

Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('Clear-Logo')

Now, when my profile script executes, it places the “Clear-Logo” text at my prompt, allowing me to just press Enter to clear the image and start doing what I do at the command line. As you can see, Clear-Logo is only there to invoke Clear-Host. Still, I like it better.

Press Enter and voilà.

In order to get a better idea of how this works — just in case, I didn’t do a great job explaining it, who knows — I’ve included a short video below. This shows me opening two new tabs in Windows Terminal and pressing Enter after each is open, and therefore, clearing the screen (the logo).

Windows Terminal Fun Suprise

I saw a Tweet today from Kayla Cinnamon: https://twitter.com/cinnamon_msft/status/1455946220476657665. Here’s the Tweet.

Although she wrote to type Alt+Shift+D, she obviously didn’t mean it that way. I looked past that, although some of the comments proved that others did not. I quickly figured out what it did and then I determined how to not use that keyboard key combination. Before we see that, let’s determine what Alt+Shift+D does.

Alt+Shift+D opens up a new terminal, or rather, it duplicates the pane. If you take a look at the Settings menu you’ll quickly discover a large number of other fun surprises (a.k.a. keyboard key combinations). Someday, I’ll take the time and work through those.

Anyway, here’s what you shouldn’t do.

[PS7.1.5][C:\] Add-Type -AssemblyName System.Windows.Forms
[PS7.1.5][C:\] 1..10 | ForEach-Object {[System.Windows.Forms.SendKeys]::SendWait('%+D')}

This lovely piece of frustration — if you can sneak it into a “friend’s” profile script — will add the System.Windows.Forms assembly and then send in the Alt+Shift+D key combination for each iteration through the ForEach-Object loop. In this example, it would be a total of ten times. So it’s been said, the SendWait method includes “%” and “+” in addition to the letter “D.” As you may have already guessed, if you didn’t know, these two symbols are used for Alt and Shift, respectively. So what’s this produce? It does what a good number of people that replied to Kayla’s Tweet showed us they could do by repetitively pressing the key combination. That said, why do it manually? It’s PowerShell!

Back to when she said to “type” the keyboard command. It did make me think to do something with the string value Alt+Shift+D.

In the top-left pane, which was the only one when I started, I created a function called Alt+Shift+D. It included the two commands from earlier. Then I tested it/invoked my new function and it added the pane on the right side. Then I created an alias for my function, because who wants to type out the entirety of Alt+Shift+D each time — not me — and tested it too. It opened the bottom-left pane. If you aren’t using Windows Terminal then you should be; it continues to get better all the time.

My Work-Specific Vacation Time Function

Edit: There were some logic errors that Mark pointed out in the comments section. I was rounding throughout the function and skewing some of my own totals. I’ve updated this post to reflect all of the changes (code, images, text, etc.). Thank you for your time, Mark, for both reading the post and passing along some beneficial information.

I did that thing where I start a post that will include a shareable PowerShell function and I’ve yet to actually write any PowerShell. I’ll get there eventually, but for now, let me explain the purpose of the function I’ll be writing. Realistically, this explanation for you ought to help me with authoring the code. It’s obvious, but once I’ve defined the specifications and needs, it should be even easier to write.

My current employer — this is about to sound like a word problem from grade school — allows me to carry forward 320 vacation hours into the next calendar year. Anything above 320 hours is forfeited. Because I was on track to have greater than 320 hours by the end of the year, I’ve been taking off every Wednesday since the last week of September. If you haven’t been to Target at 8 a.m. on a weekday morning, then you’re missing out. It was so empty; it was just me and very few people — amazing. I digress.

My local HR is adamant about employees not losing/forfeiting vacation time. In all of this, I decided I should write the code to do the arithmetic in the PowerShell console. I later decided I should write a function that would do the calculations for me, instead. Before I actually construct the code and build the function, let’s walk through some needed values.

1. Number of carry forward vacation hours: 320 (default value, same for all employees)
2. Number of current vacation hours: 362.670
3. Number of pay periods during the remainder of the calendar year: 5
4. Hours of vacation accrued per pay period: 6.77 (default value, not the same for all employees)

First, I need to determine how many hours I currently have above the carry forward vacation hours of 320. That’s the current vacation hours minus the carry forward vacation hours: 362.670 – 320. It’s possible someone using my (so far non-existent) function could have less than 320 vacation hours. This would change things, and it’s probably worth keeping in mind, as the function will need to handle this possibility.

Second, I’d like to know how many more vacation hours I’ll accrue during the remainder of the calendar year. That’s the vacation accrual hours times the number of pay periods during the remainder of the calendar year: 6.77 * 5. Let’s go.

367.670 – 320 = 42.67
6.77 * 5 = 33.85
42.67 + 33.85 = 76.52

If we divide that total by 8 hours (as in, hours worked per day), we know how many days of vacation I need to take to use up my hours: 76.52 / 8 = 9.565. It’s a little over nine days, but not quite 10. In thinking through this assignment, I’ve realized that it’s possible someone may only want to know their current overage (the subtraction problem) versus their future overage (the multiplication problem) or vice versa. Anyway, I’m off to write this function now. To you, it’ll seem like no time passed at all; for me, I won’t be nearly as lucky. Still, having defined the function’s requirements upfront, before writing any code, has already simplify things.

Well, that’s done and working. I’m going to include images of the function because they’re easier on the eyes and I think it’ll simplify my code explanations. I will, however, make the function copyable at the end of this post.

Let’s start with the parameters.

This function includes five parameters. Beginning from the top, we have CarryForwardVacationHours. This has a default value of 320, however, this can be changed by the person/process when this function is invoked. It won’t need to be for me unless my current employer changes the carry forward amount. Next, we have CurrentVacationHours. The is the parameter that’s used to supply how many hours a person currently has. The value needs to be greater than the CarryForwardVacationHours or there won’t be any overage hours.

The following parameter is VacationAccrualRate. This contains a default value of 6.77 hours. This value does vary depending on how long a person has been employed by my current employer. This is the largest possible amount, as it’s based on longevity. The second to last parameter is PayPeriods. This is how many pay periods there are until the end of the calendar year. The person invoking the function will have to gather this information themself.

And finally, there is the Round parameter. This was a late addition to this function that was discussed in a recent post, Setting the Rounding Scale. This parameter has a default value of 2. This means that any values with decimals with more than 2 values, will be rounded down to two, or to whatever the value is set to when the function is invoked. As we saw in that recent post, it’s not going to add zeros into decimal positions. If the number is 42.1 and you indicate to round to three decimal places, it’s still going to be 42.1. Read the linked post to see how to add zeros if that’s something that would ever be of use to you.

Now, let’s move on to the Process block and its first region, Current Hours. It’s not pictured in these images, but the Begin block isn’t being used in this function. This is the reason it didn’t make it into this or the previous image.

This region of this Process block, first determines if the current vacation hours are greater than the carry forward vacation hours. If it is, it does a little math, and we’ll get there in a moment. If it isn’t, it sets the overage hours and overage days to zero. We’re storing all of our values in an ordered hash table, so that’s where these would land. If the current vacation hours are greater than the carry forward vacation hours, then it subtracts the two numbers as we mentioned earlier. In order to determine how many overage days there are, it divides the result by 8 hours per day, placing both values into the hash table. The carry forward hours are added to the hash table, as well.

Following this region, let’s discuss the Future/Total Hours region.

In this region, we determine our vacation accrual hours and from that total, our vacation accrual days. Finally, we put it all together for the total hours by combining the overage hours and vacation accrual hours and our total days by combing the overage days and the vacation accrual days. Much like the carry forward hours in the above region, we include the vacation accrual rate, too.

Let’s close out with the End Block. This is simple; we convert our hash table — the keys and values — into PowerShell objects, where they become properties and values. This way, they can be easier manipulated by other PowerShell commands if we choose to do that.

Now, let’s include a few test invocations and ensure we’re getting the proper results. This first one just calculates my vacation overage hours and days.

Show-VacationTime -CurrentVacationHours 362.670

CarryForward : 320
OverageHours : 42.67
OverageDays  : 5.33
AccrualRate  : 6.77
AccrualHours : 0
AccrualDays  : 0
TotalHours   : 42.67
TotalDays    : 5.33

This one returns my vacation overages, as well as my vacation accruals.

Show-VacationTime -CurrentVacationHours 362.670 -PayPeriods 5

CarryForward : 320
OverageHours : 42.67
OverageDays  : 5.33
AccrualRate  : 6.77
AccrualHours : 33.85
AccrualDays  : 4.23 
TotalHours   : 76.52
TotalDays    : 9.57 

These last example invocations run the same command as above but include the Round parameter with a couple of different values. Remember, 2 is the default value for this parameter.

Show-VacationTime -CurrentVacationHours 362.670 -PayPeriods 5 -Round 0

CarryForward : 320
OverageHours : 43
OverageDays  : 5
AccrualRate  : 6.77
AccrualHours : 34
AccrualDays  : 4
TotalHours   : 77
TotalDays    : 10

Show-VacationTime -CurrentVacationHours 362.670 -PayPeriods 5 -Round 4

CarryForward : 320
OverageHours : 42.67
OverageDays  : 5.3338
AccrualRate  : 6.77
AccrualHours : 33.85
AccrualDays  : 4.2312
TotalHours   : 76.52
TotalDays    : 9.565

Okay, one final example of when you don’t send in the current vacation hours to determine an overage, but you do want to know your vacation accrual hours.

Show-VacationTime -PayPeriods 5

CarryForward : 320
OverageHours : 0
OverageDays  : 0
AccrualRate  : 6.77
AccrualHours : 33.85
AccrualDays  : 4.23
TotalHours   : 33.85
TotalDays    : 4.23

Finally, as I promised, here’s a copyable version of this function. I’m quite aware that there are only so many of us that can and/or would make use of this function. Still, some great concepts have been demonstrated and maybe some of them can be included in some of your own projects.

Function Show-VacationTime {
    [CmdletBinding()]
    Param (
        [Parameter()]
        $CarryForwardVacationHours = 320,

        [Parameter()]
        $CurrentVacationHours,

        [Parameter()]
        $VacationAccrualRate = 6.77,

        [Parameter()]
        [int]$PayPeriods,

        [Parameter()]
        [ValidateRange(0,4)]
        [int]$Round = 2
    ) # End Param.

    Begin {} # End Begin.

    Process {
        #region: Current Hours.
        If ($CurrentVacationHours -gt $CarryForwardVacationHours) {
            $OverageHours = $CurrentVacationHours - $CarryForwardVacationHours
            $OverageDays = $OverageHours / 8 # 8 hours.
            $Hashtable = [ordered]@{
                CarryForward = $CarryForwardVacationHours
            }
        } Else {
            $Hashtable = [ordered]@{
                CarryForward = $CarryForwardVacationHours
                OverageHours = 0
                OverageDays = 0
            }
        } # End If-Else.
        #endregion.
        #region: Future/Total Hours.
        $VacationAccrualHours = $PayPeriods * $VacationAccrualRate
        $VacationAccrualDays = $VacationAccrualHours / 8
        $TotalHours = $OverageHours + $VacationAccrualHours
        $TotalDays = $OverageDays + $VacationAccrualDays
        $Hashtable['OverageHours'] = ([math]::Round($OverageHours,$Round))
        $Hashtable['OverageDays'] = ([math]::Round($OverageDays,$Round))
        $Hashtable['AccrualRate'] = $VacationAccrualRate
        $Hashtable['AccrualHours'] = ([math]::Round($VacationAccrualHours,$Round))
        $Hashtable['AccrualDays'] = ([math]::Round($VacationAccrualDays,$Round))
        $Hashtable['TotalHours'] = ([math]::Round($TotalHours,$Round))
        $Hashtable['TotalDays'] = ([math]::Round($TotalDays,$Round))
        #endregion.
    } # End Process.

    End {
        [PSCustomObject]$Hashtable
    } # End End.
} # End Function: Show-VacationTime.

Setting the Rounding Scale

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

PowerShell Variable Description Property

I don’t remember who mentioned it, but someone in the PowerShell community recently tweeted that they had been working with PowerShell for 10 years and had just learned something new. It wasn’t new to me, but I’m sure there are things out there I’ve yet to encounter. I sure hope so, anyway!

This individual didn’t realize that variables in PowerShell have descriptions. Did you know that!? Here are a few examples of different uses. Eventually, we’ll get to the description property, as well as all the properties you get every time you create a variable in PowerShell.

[PS7.1.5][C:\] $Variable = 'This is a string variable.'
[PS7.1.5][C:\]  $Variable
This is a string variable.
[PS7.1.5][C:\]
[PS7.1.5][C:\] Get-Variable -Name Variable -ValueOnly
This is a string variable.
[PS7.1.5][C:\]
[PS7.1.5][C:\] (Get-Variable -Name Variable).Value
This is a string variable.
[PS7.1.5][C:\]
[PS7.1.5][C:\] Get-Variable -Name Variable

Name                           Value
----                           -----
Variable                       This is a string variable.

[PS7.1.5][C:\] Get-Variable -Name Variable | Select-Object -Property *

PSPath        : Microsoft.PowerShell.Core\Variable::Variable
PSDrive       : Variable
PSProvider    : Microsoft.PowerShell.Core\Variable
PSIsContainer : False
Name          : Variable
Description   :
Value         : This is a string variable.
Visibility    : Public
Module        :
ModuleName    :
Options       : None
Attributes    : {}

[PS7.1.5][C:\]  

There it is… Description. Let’s add one and then return it!

[PS7.1.5][C:\] Set-Variable -Name Variable -Description 'This is the description property of a string variable.'
[PS7.1.5][C:\] (Get-Variable -Name Variable).Description
This is the description property of a string variable.

If you didn’t know this either, then just maybe this is exciting for you, too! I liked it when I first found it. I will link an old post below that I wrote about variable descriptions the first time around. It may be much of what was presented here already. There’s another post I wish I could find, but I think it was posted on another website and it has since been lost. Somebody on some forum wanted to know the last value that a variable contained and so I wrote a post about storing the variable’s previous value in the description property each time the variable was updated to something new. That’s was fun, and unique!

Give  your Variable a Description

PowerShell Copy, Shallow and Deep

Back in March, when I started this post, I was reading over some Python code. Yeah,… you read that right. As I did that, I was introduced to a term I don’t think I’ve seen before: It was “deepcopy.” The Python module import looked like this: from copy import deepcopy. Deepcopy, what is that?

I don’t remember the name for it, but as I read up on this term and the function from this Python module, I do remember coming up against this in PowerShell. Essentially, it’s this. If you make a copy of an array—we’ll use that for this example—changes to the original array will change the copy, as well. The deepcopy option allows Python to create a copy that isn’t tied to the original. Let’s begin by looking at a shallow copy example in PowerShell.

Any change made to the original object—$Array—will be seen in the copy of the object, too. There’s a link between them. As a shallow copy, its values will continue to reference the original object. A deep copy is independent; there is no reference to the original object. Let’s work through a quick example of making a deep copy of our array in PowerShell.

I gather that there may be other ways to create deep copies, and you’re welcome to comment and share those if you’d like. This is an extremely simple and straightforward example—my favorite—of something of which you may need to be careful, as you continue to write PowerShell.

Using PowerShell for Docker Confidence

It’s been a while since I’ve written. I went from writing approximately six posts per month to nothing since last May. That’s unheard of, but alas, it’s true. A lot has changed in that time, including starting a new job, where there isn’t time for working in PowerShell nearly as much. There is, however, a need for me to up my Docker game. I used it in the past as I opted to learn and work with a CI/CD pipeline. But, as I get deeper into Docker I thought that there might be some people out there with an understanding of PowerShell, that could use it to feel comfortable looking at Docker closer. So, that’s who this is for. If you’re a Docker pro, then the things discussed in this post will likely hold little value. For me, the opportunity to write about this will only cement the Docker concepts in my own brain. With that, and for those where this post makes sense, let’s begin.

The first thing you’re going to need is… well, Docker of course. It’s been just about forever since I installed it, so don’t expect much out of me. Okay fine. Windows folks, go to https://docs.docker.com/get-docker and download the Windows .exe package. This is going to include both the command-line, docker tool, and Docker Desktop. I’ve never really used Docker Desktop before, but I’m not going to lie, after opening it up a time or two recently, I’ve gone back to it. It serves as a nice visual interface in which to help solidify the Docker concepts of containers, images, and volumes. Keep it in mind, sure, but let’s open PowerShell now that Docker is installed, and run our first Docker command.

The first command we’re going to run is a docker pull command. This will download a Docker image from hub.docker.com. Think of an image as a set of instructions to create a container. It’s a standalone, executable package of goodness with all the code, and runtimes, and libraries, and tools needed to run an application. Anyway, let’s pull down our image using the full command of docker pull mcr.microsoft.com/powershell. You can always search at hub.docker.com for the image or images, you want. They’ll even include the full docker pull command for you!

Because I already had the latest, or most recent, version of the PowerShell Docker image, there was nothing for docker to pull for me. When your pull is complete, run docker image ls to see the image(s) you have. You can also open Docker Desktop to see them listed visually. It’s helpful, it really is. In the below image you can see that I have the last two most recent versions of the Docker PowerShell image.

I wasn’t going to do it originally, but I found an additional image of interest, so I downloaded it in order that I could include a couple of additional images — like pictures — in this post. I first downloaded a Python Docker image, as you can see, and then ensured it was included in my list of images.

Now we’re going to have Docker run the PowerShell image, and therefore create a container for us, from that image. Do this using docker run --interactive --tty mcr.microsoft.com/powershell. The --interactive --tty options could’ve been written as -i -t or, even -it. This will come in handy if you don’t know it already! Once the above command is executed, you’ll have a Powershell prompt inside the Powershell Docker container. I know right; it’s awesome! It’s a little Linux container running right there on your bare metal computer or VM, completely isolated from everything else going on. If you’re new to Docker, this may be the push you need to start learning it… before you need it.

Oh yeah, here’s a quick piece of potentially interesting information. Using docker run with the options (arguments) and Docker image name, and Docker will pull down the image if you don’t already have it!

PowerShell User Group Austria – Crescendo

This morning — well, this morning my time — I was a part of a speaking event with the PowerShell User Group Austria, where I discussed the Microsoft Crescendo PowerShell module (https://github.com/PowerShell/Crescendo and https://www.powershellgallery.com/packages/Microsoft.PowerShell.Crescendo). It was clear to me that a couple of related posts of mine got the attention of Roman, and put me in a place where I could share what I knew about the project. It was fun!

The project just entered preview-2 on May 6, 2021 — a mere five days ago. While I didn’t focus on the additions from that preview, I essentially walked through my Part I and Part II, which were written with preview-1 in mind. I did, however, mention the additions that preview-2 brought us.

As I stated I would during that user group meeting, I’ve offered up my demo file. It includes several helpful links, as well as everything I used to create those things I did. The paths won’t line up with your system, so keep that in mind if you work through the examples.

PowerShell_User_Group_Austria_DemoFile (2609 downloads ) PowerShell_User_Group_Austria_JSONFile (2979 downloads )

 

Add Tags to Existing AWS Parameter Store Entries

While I work with AWS, it’s unfortunately not a regular, I’m-going-to-be-in-there-a-few-times-a-week kind of a thing. Perhaps that’ll change in time, but for now, I take myself in there occasionally when there’s something I want to try, use, fix, or experiment with. Today started when I ran up against a handful of Systems Manager Parameter Store entries I had created where I didn’t include any Tags. I didn’t want to be that guy, but I also didn’t want to be the guy that manually updated each of the 12 entries by hand. Yeah, that’s not me. There were two tags per entry. It’s not that many, but still.

The documentation to do this is available in the AWS PowerShell documentation. Even so, I’m going to share what I wrote and briefly discuss it. Perhaps someone will find it instead of, or in addition to, what’s already been written about it. Anyway, let’s get started. The below code is mostly one large, continuous code block, however, I’m breaking it up to discuss it.

The first code block creates and assigns a $CommonParams variable. This holds values that each of the AWS commands needs to have included. This is just an easier way to include these parameters and parameter values in each command. This variable, when included with an AWS command (as @CommonParams), will include the Region in which I’m working and the local AWS profile (on my computer) that I’m using to run these commands. Behind this profile is an Access Key Id and corresponding Secret Access Key.

$CommonParams = @{
	Region = 'us-west-2'
	ProfileName = 'tommymaynard_api@ecs-nonprod'
}

Using the below PowerShell we can isolate the Systems Manager Parameter Store entries that we want to modify. Each entry begins with a forward slash (/), but doesn’t begin with a forward-slash followed by fdn.

$Parameters = Get-SSMParameterList @CommonParams | Where-Object -FilterScript {
	$_.Name -like '/*' -and $_.Name -notlike '/fdn*'} | Select-Object -Property Name

This next section isn’t a part of the code. It’s being included, however, so that you’re able to view the values stored in the $Parameters variable. The entire code is included at the bottom of this evening’s post so that it’s easy to capture for those that are interested.

$Parameters

Name
----
/PowerShell/FunctionTemplate/Splunk/Logging/HEC_Token  
/PowerShell/FunctionTemplate/Splunk/Logging/HEC_URL    
/PowerShell/FunctionTemplate/Splunk/Telemetry/HEC_Token
/PowerShell/FunctionTemplate/Splunk/Telemetry/HEC_URL  
/ad_join/domain  
/ad_join/password
/ad_join/user    
/agents/duo/host 
/agents/duo/ikey 
/agents/duo/skey
/agents/omsagent/primarykey 
/agents/omsagent/workspaceid
/agents/sophos/linux_url        
/agents/tenable/nessus/host     
/agents/tenable/nessus/key      
/agents/tenable/nessus/linux_url

In order to apply Tags to each of the above Parameter Store entries, we need to first create them. In $Tag01 we’ll store the createdby key and its corresponding value. In $Tag02 we’ll store the contactid and its corresponding value.

$Tag01 = New-Object Amazon.SimpleSystemsManagement.Model.Tag
$Tag02 = New-Object Amazon.SimpleSystemsManagement.Model.Tag
$Tag01.Key = 'createdby'; $Tag01.Value = 'tommymaynard'
$Tag02.Key = 'contactid'; $Tag02.Value = $Tag01.Value

This section also isn’t a part of the code. It’s been included as verification that our Tags have been properly assigned, prior to being applied to each of the Parameter Store entries.

$Tag01; $Tag02

Key       Value
---       -----
createdby tommymaynard
contactid tommymaynard

This next code block returns the current Tags for each of the Parameter Store entries. Running this code here allows us to view the current Tags, if there are any, prior to adding the Tags we know we want on each of the entries.

$Parameters.Name | ForEach-Object {
	$_; Get-SSMResourceTag @CommonParams -ResourceType 'Parameter' -ResourceId $_; '---'
} # End ForEach-Object.

This is the code block that adds our Tags to each of the Parameter Store entries. We ensured we were working with the correct entries, we created our Tags—both the keys and the corresponding values—and now we’re applying them to each entry.

$Parameters.Name | ForEach-Object {
	Add-SSMResourceTag @CommonParams -ResourceType 'Parameter' -ResourceId $_ -Tag $Tag01,$Tag02
} # End ForEach-Object.

This is the same code block we saw two blocks earlier. All it does is return the Tags for each of the Parameter Store entries. It’s included again in order to review the changes since running the Add-SSMResourceTag command. While we might normally output this as PowerShell objects, I didn’t find that to be necessary since it was simple output that only I would see and then disregard.

$Parameters.Name | ForEach-Object {
	$_; Get-SSMResourceTag @CommonParams -ResourceType 'Parameter' -ResourceId $_; '---'
} # End ForEach-Object.

As mentioned, and since I stuck some not to be included code within the code, here’s the full code for adding new Parameter Store Tags entries.

$CommonParams = @{
	Region = 'us-west-2'
	ProfileName = 'tommymaynard_api@ecs-nonprod'
}

$Parameters = Get-SSMParameterList @CommonParams | Where-Object -FilterScript {
	$_.Name -like '/*' -and $_.Name -notlike '/fdn*'} | Select-Object -Property Name

$Tag01 = New-Object Amazon.SimpleSystemsManagement.Model.Tag
$Tag02 = New-Object Amazon.SimpleSystemsManagement.Model.Tag
$Tag01.Key = 'createdby'; $Tag01.Value = 'tommymaynard'
$Tag02.Key = 'contactid'; $Tag02.Value = $Tag01.Value

$Parameters.Name | ForEach-Object {
	$_; Get-SSMResourceTag @CommonParams -ResourceType 'Parameter' -ResourceId $_; '---'
} # End ForEach-Object.

$Parameters.Name | ForEach-Object {
	Add-SSMResourceTag @CommonParams -ResourceType 'Parameter' -ResourceId $_ -Tag $Tag01,$Tag02
} # End ForEach-Object.

$Parameters.Name | ForEach-Object {
	$_; Get-SSMResourceTag @CommonParams -ResourceType 'Parameter' -ResourceId $_; '---'
} # End ForEach-Object.

I didn’t specifically test whether this overwrote Tags of the same name that existed on each entry. That said, I believe it did in fact appear to overwrite them without a care, concern, or prompt. If this is important, then it would probably be wise to test this and/or write the conditional code to do something different, if that’s what you want.

Text-To-Speech in PowerShell

If you’re like me, you’ve used the Narrator, built into Windows, to read something to you that you’ve written. If you’re not, then you may find this whole thing, strange. But yes, for me, I’ll occasionally do this to ensure I didn’t miss a word or two, or that I didn’t use the wrong tense. Or maybe, I just wanted to hear how something sounds when it isn’t me saying it out loud, and instead, it’s one of my computer’s robot voices. So yeah, I do that sometimes.

I’m growing tired of opening Narrator, so I’ve decided I should make a PowerShell function to do the text-to-speech for me. My biggest complaint is that the Narrator tells me way too much information before it actually starts doing what I expect it to do, and yes I do understand the Narrator wasn’t included in the operating system for someone such as myself. Still, sometimes, depending on something’s importance, I prefer to have what I’ve written, read to me, to help ensure my accuracy. It’s a final safety (I’d-prefer-to-not-sound-like-an-idiot for this piece of writing) check.

I’ve had some exposure with System.Speech. But, what was that for? Ah yes, I remember. It was for the Convert-TMNatoAlphabet function. I have copied it here from its previous home on the Microsoft TechNet Gallery. This function would do exactly as the examples below indicate. It takes a string made up of numbers and letters and writes it out in the Nato phonetic alphabet. It’s an old function, so I’ve included an old image. You can’t tell in the image examples, but at some point, I added a Speak parameter. If that was included, as -Speak, not only would it display the result on the screen, but it would say them out loud, as well.

The end goal now is to write a function that I’ll keep around to read things back to me without opening the Narrator. I provide some text, and PowerShell provides me a robot voice to help me recognize if I’ve missed any words while having something read to me. I think I’ll pause here for now and go write the code. Yeah, that’s right, that hasn’t actually been done yet. So for you, back in split second, and for me it’ll be some time longer.

This got a touch more confusing than I had hoped it would, but it’s nothing you won’t be able to comprehend. We need to remember that we’re operating in a PowerShell world now — not a Windows PowerShell world. That means that we’re going to assume we’re using something greater than version 5.1, but that 5.1 is available to us (since I’m running Windows 10). We’re going to use powershell.exe (5.1 and earlier) from pwsh.exe (6.0 and later). Let’s take a look at the below-completed function in order to understand what it’s doing and then let’s test it out. One final note before we do that, however. While you may have no interest in having anything spoken by your computer, the concepts in this example may prove to be helpful if you ever need to use Windows PowerShell, from PowerShell.

Function Convert-TextToSpeech {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [string]$Text,
        [Parameter()]
        [ValidateSet(1,2,3,4,5,6,7,8,9,10)]
        [int]$SpeechSpeed = 5
    ) # End Param.

    Begin {
        Function ConvertTextToSpeech {
            [CmdletBinding()]Param (
                [Parameter()]$Text,
                [Parameter()]$SpeechSpeed
            ) # End Param.
            Add-Type -AssemblyName System.Speech
            $VoiceEngine = New-Object System.Speech.Synthesis.SpeechSynthesizer
            $VoiceEngine.Rate = $SpeechSpeed - 2
            $VoiceEngine.Speak($Text)
        } # End Function: ConvertTextToSpeech.
    } # End Begin.

    Process {
        $Session = New-PSSession -Name WinPSCompatSession -UseWindowsPowerShell
        Invoke-Command -Session $Session -ScriptBlock ${Function:ConvertTextToSpeech} -ArgumentList $Text,$SpeechSpeed
    } # End Process.

    End {
        Remove-PSSession -Name WinPSCompatSession
    } # End End.
} # End Function: Convert-TextToSpeech.

The entire code example is within a single function, which is both expected and preferred. Copy, paste, run the code, and the Convert-TextToSpeech function becomes available to be invoked within your current PowerShell session. This function does several things. The first thing to notice is that the function includes a parameter named Text, which expects a value is included for that parameter when the function is invoked. This is the text to be read out loud. It also includes a parameter named SpeechSpeed that accepts a value from 1 through 10. It includes a default value of 5, which is what we’re going to use for the normal/standard/default speed. Therefore, anything less than 5 is slower than normal and anything greater than 5 would be faster.

In the Begin block, the function creates a nested function. It uses the same name as our main function; however, it removes the dash from in between the words “Convert” and “Text,” making it a completely different function. We could have used the same name since these functions would be scoped differently, however, I thought that would probably end up being even more confusing than this already may be.

Notice that the nested function includes the identical Text parameter as our parent function. The parameter value that comes in when the parent function is invoked will be passed along to our nested function’s Text parameter. We will see that shortly. It also includes a SpeechSpeed parameter, as well. This will also be passed from the parent function to this child function. There are not as many rules about what these parameters will accept in the child function. That’s because the validation features and variable type enforcements are already requirements in the parent function. Therefore, they don’t need to be in place for this nested function, too.

That nested function’s purpose is to load what’s necessary to actually do the text-to-speech conversion before doing it. This is code that cannot be executed using .NET Core (cross-platform). It can only be executed by the .NET Framework (Windows). This is why we need Windows PowerShell. If I later determine that’s not entirely true, I’ll be sure to update this post. While we’re looking at this nested function though, do notice where we set the Rate. This is the SpeechSpeed, which I’m beginning to figure out, isn’t easy to say out loud. Meh. As it was mentioned earlier, the values the function allows for are 1 through 10 where 1 is the slowest and 10 is the fastest. You’ll notice that in the nested function we subtract two from that value. That’s because three is actually the normal speed.

The Process block only includes two lines, however, they are both quite important. The first line creates a new PowerShell Remoting session. The remoting session will not be to a remote or different computer, but instead will stay on the same computer and make use of Windows PowerShell, or powershell.exe. It will create a new “remote” Windows PowerShell session where it can run powershell.exe and make use of the .NET Framework. The nested function will be “taken” into the remote session using Invoke-Command along with the two parameter values passed into the parent function. After the text is read in the “remote” Windows PowerShell session, it will return to this function where the code will progress into the End block. From here, our function will remove the PS Remoting session and the function invocation will end.

In order to experience the below example, copy the above function, paste it into the ConsoleHost or VSCode and run it so that the function is added to the session/memory. Then, invoke the function by running your own example, or even the one below. The one below will start out slowly and gradually get faster. I’ve included the audio of the function being invoked beneath the example code. Watch your use of quotes/double quotes in your text. It could break things.

1..10 | ForEach-Object {Convert-TextToSpeech -Text "This is testing $_" -SpeechSpeed $_}

Again, you may have no interest in a function that can read text out loud, but one day you may have a need to run something inside Windows PowerShell from (just) PowerShell.

Edit: If you have a large block of text, you might rather store it in a text file and use a Get-Content command to read in the file’s contents, before it’s used as the parameter value for the Text parameter. Something like the below example.

Convert-TextToSpeech -Text (Get-Content .\Desktop\file.txt)