Category Archives: Quick Learn

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

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
    }
    ">> 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.

Apartment Hunting with PowerShell

Note: Expect a part two on this post.

I know a guy, and that guy is looking for an apartment. It turns out that apartments are going really fast and inventory is low — maybe you knew this, but it was news to me. Just about as soon as they become available, they are gone. I suggested that I might be able to lend a hand… maybe, who knows. This is not because I know someone in apartments, but rather that if there is a way to use PowerShell here, then there is a good chance I can help. He got lucky and I learned something new.

I started by going to the apartment website where he was interested and found a page that listed each apartment model and whether or not they had any availability. It was a floor plan page. I was not expecting much honestly, but I used the built-in Web Developer Tools in my browser and viewed the page source, and found some exciting news (for me and PowerShell, at least). It was enough good news that I am able to write about this whole experience.

While empty here, this data structure caught my eye. The output I had hoped to gather, was in JSON format; that was huge! Best I can tell, it is generated by a JavaScript file, which then embeds the JSON in the HTML that makes up the webpage. That is not overly important, however, but look at this structure; it is magnificent.

floorplans: [
  {...
  },
  {...
  },
  {...
  },
  {...
  },
  {...
  },
  {...
  }
],
propertyID: 60484,

Inside the floor plans JSON array ([]) are six objects, each in their own set of curly braces. Inside each of those, was a plethora of information regarding each floor plan. These properties included things like Model, Sq.Ft., Beds, Baths, etc. Let’s start by taking a look at the Watch-Apartment PowerShell function I wrote. Just a note, but in order to make this work for yourself, you will need to edit the path in the $ContentPath variable.

function Watch-Apartment {
    $Uri = 'https://theplaceatcreekside.securecafe.com/onlineleasing/the-place-at-creekside/floorplans'
    $WebRequestContent = (Invoke-WebRequest -Uri $Uri).Content
    $ContentPath = 'C:\users\tommymaynard\Documents\tommymaynard.com\Apartment Hunting\WebpageContents.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='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}} |
    Format-Table -AutoSize
}
Watch-Apartment

We will discuss the above function using its line numbers:

Line 1: Declares/creates the Watch-Apartment function.
Line 2: Stores the site’s URI inside the $Uri variable.
Line 3: Invokes an Invoke-WebRequest command using the URI and stores the Contents (as in the Contents property) inside the $WebRequestContent variable.
Line 4: Creates the ContentPath variable to hold a path to a text file that will be created in the next line/command.
Line 5: Takes the content from the webpage and writes it to a text file.

Writing to a file was not a requirement, however, it was my first choice for whatever reason and so I went with it, and then stayed with it.

Line 7: Read in the contents from the file and store them in the $File variable.
Line 8: Create a Regex pattern to allow us to collect all the content between the word “floorplans:” and “propertyID:”.
Line 9: Parse out the data we want and store it in the $ParsedPage variable.
Line 10: Trim off the white space from the beginning and end of the JSON string, and then trim off the trailing comma at the end of the JSON string.

Line 12: Assign the $JsonDocument variable the value assigned to the $ParsedPage variable after it has been converted from JSON by CovertFrom-Json.
Lines 13 – 19: Use Select-Object to select and modify our desired properties.

In the final command, we determine which floor plan is available, how many apartments there are, which model it is, how many square feet that model has, and how many bedrooms and bathrooms it has. Each line/property includes a calculated property and often, just to modify the case of the text.

I edited the friend’s PowerShell profile script and added this function. Not only is the function added to the PowerShell session by the profile script, but it also invokes the function, too. Open PowerShell, and just about instantly know whether anything is available or not.

These were the results the first time it ran back on my machine.

