Tag Archives: Format-Table

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.

Get-TMVerbSynonym 1.4

Notes: Download link at the bottom of this post.

One of my favorite PowerShell community members, June Blender, posted a question on Twitter. While it wasn’t directed at me, I couldn’t resist. She wanted the approved verb options for the verbs “populate”, “fill in”, or “load”. I couldn’t help myself, because I wrote a function for this very task!

The Get-TMVerbSynonym function’s purpose is to find synonyms for verbs and return whether they’re approved, or not. Well, her Tweet was all it took to finally make some overdue changes, including getting it posted on the PowerShell Gallery. As I called it in one of my follow-up tweets, when June asked where it was then published, I said it was a “TechNet leftover.”

Well, not anymore.

It’s since been updated to version 1.4 and placed on the PowerShell Gallery. The above image was what I posted when it was in version 1.3. This means that it doesn’t reflect two of the newest, view-able changes. The Verb property now indicates the verb it’s checking, and the old Verb property has been renamed to Synonym. This was included to support some other potential updates for an even newer version. Anyway, this means it’s up to five properties, so pipe to Format-Table -AutoSize in order that the results can be easily read.

Additionally, it now includes something I always wanted it to have: an Approved, switch parameter. This means there’s no piping to Where-Object to filter the function to only show the approved synonyms. Here’s an example of the function in action, both with and without the Approved, switch parameter.

Download it now manually, or use Install-Script -Name Get-TMVerbSynonym.

Keep PowerShell Cmdlets Powerful

I’ve seen what I’m about to show you done repeatedly in Windows PowerShell. With that thought in my mind, I’ve decided it’s time to officially write about it. The first time I noticed someone doing it, well, it was me. I did, however, write a correction to my team so they knew I was aware of my error, and as a hope they wouldn’t repeat it.

Those doing this may be new to PowerShell, but if you aren’t careful and don’t fully understand the capabilities of the cmdlets you use, then it’s possible you might make this mistake, too. What am I talking about? I’m talking about Invoke-Command and an improper way that it’s sometimes used.

Invoke-Command‘s purpose is to run a command, or commands, on remote computers and return the results back to the local computer. That said, it can be used on the local computer, as well; however, this is usually only necessary to check if PowerShell Remoting is working on the local computer. While I don’t normally do this in my examples, I’m piping my result of this example to Format-Table -AutoSize once they’re returned from the remote computer. This is so it’ll display better on this webpage.

Invoke-Command -Computer DC01 -ScriptBlock {Get-PSDrive -PSProvider FileSystem} | Format-Table -AutoSize
Name Used (GB) Free (GB) Provider Root              CurrentLocation PSComputerName
---- --------- --------- -------- ----              --------------- --------------
A                                 A:\                               DC01
C        12.91     26.75          C:\  Users\tommymaynard\Documents DC01
D                                 D:\                               DC01

This example of Invoke-Command connected to the remote computer DC01, ran a filtered Get-PSDrive command, returned the results to my computer, and then were formatted by the Format-Table cmdlet.

Now, let’s say we have… 23 computer names stored in the variable $Computers and we want to run the command we used above, against each computer. What so many people seem to do is wrap Invoke-Command in a Foreach construct (think: loop). I’m guessing, because I’ve made the error myself, that it’s because someone learned about Foreach first. Like I said, I discovered my error on my own, and only moments later. It didn’t wake me at 2 a.m. in a cold sweat after two months, which could’ve been possible.

Here’s how not to do this, and again, $Computers is holding the names of 23 computers.

Foreach ($Computer in $Computers) {
    Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
}
Name           Used (GB)     Free (GB) Provider      Root                                CurrentLocation PSComputerName
----           ---------     --------- --------      ----                                --------------- --------------
C                  20.47         59.18               C:\                                 ...rd\Documents DC02
D                                                    D:\                                                 DC02
A                                                    A:\                                                 WEB01
C                  50.36         29.30               C:\                                 ...rd\Documents WEB01
D                                                    D:\                                                 WEB01
A                                                    A:\                                                 SQL01
C                  53.59         26.06               C:\                                 ...rd\Documents SQL01
D                                                    D:\                                                 SQL01
... # Not all results included.

What we’ve done here is force Invoke-Command to run against only one computer at a time. During each iteration of the Foreach loop, Invoke-Command runs against the computer name currently stored in $Computer. This variable is updated to the next computer name in the variable at the start of each loop. It’s like this: connect to DC02 and run the command. Now, connect to WEB01 and run the command. Are you done, Invoke-Command? Okay then, now Foreach says to run the same command against the computer SQL01. That’s not how this cmdlet was designed to be used.

