Category Archives: PowerShell Archive

Helpful information and resources that were removed from the PowerShell.org website during the redesign, saved for your convenience!

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.

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.

Build-in Measure-Command


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


I don’t know about you, but I have like five or six open tabs in my PowerShell editor at all times. Each was used to briefly prove something worked, or that it didn’t. Occasionally, these will become articles, but much more often, they just sit there for what feels like forever. I can’t seem to close some of these tabs, or even bring myself to save the sample code. If I can’t get to it now — or this week or month — what makes me think I’ll save it off and then get back to it? I probably won’t.

But today, I’m going to close a tab. This, right after I’ve written about it here. I’m not sure if it was supposed to become an article, but it’s going to regardless.

This tab’s single function is called Get-Date. It probably sounds familiar, and it should. Here’s the idea. I want a user to be able to invoke my function and return the date, just like the Get-Date cmdlet will do for them. Now, before we go any further, it’s important to understand that using Get-Date — the function — will absolutely remove any ability to use Get-Date — the cmdlet — in any one of the other 500 different ways it can be used. That’s okay. Remember, this came out of a tab with an unknown future. This isn’t fully thought out or tested, production-ready code. It’s an artist’s sketchbook or a student’s rough draft. I didn’t even have to use Get-Date. It could have been Get-ADUser or Set-Content. As you’ll see, the date portion of this function is inconsequential. It’s just what I chose when I needed something with which to test.

In addition to returning the standard Get-Date result, I also wanted to build in a way to measure the length of time it takes for my command to complete. This was the whole experiment; this was the purpose behind the tab. To do this, requires a user include the Measure switch parameter at the time of the function’s invocation. This measurement value isn’t displayed by default, but instead, it’s written to yes, a global variable, the user can inspect if they so desire. It still produces the Get-Date cmdlet’s default output, but now there’s more output waiting in the wings if it’s wanted.

In the first example, our Get-Date function executes as though we’ve run the default, Get-Date cmdlet. We have in fact. Without the Measure parameter, all we do is run the Get-Date cmdlet. When you look over the function’s code momentarily, you’ll see the fully-qualified, Get-Date command. A fully-qualified PowerShell command includes the module name. This is a requirement because we’ve named our function the same name as the cmdlet. Command precedence order indicates that functions execute before cmdlets if both commands have the same name.

In the second example, we’ll include the Measure switch parameter, and then return the value stored in the global, $Measurement variable. What I wanted to work, did. I built in a way to run the command and measure its time to complete if I want that information. As it’s just Get-Date, it doesn’t take long at all.

PS> Get-Date
Monday, April 2, 2019 10:24:06 PM
PS>
PS> Get-Date -Measure
Monday, April 2, 2019 10:24:07 PM
PS>
PS> $Measurement
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 1
Ticks : 10231
TotalDays : 1.18414351851852E-08
TotalHours : 2.84194444444444E-07
TotalMinutes : 1.70516666666667E-05
TotalSeconds : 0.0010231
TotalMilliseconds : 1.0231

Again, if this command is run with the Measure parameter, the length of time it took to complete is written to a global variable named $Measurement. As you’d expect, that variable will maintain its value until the variable name is reused for something else in the global scope, it’s overwritten by this function, it’s specifically removed, or the current PowerShell session has ended.

As can be seen in the below function, if the Measure parameter isn’t included, we run the fully-qualified, built-in Get-Date cmdlet. If it is included, we run Measure-Command against the fully-qualified, built-in Get-Date cmdlet. We use the OutVariable common parameter twice. We use it once with the Get-Date command, placing the current date and time into $CommandResult, and once with Measure-Command, placing the time to complete in our global $Measurement variable. We display $CommandResults and leave $Measurement in memory in case, the user opts to view the measurement results.

Function Get-Date {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [switch]$Measure
    )
 
    If ($Measure) {
        Measure-Command -Expression {
            Microsoft.PowerShell.Utility\Get-Date -OutVariable CommandResult
        } -OutVariable Global:Measurement | Out-Null
        $CommandResult
    } Else {
        Microsoft.PowerShell.Utility\Get-Date
    } # If-Else.
} # End Function: Get-Date.

Before we wrap up, let’s look at a slightly modified version of the Get-Date function. This has been named Get-DateWithPause and hopefully, this will help indicate that the function is taking proper measurements. In this function, we’ve added two Start-Sleep commands where we pause for two seconds. Beneath this function, we’ll rerun our previous examples and spot those expected differences.

Function Get-DateWithPause { 
    [CmdletBinding()] 
    Param ( 
        [Parameter()] 
        [switch]$Measure 
    )
 
    If ($Measure) {
        Measure-Command -Expression { 
            Start-Sleep -Seconds 2 
            Microsoft.PowerShell.Utility\Get-Date -OutVariable CommandResult
        } -OutVariable Global:Measurement | Out-Null 
        $CommandResult 
    } Else { 
        Start-Sleep -Seconds 2 
        Microsoft.PowerShell.Utility\Get-Date 
    } # If-Else. 
} # End Function: Get-Date. 
PS> Get-DateWithPause
Monday, April 2, 2019 10:33:44 PM
PS>
PS> Get-DateWithPause -Measure
Monday, April 2, 2019 10:33:50 PM
PS>
PS> $Measurement
Days : 0
Hours : 0
Minutes : 0
Seconds : 2
Milliseconds : 2
Ticks : 20020567
TotalDays : 2.31719525462963E-05
TotalHours : 0.000556126861111111
TotalMinutes : 0.0333676116666667
TotalSeconds : 2.0020567

