A DriveInfo Solution
In this issue:
It is that time again. Time for me to share a solution to last month's scripting challenge. I wanted you to write a PowerShell command, functions are always preferable, to use the [System.IO.DriveInfo] .NET class to get drive information from local and remote computers. The command should return a custom object like this:
Name : C:\
DriveType : Fixed
DriveFormat : NTFS
AvailableFreeSpace : 270710976512
TotalSize : 509722226688
PctFreeSpace : 53.11
VolumeLabel : Windows
Computername : Cadenza
I can't stress enough that the value to this exercise is not the end result. We have plenty of existing commands to retrieve this kind of information. The value is on the development process. How did you approach the problem? What design decisions did you make? How did you enhance the output? Writing better PowerShell is as much about thinking as it is about coding.
Basic Operation
I gave you the .NET class to use as a starting point. However, you probably needed to discover how you could use it. I'd recommend using the Get-TypeMember command from my PSScriptTools module. You can use it to explore the static methods and properties of the class.

You would have found a static method called GetDrives().
PS C:\> Get-TypeMember System.Io.DriveInfo -MemberName GetDrives | Select Syntax
Syntax
------
$obj.GetDrives()
The syntax example is a little misleading. Because it is a static method, you need to call it from the class itself, not from an instance of the class. You can do that like this:
PS C:\> [System.Io.DriveInfo]::GetDrives()
Name : C:\
IsReady : True
RootDirectory : C:\
DriveType : Fixed
DriveFormat : NTFS
AvailableFreeSpace : 255864844288
TotalFreeSpace : 255864844288
TotalSize : 509722226688
VolumeLabel : Windows
Name : G:\
IsReady : True
RootDirectory : G:\
DriveType : Fixed
DriveFormat : FAT32
AvailableFreeSpace : 9332088832
TotalFreeSpace : 9332088832
TotalSize : 16106127360
VolumeLabel : Google Drive
The raw output will the source of my custom object output. I can prototype this in the console.
$r = [System.IO.DriveInfo]::GetDrives() | ForEach-Object {
[PSCustomObject]@{
PSTypename = 'IoDriveInfo'
Name = $_.Name
DriveType = $_.DriveType
DriveFormat = $_.DriveFormat
AvailableFreeSpace = $_.AvailableFreeSpace
TotalSize = $_.TotalSize
PctFree = [math]::Round(($_.AvailableFreeSpace / $_.TotalSize)*100,2)
VolumeLabel = $_.VolumeLabel
Computername = [Environment]::MachineName
}
}
I'm using a .NET class for the computer name in anticipation of running this on non-Windows systems where $env:COMPUTERNAME isn't defined.
PS C:\> $r[0]
Name : C:\
DriveType : Fixed
DriveFormat : NTFS
AvailableFreeSpace : 265662332928
TotalSize : 509722226688
PctFree : 52.12
VolumeLabel : Windows
Computername : CADENZA
That's a good start that meets the basic requirements.
Remoting
One of the challenges I included was to get drive information from remote computers. Invoking the .GetDrives() method must be done locally, which means you can use PowerShell remoting through Invoke-Command.
I'll continue to prototype in the console.
$r = Invoke-Command -scriptblock {
[System.IO.DriveInfo]::GetDrives() | ForEach-Object {
[PSCustomObject]@{
PSTypename = 'IoDriveInfo'
Name = $_.Name
DriveType = $_.DriveType
DriveFormat = $_.DriveFormat
AvailableFreeSpace = $_.AvailableFreeSpace
TotalSize = $_.TotalSize
PctFree = [math]::Round(($_.AvailableFreeSpace / $_.TotalSize)*100,2)
VolumeLabel = $_.VolumeLabel
Computername = [Environment]::MachineName
}
}
} -computer cadenza -HideComputerName
This mostly works.
PS C:\> $r[0]
Name : C:\
DriveType : Fixed
DriveFormat : NTFS
AvailableFreeSpace : 265661698048
TotalSize : 509722226688
PctFree : 52.12
VolumeLabel : Windows
Computername : CADENZA
RunspaceId : ff4dbbfe-d21d-4666-b7c2-a72df0a72c6a
But there is an extra property, RunspaceId, that I don't want. The other issue is that when using remoting, the object is returned as a deserialized object.
PS C:\> $r[0].PSObject.TypeNames
Deserialized.IoDriveInfo
Deserialized.System.Management.Automation.PSCustomObject
Deserialized.System.Object
Knowing that I eventually want to provide custom formatting, this may be an issue. The better approach is to use remoting to get the raw data:
$r = Invoke-Command -scriptblock {
#get the computername remotely
$CN = [Environment]::MachineName
#add it as a member to the output
[System.IO.DriveInfo]::GetDrives() |
Add-Member -MemberType NoteProperty -Name Computername -Value $CN -PassThru
#don't forget to include -Passthru
} -computer $env:COMPUTERNAME -HideComputerName
Because I want the computername, I'll get it remotely and add it as a property to each drive object using Add-Member.
Now when I return the data, I can create the custom object locally.
$out = foreach ($item in $r ) {
[PSCustomObject]@{
PSTypename = 'IoDriveInfo'
Name = $item.Name
DriveType = $item.DriveType
DriveFormat = $item.DriveFormat
AvailableFreeSpace = $item.AvailableFreeSpace
TotalSize = $item.TotalSize
PctFree = [math]::Round(($item.AvailableFreeSpace / $item.TotalSize)*100,2)
VolumeLabel = $item.VolumeLabel
Computername = $item.Computername
}
}
This provides the desired output.
PS C:\> $out
Name : C:\
DriveType : Fixed
DriveFormat : NTFS
AvailableFreeSpace : 265658888192
TotalSize : 509722226688
PctFree : 52.12
VolumeLabel : Windows
Computername : CADENZA
Name : G:\
DriveType : Fixed
DriveFormat : FAT32
AvailableFreeSpace : 9332088832
TotalSize : 16106127360
PctFree : 57.94
VolumeLabel : Google Drive
Computername : CADENZA
PS C:\> $out[0].PSObject.TypeNames
IoDriveInfo
System.Management.Automation.PSCustomObject
System.Object
Supporting SSH
Because I am writing a function for PowerShell 7, I want to support cross-platform usage since the .NET class is available on all platforms. That means I need to support SSH remoting. As I tested the .NET method on several Linux platforms, I discovered those platforms often have drives with a total size of zero. I decided to filter those out.
The major change I need to make is to use -HostName instead of -ComputerName.
$r = Invoke-Command -scriptblock {
#get the computername remotely
$CN = [Environment]::MachineName
#add it as a member to the output
#filter out drives with a total size of 0
[System.IO.DriveInfo]::GetDrives() |
Where-Object TotalSize -gt 0 |
Add-Member -MemberType NoteProperty -Name Computername -Value $CN -PassThru
#don't forget to include -Passthru
} -HostName Fred -UserName jeff -HideComputerName
$fred = foreach ($item in $r ) {
[PSCustomObject]@{
PSTypename = 'IoDriveInfo'
Name = $item.Name
DriveType = $item.DriveType
DriveFormat = $item.DriveFormat
AvailableFreeSpace = $item.AvailableFreeSpace
TotalSize = $item.TotalSize
PctFree = [math]::Round(($item.AvailableFreeSpace / $item.TotalSize)*100,2)
VolumeLabel = $item.VolumeLabel
Computername = $item.Computername
}
}
This works as expected.
PS C:\> $fred[0..1]
Name : /
DriveType : Fixed
DriveFormat : btrfs
AvailableFreeSpace : 45773504512
TotalSize : 51982106624
PctFree : 88.06
VolumeLabel : /
Computername : fred
Name : /dev
DriveType : Ram
DriveFormat : udev
AvailableFreeSpace : 4092993536
TotalSize : 4092993536
PctFree : 100
VolumeLabel : /dev
Computername : fred
Using ParameterSets
Everything I've done thus far has been in the console. I know my function will be a wrapper for Invoke-Command. The function will need to pass parameters to Invoke-Command. Because Invoke-Command uses different parameters for Windows remoting and SSH remoting, I will need to use parameter sets in my function.
You can use the Get-ParameterInfo function from the PSScriptTools module.

