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.

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!

Background Jobs and Recursive Functions

I recently had a thought: I have not written much on background jobs over the last nearly eight years. I should do more of that. Part of it stemmed from recently preparing an old post that was published elsewhere to be brought back to life here on tommymaynard.com. That one was Keeping a Continuous Total.

In that post, I wrote, “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.”

Well, I did that. While the function to do lookups in the PowerShell Gallery already existed in my profile script, I added a new function to my profile script that includes various background job commands that is invoked by my profile script. And, it turns out I was able to make it a recursive function, as well. You never think you will need one of those until the opportunity presents itself. I am so glad I noticed this opportunity.

Let’s start with my Show-PSGalleryProject function. You are welcome to use this as well. This function goes out to the PowerShell Gallery and determines the download count of each of my scripts and modules published there. This function takes some time to run, therefore, I want it to run in the background, so the results are available quicker than they would be otherwise.

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.

The invocation of the above function is controlled by the below function. So, the Show-PSGalleryProject function goes out to the gallery to collect information, and the Show-PSGalleryProjectJob function orchestrates this process. Let me explain what this section function does. But first, take a look at it and see what you can extract yourself.

Set-Alias -Name psgal-Value Show-PSGalleryProjectJob
Function Show-PSGalleryProjectJob {
	$FunctionName = $MyInvocation.MyCommand.Name
	if (-Not(Get-Job -Name $FunctionName -ErrorAction SilentlyContinue)) {
		Start-Job -Name $FunctionName -ScriptBlock ${Function:Show-PSGalleryProject} | Out-Null
	} else {
		if ((Get-Job -Name $FunctionName).State -eq 'Completed') {
			$JobEndTime = (Get-Job -Name $FunctionName).PSEndTime
			Receive-Job -Name $FunctionName |
				Select-Object -Property *,@{Name='EndTime'; Expression={$JobEndTime}} -ExcludeProperty RunspaceId |
				Format-Table -AutoSize
			Remove-Job -Name $FunctionName
			& $FunctionName
		} else {
			Write-Warning -Message "Please wait. The $FunctionName background job is $((Get-Job -Name Show-PSGalleryProjectJob).State.ToLower()) (Id: $((Get-Job -Name Show-PSGalleryProjectJob).Id))."
		}
	}
}
psgal

Buckle up; here we go.

Line 1: Create the psgal alias for the function.
Line 2 (and 18): Define the Show-PSGalleryProjectJob function.
Line 3: Create the $FunctionName variable and assign it the name of this function using the $MyInvocation variable. The function’s name is used repeatedly throughout the function, so it made sense to store it in a variable.
Line 4: Include an if-else language construct. The if portion determines if there is a background job called Show-PSGalleryProjectJobor not.
Line 5: If that background job does not exist, it should be created and started. The Start-Job cmdlet’s ScriptBlock property invokes the Show-PSGalleryProject function.
Line 6: This is the beginning of else portion. The lines beneath it will run if there is already a background job running called Show-PSGalleryProjectJob.
Line 7 – 13: Nested in the else portion is another if-else construct. If the job is complete we run the commands in the if portion. If the job is not yet complete, we run the commands in the else portion below. The if portion does all of the following: it collects the end time of the job and stores it in $JobEndTime, it uses Receive-Job to collect the results of the completed background job, it pipes those results to Select-Object and displays all the default properties, as well as the end time we add using a calculated property. It takes those results and pipes them to Format-Table -AutoSize . Once it is done with those steps, it uses Remove-Job to remove/delete the background job.

Now, for the recursion. The final step inside the if portion of this nested if-else is to invoke the Show-PSGalleryProjectJob function. That is right. The function invokes or calls itself. It starts this whole process over again. It does this using the call operator (&) and the $FunctionName variable. Remember, that variable holds the name of the function. Without the call operator, it would just echo the value in the variable. That operator also called the invocation operator, invokes the function again. Every time the job is completed and the values are returned, the process starts over. On a side note, I have written about recursive functions once before.

Line 14 – 16: The else portion issues a Write-Warning message indicating that the job is not yet complete.
Line 19: The alias invokes the Show-PSGalleryProjectJob function.

The below results show the functions working together. Consider background jobs for longer running tasks and consider recursive functions when a situation presents itself where a function should call, or invoke, itself.

[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 1).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 1).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] # Here is where I waited from some time to pass...
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal

Name                Type   Version Downloads TotalDownloads EndTime
----                ----   ------- --------- -------------- -------
TMOutput            Module 1.1     2994                2994 2/10/2022 5:07:50 PM
Start-1to100Game3.0 Script 3.0     266                 3260 2/10/2022 5:07:50 PM 
Get-TMVerbSynonym   Script 1.4     293                 3553 2/10/2022 5:07:50 PM 
SinkProfile         Module 1.0     304                 3857 2/10/2022 5:07:50 PM 
Show-PSDriveMenu    Script 1.1     186                 4043 2/10/2022 5:07:50 PM 
Switch-Prompt       Script 1.2.0   236                 4279 2/10/2022 5:07:50 PM 

[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 3).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 3).
[PS7.2.1][C:\]

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.