I’ve got a few more tabs with random pieces of PowerShell code. I’ll take a look through those, too. Just maybe I can close a few more tabs by giving their content a purpose here, even if they’re barely worthy. That said, I think this turned out alright. Someday maybe, I’ll get a measurement option builtin to all the functions I author.

Edit: In republishing this post here on tommymaynard.com, I decided to update the function a small bit. This version includes a second switch parameter (MeasureAndShow) that will both measure the command’s duration and display it at the same time, too. With more time, I might have implemented this differently, but this works.

Function Get-Date {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [switch]$Measure,
		[Parameter()]
		[switch]$MeasureAndShow
    )

    If ($Measure -or $MeasureAndShow) {
        Measure-Command -Expression {
            Microsoft.PowerShell.Utility\Get-Date -OutVariable CommandResult
        } -OutVariable Global:Measurement | Out-Null
        $CommandResult
		if ($MeasureAndShow) {
			$Global:Measurement
		}
    } Else {
        Microsoft.PowerShell.Utility\Get-Date
    } # If-Else.
} # End Function: Get-Date.
[PS7.2.1][C:\] Get-Date

Sunday, February 13, 2022 8:17:48 AM

[PS7.2.1][C:\] Get-Date -Measure

Sunday, February 13, 2022 8:17:52 AM

[PS7.2.1][C:\] $Measurement

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 2
Ticks             : 25986
TotalDays         : 3.00763888888889E-08
TotalHours        : 7.21833333333333E-07
TotalMinutes      : 4.331E-05
TotalSeconds      : 0.0025986
TotalMilliseconds : 2.5986


[PS7.2.1][C:\] Get-Date -MeasureAndShow

Sunday, February 13, 2022 8:18:01 AM

Ticks             : 1781
Days              : 0
Hours             : 0
Milliseconds      : 0
Minutes           : 0
Seconds           : 0
TotalDays         : 2.06134259259259E-09
TotalHours        : 4.94722222222222E-08
TotalMilliseconds : 0.1781
TotalMinutes      : 2.96833333333333E-06
TotalSeconds      : 0.0001781

Linux Prompt X


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 March 30, 2019.


Note: Because this is republished post, I know there is an update coming on this topic. Be sure to watch for this on Wednesday, February 17, 2021. I will do my best to remember to link to it once it is available.

“This is the one.” -Me (about ten seconds before starting this article)

For a decent amount of time now, I’ve used a personalized prompt. On several occasions even, I’ve written and shared it with the PowerShell community. Well, I’m about to do the same thing today. That said, I’ve never quite felt what I’m feeling now. After all those iterations, I feel that this version — version X (ten) — is what I was always after. It makes sense on Windows and Linux, and it provides everything I could want in a prompt.

For full disclosure, I’ve actually… said that last sentence before.

