Category Archives: Quick Learn

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

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)

“Logon” Processing Code

I’m working on something new and there’s not much on this topic in relation to PowerShell. Compared to some other topics, there’s not much on it anyway. I want to use PowerShell to authenticate with Shibboleth, and I want to use its ECP profile. Shibboleth is typically implemented with a browser and its SSO profile. My goal, while I’m not sure if I’ll get there or not, is to authenticate with Shibboleth from a non-browser-based client: my PowerShell ConsoleHost.

As a part of this effort, I wrote some Proof of Concept (PoC) code that requires a logon. There’s nothing special about this code, but I’m going to dump it here, right on my website, just in case anyone wants to read through it. It’s mostly straightforward, but it does present a momentary challenge as you walk through what does what. You might just use it for that — a challenge — while I’ll use this post for the storage of the code itself. Feel free to read over the code and then the short information section beneath it. Again, just locking the ability to use a specific function until I’m “logged” on.

Function Get-Process5 {
	$Script:OriginalPrompt = prompt
	If ($Script:LoggedOn) {
		Get-Process | Select-Object -First 5
		Disconnect-Shibboleth
	} Else {
		$Script:CommandToInvoke = $MyInvocation.MyCommand.Name
		Invoke-UserIDPassPrompt
	} # End If-Else.
} # End Function: Get-Process5.

Function Invoke-UserIDPassPrompt {
	$Script:UserIDUserName = Read-Host -Prompt "$Script:OriginalPrompt   UserID"
	$Script:UserIDPassword = Read-Host -Prompt "$Script:OriginalPrompt Password" -AsSecureString
	Connect-Shibboleth
} # End Function: Invoke-UserIDPassPrompt.

Function Connect-Shibboleth {
	If ($Script:UserIDUserName -and $Script:UserIDPassword) {
		$Script:LoggedOn = $true
		& $Script:CommandToInvoke
	} # End If.
} # End Function: Connect-Shibboleth.

Function Disconnect-Shibboleth {
	$Message = 'Do you want to disconnect from Shibboleth [Y/N]'
	Do {
		$Response = Read-Host -Prompt $Message
		If ($Response -eq 'y') {$Script:LoggedOn = $false} # End If.
	} Until ($Response -eq 'y' -or $Response -eq 'n') # End Do-Until.
} # End Function: Disconnect-Shibboleth.

There are four separate functions: Get-Process5, Invoke-UserIDPassPrompt, Connect-Shibboleth, and Disconnect-Shibboleth. When the above code is executed, it will first attempt to invoke the Get-Process5 function. If you’re “logged on,” it will return the first five processes running on the computer and then prompt you to disconnect (think “logoff”). I put quotes around “logged on” and “logoff” because there is no true log on/off going on here. It really is just PoC code that runs regardless of the UserID and or password that is entered. If this is of interest, then have a peek. For me, I doubt I’ll actually be back for this, as most of what I needed to write (so far), has been written. Still, a neat little moment in time that produced some PowerShell worth more than being fully discarded.

AWS PowerShell Command Count

Back in the day, I was astonished by the number of PowerShell commands that AWS (Amazon Web Services) had written. It was a huge number—I believe it was around 5,000. There’s probably a post or two on this site where it’s mentioned. Based on the number, it was clear that AWS had made a commitment to (wrapping their APIs with) PowerShell. Back then, it was easy to calculate the number of commands because of that single, PowerShell module. Install the module, count the commands (programmatically of course), and done. Over the last two or three years—I’m not 100% sure—-they moved to a model where modules are separated out by service. I greatly suspect that the single-module model was no longer suitable. Who wants to import a few thousand commands by default, when they only needed maybe one or two? Over time, I suspect that it probably wasn’t the most efficient way in which to handle the high number of commands.

Now, AWS has modules with names that begin with AWS.Tools. In the below example, I have assigned the $AWSModules variable with all the AWS modules located in the PowerShell Gallery that begin with the name AWS.Tools.*.