I don't need to support all parameters, so I'll start with this:
function Get-IoDriveInfo {
[cmdletbinding(DefaultParameterSetName = 'Computername')]
[OutputType('IoDiskInfo')]
[alias('gidi')]
param(
[Parameter(
Position = 0,
ValueFromPipeline,
ValueFromPipelineByPropertyName,
HelpMessage = 'Specify the name of a computer to query.',
ParameterSetName = 'Computername'
)]
[alias('CN')]
[ValidateNotNullOrEmpty()]
[string]$ComputerName = [System.Environment]::MachineName,
[Parameter(
ParameterSetName = 'Computername',
HelpMessage = 'Specifies a user account that has permission to perform this action. The default is the current user. This will be ignored for the localhost.'
)]
[PSCredential]$Credential,
[Parameter(
ParameterSetName = 'ssh',
Mandatory,
ValueFromPipelineByPropertyName,
HelpMessage = 'Specify the host name of a computer to query using SSH.'
)]
[ValidateNotNullOrEmpty()]
[string]$Hostname,
[Parameter(
ParameterSetName = 'ssh',
ValueFromPipelineByPropertyName,
HelpMessage = 'Specifies the username for the account used to run a command on the remote computer. The user authentication method depends on how Secure Shell (SSH) is configured on the remote computer.'
)]
[ValidateNotNullOrEmpty()]
[string]$Username,
[Parameter(
ParameterSetName = 'ssh',
HelpMessage = 'Specifies a key file path used by Secure Shell (SSH) to authenticate a user on a remote computer.'
)]
[ValidateScript({ Test-Path $_ }, ErrorMessage = 'Cannot find the specified key file {0}')]
[Alias('IdentityFilePath')]
[string]$KeyFilePath
)
...
I am making ComputerName the default parameter set. The Credential parameter is only valid for Windows remoting, so it is in the Computername parameter set. The Hostname, Username, and KeyFilePath parameters are only valid for SSH remoting, so they are in the ssh parameter set.
I can verify this with Get-ParameterInfo.