Let me explain a bit about it for those that may be new. I’ve always appreciated the Unix/Linux prompt, as it includes with it, the user name, the host/computer name, and often, an indication of whether or not I’m running as root ($ vs. # [# is root/admin]). Over time, I brought this and more into my prompt on Windows. It was the Linux prompt on Windows. And now, it’s even the Linux prompt on Linux, too. You know, that whole PowerShell Core, cross-platform thing. No kidding, but I was actually kind of surprised when I installed PowerShell Core for the first time and it used the same, standard PowerShell prompt. I suppose it’s something we’ve come to expect, however.

Before we discuss how to get the code, let’s discuss what I’ve done. I’ve now included everything, inside of a single advanced function called Switch-Prompt. If you run it with just the command name, you’ll get the standard, PowerShell prompt. That’s because the default value for the Type parameter is “Standard.” It can also be run using that parameter and parameter value, as seen in the below example.

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

When using the Type parameter value of “Standard,” the prompt will look and function just as it typically does in PowerShell. Until Switch-Prompt is invoked again, this will be the prompt each time a new prompt is created (Enter is pressed).

The fun begins when we change out the “Standard” parameter value to “Linux.” This prompt will … well, let me just show you.

PS > Switch-Prompt -Type Linux
[tommymaynard@tmlaptop Lang]$ Switch-Prompt
PS C:\Program Files\7-Zip\Lang>

This option will create the prompt as open square bracket, current user name, @ symbol, current computer name, current directory, close square bracket, and either a dollar sign ($) or hash symbol (#). Remember, the hash symbol indicates that the user is running as root, or admin and the dollar sign indicates that the user is running as a normal user.

Using “Linux” as the value for the Type parameter creates two dynamic parameters. The FullPath parameter will include the full path to the current directory, and the Version parameter will include the version of PowerShell between the closing square bracket and the dollar sign or hash symbol. These two parameters can be used one at a time, or together — it makes no difference. In the below examples, I’ve pressed Enter a couple of times between commands, so you can see that the new prompt stays around until Switch-Prompt is invoked again, in a different manner than it was previously.

PS C:\Program Files\7-Zip\Lang> Switch-Prompt -Type Linux
[tommymaynard@tmlaptop Lang]$
[tommymaynard@tmlaptop Lang]$
[tommymaynard@tmlaptop Lang]$ Switch-Prompt -Type Linux -FullPath
[tommymaynard@tmlaptop c/Program Files/7-Zip/Lang]$
[tommymaynard@tmlaptop c/Program Files/7-Zip/Lang]$
[tommymaynard@tmlaptop c/Program Files/7-Zip/Lang]$ Switch-Prompt -Type Linux -Version
[tommymaynard@tmlaptop Lang]5.1.1$
[tommymaynard@tmlaptop Lang]5.1.1$
[tommymaynard@tmlaptop Lang]5.1.1$ Switch-Prompt -Type Linux -FullPath -Version
[tommymaynard@tmlaptop c/Program Files/7-Zip/Lang]5.1.1$
[tommymaynard@tmlaptop c/Program Files/7-Zip/Lang]5.1.1$

At an earlier time in this prompt’s development, I opted to have it allow me to choose the username and computer name. That’s still with us. When we use “LinuxCustom” as the value for the Type parameter, we get even more dynamic parameters. We still have FullPath and Version, but now, we also have UserName and ComputerName. Unlike FullPath and Version, these are not switch parameters; they require a value to be included along with them. Take a look at these final examples, and then you can get the code for yourself!

[tommymaynard@tmlaptop c/Program Files/7-Zip/Lang]5.1.1$ Switch-Prompt -Type LinuxCustom
[fake_user@fake_computer Lang]$
[fake_user@fake_computer Lang]$
[fake_user@fake_computer Lang]$ Switch-Prompt -Type LinuxCustom -UserName tm
[tm@fake_computer Lang]$
[tm@fake_computer Lang]$
[tm@fake_computer Lang]$ Switch-Prompt -Type LinuxCustom -UserName tm -ComputerName srvx
[tm@srvx Lang]$
[tm@srvx Lang]$
[tm@srvx Lang]$ Switch-Prompt -Type LinuxCustom -UserName tm -ComputerName srvx -FullPath -Version
[tm@srvx c/Program Files/7-Zip/Lang]5.1.1$

Now, it’s time for you to try it out yourself. In the past, I opted to save it in a public Gist, but this time, I think it’s good enough to go straight to the PowerShell Gallery. Here’s the Switch-Prompt’s page on the gallery, and here’s the command to issue in PowerShell to install it on your computer, as the current user. Remember, it’s a script, not a module. Therefore, you need to dot-source it, to use it, which is something that could be easily added to your profile script. That command is below, as well.

PS > Install-Script -Name Switch-Prompt -Scope CurrentUser
PS > . "$HOME\Documents\WindowsPowerShell\Scripts\Switch-Prompt.ps1"

Be sure to swap out “WindowsPowerShell” for “PowerShell” in the above path, if you installed it using PowerShell Core. It works there too! Even in the recently released 6.2 version.

Really, Remove the Module


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 March 13, 2019.


I’m on a project. As a part of that project, I’m to deliver a PowerShell module that does — I don’t know — ten different things, we’ll say. The final, final thing that module does is remove itself from the computer. For real, it. deletes. itself.

The desktop admin, or whomever, will issue the Remove-Module command and via that function, the module will be removed from the system, as well as the session. Maybe you just had the thought I did: Yes, this module will live in the user’s module path. Therefore, the user will have the ability to delete the module from the system.

Let’s call our function Remove-MyModule and let’s assume it’s a member of the module called MyModule. I’ll also include a second function in MyModule called Get-MyModuleType. This way we have a command that offers some proof that my module is working, before the module is gone. Okay, let’s start by importing our module and using Get-Command to prove what I said we’d have up to this point. Remember, our Get-MyModuleType function tells us what type of module we have, and our Remove-MyModule function removes the whole module — including both commands — from existence.

PS> Import-Module -Name MyModule
PS> Get-Command -Module MyModule | Format-Table -AutoSize
CommandType Name             Version Source
----------- ----             ------- ------
Function    Get-MyModuleType 1.0.0   MyModule
Function    Remove-MyModule  1.0.0   MyModule

Before we move on, here’s the function code that makes up the Get-MyModuleType function — simple stuff. We collect the command name, and from it, the name of the containing module. And finally, we return the ModuleType property.

Function Get-MyModuleType {
    [CmdletBinding()]
    Param (
    )
 
    Begin {
        $CmdName = "$($MyInvocation.MyCommand.Name)"
        $CurrentModule = (Get-Command -Name $CmdName).Source
    } # End Begin.
 
    Process {
        (Get-Module -Name $CurrentModule).ModuleType
    } # End Process.
 
    End {
    } # End End.
} # End Function: Get-MyModuleType.

Now, let’s prove that our module’s Get function works. We invoke Get-MyModuleType and it outputs an indication that my module is a script module. Sure, we knew that, but with this first function, we have some indication that things are working, before we try the function in the module, that removes the module.

PS> Get-MyModuleType
Script

Let’s take a look at the second function in our module. As you look over the included code shortly, keep in mind that we have a few things going on. In our Begin block, we mostly do the same thing we did in the above function: return the name of the function and use it to return the name of the module. In addition to this, the Remove-MyModule function returns the module’s path, as well.

Once we exit the Begin block and enter the Process block, three tasks take place in succession. First, the function deletes the module folder from my computer (so have a backup if you’re playing along). It’s okay that the module is gone though. The function can continue to execute for now, as the module and its functions are still loaded in our current PowerShell session, even though its source is no longer on the disk. Next, it removes the module from the PowerShell session (think: from memory, where it’s still being stored). Lastly, our function removes the two functions from within the module (Get-MyModuleType and Remove-MyModule) from the Function PSDrive — we’ll discuss more shortly, after you’ve taken a look at the code that makes up the Remove-MyModule function.

Function Remove-MyModule {
    [CmdletBinding()]
    Param (
    )
 
    Begin {
        $CmdName = "$($MyInvocation.MyCommand.Name)"
        $CurrentModule = (Get-Command -Name $CmdName).Source
        $CurrentModulePath = (Get-Module -Name (Get-Command -Name $CmdName).Source).Path
    } # End Begin.
 
    Process {
        #region Remove (delete) module.
        try {
            $Path = "$(($CurrentModulePath -split $CurrentModule)[0])$CurrentModule"
            Remove-Item -Path $Path -Recurse -ErrorAction Stop
        } catch {
            Write-Warning -Message "Unable to remove (delete) the $CurrentModule PowerShell module."
        } # End try-catch.
        #endregion.
 
        #region Remove (unload) module.
        try {
            Remove-Module -Name $CurrentModule -ErrorAction Stop
        } catch {
            Write-Verbose -Message "Unable to remove (unload) the $CurrentModule PowerShell module."
        } # End try-catch.
        #endregion.
 
        #region Remove module functions from function PSDrive.
        Get-ChildItem -Path 'Function:\*-MyModule*' | Remove-Item
        #endregion.
    } # End Process.
 
    End {
    } # End End.
} # End Function: Remove-MyModule.

Typically, when we remove a PowerShell module from the PowerShell session using Remove-Module, it takes all the parts and pieces along. But, because we’ve already removed the module from the system, removing the module from the PowerShell session leaves the functions in the Function PSDrive. That’s one thought. The other, although I don’t believe this one so strongly (because both functions are left behind), is that they are left behind because we’re right in the middle of the Remove-MyModule function invocation.

Regardless of knowing exactly how this works, it’s fair to believe that traditional clean-up doesn’t work so well in this situation. The function we were executing was just removed from the computer in every way, and the function was completed without a problem. Sometimes, like this time, that’s good enough for me. The project is done. Okay fine, just that function part anyway. Let me hear your thoughts and ideas!

Looking Busy with PowerShell


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 February 27, 2019.


Every once in a while, I write a short little article — if, I can even call it that — and then it sits around for a month or so doing nothing. It just hangs out in my drafts and stares at me. It’s happened again. Instead of focusing on it any longer, I’m writing this paragraph as my excuse, so I can publish this now and move on, already.

I saw the below Tweet late last year and I had a thought.

I love how having several windows of #Powershell open makes you look busy and/or like you know what you are doing.
— John Dalek (@DeckerDalek) November 20, 2018

I’ll go ahead and agree; why not?

I can see how this may give off that impression. With that quick and internal agreement, I had an idea. If PowerShell can make you look busy, then let’s use PowerShell to make it appear, you’re busy. We’ll open some consoles and execute some “work,” all with a single invocation of a single function. I’ll just be over here waiting for PowerShell to catch up with me.

The below function creates this illusion, just in case this is something you’re after. Hopefully, it’s not, as we all likely have some real PowerShell to read, write, and review. Me included.

Function Show-MeBeingSuperBusy {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateRange(1,10)]
        [int]$ConsoleCount = 1
    )
    
    Begin {
        $Argument = '-NoProfile -Command & {1..50 | ForEach-Object {Get-PSDrive}}',
            '-NoProfile -Command & {1..50 | ForEach-Object {Get-Process}}',
            '-NoProfile -Command & {1..50 | ForEach-Object {Get-Service}}',
            '-NoProfile -Command & {1..50 | ForEach-Object {Get-Item -Path env:\}}'
    } # End Begin.
    
    Process {
        For ($i = 1; $i -le $ConsoleCount; $i++) {
            Start-Process -FilePath powershell.exe -ArgumentList ($Argument | Get-Random)
        } # End For.
    } # End Process.
    
    End {
    } # End End.
} # End Function: Show-MeBeingSuperBusy.
 