$AWSModules = Find-Module -Name AWS.Tools.*
$AWSModules.Count
241
$AWSModules | Select-Object -First 10 -Property Name,Version
Name                              Version
----                              -------
AWS.Tools.Common                  4.1.10.0
AWS.Tools.EC2                     4.1.10.0
AWS.Tools.S3                      4.1.10.0
AWS.Tools.Installer               1.0.2.0
AWS.Tools.SimpleSystemsManagement 4.1.10.0
AWS.Tools.SecretsManager          4.1.10.0
AWS.Tools.SecurityToken           4.1.10.0
AWS.Tools.IdentityManagement      4.1.10.0
AWS.Tools.Organizations           4.1.10.0
AWS.Tools.CloudFormation          4.1.10.0

Once I determined that there were over 240 individual modules, I piped the $AWSModule variable to the Get-Member command to view its methods and properties. I didn’t know what I was after, but out of the returned properties, I was intrigued by the Includes property. I used that property as a part of the below command. Each of the 241 entries includes a Command, DscResource, Cmdlet, Workflow, RoleCapability, and Function nested property inside the Includes property.

$AWSModules.Includes | Select-Object -First 3
Name Value
---- -----
Command {Clear-AWSHistory, Set-AWSHistoryConfiguration, Initialize-AWSDefaultConfiguration, Clear-AWSDefaultConfiguration…}
DscResource {}
Cmdlet {Clear-AWSHistory, Set-AWSHistoryConfiguration, Initialize-AWSDefaultConfiguration, Clear-AWSDefaultConfiguration…}
Workflow {}
RoleCapability {}
Function {}
Command {Add-EC2CapacityReservation, Add-EC2ClassicLinkVpc, Add-EC2InternetGateway, Add-EC2NetworkInterface…}
DscResource {}
Cmdlet {Add-EC2CapacityReservation, Add-EC2ClassicLinkVpc, Add-EC2InternetGateway, Add-EC2NetworkInterface…}
Workflow {}
RoleCapability {}
Function {}
Command {Add-S3PublicAccessBlock, Copy-S3Object, Get-S3ACL, Get-S3Bucket…}
DscResource {}
Cmdlet {Add-S3PublicAccessBlock, Copy-S3Object, Get-S3ACL, Get-S3Bucket…}
Workflow {}
RoleCapability {}
Function {}

After some more inspection, I decided that each command populated the Command property and the Cmdlet or the Function property, as well, depending on what type of commands were included in the module. Next, I just returned the commands using dot notation. This isn’t all of them, but you get the idea.

$AWSModules.Includes.Command
Clear-AWSHistory
Set-AWSHistoryConfiguration
Initialize-AWSDefaultConfiguration
Clear-AWSDefaultConfiguration
Get-AWSPowerShellVersion
...
Remove-FISExperimentTemplate
Remove-FISResourceTag
Start-FISExperiment
Stop-FISExperiment
Update-FISExperimentTemplate

If you’re curious, as I was, Update-FISExperimentTemplate, “Calls the AWS Fault Injection Simulator UpdateExperimentTemplate API operation.” I have no idea.

With a little more dot notation, I was able to get the count.

$AWSModules.Includes.Command.Count
8942

And now, I know what I used to know. I also know that AWS has been busy. And, that they’ve continued to make a huge effort in the PowerShell space. If you don’t go straight to the PowerShell Gallery, and I would recommend you do, you can always start at the AWS PowerShell webpage.

PowerShell Approved Verb Synonyms

One of the best design decisions, when PowerShell was initially being created, was using approved verbs in naming a command. When people use those, we can guarantee some consistency between command names. As commands — both cmdlets and functions — we expect to see an approved verb followed by a dash and then a singular noun or nouns. Sometimes there’s a prefix after the dash, but before the noun(s). Here are a few examples of some random commands: Import-Module, Get-VMGroup, Add-AzVhd, Export-AzDataLakeStoreItem, Set-WsusProduct, and Stop-SSMCommand. While these commands display a few of the approved verbs, it’s not all of them. Use the Get-Verb command to view the full list of the approved verbs, from which you should choose when writing a new function or cmdlet.

