AWS UserData Multiple Run Framework Part IV (b)


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on September 13, 2019.


This article began with Part a. Read that, if you haven’t already.

First off, the code-producing function has been renamed from New-AWSMultiRunTemplate to New-AWSUserDataMultipleRunTemplate. Sure, it’s longer in name, but it’s more clear (to me at least) when I see its name. Other small changes were the removal of unnecessary code comments. While these may have been in the New-AWSUserDataMultipleRunTemplate function itself, they were definitely changed in the produced code. Additionally, I’ve added a ProjectName parameter. The value, supplied to this parameter, is used throughout the generated code for naming purposes within the Registry. There are code changes to the produced code to run against Server 2016 and greater, as well. Therefore, it runs against Windows Server 2012 and 2012 R2 (and probably older), as well as Server 2016 and 2019. Hopefully, it will be extended further, but only AWS knows that for sure. Server 2012 R2 and earlier used EC2Config to configure a Windows instance, while 2016 and 2019 used EC2Launch.

If (test1) {
    {<statement list 1>}
} # End If.

As seen above, I tend to comment on my closing language construct brackets (see # End If.). These are now included in the produced code, both statically and dynamically. It’s a personal preference (that you’ll have to deal with if you use this free, code offering).

This is the new, New-AWSUserDataMultipleRunTemplate code producing function. This isn’t in the PowerShell Gallery, or anywhere else. It probably will never be, either. In fact, this is likely the last time I’ll prep it for public consumption. So again, this is the code you run to create the code you enter in UserData. We won’t discuss what’s in this code; however, we’ll run it further below and discuss that at a minimum.

Function New-AWSUserDataMultipleRunTemplate {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [string]$ProjectName,

        [Parameter()]
        [ValidateRange(1,10)]
        [int]$CodeSectionCount = 2,

        [Parameter()]
        [ValidateSet('All','AllButLast')]
        [string]$EnableUserData = 'AllButLast',

        [Parameter()]
        [ValidateSet('All','AllButLast')]
        [string]$EnableRestart = 'AllButLast'
    )

    Begin { 
        #region Set Write-Verbose block location.
        $BlockLocation = '[BEGIN  ]'
        Write-Verbose -Message "$BlockLocation Entering the Begin block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Storing the $ProjectName template's function to memory."
        Write-Verbose -Message "$BlockLocation Ensure the server where the code will reside does not already have the ""HKLM:\SOFTWARE\$ProjectName"" path."
        $TemplateFunction = @"
# >> Add function to memory.
Function Set-SystemForNextRun {
    Param (
        [string]`$CodeSectionComplete,
        [switch]`$ResetUserData,
        [switch]`$RestartInstance
    )
    If (`$CodeSectionComplete) {
        [System.Void](New-ItemProperty -Path 'HKLM:\SOFTWARE\$ProjectName' -Name "CodeSection`$CodeSectionComplete" -Value 'Complete')
    } # End If.
    If (`$ResetUserData) {
        try {
            `$Path = 'C:\Program Files\Amazon\Ec2ConfigService\Settings\config.xml'
            [xml]`$ConfigXml = Get-Content -Path `$Path -ErrorAction Stop
            (`$ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
                Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
            `$ConfigXml.Save(`$Path)
        } catch {
            C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeInstance.ps1 -Schedule
        } # End try-catch.
    } # End If.
    If (`$RestartInstance) {
        Restart-Computer -Force
    } # End If.
} # End Function: Set-SystemForNextRun.

# >> Create/Check for Registry Subkey.
If (-Not(Get-Item -Path 'HKLM:\SOFTWARE\$ProjectName' -ErrorAction SilentlyContinue)) {
    [System.Void](New-Item -Path 'HKLM:\SOFTWARE\' -Name '$ProjectName')
} # End If.

# >> Run user code/invoke Set-SystemForNextRun function.

"@
    } # End Begin.

    Process {
        #region Set Write-Verbose block location.
        $BlockLocation = '[PROCESS]'
        Write-Verbose -Message "$BlockLocation Entering the Process block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Beginning to create the If-ElseIf code for the template."
        1..$CodeSectionCount | ForEach-Object {
            If ($_ -eq 1) {
                $Start = 'If'
                If ($CodeSectionCount -eq 1) {
                    $End = '# End If.'
                } # End If.
            } ElseIf ($_ -eq $CodeSectionCount -and $CodeSectionCount -eq 2) {
                $Start = 'ElseIf'; $End = '# End If-ElseIf.'
            } ElseIf ($_ -eq $CodeSectionCount -and $CodeSectionCount -ne 2) {
                $End = "# End If-ElseIf x$($CodeSectionCount - 1)."
            } Else {
                $Start = 'ElseIf'
            } # End If.

            If ($EnableUserData -eq 'All') {
                $UserData = '-ResetUserData '
            } ElseIf ($_ -eq $CodeSectionCount) {
                $UserData = $null
            } Else {
                $UserData = '-ResetUserData '
            } # End If.

            If ($EnableRestart -eq 'All') {
                $Restart = '-RestartInstance'
            } ElseIf ($_ -eq $CodeSectionCount) {
                $Restart = $null
            } Else {
                $Restart = '-RestartInstance'
            } # End If.

            $TemplateIfElseIf += @"
$Start (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\$ProjectName').CodeSection$_ -eq 'Complete')) {

    # CodeSection $_.

    Set-SystemForNextRun -CodeSectionComplete $_ $UserData$Restart
} $End
"@
        } # End ForEach-Object.
    } # End Process.

    End {
        #region Set Write-Verbose block location.
        $BlockLocation = '[END    ]'
        Write-Verbose -Message "$BlockLocation Entering the End block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Creating the AWS UserData Mulitple Run Framework code with $CodeSectionCount code section$(If ($CodeSectionCount -gt 1) {'s'})."
        "$TemplateFunction$TemplateIfElseIf"
    } # End End.
} # End Function: New-AWSUserDataMultipleRunTemplate.

Copy and paste the above, New-AWSUserDataMultipleRunTemplate function into VS Code, or another preferred PowerShell development environment, if there is such a thing. You’re not still using the ISE, are you? Then, add the function to memory for use (in VS Code, that’s Ctrl + A to select all, then F8 to run the selection). Once the function is sitting in memory, we can use it to create our UserData code, as seen below. In this version—it doesn’t even really have a version number, which is weird—there’s now a mandatory ProjectName parameter. Keep this short and simple, and if it were me, I keep spaces and odd characters out of it. This is the value that will be used in the Windows Registry, and within the code that’s produced for UserData.

PS > New-AWSUserDataMultipleRunTemplate -ProjectName MistFit
# >> Add function to memory.
Function Set-SystemForNextRun {
    Param (
       [string]$CodeSectionComplete,
       [switch]$ResetUserData,
        [switch]$RestartInstance
    )
    If ($CodeSectionComplete) {
        [System.Void](New-ItemProperty -Path 'HKLM:\SOFTWARE\MistFit' -Name "CodeSection$CodeSectionComplete" -Value 'Complete')
    } # End If.
    If ($ResetUserData) {
        try {
            $Path = 'C:\Program Files\Amazon\Ec2ConfigService\Settings\config.xml'
            [xml]$ConfigXml = Get-Content -Path $Path -ErrorAction Stop
            ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
                Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
            $ConfigXml.Save($Path)
        } catch {
            C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeInstance.ps1 -Schedule
        } # End try-catch.
    } # End If.
    If ($RestartInstance) {
        Restart-Computer -Force
    } # End If.
} # End Function: Set-SystemForNextRun.

# >> Create/Check for Registry Subkey.
If (-Not(Get-Item -Path 'HKLM:\SOFTWARE\MistFit' -ErrorAction SilentlyContinue)) {
    [System.Void](New-Item -Path 'HKLM:\SOFTWARE\' -Name 'MistFit')
} # End If.

# >> Run user code/invoke Set-SystemForNextRun function.
If (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\MistFit').CodeSection1 -eq 'Complete')) {

    # CodeSection 1.

Set-SystemForNextRun -CodeSectionComplete 1 -ResetUserData -RestartInstance
} ElseIf (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\MistFit').CodeSection2 -eq 'Complete')) {

    # CodeSection 2.

Set-SystemForNextRun -CodeSectionComplete 2
} # End If-ElseIf.
PS C:>

By default, it still creates two code sections, as can be seen just above in the If-ElseIf statement. It can create just one, although that makes less sense for something that can provide multiple opportunities for configuration between restarts. Even if you only need one, this may still be the framework for you. Maybe you need a restart after a single configuration pass. It will do up to 10 code sections if, for some reason, you need that many opportunities to configure a single instance. Not me. I’ve only ever needed three or four total. The New-AWSUserDataMultipleRunTemplate function still includes the EnableUserData and EnableRestart switch parameters. Both have the default parameter value AllButLast, however, both parameters can accept All as the value, too. If this is used for the EnableRestart switch parameter, the EC2 instance will restart after the last time it’s configured (the last code section). In my experience, it’s not always necessary to restart an instance after its final configuration, but this would allow for the time it’s needed. Once you get to know the produced code well, you can manually edit it if necessary.

The produced code has three definitive sections. We’ll start with the below, third section. By default, the New-AWSUserDataMultipleRunTemplate function creates two code sections within the third section. Read that a few times; it’s confusing. It’s like this: The produced code consists of three sections (they each start with # >>). Inside the third one, is where you can have multiple code sections, where user code, that someone else provides, maybe you, is executed against an instance. Notice that there are two comments inside our If-ElseIf statement. On the first pass, it’ll run whatever the code that the user enters to replace “# CodeSection 1.” After the UserData is enabled and the instance is restarted, it’ll run whatever code is entered to replace “# CodeSection 2.” We’ll see more about how it does this shortly.

# >> Run user code/invoke Set-SystemForNextRun function.
If (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\MistFit').CodeSection1 -eq 'Complete')) {

    # CodeSection 1.

    Set-SystemForNextRun -CodeSectionComplete 1 -ResetUserData -RestartInstance
} ElseIf (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\MistFit').CodeSection2 -eq 'Complete')) {

    # CodeSection 2.

    Set-SystemForNextRun -CodeSectionComplete 2
} # End If-ElseIf.

Now that we’ve spent some time with the third section of our produced code, let’s move upward and focus on the middle, or second section, of the code that’s been produced. Here’s that second section, now. It’s real simple. If a specific Windows Registry Subkey doesn’t exist, it’s created. The need to create this will only happen once (the first run). Every other time this If statement fires, it’ll be false and therefore, it won’t attempt to create (something that’s already been created).

# >> Create/Check for Registry Subkey.
If (-Not(Get-Item -Path 'HKLM:\SOFTWARE\MistFit' -ErrorAction SilentlyContinue)) {
    [System.Void](New-Item -Path 'HKLM:\SOFTWARE\' -Name 'MistFit')
} # End If.

And, here’s the first section, last. This function, Set-SystemForNextRun, is placed into memory after this portion of the UserData is executed. It’s invoked by the third section. If you go back up to where we discussed the third section, you’ll see where.

# >> Add function to memory.
Function Set-SystemForNextRun {
    Param (
        [string]$CodeSectionComplete,
        [switch]$ResetUserData,
        [switch]$RestartInstance
    )
    If ($CodeSectionComplete) {
        [System.Void](New-ItemProperty -Path 'HKLM:\SOFTWARE\MistFit' -Name "CodeSection$CodeSectionComplete" -Value 'Complete')
    } # End If.
    If ($ResetUserData) {
        try {
            $Path = 'C:\Program Files\Amazon\Ec2ConfigService\Settings\config.xml'
            [xml]$ConfigXml = Get-Content -Path $Path -ErrorAction Stop
            ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
                Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
            $ConfigXml.Save($Path)
        } catch {
            C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeInstance.ps1 -Schedule
        } # End try-catch.
    } # End If.
    If ($RestartInstance) {
        Restart-Computer -Force
    } # End If.
} # End Function: Set-SystemForNextRun.

In the first code section of the third overall section, Set-SystemForNextRun was first invoked with all three possible parameters. The first parameter is sent in a numeric 1 to the CodeSectionComplete parameter. This created a new Registry value in the “HKLM:\SOFTWARE\MistFit” path. It coerced the number into a string, creating a Registry value named CodeSection1 that had a string value of Complete. These values are how the code knows what’s been done before, and what still needs to be completed. The second parameter was the ResetUserData switch parameter, indicating to enable UserData to run again, the next time the computer is restarted. The third parameter, RestartInstance, as seen below, restarts the computer, right then and there.

Set-SystemForNextRun -CodeSectionComplete 1 -ResetUserData -RestartInstance

In the second code section, it was invoked differently. This time it only updated the Registry. As it was the last code section to execute against the instance, we didn’t opt to enable UserData again or restart the instance. This won’t always be the case. It’s Windows; we may want a final, nerve-calming restart to take place.

Set-SystemForNextRun -CodeSectionComplete 2

This is a good amount of information in which to wrap your head around. The whole PowerShell function to create PowerShell can make it difficult. I did a bunch of renaming of parameters in this version and tripped myself up a few times. Luckily, it was only briefly. Had it not been, I wouldn’t be able to finish writing now and go to bed. All that said, if you have any questions, I shouldn’t be too difficult to track down.

AWS UserData Multiple Run Framework Part IV (a)


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on September 12, 2019.


This is the fourth installment of this series. I never thought I’d write another article about it and yet, here I am. One thing before we recap, I’m breaking this fourth installment into two parts: a and b. It’s too much to read at once. Well, it might be for me at least, if I wasn’t the one writing it. The link for Part b will be at the bottom of this post when it’s published, too.

This series of articles began in 2017. First, I’ll explain what each article brought to this series. Then, I’ll discuss the newest changes—the purpose of this fourth article. You know how it goes, though. You get a chance to see your old code, and you almost immediately find things you would’ve done differently. That’s what this is partially about. That and the fact that I may use this framework again here soon.

When you provision a new Windows virtual server (an EC2 Instance) in AWS or Amazon Web Services, you get an option to run batch and/or PowerShell against the instance at the first launch. This allows you, by default, a one-time shot to configure your server as you see fit. In my AWS UserData Multiple Run Framework, you’ve long been able to configure an instance multiple times between multiple restarts. Follow along, as I quickly catch you up and introduce the changes. This is partially due to how AWS has changed things from Server 2012 R2 and earlier, and Server 2016 and later. There’s also some of that traditional code clean-up, to which I eluded, and the addition of new features, as well.

In the first article, I introduced the AWS UserData Multiple Run Framework. It used text files, stored at the root of the C:\ drive, to determine where in the UserData code it should proceed after each restart.

In the second article, I picked up where the first one left off. This article was the first one to include code to create the code that would be used in the EC2 instance’s UserData. It was, and still is, PowerShell creating PowerShell. It still used text files, as it ran each code section against the server.

In the third article, things changed. Here I introduced using the Windows Registry to maintain what had and hadn’t yet been run in UserData. Additionally, I included an updated New-AWSMultiRunTemplate (as you’ll see, the name has changed) function to create the UserData code. As stated in that article and above, this isn’t the code you place inside the UserData section—it’s the code that creates the code you place inside the UserData section. It’s an important distinction.

In this fourth article—as you’ll see in part b—I’ve added and corrected a good deal. I’ve made additions, removals, and changes in both the function that produces the UserData code and within the UserData produced code, as well. In part b we’ll do a quick rundown on the modifications I remember making. Then, we’ll include and invoke our code-producing code. Following that, we’ll cover the produced code for those unfamiliar with this project. This is backward from the previous articles; however, it’s more of a logical flow for moving forward.

Part b

Read-Only and Constant Variables


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on September 23, 2019.


Quick reminder: You can make variables in PowerShell read-only, or constant.

I’ve written about this before, but it’s time to cover it again for any newcomers to PowerShell, and because I actually found a need for it. Previously, it was just pure learning and sharing. Today, I’ll let you in on how I recently used a read-only variable, and bonus, something else I discovered, too! Let’s begin with how to use these variable options.

When you’re new to PowerShell, you learn to create variables by using the assignment operator. No, not the equals sign. Look at the below example, and then say it out loud as, “ADUser (or dollar sign ADUser) is assigned the values returned by the Get-ADUser Cmdlet.” Don’t say equals; it’s not equals. Lose that habit, if you have it. And yes, people will be listening for it. Okay fine. It may just be me.

PS C:\> $ADUser = Get-ADUser -Identity tommymaynard

You can also create a variable and assign it a value by using the New-Variable Cmdlet. Notice that we don’t use a dollar sign when we do it this way. The same goes for all of the *-Variable cmdlets (Get-Command -Name *-Variable). Dollar signs aren’t a part of a variable, as much as they’re there to indicate to the parser, that what follows a dollar sign, is a variable. That’s how I’ve come to understand it, anyway.

PS C:\> New-Variable -Name User -Value tommymaynard
PS C:\> $User
tommymaynard
PS C:\> $User.GetType().Name
String
PS C:\> Get-Variable -Name User

Name                           Value
----                           -----
User                           tommymaynard

We can use Set-Variable to reassign our previously existing variable. If we use Set-Variable against a variable that doesn’t already exist, it essentially runs New-Variable. It may actually run New-Variable.

PS C:\> Set-Variable -Name User -Value 'maynard, tommy'
PS C:\> $User
maynard, tommy
PS C:\>

Now, one last piece of information before we move on. New-Variable (as well as Set-Variable), has an Option parameter. It’ll accept a handful of predetermined arguments or parameter values, but today we’ll cover the ReadOnly value. We’ll touch on the Constant value, too. A read-only variable is one where you can’t change the value after the variable has been assigned. Okay, that’s not entirely true. You can reassign a read-only variable if you use the Force parameter. If we use Constant instead of ReadOnly for the Option parameter’s value then there would be no way to reassign the variable ever. Additionally, you can only make a variable a constant when it’s first created. That’s pretty much the difference between those two.

PS C:\> New-Variable -Name Var -Value 'testing' -Option ReadOnly
PS C:\> Set-Variable -Name Var -Value 'not testing'
Set-Variable : Cannot overwrite variable Var because it is read-only or constant.
At line:1 char:1
+ Set-Variable -Name Var -Value 'not testing'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : WriteError: (Var:String) [Set-Variable], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable,Microsoft.PowerShell.Commands.SetVariableCommand
PS C:\> $Var = 'not testing'
Cannot overwrite variable Var because it is read-only or constant.
At line:1 char:1
+ $Var = 'not testing
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo          : WriteError: (Var:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable
PS C:\> $Var
testing
PS C:\> Set-Variable -Name Var -Value 'not testing' -Force
PS C:\> Get-Variable -Name Var

Name                           Value
----                           -----
Var                            not testing

PS C:\> (Get-Variable -Name Var).Value
not testing
PS C:\>

I have what I’ve written and called, the Advanced Function Template, for work. I won’t go into all the neat things it includes, but a few years later and I’m still quite proud of all the things I’ve stuffed into it. I use it as my starting point for every function I author. One thing it does is logging (to the screen, to a file, or both). As a part of its logging, it lists the block location. Here’s an example of a function created with the template (that has no, non-template code logging, at minimum).

VERBOSE: [INFO   ] Invoking the Get-WorkDomainComputer function.
VERBOSE: [INFO   ] Invoking user is "MYDOMAIN\tommymaynard."
VERBOSE: [INFO   ] Invoking on Saturday, September 14, 2019 9:47:12 PM.
VERBOSE: [INFO   ] Invoking on the "TOMMAY-LAPTO" computer.
VERBOSE: [PARAM  ] Including the "Log" parameter with the "ToScreen" value.
VERBOSE: [BEGIN  ] Entering the Begin block [Function: Get-WorkDomainComputer].
VERBOSE: [PROCESS] Entering the Process block [Function: Get-WorkDomainComputer].
VERBOSE: [END    ] Entering the End block [Function: Get-WorkDomainComputer].

Okay, Info and Param aren’t block locations like Begin, Process, and End. Even so, I’ve found them to be helpful. This value is stored in the $BL variable originally, and, as you can see, the value is changed while a function is executing. It occurred to me recently, and randomly, that someone else that builds a tool from this same function template might create their own $BL, or $bl (it doesn’t matter) variable. This would cause a mess! This whole thing would be better if the variable were protected: enter the variable read-only option. While I could’ve made it a constant, the function (my code) wouldn’t be able to change its value, but you knew that already.

So, I changed all my $BL variable assignments to these, which are a part of the template function code now.

New-Variable -Name BL -Value '[INFO   ]' -Option ReadOnly
Set-Variable -Name BL -Value '[PARAM  ]' -Force
Set-Variable -Name BL -Value '[BEGIN  ]' -Force
Set-Variable -Name BL -Value '[PROCESS]' -Force
Set-Variable -Name BL -Value '[END    ]' -Force

Now, if a function author uses my function template and without knowing they shouldn’t, tries to create a variable using $BL, they won’t break what the function template is doing in the background. PowerShell will put a stop to that before they get too far (without making the function template explode). Here’s how it appears in my testing; this should look familiar.

Cannot overwrite variable BL because it is read-only or constant.
At line:222 char:9
+         $BL = 'testing'
+         ~~~~~~~~~~~~~~~
    + CategoryInfo          : WriteError: (BL:String) [], SessionStateUnauthorizedAccessException
    + FullyQualifiedErrorId : VariableNotWritable

Now, I’m going to need to consider the nested functions in the function template and their protection, too. It would be less likely they’d choose the same name of those, but I’m of the mindset that we should put in protection even if we don’t think we’ll need it.

Oh, the last thing (but not really). I had this thought: Can I make a [preference variable]() read-only?

PS C:\> $VerbosePreference
SilentlyContinue
PS C:\> Set-Variable -Name VerbosePreference -Option ReadOnly
PS C:\> $VerbosePreference = 'Continue'
Cannot overwrite variable VerbosePreference because it is read-only or constant.
At line:1 char:1
+ $VerbosePreference = 'Continue'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : WriteError: (VerbosePreference:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable

PS C:\>

I can! I can see where this may be helpful for my function template, too!! I didn’t bother trying it, but this variable likely can’t be made to be a constant. This option has to be applied at the time a variable is created, unlike read-only, which can be applied to a variable after it’s already been created. Important note, which I mentioned earlier.

And finally, in case you cared to see the first, Active Directory example in this article using New-Variable, then here you go. It works like Math. You know, Order of Operations: complete what’s in the parentheses first, and then continue. If you didn’t include the parentheses, it would make Get-ADuser a string and assign it as the value of the $ADUser2 variable. Then, it would fail when it thought, you thought the New-Variable Cmdlet had an Identity parameter. Uhhh, no.

PS C:\> New-Variable -Name ADUser2 -Value (Get-ADUser -Identity tommymaynard)
PS C:\> $ADuser2.GivenName
Tommy
PS C:\>

AWS Service Acronyms


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on August 30, 2019.


I work with some of the most intelligent people I may have ever met.

It seems that a good deal of these brilliant minds focus on AWS or Amazon Web Services. It’s also much of what’s important to our enterprise right now, so it’s logical. I also focus on AWS (partly). A portion of the people with whom I work use the acronym CF for AWS CloudFormation. But, that acronym is reserved for Amazon CloudFront. How do they not know this? How do I know this!? It’s of no surprise, but I see this acronym used incorrectly outside work, as well. Here’s one: https://stackoverflow.com/questions/54752989/aws-cloudformation-how-to-use-if-else-conditions. I won’t deny that CF does make sense. I’ll agree to that, but not alongside the consideration of Amazon CloudFront.

I’ve yet to find that single source of AWS acronyms. Or have I?

AWS has authored two PowerShell modules (one for Windows PowerShell and one for the cross-platform version: PowerShell), with the term AWSPowerShell included in the name. They are both at version 3.3.563.1 and have over 5,900 Cmdlets (pronounced, but not spelled as, “commandlets” [for those that don’t work closely with PowerShell in any form but may end up here]). There are over 6,200 total items in each of these two modules if you include the aliases. The PowerShell command naming convention best practice is to use an approved verb (the Get-Verb Cmdlet returns a list of those), a dash, and a singular noun. If more than one noun is used, developers should use PascalCase—this happens quite often. And again, this is for the non-PowerShell people in the room—if you’re out there.

Some cmdlet examples from these modules are Get-CFDistribution, Write-S3ObjectTagSet, Set-SQSQueueAttribute, and New-WAFRule. These commands work just as the AWS CLI does, in that they call an API at Amazon. As far as best practice goes, another thing both individuals and vendors often do is include a prefix after the dash, but before the noun(s) in their command names. Each of the AWSPowerShell Cmdlets I included above makes use of a prefix. There were CF, S3, SQS, and WAF. Another one of the cmdlets in these modules is called Get-AWSPowerShellVersion (AWS prefix); I’ve brought it up here before. It spits out some version text, which is all it should do. However, if you use the ListServiceVersionInfo parameter provided by this cmdlet, you can return all the services and their matching noun prefixes, as well. You can return the API Versions too, which are dates. I didn’t include those below, however.

PS> Get-AWSPowerShellVersion -ListServiceVersionInfo | Select-Object -Property Service,'Noun Prefix' | Sort-Object -Property 'Noun Prefix'
Service                                               Noun Prefix
-------                                               -----------
Application Auto Scaling                              AAS
AWS Certificate Manager                               ACM
Application Discovery Service                         ADS
Amazon API Gateway                                    AG
Amazon API Gateway V2                                 AG2
Amazon API Gateway Management API                     AGM
Alexa For Business                                    ALXB
AWS Amplify                                           AMP
AWS App Mesh                                          AMSH
AWS AppStream                                         APS
Auto Scaling                                          AS
AWS Support API                                       ASA
AWS Auto Scaling Plans                                ASP
AWS AppSync                                           ASYN
Amazon Athena                                         ATH
Amazon Backup                                         BAK
AWS Batch                                             BAT
AWS Budgets                                           BGT
AWS Cloud9                                            C9
AWS CodeBuild                                         CB
AWS CodeCommit                                        CC
AWS CodeDeploy                                        CD
AWS Cloud Directory                                   CDIR
AWS Cost Explorer                                     CE
Amazon CloudFront                                     CF
AWS Config                                            CFG
AWS CloudFormation                                    CFN
Amazon Cognito Identity                               CGI
Amazon Cognito Identity Provider                      CGIP
Amazon Cognito Sync                                   CGIS
Amazon Chime                                          CHM
AWS Comprehend Medical                                CMPM
Amazon Comprehend                                     COMP
Amazon Connect Service                                CONN
AWS CodePipeline                                      CP
Amazon CloudSearch                                    CS
Amazon CloudSearchDomain                              CSD
AWS CodeStar                                          CST
AWS CloudTrail                                        CT
AWS Cost and Usage Report                             CUR
Amazon CloudWatch                                     CW
Amazon CloudWatch Application Insights                CWAI
Amazon CloudWatch Events                              CWE
Amazon CloudWatch Logs                                CWL
Amazon DynamoDB Accelerator (DAX)                     DAX
AWS Direct Connect                                    DC
Amazon DynamoDB                                       DDB
AWS Device Farm                                       DF
Amazon Data Lifecycle Manager                         DLM
AWS Database Migration Service                        DMS
Amazon DocumentDB                                     DOC
AWS Data Pipeline                                     DP
AWS Directory Service                                 DS
AWS DataSync                                          DSYN
AWS Elastic Beanstalk                                 EB
Amazon ElastiCache                                    EC
Amazon Elastic Compute Cloud                          EC2
Amazon EC2 Container Registry                         ECR
Amazon EC2 Container Service                          ECS
Amazon Elastic File System                            EFS
Amazon Elastic Container Service for Kubernetes       EKS
Elastic Load Balancing                                ELB
Elastic Load Balancing V2                             ELB2
AWS Elemental MediaConvert                            EMC
AWS Elemental MediaConnect                            EMCN
AWS Elemental MediaLive                               EML
AWS Elemental MediaPackage                            EMP
AWS Elemental MediaPackage VOD                        EMPV
Amazon Elastic MapReduce                              EMR
AWS Elemental MediaStore                              EMS
AWS Elemental MediaStore Data Plane                   EMSD
AWS Elemental MediaTailor                             EMT
Amazon Elasticsearch                                  ES
Amazon Elastic Transcoder                             ETS
Amazon EventBridge                                    EVB
Firewall Management Service                           FMS
Amazon FSx                                            FSX
AWS Global Accelerator                                GACL
Amazon GuardDuty                                      GD
AWS Greengrass                                        GG
Amazon Glacier                                        GLC
AWS Glue                                              GLUE
Amazon GameLift Service                               GML
AWS Ground Station                                    GS
AWS Health                                            HLTH
AWS Cloud HSM                                         HSM
AWS Cloud HSM V2                                      HSM2
AWS Identity and Access Management                    IAM
AWS Import/Export                                     IE
Amazon Inspector                                      INS
AWS IoT                                               IOT
AWS IoT Events                                        IOTE
AWS IoT Events Data                                   IOTED
AWS IoT Jobs Data Plane                               IOTJ
AWS IoT Things Graph                                  IOTTG
Amazon Kinesis                                        KIN
Amazon Kinesis Analytics                              KINA
Amazon Kinesis Analytics (v2)                         KINA2
Amazon Kinesis Firehose                               KINF
AWS Key Management Service                            KMS
Amazon Kinesis Video Streams                          KV
Amazon Kinesis Video Streams Media                    KVM
Amazon Lex                                            LEX
AWS License Manager                                   LICM
AWS Lambda                                            LM
Amazon Lex Model Building Service                     LMB
Amazon Lightsail                                      LS
Amazon Macie                                          MAC
Amazon Managed Blockchain                             MBC
AWS Marketplace Commerce Analytics                    MCA
AWS Marketplace Entitlement Service                   MES
AWS Migration Hub                                     MH
Amazon Machine Learning                               ML
AWS Marketplace Metering                              MM
AWS Mobile                                            MOBL
Amazon MQ                                             MQ
Managed Streaming for Kafka                           MSK
Amazon MTurk Service                                  MTR
Amazon Neptune                                        NPT
AWS OpsWorks                                          OPS
AWS Organizations                                     ORG
AWS OpsWorksCM                                        OWCM
AWS Certificate Manager Private Certificate Authority PCA
AWS Personalize                                       PERS
Amazon Personalize Events                             PERSE
AWS Personalize Runtime                               PERSR
AWS Performance Insights                              PI
Amazon Pinpoint                                       PIN
Amazon Pinpoint Email                                 PINE
AWS Price List Service                                PLS
Amazon Polly                                          POL
Amazon QuickSight                                     QS
Amazon Route 53                                       R53
Amazon Route 53 Domains                               R53D
Amazon Route 53 Resolver                              R53R
AWS Resource Access Manager                           RAM
Amazon Relational Database Service                    RDS
AWS RDS DataService                                   RDSD
Amazon Rekognition                                    REK
AWS Resource Groups                                   RG
AWS Resource Groups Tagging API                       RGT
AWS RoboMaker                                         ROBO
Amazon Redshift                                       RS
Amazon Simple Storage Service                         S3
Amazon S3 Control                                     S3C
AWS Serverless Application Repository                 SAR
AWS Service Catalog                                   SC
Amazon Route 53 Auto Naming                           SD
AWS Secrets Manager                                   SEC
Amazon Simple Email Service                           SES
AWS Step Functions                                    SFN
AWS Storage Gateway                                   SG
AWS Shield                                            SHLD
AWS Security Hub                                      SHUB
Amazon SageMaker Service                              SM
Amazon SageMaker Runtime                              SMR
Amazon Server Migration Service                       SMS
AWS Import/Export Snowball                            SNOW
Amazon Simple Notification Service                    SNS
AWS Service Quotas                                    SQ
Amazon Simple Queue Service                           SQS
AWS Systems Manager                                   SSM
AWS Security Token Service                            STS
AWS Simple Workflow Service                           SWF
AWS Transfer for SFTP                                 TFR
Amazon Translate                                      TRN
Amazon Transcribe Service                             TRS
Amazon Textract                                       TXT
AWS WAF                                               WAF
AWS WAF Regional                                      WAFR
Amazon WorkDocs                                       WD
Amazon WorkSpaces                                     WKS
Amazon WorkLink                                       WL
Amazon WorkMail                                       WM
AWS X-Ray                                             XR

This is the closest we have to an official, AWS acronym list that I’ve yet to find. And yes, I’ve looked.

As can be seen, the CF acronym is used for CloudFront and CFN for CloudFormation. Use whatever you like, I suppose. With my teammates, I can’t think of a time when I didn’t know it was CloudFormation due to the context. I may have lost a few valuable seconds in life rereading a few sentences a few times, but I’ve so far survived. Still, I felt like it should be mentioned, so I can say I did my part to sleep better at night. Just kidding. It doesn’t keep me awake at night.

I’ll continue to use CFN. Perhaps they’ll think it’s me.

Keeping a Continuous Total


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on July 15, 2019.


There’s a function stored in my $PROFILE (CurrentUserAllHosts). Maybe you’ve read about it before; it’s come up. Its purpose is to go out to the PowerShell Gallery and determine how many downloads I have of each PowerShell script and/or module I’ve published there. In the past, my tool provided a download total for each project along with a single total of all the downloads. Today that changes, but before it does (I seriously haven’t the updated code yet — typical), let’s show the code and results, as it currently stands.

Function Showw-PSGalleryProject {
    Param (
        [System.Array]$Projects = ('TMOutput','Start-1to100Game3.0','Get-TMVerbSynonym','SinkProfile','Show-PSDriveMenu','Switch-Prompt')
    )

    Foreach ($Project in $Projects) {
        If (Find-Module -Name $Project -ErrorAction SilentlyContinue) {
            $TempVar = Find-Module -Name $Project
        } ElseIf (Find-Script -Name $Project) {
            $TempVar = Find-Script -Name $Project
        }
        [PSCustomObject]@{
            Name = $TempVar.Name
            Version = $TempVar.Version
            Downloads = $TempVar.AdditionalMetadata.downloadCount
        }
        [int]$TotalDownloads += $TempVar.AdditionalMetadata.downloadCount
    }
    ">> Total downloads: $TotalDownloads"
} # End Function: Show-PSGalleryProject.
Show-PSGalleryProject
Name                Version Downloads
----                ------- ---------
TMOutput            1.1     1193
Start-1to100Game3.0 3.0     170
Get-TMVerbSynonym   1.4     163
SinkProfile         1.0     100
Show-PSDriveMenu    1.1     69
Switch-Prompt       1.2.0   84
>> Total downloads: 1779

See how we only have the single Total downloads above (the 1779 value)? I’ve decided I don’t like that, so it’s about to change. I want the total downloads as each of these objects are being returned (, calculated,) and written.

In the newest version of this little $PROFILE-based function — there was an earlier one — the first thing I’ve done is added the [CmdletBinding] Attribute. It’s to add the OutVariable Common Parameter. We’ll see how that’s helpful momentarily. The second thing we’ve done is begun collecting and displaying, what I’ve called the Type (find in the below code: ; $Type = Module or Script). This will differentiate when added to each object, whether one of my projects in the PowerShell Gallery is a module or script. Here’s the updated code with these two additions. Notice also, the object created inside [PSCustomObject] now includes a Type property. This is where the Module or Script determinations are included.

Function Show-PSGalleryProject {
    [CmdletBinding()]
    Param (
        [System.Array]$Projects = ('TMOutput','Start-1to100Game3.0','Get-TMVerbSynonym','SinkProfile','Show-PSDriveMenu','Switch-Prompt')
    )

    Foreach ($Project in $Projects) {
        If (Find-Module -Name $Project -ErrorAction SilentlyContinue) {
            $TempVar = Find-Module -Name $Project; $Type = 'Module'
        } ElseIf (Find-Script -Name $Project) {
            $TempVar = Find-Script -Name $Project; $Type = 'Script'
        }
        $TotalDownloads = [int]$TotalDownloads + [int]$TempVar.AdditionalMetadata.downloadCount
        [PSCustomObject]@{
            Name = $TempVar.Name
            Type = $Type
            Version = $TempVar.Version
            Downloads = $TempVar.AdditionalMetadata.downloadCount
            TotalDownloads = $TotalDownloads
        }
    } # End Foreach.
} # End Function: Show-PSGalleryProject.
Show-PSGalleryProject

The other recent addition to this code is the addition of my $TotalDownloads variable. Through each iteration, we update it so it has the current value and then displays said current value with the current object. I may have been a little too generous with my [int] casting; however, do remember that the plus sign (+) is also a concatenation operator. I want a total download count — not a string of individual download counts, strung together.

In closing, there’s something else to remember. By default (and there are ways around this), when we have more than four properties being returned per object, the “table” output becomes a “list” output — have a look.

Name           : TMOutput
Type           : Module  
Version        : 1.1     
Downloads      : 1193    
TotalDownloads : 1193    

Name           : Start-1to100Game3.0
Type           : Script
Version        : 3.0   
Downloads      : 170   
TotalDownloads : 1363  

Name           : Get-TMVerbSynonym
Type           : Script
Version        : 1.4   
Downloads      : 163   
TotalDownloads : 1526  

Name           : SinkProfile
Type           : Module
Version        : 1.0   
Downloads      : 100   
TotalDownloads : 1626  

Name           : Show-PSDriveMenu
Type           : Script
Version        : 1.1   
Downloads      : 69    
TotalDownloads : 1695  

Name           : Switch-Prompt
Type           : Script
Version        : 1.2.0 
Downloads      : 84    
TotalDownloads : 1779

While using the newly added OutVariable is helpful if I want to display the above output and capture the output into a variable at the same time, here’s how I run this now.

$PS = Show-PSGalleryProject
$PS | Format-Table -AutoSize
Name                Type   Version Downloads TotalDownloads
----                ----   ------- --------- --------------
TMOutput            Module 1.1     1193                1193
Start-1to100Game3.0 Script 3.0     170                 1363
Get-TMVerbSynonym   Script 1.4     163                 1526
SinkProfile         Module 1.0     100                 1626
Show-PSDriveMenu    Script 1.1     69                  1695
Switch-Prompt       Script 1.2.0   84                  1779

Next up is likely putting this code into a background job; it’s not the quickest thing I’ve written (although I blame that on the speed of the PowerShell Gallery lookup process, perhaps). Maybe a background job that runs in, or starts at, the end of the profile script. This, in order that these slow-to-obtain results are available sooner and with minimal impact.

Edit: 2/5/2022 –  I never did do that, although this function does still exists in my $PROFILE script. Now that I have seen this post again, perhaps there will be another push to try that. Time will tell. Oh, and here are my updated totals since this post was first written and published.

Name                Type   Version Downloads TotalDownloads
----                ----   ------- --------- --------------
TMOutput            Module 1.1     2980                2980
Start-1to100Game3.0 Script 3.0     266                 3246
Get-TMVerbSynonym   Script 1.4     293                 3539
SinkProfile         Module 1.0     302                 3841
Show-PSDriveMenu    Script 1.1     186                 4027
Switch-Prompt       Script 1.2.0   236                 4263

PowerShellGet Find-Both


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on June 11, 2019.


For years now, I’ve had a handful of personal functions living as a part of my $PROFILE script — the CurrentUserAllHosts version. This specific profile script ensures it’s available for use regardless of which PowerShell host program I use: the ConsoleHost, the ISE, VS Code, etc. One of these functions — Show-PSGalleryProject — goes out to the PowerShell Gallery and returns the current download count of all the modules and scripts I’ve added to the gallery. It also determines the combined download total.

The below image was rescued from an old Twitter post, back when the function worked as expected. Don’t mind the black background color. It was a demo of my TMOutput module. Again, this was the output produced back when the function originally worked. It’s not currently working, but with any luck that’s about to change.

Before we continue, let’s take a quick look at the underlying code that created the above output.

Set-Alias -Name Watch-PSGalleryProject -Value Show-PSGalleryProject
Function Show-PSGalleryProject {
    Param (
        [System.Array]$Projects = ('TMOutput','Start-1to100Game3.0','Get-TMVerbSynonym','SinkProfile','Show-PSDriveMenu','Switch-Prompt')
    )
 
    Foreach ($Project in $Projects) {
        $TempVar = Find-Module -Name $Project
        [PSCustomObject]@{
            Name = $TempVar.Name
            Version = $TempVar.Version
            Downloads = $TempVar.AdditionalMetadata.downloadCount
        }
        [int]$TotalDownloads += $TempVar.AdditionalMetadata.downloadCount
    }
    ">> Total downloads: $TotalDownloads"
}

At some point, and I’m not sure when the Find-Module function stopped working against scripts in the PowerShell Gallery. Additionally, Find-Script potentially stopped working against modules. Now that said, I’m not even certain when Find-Script was added and if it ever worked against modules hosted in the PowerShell Gallery. Regardless, I need a way now to rewrite this personal function, so that it invokes the correct PowerShellGet function against the proper project: Find-Module against modules and Find-Script against scripts. Before we work on that solution, here’s what my function returns now. Ugh. The Show-PSGalleryProject function’s failed invocation.

I started writing this post prior to writing the solution. Once I dug in, it really only took a few changes to see that this was working again. While it wasn’t difficult, I’ve opted to continue the post and share my results, even if, you’ve already calculated a way in which a single function could check one function (Find-Module), before another (Find-Script). Here’s my updated code and the current, and corrected, results.

Set-Alias -Name Watch-PSGalleryProject -Value Show-PSGalleryProject
Function Show-PSGalleryProject {
    Param (
        [System.Array]$Projects = ('TMOutput','Start-1to100Game3.0','Get-TMVerbSynonym','SinkProfile','Show-PSDriveMenu','Switch-Prompt')
    )
 
    Foreach ($Project in $Projects) {
        If (Find-Module -Name $Project -ErrorAction SilentlyContinue) {
            $TempVar = Find-Module -Name $Project
        } ElseIf (Find-Script -Name $Project) {
            $TempVar = Find-Script -Name $Project
        }
        [PSCustomObject]@{
            Name = $TempVar.Name
            Version = $TempVar.Version
            Downloads = $TempVar.AdditionalMetadata.downloadCount
        }
        [int]$TotalDownloads += $TempVar.AdditionalMetadata.downloadCount
    }
    "&gt;&gt; Total downloads: $TotalDownloads"
}

It’s not too much slower than it used to be, but it’s always been somewhat slow. There is without question, a noticeable delay doing separate and individual searches against the PowerShell Gallery (and -Filter didn’t seem to return more than a single result the last time I checked). It’s fine for me, however (for now). I have considered doing the search as a part of a background process or when the profile is loaded. Either way, I’m back in business for today.

Apartment Hunting with PowerShell II

If you did not read the first post in this series, then do now. It is vital, as I am not about to explain everything that is going on with the updated function that will be a part of this post. So, from this point forward, it is assumed that you have read it.

In the first post of this series, there was only a single apartment complex to watch. Now there are three. I mentioned that inventory is low. Therefore, the first option may not actually be an option for him. He, and I, kind of lucked out here. That is because the other two complexes of interest must be owned/run/whatever by the same management company. Lucky for everyone I suppose, the floor plan webpages all have the same embedded JSON. Let’s start with an image of the function from part one of the series. You may want to open this in a separate tab. It depends on how close you are going to follow along.

The changes that have been made from the above function to the below are function are the following.

  1. The CmdletBinding attribute was added along with a Param block.
  2. A parameter, named ApartmentComplex, was added.
    Note: This uses ValidateSet and includes each of the three apartment complexes. Additionally, when the function is invoked by default all three complexes are chosen.
  3. A variable reassignment happens to the $ApartmentComplex variable using Select-Object -Unique.
    Note: I have been known to do this to remove duplicates, such as Watch-Apartment -ApartmentComplex Edgewood,Creekside,Edgewood.
  4. The $ComplexUriHash hash table is created in order that we have a unique URI available for each apartment complex.
  5. A Foreach loop was added so we are able to loop through each apartment complex.

If you ever need to loop through an array and select a specific key-value pair from a hash table during each iteration, then this is the post to remember. I cannot be sure if I have done this before, but I know I have now. Up until I search some search engine and end up back on my own site; it has happened.

Set-Alias -Name wa -Value Watch-Apartment
function Watch-Apartment {
	# Version 2.0.0
	[CmdletBinding()]
	Param (
		[Parameter()]
		[ValidateSet('Creekside','Edgewood','Presidio')]
		[System.Array]$ApartmentComplex = ('Creekside','Edgewood','Presidio')
	)

	$ApartmentComplex = $ApartmentComplex | Select-Object -Unique
	$ComplexUriHash = @{
		Creekside = 'https://theplaceatcreekside.securecafe.com/onlineleasing/the-place-at-creekside/floorplans'
		Edgewood  = 'https://mclife.securecafe.com/onlineleasing/the-place-at-edgewood/floorplans'
		Presidio  = 'https://mclife.securecafe.com/onlineleasing/the-place-at-presidio-trails/floorplans'
	}

	Foreach ($Complex in $ApartmentComplex) {
		if ([System.Boolean]$ComplexUriHash[$Complex] -eq $true) {
			$Uri = $ComplexUriHash[$Complex]
			$WebRequestContent = (Invoke-WebRequest -Uri $Uri).Content
			$ContentPath = "C:\users\tmaynard\Dropbox\_tommymaynard.com\Apartment Hunting with PowerShell\WebpageContents$Complex.txt"
			# $ContentPath = "C:\users\tmaynard\Dropbox\_tommymaynard.com\Apartment Hunting with PowerShell\WebpageContentsCreekside.txt"
			Set-Content -Path $ContentPath -Value $WebRequestContent

			$File = Get-Content -Path $ContentPath
			$Pattern = "floorplans:(.*?)propertyID:"
			$ParsedPage = [regex]::Match($File,$Pattern).Groups[1].Value
			$ParsedPage = $ParsedPage.Trim(); $ParsedPage = $ParsedPage.TrimEnd(',')

			$JsonDocument = ConvertFrom-Json -InputObject $ParsedPage
			$JsonDocument |
				Select-Object -Property @{Name='Complex';Expression={$Complex}},
					@{Name='Available';Expression={if ($_.isFullyOccupied -eq 0) {"Yes ($($_.availableCount))"} else {'No'}}},
					@{Name='Model';Expression={$_.name}},
					@{Name='Sq.Ft.';Expression={$_.sqft}},
					@{Name='Beds';Expression={$_.beds}},
					@{Name='Baths';Expression={$_.baths}},
					@{Name='Price';Expression={$_.tilePrice}} |
				Format-Table -AutoSize
		}
	}
}

The below image shows the results when this is run against all three apartment complexes. Fun project, but no more updates for me on this one! I did not mention these above, but there are a couple more additions, however. On the left, I have added the Complex name so these are distinguishable from one another. And on the right, I added the prices. You can see the modified Select-Object in the above function. Again, I am done here.

Filled-In AD Notes Field


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on May 31, 2019.


There’s this thing that might happen. Even as it’s unlikely, I’ve added a protection I’ll probably never even need. I suppose I’d rather be complete and as confident in my code as possible. I don’t want to be the guy, who wrote the code that didn’t protect against the thing. Instead, I’d rather be the guy that wrote the code, that did the thing, even when the protection, was never thought to be needed. It’s like the floor mat at Sears, in front of the escalator, that I saw as a kid that I’ve never forgotten: “Safety First.” It was red, with white writing. There’s plenty of production code out there we’ve written that we’d write differently now, knowing what we’ve already learned.

Just because the code I was working with won’t (likely) ever fill up an Active Directory Group Notes field, doesn’t mean I shouldn’t be sure it never attempts to go over that 1,024 character limit allowed by the field. Knowing that goal, reasonable or otherwise, I set out to use the Notes field and prevent ever exceeding that character limit.

Let’s say, there’s a set of groups in Active Directory — department groups. While we’ll keep their names generic: DEPT 00001, DEPT 00002, etc., we’ll put their true department name in their AD object’s Notes field. In case you don’t know, the attribute name for this field is Info on the back end, and Notes in the UI.

Now, let’s also say that department names change over time. Therefore, it’s bound to happen that DEPT 00001, for example, will need its newest department name added to the Info property. Now, we want to keep all the previous names in the Info property and add the current one to the others. Between each name, we’ll include a pipe character: “|”. This delimiter will assist in helping us determine where one name ends and the next begins. Do consider that department names can change multiple times. If it happens frequently enough, you can see how we might hit our 1,024 character limit in this property. Therefore, it’s a good idea to add in this protection, in order to avoid throwing an error someday.

I’ll include the code I used below, and then we’ll discuss what’s happening further down.

$GroupInfo = $null
$GroupInfo = (Get-ADGroup -Identity $GroupExist.Name -Property Info).Info
If ($GroupInfo) {
	# Edit Notes/Info field (previously populated).
	$GroupInfoSplit = $GroupInfo -split '\|'
	If (-Not($DeptName -in $GroupInfoSplit)) {
		$GroupInfo += "|$DeptName"

	If ($GroupInfo.Length -gt 1024) {
		# Remove previous groups if over 1024 characters.
		Do {
		$GroupInfo = ($GroupInfo -split '\|',2)[1]
		} Until ($GroupInfo.Length -le 1024) # End Do-Until.
	} # End If.

	Set-ADGroup -Identity $GroupExist -Replace @{Info=$GroupInfo}
	} # End If.
} Else {
	# Add to Notes/Info for the first time.
	If ($DeptName.Length -le 1024) {
	        Set-ADGroup -Identity $GroupExist -Add @{Info=$DeptName}
	} # End If.
} # End If-Else

The first thing to know is that this code is inside of a looping construct — a Foreach loop to be exact. Each time we start a new loop, we bring in a different one of our departments, stored in the $GroupExist variable. On line 3, we use its Name property as a part of a Get-ADGroup command and return only the value inside the Info field. Remember, this is the Notes field in the UI. It’s either going to have some data in it, or it’s not. All previously existing departments will already have something in here. It’s new departments that will not.

Once we have, or we don’t have, this data, we go one of two ways. As stated, every department is going to have its department name added to its Notes field. If we don’t return any info from our Get-ADGroup command, we can rest assured, that there’s nothing in this field already and that we only need to add the department name, without any worry about any previously existing data in this field. This If statement begins on line 4, however, in this situation, we’re taking the Else path on line 19. Once there, we check if the length of the department name, stored in $DeptName is longer than 1,024 characters. That will never happen — seriously — but I added the check just in case. I do want to note here that this $DeptName variable changes within our unseen Foreach loop, during each iteration as well.

If there is already a department name or names, then we follow the If path. Let’s deconstruct that, as well. It’s a little more complex.

If our Info/Notes field is already populated, then we need to make some checks before we potentially add a department’s current department name. The first thing we do with the value we’ve returned, as $GroupInfo, is split it at our delimiter — the pipe character on line 6. If there’s only one department name (no delimiter), it will only return that one department name, and that’ll work out just fine. Next, we compare our department name in $DeptName to the value, or values, returned by our split operation. If the current department name is included in our results, we exit — there’s nothing more to do.

Now, if our current department name is not included in our results, we append it to our $GroupInfo variable. At that point, we’ll have this variable with the newest department name appended to the end of this string, where each previous department name is also included with the pipe character in between each. Following this step, our code checks the length of the value stored in $GroupInfo and will only act if its length is greater than those 1,024 characters.

If it is, we run our variable through a Do-Until language construct. For every iteration through the Do-Until, we split our value at the first pipe character from the left of the string and keep everything to the right. We’ll continue to do this, over and over, until our length is less than 1,024 characters.

And that it’s. We can maintain all of a department’s previous department names and ensure they’ll always fit into our Notes field.

Republished Work Table of Contents I

Over the last week and a few days of February 2022, I have been ensuring that content I wrote on another site, that was lost, has been republished here on tommymaynard.com. I am now, half of the way done! In arriving at this midpoint, I wanted to include a table of contents for these old posts with a corresponding link and explanation about each post.

Top of the Next Hour – February 7, 2022
Determine the “top” of the next hour and create a ScriptMethod for this purpose.

CIDR Notation Host Count – February 8, 2022
Determine the number of available hosts in a CIDR IP address range.

Return Only the Fixed Disks – February 9, 2022
Filter out optical drives and network mapped drives regardless of the count of either.

Clear Host Deconstructed – February 10, 2022
Learn how the Clear-Host function does what it does.

Looking Busy with PowerShell – February 11, 2022
Make PowerShell appear that you are busy.

Really, Remove the Module – February 12, 2022
Make a self-deleting PowerShell module.

Linux Prompt X – February 13, 2022
Create a Unix/Linux prompt on Windows.

Build-in Measure-Command – February 14, 2022
Make built-in commands measure their own execution time.

Get-History Modified – February 15, 2022
Get Get-History to return the time a command took to run.

Switch-Prompt – February 16, 2022
Create a Unix/Linux prompt on Windows, but better.

Switch-Prompt 1.2.0


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on April 29, 2019.


Back toward the end of March 2019, I published a script to the PowerShell Gallery called Switch-Prompt. It’s a function, packaged as a .ps1 file, that allows a user to switch their prompt between the standard-issued, Microsoft Prompt, a Linux look-alike prompt, and a customizable Linux look-alike prompt. I wrote about it right here, on tommymaynard.com.

I indicated in that article that that version was the one. Yeah, so that didn’t last long. Well, as of today and version 1.1.0, it now includes a minimal prompt, as well. You know, this guy: PS>. I’ve taken 1.0.0 to 1.1.0 — wait, timeout — I just had another idea. Here we go again, we’re moving this up to version 1.2.0. This is going to be the one, and I may just really mean it this time.

You couldn’t tell, but the time between the first two paragraphs and this one was a few days. It’s worth it, as the Switch-Prompt function is now at version 1.2.0 and it’s awesome. And to think, I thought I was done back at 1.0.0; I should’ve known. But seriously, this is the last prompt you’re ever going to need and I suspect, that this is the last time I’ll need to make changes — it does everything! It’s come a really long way. It can create a minimal prompt, a standard prompt, a Linux prompt, a customizable Linux prompt, and now, a completely custom prompt. Anything you want! All you have to do is place your code in a ScriptBlock. We’ll see some examples.

Before we see how it can be used, let’s get it installed. It’s available in the PowerShell Gallery, so the below command will get the newest version installed for the current user. Use the Force switch parameter if you’ve installed a previous version, and also use the Verbose switch parameter, so you know what’s taking place during its installation. It’s not a requirement, but it can be a learning experience for a process that produces no output by default.

PS C:\Program Files\7-Zip> Install-Script -Name Switch-Prompt -Scope CurrentUser

Because the script is delivered as a function in a script file (a .ps1), you’re required to dot-source the script in order to add the function to the current session (notice the dot before the script’s path). This will need to be done every time a new session begins and you want to use the function. To avoid that, it’s best to add the below command to a profile script that executes at the start of every new PowerShell session. If you choose to obtain the Switch-Prompt another way, then here’s the main page for it on the PowerShell Gallery: https://www.powershellgallery.com/packages/Switch-Prompt/1.2.0.

PS> . C:\Users\tommymaynard\Documents\WindowsPowerShell\Scripts\Switch-Prompt.ps1

If you’re dot sourcing the script file from your profile script, such as we’ve done above, then be sure to include your Switch-Prompt command there, as well. You’ll see plenty of examples below and even more in the function’s comment-based help. On that note, instead of explaining all the Switch-Prompt options, I’m opting to copy in some of the comment-based help’s examples. In this first example, we’ll see how to switch to a minimal prompt. This was the 1.1.0 update — big whoop now, as you’ll soon see.

PS C:\Program Files\7-Zip\Lang> Switch-Prompt -Type Minimal
PS> 
PS> 

Next, we’ll move from the minimal prompt back to the default, standard prompt. This is the default action when the Type parameter and a value are not included. Therefore, using -Type Standard isn’t actually necessary to recreate the Standard prompt.

PS> Switch-Prompt -Type Standard
PS C:\Program Files\7-Zip\Lang>
PS C:\Program Files\7-Zip\Lang>

From here, we’ll try out the new Custom type prompt. If the Prompt parameter and value aren’t included, Switch-Prompt uses its built-in default, which actually indicates to use the Prompt parameter. Doing so — using that Prompt parameter — is what sets this type, and its possibilities, apart. We’ll see that after this first example.

PS C:\Program Files\7-Zip\Lang> Switch-Prompt -Type Custom
Default (use Prompt parameter)>
Default (use Prompt parameter)> 

In this example, we create a simple static — and you’ll see what I mean in a moment — prompt. It’s a simple text-based prompt.

Default (use Prompt parameter)> Switch-Prompt -Type Custom -Prompt {'PWRSHLL > '}
PWRSHLL >
PWRSHLL >

Next, we’ll start adding some dynamic elements to our prompt. This example includes the current date and time, each time the prompt is written. It’s about now that you should recognize that the Switch-Prompt’s dynamic parameter, Prompt, requires a ScriptBlock parameter value. Ensure you’re using the opening and closing curly braces, whether or not you use a static or dynamic prompt.

PWRSHLL > Switch-Prompt -Type Custom -Prompt {"$(Get-Date) > "}
04/26/2019 21:55:56 >
04/26/2019 21:55:57 >

In this example, we’ll use some environmental variables to help create our prompt.

04/26/2019 21:56:10 > Switch-Prompt -Type Custom -Prompt {"$env:USERDOMAIN\$env:COMPUTERNAME --> "}
MYDOMAIN\TMLAPTOP -->
MYDOMAIN\TMLAPTOP --> 

This will be the final example for this article. This also uses the Custom Type, but now we’ll include an If-ElseIf-Else construct as the value for our Prompt parameter. When using a ScriptBlock parameter value, there’s really nothing we can’t dream up for our prompt. Just remember that you may need to use the ToString() method if your commands are not inside a quoted string (which forces a string). Be sure to see one of the other Get-Date examples, from the function’s comment-based help, where this is shown.

MYDOMAIN\TMLAPTOP --> Switch-Prompt -Type Custom -Prompt {If ($env:COMPUTERNAME -match 'laptop') {"$($env:COMPUTERNAME)|LPT: "} ElseIf ($env:COMPUTERNAME -match 'desktop') {"$($env:COMPUTERNAME)|DKT: "} Else {'[--PS--]> '}}
TMLAPTOP|LPT: 

We haven’t covered it here with examples, so be sure to check out the Linux and LinuxCustom Types, as well. Switch-Prompt includes 14 comment-based help examples. Additionally, there were some examples in this first article: https://tommymaynard.com/linux-prompt-x/. What you’re looking for is likely in one of these two places, and if not, there’s probably enough in there to inspire you to come up with something unique. For real, I think I’m done at 1.2.0, but only time will tell.