Show-MeBeingSuperBusy -ConsoleCount 5

And that’s it. Short, simple, and hardly very helpful. Now that this “article” is gone from my drafts, I should be able to focus on something a little more helpful — we’ll see. This might be all the help someone needed from me today, however.

Edit: As a part of bringing this old content back to tommymaynard.com, I tried out this function. It works, but be sure to give it a moment before you begin to think it is not working!

Clear-Host Deconstructed


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 January 31, 2019.


Sometimes, just sitting inside my console, I issue a few consecutive commands, and suddenly, I have something worth sharing. Recently, as I prepared to work with something other than PowerShell, I cleared the host. It’s a common occurrence — probably for us both, even. After doing so, I decided to take a closer look at the command I had just issued.

PS> Get-Command -Name cls
CommandType     Name                   Version    Source
-----------     ----                   -------    ------
Alias           cls -> Clear-Host
PS> Get-Command -Name Clear-Host
CommandType     Name                   Version    Source
-----------     ----                   -------    ------
Function        Clear-Host

In the first above command, we ran Get-Command against cls and determined that it’s an alias for Clear-Host. In the second command, we followed up by determining that Clear-Host is a function. For me, the absolute first thing I do when I realize a command is a function, and not a cmdlet, is peer into the code that makes it do, what it does.