There may not have always been this many, but in 7.2.0-preview4, there are 100 approved verbs.

[PS7.2.0-preview.4][C:\] Get-Verb | Measure-Object

Count             : 100
Average           :
Sum               :
Maximum           :
Minimum           :
StandardDeviation :
Property          :

Even though there’s a good number of options from which to choose, there may not always feel like there’s one for your newest command. While I’ve brought this up to the community at least a couple of times before, it’s time to do it again. It’s the Get-TMVerbSynonym function. I authored it in June 2016, I updated it in 2017, and I used it earlier today, in 2021. It can be found in the Powershell Gallery and installed using the below command:

[PS7.2.0-preview.4][C:\] Install-Script -Name Get-TMVerbSynonym

The file it downloads is a function inside of a script file — a .ps1 file. In order to add a function inside a script file to the current PowerShell session, you need to dot source the file. That’s what the mystery dot is doing between my prompt and the path to the .ps1 file below.

[PS7.2.0-preview.4][C:\] . C:\Users\tommymaynard\Documents\PowerShell\Scripts\Get-TMVerbSynonym.ps1

Once it’s dot sourced, you can use the function. In closing, here are few examples of the function in action. Each of these commands uses the Format-Table -AutoSize command and parameter in order that the results are easier to read.

[PS7.1.3][C:\] Get-TMVerbSynonym -Verb change | Format-Table -AutoSize

Verb   Synonym     Group  Approved Notes
----   -------     -----  -------- -----
Change Alter                 False
Change Commute               False
Change Convert     Data       True
Change Deepen                False
Change Dress                 False Generic Term
Change Exchange              False
Change Get Dressed           False Generic Term
Change Go                    False Generic Term
Change Interchange           False
Change Locomote              False Generic Term
Change Modify                False
Change Move        Common     True Generic Term
Change Replace               False Generic Term
Change Shift                 False
Change Stay                  False Antonym
Change Switch      Common     True
Change Transfer              False
Change Transfer              False Generic Term
Change Travel                False Generic Term
Change Vary                  False

[PS7.1.3][C:\] Get-TMVerbSynonym -Verb change -Approved | Format-Table -AutoSize

Verb   Synonym Group  Approved Notes
----   ------- -----  -------- -----
Change Convert Data       True
Change Move    Common     True Generic Term
Change Switch  Common     True

[PS7.1.3][C:\] 

If there’s ever a verb you want to use, but it’s not approved, then try this function. You’ll likely be able to choose something that’s close enough, and is approved.

Encoding and Decoding PowerShell Strings

Every few months Base64 comes up and I have to go looking for that one post I saw that one time. It’s because that code, on that one site, hasn’t been memorized — not by me, anyway. So, here it is. The below example shows how to encode and decode a string using Base64. Keep in mind that different things, such as HTTP Headers, might require different character sets. In this example, I’m using UTF-8, but it could have been ASCII, or Unicode, or something else.

Clear-Host
$UserName = 'tommymaynard'
$Password = 'password'

"The UserName is '$UserName' and the password is '$Password'."
"Encoding as $($UserName):$($Password)"

$Text = "$($UserName):$($Password)"
$Bytes = [System.Text.Encoding]::UTF8.GetBytes($Text)
$EncodedText =[Convert]::ToBase64String($Bytes)
"Encoded text: $EncodedText"

$DecodedText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($EncodedText))
"Decoded text: $DecodedText"

When the above code is executed it produces the below results.

The UserName is 'tommymaynard' and the password is 'password'.
Encoding as tommymaynard:password
Encoded text: dG9tbXltYXluYXJkOnBhc3N3b3Jk
Decoded text: tommymaynard:password

And, now that’s that. When I need this next, it’ll be right here on my own blog. Now back to that Python to PowerShell project where I need Base64 encoding. By the way, this is a great online encode/decode tool that you might find helpful: https://www.base64decode.org.

Dynamic PowerShell Version in Windows Terminal Tabs Part II