It was a good thing that the Available property included both the isFullyOccupied (“Yes” versus “No”) and the availableCount (# of apartments) information. Take a look at the next image to see why.

In the above image, it still says, “Yes,” but the count is zero. Apparently, my decision to include both values was the right choice, as this information is not all updated at the same time.

Later that same day, it cleared up.

Now, he waits, as my work is done.

Note: As stated at the top of this post, expect a part two. There is more than one apartment complex now.

Get-History Modified


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 10, 2019.


It was a couple of articles ago where I took an unused, leftover example I had sitting in a tab inside my PowerShell editor, and used it to write an article. This happens often, but usually only when they’re good examples. In this recent case, while I believe it turned out well, I used something that wasn’t overly amazing and made it worthy. I’m doing that today. Again. I think.

What I can tell you, is that the content in this second tab was small, yet complete. Its home, in fact, had been only one tab over from the function that brought us the Build In Measure-Command post, I’ve vaguely mentioned.

To prep, let’s discuss the Get-History cmdlet. It’s been around for as long as I can remember, and its purpose, as indicated by its synopsis is this: “The Get-History cmdlet gets the session history, that is, the list of commands entered during the current session.” You enter a command and the command is added to the history. This cmdlet allows you to view your previously entered command(s) later if you choose to do that. Let’s start with a few PowerShell commands.

PS> Get-Date
Wednesday, April 7, 2019 1:47:56 PM
PS> Get-Random
217097233
PS> (Get-Process | Select-Object -First 1).ProcessName
AGMService
PS> (Get-Alias -Name gci).DisplayName
gci -> Get-ChildItem

After knowing we’ve invoked these four commands, we can invoke the Get-History cmdlet to see them in succession. The Get-History default output returns two properties: Id and CommandLine.

PS> Get-History
  Id CommandLine
  -- -----------
   1 Get-Date
   2 Get-Random
   3 (Get-Process | Select-Object -First 1).ProcessName
   4 (Get-Alias -Name gci).DisplayName

You can’t tell using this output, but there are properties that aren’t shown in the default, Get-History output. In addition to the Id and CommandLine properties, there is an ExecutionStatus property, a StartExecutionTime property, and finally, an EndExecutionTime property. Here’s an example that includes them all.

PS> Get-History | Select-Object -Property *
Id                 : 1
CommandLine        : Get-Date
ExecutionStatus    : Completed
StartExecutionTime : 4/7/2019 1:50:35 PM
EndExecutionTime   : 4/7/2019 1:50:35 PM
 
Id                 : 2
CommandLine        : Get-Random
ExecutionStatus    : Completed
StartExecutionTime : 4/7/2019 1:50:37 PM
EndExecutionTime   : 4/7/2019 1:50:37 PM
 
Id                 : 3
CommandLine        : (Get-Process | Select-Object -First 1).ProcessName
ExecutionStatus    : Completed
StartExecutionTime : 4/7/2019 1:50:45 PM
EndExecutionTime   : 4/7/2019 1:50:45 PM
 
Id                 : 4
CommandLine        : (Get-Alias -Name gci).DisplayName
ExecutionStatus    : Completed
StartExecutionTime : 4/7/2019 1:51:00 PM
EndExecutionTime   : 4/7/2019 1:51:00 PM
 
Id                 : 5
CommandLine        : Get-History
ExecutionStatus    : Completed
StartExecutionTime : 4/7/2019 1:51:09 PM
EndExecutionTime   : 4/7/2019 1:51:09 PM

If you just found this out, and you thought about what I did when I first found out, then you may have realized that we can determine how long a command took to complete, if we do a little subtraction. Subtract the start time from the end time and we know the amount of time each command has taken. And that’s the little teeny chunk of code I rescued from a soon-to-be abandoned VS Code tab. I’ve included this simple function below and the modified output from above. While the TimeTaken property doesn’t really help with the previous commands we ran, as they all ended so quickly, it easily may help for long-running commands and scripts.

Function Get-History {
    $History = Microsoft.PowerShell.Core\Get-History | Select-Object -Property *
    $History | Select-Object Id,CommandLine,
        @{Name='TimeTaken';Expression={($_.EndExecutionTime) - ($_.StartExecutionTime)}}
} # End Function: Get-History.

PS> Get-History
Id CommandLine                                        TimeTaken
-- -----------                                        ---------
 1 Get-Date                                           00:00:00.0154352
 2 Get-Random                                         00:00:00
 3 (Get-Process | Select-Object -First 1).ProcessName 00:00:00.0110123
 4 (Get-Alias -Name gci).DisplayName                  00:00:00
 5 Get-History                                        00:00:00.0154351
 6 Get-History | Select-Object -Property *            00:00:00.0158662

While everyone is still paying attention, let’s assume we have a .ps1 file saved to our Desktop on Windows. Its name is sleep.ps1 and literally, all it does is sleep for 10 seconds. After we’ve run it as . .\Desktop\sleep.ps1let’s rerun our modified Get-History command. It does exactly what we would expect; it indicates that the TimeTaken property is 10 seconds.

PS> Get-History
Id CommandLine                                        TimeTaken
-- -----------                                        ---------
 1 Get-Date                                           00:00:00.0154352
 2 Get-Random                                         00:00:00
 3 (Get-Process | Select-Object -First 1).ProcessName 00:00:00.0110123
 4 (Get-Alias -Name gci).DisplayName                  00:00:00
 5. Get-History                                       00:00:00.0154351
 6. Get-History | Select-Object -Property *           00:00:00.0158662
 7. . .\Desktop\sleep.ps1                             00:00:10.0229630

That’s it. A quick and simple way to determine the time each command takes to complete. Add this function to your $PROFILE script and it’ll always run, instead of the standard, built-in Get-History cmdlet.