In the below example, we return the ScriptBlock property of our Clear-Host function. While you can instead return the Definition property, I’ve always just used ScriptBlock. It makes more sense to me and is easier to remember. But in my experience, functions have the same code twice, in two different properties.

PS> (Get-Command -Name Clear-Host).ScriptBlock

$RawUI = $Host.UI.RawUI
$RawUI.CursorPosition = @{X=0;Y=0}
$RawUI.SetBufferContents(
    @{Top = -1; Bottom = -1; Right = -1; Left = -1},
    @{Character = ' '; ForegroundColor = $rawui.ForegroundColor; BackgroundColor = $rawui.BackgroundColor})
# .Link
# https://go.microsoft.com/fwlink/?LinkID=225747
# .ExternalHelp System.Management.Automation.dll-help.xml

Edit: The link in the above code no longer works. It just ends up at Bing. The current link, when viewing this ScriptBlock now, is https://go.microsoft.com/fwlink/?LinkID=2096480. Other than that, it does not appear anything else has changed since 2019 when this post was originally published.

The Clear-Host function includes a few things I decided to go over myself, and then share here, as well. We essentially have three commands inside this function. The very first command sets the variable $RawUI to the values stored in the host’s RawUI property. The host in the discussion here refers to the host program that’s hosting the PowerShell engine and has nothing to do with a computer host. These settings include things like the ForegroudColor and BackgroundColor, the WindowSize, WindowsTitle, and more. The Idea, however, is that only the Foreground and Background color will be used in the third command discussed below.

After the $RawUI variable is created and populated, we then alter the location of the cursor within the host program. It’s moved from its current location in the host program, wherever that might be, to the topmost and leftmost position within the host (0,0). While it’ll be for a millisecond or two, do keep in mind that it will eventually be forced into its final position by the execution of the prompt function, moving it to the right as is necessary. Even so, this movement won’t happen until after the Clear-Host script block is complete, and there’s still a final command to execute.

The third and final command to execute when the Clear-Host function is invoked uses the SetBufferContents method. This method requires two arguments. The first argument is the Rectangle. As Clear-Host uses -1 for the Top, Bottom, Right, and Left, the entire screen of the host program will be filled in. Think of the console screen — this host program — as the rectangle. And yes, you read that correctly. We’re going to fill in our host program’s screen.

The second argument, which is also a hash table such as the first argument was, includes three keys, not four as we just saw. The first is Character. This indicates what single character should be used to fill in the screen. Clear-Host uses an empty space, such as ‘ ‘. As you start testing (or planning pranks, duh), be sure to change this to other single characters, such I did to create the below image.

Note: The originally published image was unrecoverable.

Finally, the last two keys in the hash table that make up this second argument, allow us to choose the foreground and background colors to be used inside the host. The default that my Windows PowerShell host wants to use is black, over the Windows PowerShell standard blue, so this needs to be included.

And that’s it. After the variable assignment, the Clear-Host function essentially has two commands. To review, it positions the cursor at the top-left corner of the host, fills in the screen with an empty space at every position inside the host program, and sets the foreground and background color to what it already was.

Before we wrap it up here, I had an idea (that I would never really implement). Instead of using the default colors used by Clear-Host, you can create your own Clear-Host function that uses random colors for the foreground and background colors. I won’t bother providing a demonstration, but take the below code and supplant it into the above code and enjoy!

ForegroundColor = ([System.Enum]::GetNames([System.ConsoleColor]) | Get-Random); BackgroundColor = ([System.Enum]
::GetNames([System.ConsoleColor]) | Get-Random)})

Return Only the Fixed Disks


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 January 15, 2019.


As a part of a recent engagement (with a company you’ve likely heard of), we had some code written and provided to us. In my review of what was provided to scan drives using Windows Defender, I noticed that there were some problems. One, there was an assumption that a computer would only ever have a single optical drive, and two, mapped network drives may have ended up being scanned, as well. Now, I’m not sure if Defender would actually scan a network drive, but I assume it would and don’t really care to find out.

We needed a way to filter out optical drives and network mapped drives regardless of the count of either before we started a Windows Defender scan. I’ll start with the code I used, followed by a second option that occurred more recently — it was a would this work idea. It does, so I’ll explain them both and perhaps we’ll all be better off seeing two different options. Before we get deeper into this, take a look at the output provided by Get-PSDrive (when piped to Format-Table and the AutoSize parameter). Clearly, it’s found a good number of drives on my system.