Before we go any further, I wrapped this command inside the Measure-Command cmdlet to determine how long it took my system to run this against the computers in $Computers. It took nearly 30 seconds. While that’s not an eternity, we’re only talking about 23 computers, and we’re only talking about a single command.

Measure-Command -Expression {
    Foreach ($Computer in $Computers) {
        Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
    }
}
Days : 0
Hours : 0
Minutes : 0
Seconds : 27
Milliseconds : 513
Ticks : 275132689
TotalDays : 0.000318440612268519
TotalHours : 0.00764257469444444
TotalMinutes : 0.458554481666667
TotalSeconds : 27.5132689
TotalMilliseconds : 27513.2689

Now, let’s set this command up the proper way. The ComputerName parameter of Invoke-Command can take a collection of computers. It should be said that many cmdlets can take more than one computer as a value to their ComputerName parameter. The difference is that Invoke-Command can run against all of the 23 computers at the same time. In fact, the default throttle limit—the number of computers in which the command will run against simultaneously—is 32. It can be changed by including the ThrottleLimit parameter, such as -ThrottleLimit 50 or -ThrottleLimit 15.

Now, here’s how this command should’ve been written.

Invoke-Command -ComputerName $Computers -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
Name           Used (GB)     Free (GB) Provider      Root                                CurrentLocation PSComputerName
----           ---------     -------u-- --------      ----                                --------------- --------------
A                                                    A:\                                                 DC02
A                                                    A:\                                                 WEB01
C                  41.72         37.93               C:\                                 ...rd\Documents WEB01
C                  41.13         38.52               C:\                                 ...rd\Documents DC02
D                                                    D:\                                                 DC02
D                                                    D:\                                                 WEB01
A                                                    A:\                                                 SQL01
C                  46.75         32.91               C:\                                 ...rd\Documents SQL01
D                                                    D:\                                                 SQL01
... # Not all results included.

I measured this command five times, and the average of those five runs was only 2.8 seconds. Remember, the Foreach loop took almost 30 seconds to get the same results. This is due to the fact that, again, Invoke-Command will work with the remote computers simultaneously (when it’s not inside a Foreach). There’s never any waiting to run the command on any additional computers unless there’s more computers than the value of ThrottleLimit.

Please keep the topic in this post in mind, as it’s possible to momentarily forget this feature just long enough to wrap the Invoke-Command cmdlet in a Foreach, or other looping construct.

 

Return File Sizes in Bytes, KBs, MBs, and GBs, at the Same Time

I scanned a recent forum post on PowerShell.org where the user seemed (again, I scanned it) to want to run some files through Get-ChildItem and Select-Object, and report the size in something other than the bytes default. It didn’t appear he, or she, wanted all the files in one of the measurement groups: B, KB, MB, or GB, but instead in their proper measurement group, dependent on the file’s size.

That may be hard to understand at first read, so let me try and explain it another way. If a file has less than 1,024 bytes, then it should be reported in bytes, if it has 1,024 – 1,048,575 bytes, then it should be reported in Kilobytes, if it has 1,048,576 – 1,073,741,824 bytes then it should be reported Megabytes, and if it has 1,073,741,825 or more bytes, then it should be reported in Gigabytes.

I wrote a long, “one-liner” to do this and have included it, and some sample output, below. I don’t profess to guarantee this doesn’t have any errors, so if you find some, then let me know. In addition, it shows a nice example of a switch statement inside the Expression portion of a calculated property. Fancy.

Get-ChildItem -Path 'C:\test' |
    Select-Object Name,
        @{L='Size';E={
                switch ($_.Length) {
                    # Bytes
                    {$_ -eq 0} {"$('{0:N2}' -f $_) bytes"; break}
                    {$_ -eq 1} {"$('{0:N2}' -f $_) byte"; break}
                    {($_ -gt 1) -and ($_ -le 1023)} {"$('{0:N2}' -f $_) bytes"; break}

                    # Kilobytes
                    {$_ -eq 1024} {"$('{0:N2}' -f ($_/1KB)) Kilobyte"; break}
                    {($_ -gt 1024) -and ($_ -le 1048575)} {"$('{0:N2}' -f ($_/1KB)) Kilobytes"; break}

                    # Megabytes
                    {$_ -eq 1048576} {"$('{0:N2}' -f ($_/1MB)) Megabyte"; break}
                    {($_ -gt 1048576) -and ($_ -le 1073741824)} {"$('{0:N2}' -f ($_/1MB)) Megabytes"; break}

                    # Gigabytes
                    {$_ -eq 1073741825} {"$('{0:N2}' -f ($_/1GB)) Gigabyte"; break}
                    {$_ -gt 1073741825} {"$('{0:N2}' -f ($_/1GB)) Gigabytes"; break}

                    default {Write-Warning -Message 'Unknown Error.'}
                }
            }
        } | Format-Table -AutoSize