Maybe no one noticed. Or, maybe they did and just didn’t say anything. As you’ll see, I’ve written a quick fix to a post I wrote last week.

I shouldn’t have, but back in Part I, I put reliance on my prompt function for the Windows Terminal tab titles. This means that I have to ensure that I never change my prompt function. If I do, it’s very possible that adding the version of PowerShell to the tab titles will break. This isn’t good. That dependence, while it works just fine now, could become a problem in time.

Knowing that, let’s remove it. Let’s get the version in the Windows Terminal tab titles from something other than my prompt. Let’s put it where it belongs, on the version itself — on the $PSVersionTable automatic variable. My prompt Itself uses that, so why shouldn’t this Windows Terminal tab title code, too? This makes much more sense than coupling the tab titles with my prompt function.

$Host.UI.RawUI.WindowTitle = "$($Host.UI.RawUI.WindowTitle) $($PSVersionTable.PSVersion.ToString())"

The tabs look exactly like they did before, but now my prompt function doesn’t play into the versions making their way to the tabs at all. Altering the prompt doesn’t matter now, or now, at any time in the future.

Dynamic PowerShell Version in Windows Terminal Tabs

There’s a Part II to this post now, be sure to read it after you’ve read this post.

I find myself inside the Window Terminal’s JSON Settings file each time I update the version of PowerShell on my system. That includes non-preview and preview versions of PowerShell. Well, as of today, that’s changed. Previously I would install an updated version, jump into the JSON Settings file, and modify the Name and TabTitle properties of the corresponding profile. I’ve long wanted the version number on the Tab and so I was hard coding the values. No longer.

Let’s take a look at my prompt function. It’s mostly the default one, with some slight changes. Here’s the prompt code from my $PROFILE. Preview version or not, they both use the same $PROFILE script; therefore, they use the same prompt function.

Function Prompt {
    "$("[PS$($PSVersionTable.PSVersion.ToString())][$($executionContext.SessionState.Path.CurrentLocation)]" * ($nestedPromptLevel + 1)) ";
    # .Link
    # https://go.microsoft.com/fwlink/?LinkID=225750
    # .ExternalHelp System.Management.Automation.dll-help.xml
} # End Function: Prompt.

Here’s how the prompt renders in both the non-preview and preview versions of PowerShell.



What I do, and you’ll see it momentarily is extract the version number — “7.1.3” and “7.2.0-preview.4” from the above prompts and write them to the WindowTitle along with what was already there (the word “PowerShell”). The below code is beneath the Prompt function in my $PROFILE script.

$Prompt = prompt
$Host.UI.RawUI.WindowTitle = "$($Host.UI.RawUI.WindowTitle) $(($Prompt.Split('][').TrimStart('[PS')[0]))"