PS> Get-PSDrive | Format-Table -AutoSize
Name     Used (GB) Free (GB) Provider    Root                      CurrentLocation
----     --------- --------- --------    ----                      ---------------
Alias                        Alias
C           698.18    232.15 FileSystem  C:\                           Users\tommy
Cert                         Certificate \
D                            FileSystem  D:\
E                            FileSystem  E:\
Env                          Environment
Function                     Function
HKCU                         Registry    HKEY_CURRENT_USER
HKLM                         Registry    HKEY_LOCAL_MACHINE
MDI                          FileSystem  \\mydomain.com\data\in...
MDT                          FileSystem  \\mydomain.com\data\to...
I           112.80    352.96 FileSystem  I:\
P             0.15    465.60 FileSystem  P:\
Variable                     Variable
W           676.59    254.92 FileSystem  W:\
WSMan                        WSMan

Now, let’s modify our command and only return our FileSystem drives. Unfortunately, there are a couple of optical drives (although you don’t really know that yet), and two mapped network drives that we don’t want or need in our results.

PS> Get-PSDrive | Format-Table -AutoSize
Name     Used (GB) Free (GB) Provider    Root                      CurrentLocation
----     --------- --------- --------    ----                      ---------------
Alias                        Alias
C           698.18    232.15 FileSystem  C:\                           Users\tommy
Cert                         Certificate \
D                            FileSystem  D:\
E                            FileSystem  E:\
Env                          Environment
Function                     Function
HKCU                         Registry    HKEY_CURRENT_USER
HKLM                         Registry    HKEY_LOCAL_MACHINE
MDI                          FileSystem  \\mydomain.com\data\in...
MDT                          FileSystem  \\mydomain.com\data\to...
I           112.80    352.96 FileSystem  I:\
P             0.15    465.60 FileSystem  P:\
Variable                     Variable
W           676.59    254.92 FileSystem  W:\
WSMan                        WSMan

In this next example, we’ll remove the mapped drives from our results. In the end, we have our fixed drives and the D:\ and E:\ drives that have no used or free space. Perhaps those are the optical drives and there are no actual disks in either one. Before we move past this example, however, let’s get the results of this command into a variable, as well. The $FixedDrives variable is assigned toward the bottom of the below example.

PS> Get-PSDrive -PSProvider FileSystem | Where-Object -Property Root -notlike '\\*' | Format-Table -Autosize

Name     Used (GB) Free (GB) Provider    Root CurrentLocation
----     --------- --------- --------    ---- ---------------
C           698.18    232.15 FileSystem  C:\      Users\tommy
D                            FileSystem  D:\
E                            FileSystem  E:\
I           112.80    352.96 FileSystem  I:\
P             0.15    465.60 FileSystem  P:\
W           676.59    254.92 FileSystem  W:\

PS> $FixedDrives = Get-PSDrive -PSProvider FileSystem | Where-Object -Property Root -notlike '\\*'
PS> # Noticed we removed Format-Table -- that was _only_ there for the onscreen display.

Now, let’s get a hold of our optical drives. Because we’ll need them in a variable, we’ll go ahead and make that assignment in this next example, as well.

PS> Get-WmiObject -Class Win32_CDROMDrive | Format-Table -AutoSize

Caption                           Drive Manufacturer             VolumeName
-------                           ----- ------------             ----------
ASUS DRW-1814BLT ATA Device       D:    (Standard CD-ROM drives)
ELBY CLONEDRIVE SCSI CdRom Device E:    (Standard CD-ROM drives)

PS> $OpticalDrives = Get-WmiObject -Class Win32_CDROMDrive

Now that that’s done — oh look, it is the D:\ and E:\ drives — let’s run a comparison against the fixed drives and optical drives we’ve returned. Again, this first example is how I rewrote the code that was provided to us. Once we’ve seen this, then we’ll try the comparison I considered over some recent weekend. This example compares the Get-PSDrive‘s Root property (with the backslash removed) against the Get-WmiObject‘s Drive property. If they don’t match, it stays. Otherwise, it’s filtered out. As you’ll see, when we assign and return our new $DrivesToTest variable, we can see that our D:\ and E:\ drives — our optical drives — have been removed. Perfect.