Name           Size
----           ----
My-A-File.txt  1.00 byte
My-A-File2.txt 445.00 bytes
My-B-File.txt  1.00 Kilobyte
My-B-File2.txt 1.30 Kilobytes
My-C-File.txt  2.09 Megabytes
My-D-File.txt  3.59 Gigabytes

I can easily see how someone might want to remove the singular vs. plural: Kilobyte vs. Kilobytes, and just use B, KB, MB, and GB, and so…

Update: I gave this some more thought today, and I really didn’t feel like it was okay to not include a modified version that uses abbreviations (B, KB, MB, GB) instead of the full words, as in the previous example. It has removed some complexity, as well, as you can see below.

Get-ChildItem -Path 'C:\test' |
    Select-Object Name,
        @{L='Size';E={
                switch ($_.Length) {
                    # Bytes
                    {($_ -ge 0) -and ($_ -le 1023)} {"$('{0:N2}' -f $_) B"; break}

                    # Kilobytes
                    {($_ -ge 1024) -and ($_ -le 1048575)} {"$('{0:N2}' -f ($_/1KB)) KB"; break}

                    # Megabytes
                    {($_ -ge 1048576) -and ($_ -le 1073741824)} {"$('{0:N2}' -f ($_/1MB)) MB"; break}

                    # Gigabytes
                    {$_ -ge 1073741825} {"$('{0:N2}' -f ($_/1GB)) GB"; break}

                    default {Write-Warning -Message 'Unknown Error.'}
                }
            }
        } | Format-Table -AutoSize

Name           Size
----           ----
My-A-File.txt  1.00 B
My-A-File2.txt 445.00 B
My-B-File.txt  1.00 KB
My-B-File2.txt 1.30 KB
My-C-File.txt  2.09 MB
My-D-File.txt  3.59 GB

Give your Variable a Description

When we think of variables in Windows PowerShell, we often only consider two parts: the name of the variable, and the value of the variable. Well, it turns out that those aren’t the only properties; let’s discuss the description property.

I can’t say I’ve ever added a description to a variable until now, but I can think of times, when looking at someone’s code, where I wish I knew the reason why a variable existed. Best practice would indicate we use meaningful names for our variables, so it’s probably safe to assume that the same people that might use a poorly-named variable, probably don’t know or care that variables can have descriptions… but, I digress.

In this first example, we’ll create a new variable using what I’d call the standard method — the equal sign assignment operator.

PS C:\> $a = 12345
PS C:\> $a
12345
PS C:\> Get-Variable a | Select-Object *

Name        : a
Description :
Value       : 12345
Visibility  : Public
Module      :
ModuleName  :
Options     : None
Attributes  : {}

In line 1, we created a new variable, $a, and set its value to the numeric 12345. In the next line, we simply echoed the variable’s value. In line 4, we used the Get-Variable cmdlet and piped it to the Select-Object cmdlet and * so we were able to view all of its properties. If you take a look at the results, you’ll see the description property, and notice that it’s currently blank.

In the next example, we’ll use the Set-Variable cmdlet to modify the variable’s description.

PS C:\> Set-Variable a -Description 'This variable contains a 5-digit number.'
PS C:\> Get-Variable a | Select-Object *

Name        : a
Description : This variable contains a 5-digit number.
Value       : 12345
Visibility  : Public
Module      :
ModuleName  :
Options     : None
Attributes  : {}

PS C:\>

Once we use the Get-Variable cmdlet, and return all the properties again, we can see that our variable now contains a description value. Now, while many user-defined variables, such as we created here, don’t often contain descriptions, many of the automatic and preference variables do — take a look at a segment of the results returned by the next command.

PS C:\> Get-ChildItem variable: | Select-Object Name,Description | Format-Table -AutoSize

Name                       Description
----                       -----------
...
MaximumAliasCount          Maximum number of aliases allowed in a session
MaximumDriveCount          Maximum number of drives allowed in a session
MaximumErrorCount          Maximum number of errors to retain in a session
MaximumFunctionCount       Maximum number of functions allowed in a session
MaximumHistoryCount        Maximum number of history objects to retain in a session
MaximumVariableCount       Maximum number of variables allowed in a session
MyInvocation
NestedPromptLevel          Dictates what type of prompt should be displayed for the current nesting level
...

Well, there you go: something you might not have known about, and something you may never use.