First, it captures the prompt into a variable called Prompt. Then it manipulates the contents of the variable until we only have what we want. This first thing it does consists of splitting the prompt string at the back-to-back closing and opening square brackets: “][“. In the case of the non-preview prompt, this leaves me with two strings: [PS7.1.3 and C:\]. Next, it trims off the [PS from the start of the first of the two strings. As you can tell, we only have an interest in our first string — the value in the zero index: [0]. Once’s the parsing is complete, our value is appended to the existing value in the WindowTitle property and it is all written back to the WindowTitle property.

It’s from here forward that the title includes not just the word “PowerShell,” but also the version. That’s it. A little extra work now to remove all the work from later on.

There’s a Part II to this post now, be sure to read it.

Simple Simple Microsoft Crescendo Example Part II

The Microsoft.PowerShell.Crescendo module is mostly brand new. It’s still early on in its development. It’s currently at version 0.4.1. This isn’t my first discussion on this topic, so please read Part I on this topic. That’ll give you an idea of my first experience working with the module. In that post, we created a PowerShell module that contained a single command that wrapped the mstsc.exe Remote Desktop Connection executable. Although there are several, I only wrote it with a single parameter: a stand-in for /v. This parameter expects the name of the computer or IP address, such as mstsc /v Server01.

In that example I copied, pasted, and edited an existing *.Crescendo.json file. Today, we’ll wrap the same command (even though I alluded to using something else), however, we’ll do it using other cmdlets from the module. These will include New-CrescendoCommand, New-ParameterInfo, New-UsageInfo, and New-ExampleInfo. I had no idea what these were for as the help is limited, but I think I figured it out for now. If you know more, then of course you’re welcome to comment and participate.

In the first example, we invoked New-CrescendoCommand. The available parameters at the time of this writing are -Verb and -Noun. Knowing those parameters, it’s straightforward what this was about. This is the naming for our new PowerShell command. As you can see, I supplied Connect and RemoteComputer, respectively. These are the same values that we supplied in Part I of this series. As you can likely tell, the command creates an object (a [TypeName] Command object to be exact). We send that object to ConvertTo-Json, as we’ll need everything in JSON before creating our mstsc.Crescendo.json file. As you can also tell, the object — both the PowerShell object and JSON object — has several other properties. As best as I could determine, there was no way to include these when invoking the New-CrescendoCommand. Do notice that the OriginalName property is blank and that there wasn’t a way to include that when invoking the command. We’ll be back to discuss this later.

[PS7.1.0] $Command = New-CrescendoCommand -Verb Connect -Noun RemoteComputer
[PS7.1.0] $Command

Verb : Connect
Noun : RemoteComputer
OriginalName :
OriginalCommandElements :
Aliases :
DefaultParameterSetName :
SupportsShouldProcess : False
SupportsTransactions : False
NoInvocation : False
Description :
Usage :
Parameters : {}
Examples : {}
OriginalText :
HelpLinks :
OutputHandlers :

[PS7.1.0] $Command = $Command | ConvertTo-Json
[PS7.1.0] $Command
{
  "Verb": "Connect",
  "Noun": "RemoteComputer",
  "OriginalName": null,
  "OriginalCommandElements": null,
  "Aliases": null,
  "DefaultParameterSetName": null,
  "SupportsShouldProcess": false,
  "SupportsTransactions": false,
  "NoInvocation": false,
  "Description": null,
  "Usage": null,
  "Parameters": [],
  "Examples": [],
  "OriginalText": null,
  "HelpLinks": null,
  "OutputHandlers": null
}

In this code example, we essentially do the same thing. We create a PowerShell object and then convert it to JSON. This is the addition of a new parameter.

[PS7.1.0] $Parameter = New-ParameterInfo -Name ComputerName -OriginalName /v
[PS7.1.0] $Parameter

ParameterType                   : object
Position                        : 2147483647
Name                            : ComputerName
OriginalName                    : /v
OriginalText                    :
Description                     :
DefaultValue                    :
DefaultMissingValue             :
AdditionalParameterAttributes   :
Mandatory                       : False
ParameterSetName                :
Aliases                         :
OriginalPosition                : 0
ValueFromPipeline               : False
ValueFromPipelineByPropertyName : False
ValueFromRemainingArguments     : False
NoGap                           : False

[PS7.1.0] $Parameter = $Parameter | ConvertTo-Json
[PS7.1.0] $Parameter
{
  "ParameterType": "object",
  "Position": 2147483647,
  "Name": "ComputerName",
  "OriginalName": "/v",
  "OriginalText": null,
  "Description": null,
  "DefaultValue": null,
  "DefaultMissingValue": null,
  "AdditionalParameterAttributes": null,
  "Mandatory": false,
  "ParameterSetName": null,
  "Aliases": null,
  "OriginalPosition": 0,
  "ValueFromPipeline": false,
  "ValueFromPipelineByPropertyName": false,
  "ValueFromRemainingArguments": false,
  "NoGap": false
}

And in this code example, we essentially do the same thing, too. This PowerShell object converted to JSON has to do with Usage. I’m still working that one over in my mind, but we’ll take a closer look.

[PS7.1.0] $Usage = New-UsageInfo -usage 'Runs Remote Desktop Connection'
[PS7.1.0] $Usage

Synopsis                       SupportsFlags HasOptions
--------                       ------------- ----------
Runs Remote Desktop Connection False         False

[PS7.1.0] $Usage = $Usage | ConvertTo-Json
[PS7.1.0] $Usage
{
  "Synopsis": "Runs Remote Desktop Connection",
  "SupportsFlags": false,
  "HasOptions": false,
  "OriginalText": null
}

Finally, we create a comment-based help example and ensure it’s in the JSON format.

[PS7.1.0] $Example = New-ExampleInfo -command Connect-RemoteComputer -originalCommand 'C:\Windows\System32\mstsc.exe' -description 'Wraps Remote Desktop Connection' 
[PS7.1.0] $Example

Command                OriginalCommand               Description
-------                ---------------               -----------
Connect-RemoteComputer C:\Windows\System32\mstsc.exe Wraps Remote Desktop Connection

[PS7.1.0] $Example = $Example | ConvertTo-Json
[PS7.1.0] $Example
{
  "Command": "Connect-RemoteComputer",
  "OriginalCommand": "C:\\Windows\\System32\\mstsc.exe",
  "Description": "Wraps Remote Desktop Connection"
}

Next, we have to manually put these pieces of JSON together into our single, mstsc.Crescendo.json file. Let’s do that next. We’ll begin with the Parameter JSON we created and add to it the Command JSON. In the Command JSON section, we had an entry that said "Parameters": [], In between the square brackets, we have to add the Parameter JSON. This has been done below.

{
    "Verb": "Connect",
    "Noun": "RemoteComputer",
    "OriginalName": null,
    "OriginalCommandElements": null,
    "Aliases": null,
    "DefaultParameterSetName": null,
    "SupportsShouldProcess": false,
    "SupportsTransactions": false,
    "NoInvocation": false,
    "Description": null,
    "Usage": null,
    "Parameters": [
        {
            "ParameterType": "object",
            "Position": 2147483647,
            "Name": "ComputerName",
            "OriginalName": "/v",
            "OriginalText": null,
            "Description": null,
            "DefaultValue": null,
            "DefaultMissingValue": null,
            "AdditionalParameterAttributes": null,
            "Mandatory": false,
            "ParameterSetName": null,
            "Aliases": null,
            "OriginalPosition": 0,
            "ValueFromPipeline": false,
            "ValueFromPipelineByPropertyName": false,
            "ValueFromRemainingArguments": false,
            "NoGap": false
        }
    ],
    "Examples": [],
    "OriginalText": null,
    "HelpLinks": null,
    "OutputHandlers": null
}

Next, we’ll add our Usage JSON to its place in our mstsc.Crescendo.json file

{
    "Verb": "Connect",
    "Noun": "RemoteComputer",
    "OriginalName": null,
    "OriginalCommandElements": null,
    "Aliases": null,
    "DefaultParameterSetName": null,
    "SupportsShouldProcess": false,
    "SupportsTransactions": false,
    "NoInvocation": false,
    "Description": null,
    "Usage": {
        "Synopsis": "Runs Remote Desktop Connection",
        "SupportsFlags": false,
        "HasOptions": false,
        "OriginalText": null
    },
    "Parameters": [
        {
            "ParameterType": "object",
            "Position": 2147483647,
            "Name": "ComputerName",
            "OriginalName": "/v",
            "OriginalText": null,
            "Description": null,
            "DefaultValue": null,
            "DefaultMissingValue": null,
            "AdditionalParameterAttributes": null,
            "Mandatory": false,
            "ParameterSetName": null,
            "Aliases": null,
            "OriginalPosition": 0,
            "ValueFromPipeline": false,
            "ValueFromPipelineByPropertyName": false,
            "ValueFromRemainingArguments": false,
            "NoGap": false
        }
    ],
    "Examples": [],
    "OriginalText": null,
    "HelpLinks": null,
    "OutputHandlers": null
}

And finally, we’ll add our Example JSON.

{
    "Verb": "Connect",
    "Noun": "RemoteComputer",
    "OriginalName": null,
    "OriginalCommandElements": null,
    "Aliases": null,
    "DefaultParameterSetName": null,
    "SupportsShouldProcess": false,
    "SupportsTransactions": false,
    "NoInvocation": false,
    "Description": null,
    "Usage": {
        "Synopsis": "Runs Remote Desktop Connection",
        "SupportsFlags": false,
        "HasOptions": false,
        "OriginalText": null
    },
    "Parameters": [
        {
            "ParameterType": "object",
            "Position": 2147483647,
            "Name": "ComputerName",
            "OriginalName": "/v",
            "OriginalText": null,
            "Description": null,
            "DefaultValue": null,
            "DefaultMissingValue": null,
            "AdditionalParameterAttributes": null,
            "Mandatory": false,
            "ParameterSetName": null,
            "Aliases": null,
            "OriginalPosition": 0,
            "ValueFromPipeline": false,
            "ValueFromPipelineByPropertyName": false,
            "ValueFromRemainingArguments": false,
            "NoGap": false
        }
    ],
    "Examples": [
        {
            "Command": "Connect-RemoteComputer",
            "OriginalCommand": "C:\\Windows\\System32\\mstsc.exe",
            "Description": "Wraps Remote Desktop Connection"
        }
    ],
    "OriginalText": null,
    "HelpLinks": null,
    "OutputHandlers": null
}

I don’t think it’s there yet, but one day there will likely be a command that does all this for us. Maybe there already is and somehow I’ve overlooked it. With this JSON created and saved as mstsc.Crescendo.json, we can attempt to use it with Export-CrescendoModule as we did in Part I.

[PS7.1.0] Export-CrescendoModule -ConfigurationFile 'C:\Users\tommymaynard\Documents\PowerShell\Modules\Microsoft.PowerShell.Crescendo\0.4.1\Samples\mstsc.Crescendo.json' -ModuleName 'RemoteComputer.psm1'

If you try this, you’ll notice it fails.

This is because the New-CrescendoCommand didn’t include an OriginalName parameter and therefore our JSON didn’t include a much-needed value. Without it, the command doesn’t point to an existing command on the computer, which becomes the reason behind the above error message. Even though it throws an error, it still creates the file (although it’s missing much of the good stuff). You’ll have to remove the file or you’ll get another error about the file already existing.

Our JSON ends up with this: “OriginalName”: null, instead of this: “OriginalName”:”/Windows/System32/mstsc.exe”,. As you can see below, I’ve added this in, so the code here is complete.

{
    "Verb": "Connect",
    "Noun": "RemoteComputer",
    "OriginalName": "/Windows/System32/mstsc.exe",
    "OriginalCommandElements": null,
    "Aliases": null,
    "DefaultParameterSetName": null,
    "SupportsShouldProcess": false,
    "SupportsTransactions": false,
    "NoInvocation": false,
    "Description": null,
    "Usage": {
        "Synopsis": "Runs Remote Desktop Connection",
        "SupportsFlags": false,
        "HasOptions": false,
        "OriginalText": null
    },
    "Parameters": [
        {
            "ParameterType": "object",
            "Position": 2147483647,
            "Name": "ComputerName",
            "OriginalName": "/v",
            "OriginalText": null,
            "Description": null,
            "DefaultValue": null,
            "DefaultMissingValue": null,
            "AdditionalParameterAttributes": null,
            "Mandatory": false,
            "ParameterSetName": null,
            "Aliases": null,
            "OriginalPosition": 0,
            "ValueFromPipeline": false,
            "ValueFromPipelineByPropertyName": false,
            "ValueFromRemainingArguments": false,
            "NoGap": false
        }
    ],
    "Examples": [
        {
            "Command": "Connect-RemoteComputer",
            "OriginalCommand": "C:\\Windows\\System32\\mstsc.exe",
            "Description": "Wraps Remote Desktop Connection"
        }
    ],
    "OriginalText": null,
    "HelpLinks": null,
    "OutputHandlers": null
}

Have fun and keep watching for newer versions. I suspect I will.

Invoke-RestMethod with a SOAP API

Did it again. I’m back here this evening to write another post after authoring some PoC code for a project. I’ve got to put it somewhere for later. In doing that, there’s no reason it shouldn’t be content in which others can consume.

In the past, I have absolutely written PowerShell to interact with REST (REpresentational State Transfer) APIs. What I haven’t done until now, is interact with SOAP (Simple Object Access Protocol) APIs. The below example is a full-featured PowerShell function. Its purpose, with the help of W3Schools public SOAP API, is to convert Fahrenheit temperatures to Celsius and vice versa. Typical normal stuff, except that I’m not doing the calculations. Instead, I’m sending off a temperature to an API to be converted. One of the big differences between REST and SOAP is the content of the payload. With SOAP we’re sending XML, whereas, with REST, we’d likely send JSON.

While you can inspect the code yourself, I will at minimum tell you how the function can be invoked. Its name is Convert-Temperature and it includes two parameters: Temperature and TemperatureScale. Enter an integer for the Temperature parameter and either Fahrenheit or Celsius for the TemperatureScale parameter. It’ll send off the information as XML to the API. The returned value will be extracted from the returned XML and rounded to two decimal places. I was going to make the rounding optional but I obviously changed my mind. I didn’t find any value in allowing a user to make this determination.

Here are a couple of invocation examples first, and then, the code. Maybe this will be helpful for you. I suspect it will be for me in time, as I’m going to likely have to make use of a SOAP API.

[PS7.1.0] [C:\] Convert-Temperature -Temperature 212 -TemperatureScale Fahrenheit

TemperatureScale Temperature ConvertedTemperature
---------------- ----------- --------------------
Fahrenheit               212                  100

[PS7.1.0] [C:\] Convert-Temperature -Temperature 100 -TemperatureScale Celsius

TemperatureScale Temperature ConvertedTemperature
---------------- ----------- --------------------
Celsius                  100                  212
Function Convert-Temperature {
	[CmdletBinding()]
	Param (
		[Parameter(Mandatory)]
		[int]$Temperature,

		[Parameter(Mandatory)]
		[ValidateSet('Fahrenheit','Celsius')]
		[string]$TemperatureScale
	) # End Param

	Begin {
		$Headers = @{'Content-Type' = 'text/xml'}
	} # End Begin.

	Process {
		If ($TemperatureScale -eq 'Fahrenheit') {
			$Body = @"
<soap12:Envelope xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xmlns:xsd=`"http://www.w3.org/2001/XMLSchema`" xmlns:soap12=`"http://www.w3.org/2003/05/soap-envelope`">
	<soap12:Body>
		<FahrenheitToCelsius xmlns=`"https://www.w3schools.com/xml/`">
			<Fahrenheit>$Temperature</Fahrenheit>
		</FahrenheitToCelsius>
	</soap12:Body>
</soap12:Envelope>
"@
		} Else {
			$Body = @"
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
	<soap12:Body>
		<CelsiusToFahrenheit xmlns="https://www.w3schools.com/xml/">
			<Celsius>$Temperature</Celsius>
		</CelsiusToFahrenheit>
	</soap12:Body>
</soap12:Envelope>
"@
		} # End If-Else.
	} # End Process.

	End {
		$Response = Invoke-RestMethod -Uri 'https://www.w3schools.com/xml/tempconvert.asmx' -Method 'POST' -Headers $Headers -Body $Body
		If ($TemperatureScale -eq 'Fahrenheit') {
			$ConvertedTemperature = $([System.Math]::Round($Response.Envelope.Body.FahrenheitToCelsiusResponse.FahrenheitToCelsiusResult,2))
		} Else {
			$ConvertedTemperature = ([System.Math]::Round($Response.Envelope.Body.CelsiusToFahrenheitResponse.CelsiusToFahrenheitResult,2))
		} # End If-Else.

		[PSCustomObject]@{
			TemperatureScale = $TemperatureScale
			Temperature = $Temperature
			ConvertedTemperature = $ConvertedTemperature
		}
	} # End End.
} # End Function: Convert-Temperature.