PS> $DrivesToTest = ($FixedDrives.Root).TrimEnd('\') | Where-Object {$OpticalDrives.Drive -notcontains $_}
PS> $DrivesToTest
C:
I:
P:
W:

Let’s use our $FixedDrives and $OpticalDrives variables again, but this time with the Compare-Object cmdlet. This was the additional idea I had to determine if we can simplify things even more. As some have noticed — I have not shied away from the fact that I tend to do things the hard way, the first time. In case it makes a difference, and I hope it doesn’t, Compare-Object was first introduced in PowerShell 3.0.

PS> (Compare-Object -ReferenceObject ($FixedDrives.Root).TrimEnd('\') -DifferenceObject $OpticalDrives.Drive).InputObject
C:
I:
P:
W:

Just like that, we’ve got the same results, with less work. Now, I can get back to doing whatever it was before I began reviewing this code. That and our users can safely get to work scanning machines with Windows Defender, without the concern anyone will scan against any number of optical drives, or mapped network drives.

CIDR Notation Host Count


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 January 15, 2019.


Late last year, I spent some time studying for the Amazon Web Services (AWS) Solutions Architect — Associate exam. In doing so, I briefly ended up covering CIDR again and the two-step math problem required to determine the number of available hosts in a CIDR IP address range.

As a recap, it works this way. Let’s consider the largest VPC (Virtual Private Cloud), or a virtual network, one can define in AWS. That’s 10.0.0.0/16. The way to determine the number of available host IPs is to subtract 16 (as indicated by the /16) from 32 — a constant value. In this example, that total is also 16. We then raise 2 (the base — another constant) to the power of 16 (the exponent/our difference), which results in 65,536 possible hosts. Because PowerShell can often distract me from AWS, let’s take a look at a small function I quickly wrote out — yes, during my AWS study time — to do the conversion for me.

Function Get-CidrHostCount {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [ValidateRange(1,32)]
        $Cidr
    )

    Begin {
    } # End Begin.

    Process {
        "Number of hosts for /$Cidr`: $([System.Math]::Pow(2,32-$Cidr))"
    } # End Process.

    End {
    } # End End.
} # End Function: Get-CidrHostCount.

When invoked, the above Get-CidrHostCount function will accept any numeric value from 1 through 32 and determine how many hosts the CIDR range would allow. This 1 to 32 value is equivalent to the number after the forward-slash in the CIDR notation. Do notice our two-step math problem. The System namespace’s Math class includes a method called Pow. This method accepts two values. The first value is our base again — 2 — and the second number is the exponent. In the function, we use the constant value of 32 and subtract the CIDR value that’s passed in when the function is invoked. These two values are then used to complete the calculation.

The below examples display a few results but eventually return all the possible results.

PS> Get-CidrHostCount -Cidr 16

Number of hosts for /16: 65536

PS> Get-CidrHostCount -Cidr 20

Number of hosts for /20: 4096

PS> Get-CidrHostCount -Cidr 24

Number of hosts for /24: 256

PS> Get-CidrHostCount -Cidr 28

Number of hosts for /28: 16

PS> 1..32 | ForEach-Object {
>> Get-CidrHostCount -Cidr $_
>> }

Number of hosts for /1: 2147483648
Number of hosts for /2: 1073741824
Number of hosts for /3: 536870912
Number of hosts for /4: 268435456
Number of hosts for /5: 134217728
Number of hosts for /6: 67108864
Number of hosts for /7: 33554432
Number of hosts for /8: 16777216
Number of hosts for /9: 8388608
Number of hosts for /10: 4194304
Number of hosts for /11: 2097152
Number of hosts for /12: 1048576
Number of hosts for /13: 524288
Number of hosts for /14: 262144
Number of hosts for /15: 131072
Number of hosts for /16: 65536
Number of hosts for /17: 32768
Number of hosts for /18: 16384
Number of hosts for /19: 8192
Number of hosts for /20: 4096
Number of hosts for /21: 2048
Number of hosts for /22: 1024
Number of hosts for /23: 512
Number of hosts for /24: 256
Number of hosts for /25: 128
Number of hosts for /26: 64
Number of hosts for /27: 32
Number of hosts for /28: 16
Number of hosts for /29: 8
Number of hosts for /30: 4
Number of hosts for /31: 2
Number of hosts for /32: 1

Before we really wrap it up here, let’s change the results as many of us would prefer to see them. Here’s a mildly modified version of the function and the last above command run again.

Function Get-CidrHostCount {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [ValidateRange(1,32)]
        $Cidr
    )

    Begin {
    } # End Begin.

    Process {
        "Number of hosts for /$Cidr`: $('{0:N0}' -f [System.Math]::Pow(2,32-$Cidr))"
    } # End Process.

    End {
    } # End End.
} # End Function: Get-CidrHostCount.
PS> 1..32 | ForEach-Object {
>> Get-CidrHostCount -Cidr $_
>> }

Number of hosts for /1: 2,147,483,648
Number of hosts for /2: 1,073,741,824
Number of hosts for /3: 536,870,912
Number of hosts for /4: 268,435,456
Number of hosts for /5: 134,217,728
Number of hosts for /6: 67,108,864
Number of hosts for /7: 33,554,432
Number of hosts for /8: 16,777,216
Number of hosts for /9: 8,388,608
Number of hosts for /10: 4,194,304
Number of hosts for /11: 2,097,152
Number of hosts for /12: 1,048,576
Number of hosts for /13: 524,288
Number of hosts for /14: 262,144
Number of hosts for /15: 131,072
Number of hosts for /16: 65,536
Number of hosts for /17: 32,768
Number of hosts for /18: 16,384
Number of hosts for /19: 8,192
Number of hosts for /20: 4,096
Number of hosts for /21: 2,048
Number of hosts for /22: 1,024
Number of hosts for /23: 512
Number of hosts for /24: 256
Number of hosts for /25: 128
Number of hosts for /26: 64
Number of hosts for /27: 32
Number of hosts for /28: 16
Number of hosts for /29: 8
Number of hosts for /30: 4
Number of hosts for /31: 2
Number of hosts for /32: 1

And that’s it — numbers I can actually read. And yes, I finally did get back to studying for my exam.

Top of the Next Hour


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


The Get-Date cmdlet has always been helpful, but just when I thought it had me fully covered, I determined it fell short. That said, it does have enough to get me what I want, even if there isn’t a simple, single built-in method for it.

I’m working on a project that requires me to add an additional trigger to a previously created scheduled task. When the scheduled task was initially deployed, it only had a single trigger. It was to run at midnight the following day (from the day in which the task was first created) and then every hour forever (at the top of the hour), until the end of time. Well, for the next 10,675,199 days at least.*

As a part of updating this project with a new trigger, I’m also going to be overwriting the trigger that begins at midnight. I’m doing that now because, at this point in this project, I don’t want to wait until tomorrow morning (at midnight) to have the task up and running again, I need a better starting time now that the task has been operating successfully for months. While I can do an hour from now — whatever time that may be — for the New-ScheduledTaskTrigger’s At parameter…

PS> (Get-Date).AddHours(1)

Saturday, January 5, 2019 8:14:29 PM

I don’t want that.

What this would mean, is that my fleet of AWS EC2 instances would all have random times in which they execute this task, dependent on when the task was updated. To better complete this picture, this task downloads a PowerShell module from AWS S3 and plops it on the EC2 instance running the task. Historically, or currently, rather, I’ve greatly appreciated knowing that any modifications to the PowerShell module uploaded to S3, are downloaded to all the EC2 instances, at the top of any, and every, hour. At 10:00 a.m., the module is replaced. At 11:00 a.m., the module is replaced, and so on. A collection of random, unknown times would be horrible going forward. I need to know that any modifications to the PowerShell module in S3 will be on all the instances, every hour, at the same time. And even if there are no modifications to the module, it gets downloaded anyway. It’s just easier that way (for now, perhaps).

Therefore, I need to know the top of the next hour. You know, if it’s 7:16 p.m., I need Get-Date to return 8:00 p.m. (on the same day, of course). If it’s 4:30 a.m, I need 5:00 a.m. returned. While I didn’t find a built-in method to accomplish this, as stated, I was able to write something myself, after a short amount of time head down in the console. Take a look at the below commands, and then let’s discuss them.

PS> Get-Date

Saturday, January 5, 2019 7:19:17 PM

PS> (Get-Date).AddMinutes(59 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)

Saturday, January 5, 2019 8:00:00 PM

The first above example returns the current date and time, as we’d expect that it would. The second, above example, indicates how we can ensure the value returned by Get-Date is the same date as today — no changes wanted there — with the time set in the future, at the top of the next hour.

For fun, we’ll pretend the time is 10:43:19 a.m.

Here’s what would happen, if the second command ran against this time. First, it would use Get-Date‘s AddMintues method. That makes sense, as no matter what time it is, we’ll need to add time to the current time, to get to the top of the next hour. Therefore, within the AddMinutes method, we take 59 and subtract the current minute of the current time.

10:43:19

59 – 43 = 16 minutes

Next, we’d add some seconds to our time, as well. We would take the value of 60 and subtract the current second of the current time.

10:43:19

60 – 19 = 41 seconds

Adding 41 seconds to 10:43:19 makes it 10:44:00. Adding in those 16 minutes takes us to 11:00:00. If you didn’t catch it, the command uses 59, not 60, when calculating AddMinutes. This is because the AddSeconds method is going to make up our “missing” minute.

Take a look at the following two examples. I won’t bother to explain them, but perhaps at this point, you can understand why they produce the results they do.

PS> (Get-Date).AddMinutes(60 - (Get-Date).Minute).AddSeconds(59 - (Get-Date).Second)

Saturday, January 5, 2019 8:00:59 PM

PS> (Get-Date).AddMinutes(60 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)

Saturday, January 5, 2019 8:01:00 PM

Now, no matter when my instances have their trigger updated, for the same task across each of them, I can ensure this task is back to updating my PowerShell module on those instances, at the top of every hour.

I took this one step further, and not necessarily because it had anything to do with scheduled tasks. What if I wanted my own method to do this? I quickly wrote a ScriptMethod for my own instance of a Get-Date object. I don’t have an opportunity, or need to do this often, so every little bit of practice is helpful.

$Date = Get-Date
 
Add-Member -InputObject $Date -MemberType ScriptMethod -Name GetNextTopHour -Value {
    $this.AddMinutes(59 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)
}
 
$Date = $Date.GetNextTopHour()

Now we can use our datetime object, and return the top of the next hour.

PS> $Date

Saturday, January 5, 2019 8:00:00

Now back to thinking through the task that started this whole line of thought, anyway.

* For anyone curious, when the scheduled task’s, task trigger was originally created using the New-ScheduledTaskTrigger function, the value used for the RepetitionDuration parameter was set as [System.TimeSpan]::MaxValue. Take a look at the below example, and you’ll see where this 10 million-plus day count is derived.

PS> [System.TimeSpan]::MaxValue
Days              : 10675199
Hours             : 2
Minutes           : 48
Seconds           : 5
Milliseconds      : 477
Ticks             : 9223372036854775807
TotalDays         : 10675199.1167301
TotalHours        : 256204778.801522
TotalMinutes      : 15372286728.0913
TotalSeconds      : 922337203685.478
TotalMilliseconds : 922337203685477

In order that we’re all on the same page, those 10 million-plus days, equate to “indefinitely” in a scheduled task’s Triggers tab, when viewing the task in the GUI. If you’re ever after a scheduled task repetition that never ends, and you’re using PowerShell to piece your task together, then MaxValue is the property to use.