Yesterday I shared some PowerShell code I use to managing my PSDrive assignments. My code works for me in my environment. But that doesn't mean it is necessarily right for you and your environment. There are plenty of ways to use PowerShell to achieve the same results as my code. This is something you should always keep in mind when looking at someone else's code. But enough caveats. In the previous post I left you with a tease that there is more to be done with PSDrives. Let's look at that today.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Format Extensions
By now, you should know that I am a big fan of custom formatting. It saves time and allows me to see information in a format that meets a particular need. Formatting extensions require an XML file, but if you've been reading me for a while, you know about my New-PSFormatXML function that is part of the PSScriptTools module. I used this command to add a few custom views for PowerShell PSDrives.
When you run Get-PSDrive, PowerShell formats the results using a default view named Drive. I added a couple of new table views. One of them displays PSDrives grouped by provider.
In order to group properly, you need to remember to sort on the Provider property. Or get a bit more granular with Get-PSDrive.
The Info view is similar but doesn't include the provider.
Get-FileSystemDrive
As I was working on these extensions, I realized the FileSystem PSDrives are of more value to me. One thing that has always annoyed me is that I get size and usage information, even for PSDrives that point to folders.
I wanted something better. This was going to mean more than a format extension. I wrote my own command.
#requires -version 5.1
Function Get-FileSystemDrive {
[cmdletbinding()]
[OutputType("PSFileSystemDrive")]
[alias("gfsd")]
Param(
[Parameter(HelpMessage = " Specifies, as a string array, the name or name of FileSystem PSDrives. Type the drive name or letter without a colon (:)")]
[string[]]$Name
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Getting FileSystem PSDrives"
Try {
$drives = Get-PSDrive @PSBoundParameters -PSProvider FileSystem -ErrorAction Stop
}
Catch {
Throw $_
}
#create a custom object from the drive information
if ($drives) {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Found $($drives.count) FileSystem PSDrives"
foreach ($drive in $drives) {
#only get size information for root drives
#need to account for non-Windows systems
if ($drive.root -match "^(([A-Za-]:)\\|\/)$") {
$info = [system.io.driveinfo]::new($drive.root)
if ($info.DriveType -eq 'Fixed') {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Getting root drive detail for $($info.name)"
$Free = $info.AvailableFreeSpace
$Used = $info.TotalSize - $info.AvailableFreeSpace
$Format = $info.DriveFormat
$Size = $info.totalSize
$IsPhysical = $True
}
else {
$Free = $null
$Used = $null
$Format = $Null
$Size = $null
$IsPhysical = $False
}
}
else {
$Free = $null
$Used = $null
$Format = $Null
$Size = $Nuyll
$IsPhysical = $False
}
[PSCustomObject]@{
PSTypeName = "PSFileSystemDrive"
Name = $drive.Name
Root = $drive.Root
CurrentLocation = $drive.CurrentLocation
Description = $drive.Description
Free = $free
Used = $used
Size = $Size
Format = $Format
IsPhysical = $IsPhysical
}
}
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
} #close Get-FileSystemDrive
The function is a wrapper for Get-PSDrive that only returns FileSystem drives. For each drive, I create my own custom object using a typename of PSFileSystemDrive. I need a unique typename for custom format and type extensions that are coming. The custom object makes it easier to differentiate between root physical drives like C: and D: from a mapping like T which goes to C:\Training.
In the same file as the function, I also define a custom type extension to define a script property.
#add a script property to the custom object
$value = {
if ($this.IsPhysical) {
($this.used/$this.size)*100
}
else {
$Null
}
}
Update-TypeData -TypeName 'PSFileSystemDrive' -MemberType ScriptProperty -MemberName Utilization -Value $value -force
If the PSDrive is a physical drive, I'm calculating the utilization percentage. I could have made this a static property when I defined the object. And yes, I could have defined my custom object as a PowerShell class. Which I still might do because I am getting other ideas as I write this. But for now, this is what I have.
I also used New-PSFormatXML to define a default formatted view.
I find this output to be more concise and meaningful. Notice that I only get size and usage information for physical drives. My format ps1xml file also uses scriptblocks and ANSI sequences to display usage percentages in color. If the percentage is 90 or above it will be shown in red. Although, if you are using Windows Terminal, the colors may be slightly different depending on your color scheme.
I also created a second view called Status, which works best with physical drives.
This view doesn't have the ANSI highlighting for utilization, although you could certainly add it.
All of the type extensions are in the same file.
<!--
Format type data generated 09/22/2021 09:13:30 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 09/22/2021 09:13:30 by PROSPERO\Jeff-->
<Name>provider</Name>
<ViewSelectedBy>
<TypeName>System.Management.Automation.PSDriveInfo</TypeName>
</ViewSelectedBy>
<GroupBy>
<ScriptBlock>$_.provider.Name</ScriptBlock>
<Label>Provider</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.
<AutoSize />-->
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>12</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Root</Label>
<Width>45</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>CurrentLocation</Label>
<Width>20</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Description</Label>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<Wrap />
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Root</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>CurrentLocation</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Description</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<!--Created 09/22/2021 11:15:30 by PROSPERO\Jeff-->
<Name>info</Name>
<ViewSelectedBy>
<TypeName>System.Management.Automation.PSDriveInfo</TypeName>
</ViewSelectedBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.-->
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>7</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Root</Label>
<Width>7</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>CurrentLocation</Label>
<Width>18</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Description</Label>
<Width>14</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<Wrap />
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Root</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>CurrentLocation</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Description</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<!--Created 09/22/2021 10:02:46 by PROSPERO\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>PSFileSystemDrive</TypeName>
</ViewSelectedBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.-->
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>7</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Root</Label>
<Width>7</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Current</Label>
<Width>18</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>SizeGB</Label>
<Width>15</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PctUsed</Label>
<Width>21</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Format</Label>
<Width>9</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Root</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>CurrentLocation</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if ($_.size) {
$_.Size/1GB -AS [int]
}
else {
$null
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if ($_.Utilization) {
<!-- display using ANSI-->
if ($host.name -match "console|code|ServerRemoteHost") {
$value = [math]::Round($_.Utilization,2)
Switch ($value) {
{$_ -ge 90} {
<!--red-->
"$([char]27)[90m$value$([char]27)[0m"
}
{$_ -ge 75} {
<!--yellow-->
"$([char]27)[38;5;227m$value$([char]27)[0m"
}
{$_ -ge 50} {
<!--purple-->
"$([char]27)[95m$value$([char]27)[0m"
}
default {
<!--green-->
"$([char]27)[92m$value$([char]27)[0m"
}
}
}
else {
[math]::Round($_.Utilization,2)
}
}
else {
$null
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Format</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<!--Created 09/22/2021 12:39:34 by PROSPERO\Jeff-->
<Name>status</Name>
<ViewSelectedBy>
<TypeName>PSFileSystemDrive</TypeName>
</ViewSelectedBy>
<GroupBy>
<ScriptBlock>
<!--display name using ANSI if supported-->
if ($host.name -match "console|code|ServerRemoteHost") {
$n = "$([char]27)[92m$($_.Name)$([char]27)[0m"
}
else {
$n = $_.name
}
"{0}`n Root: {1}" -f $n,$_.Root
</ScriptBlock>
<Label>Name</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.-->
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>CurrentLocation</Label>
<Width>18</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>SizeGB</Label>
<Width>7</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>FreeGB</Label>
<Width>7</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Utilization</Label>
<Width>20</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Description</Label>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<Wrap />
<TableColumnItems>
<TableColumnItem>
<PropertyName>CurrentLocation</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if ($_.IsPhysical) {
$_.Size/1GB -as [int]
}
else {
$null
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if ($_.IsPhysical) {
$_.Free/1GB -as [int]
}
else {
$null
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if ($_.IsPhysical) {
[math]::Round($_.Utilization,2)
}
else {
$null
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Description</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
In the file that defines the function, I add this snippet:
Update-FormatData $psscriptroot\PSDriveInfo.format.ps1xml
All of the files are in the same directory which is why I am using $PSScriptRoot.
Now I have commands and options that make my work easier. Sure, I can still use a command like Get-Volume. Or I can use my own command (which has a defined alias).
I hope you'll grab the code and play with it. That is a great way to improve your PowerShell scripting. Have fun!