Skip to content

Instantly share code, notes, and snippets.

@sba923
Last active October 13, 2025 19:03
Show Gist options
  • Save sba923/d0d5ad16f2b12d7785adf830b0395dc2 to your computer and use it in GitHub Desktop.
Save sba923/d0d5ad16f2b12d7785adf830b0395dc2 to your computer and use it in GitHub Desktop.
*nix work-alike's for PowerShell: df, dirname, whatis, whence
<#
.SYNOPSIS
Displays disk usage statistics for local and network drives, similar to the Unix 'df' command.
.DESCRIPTION
This script lists all logical drives on the system, showing their total size, free space, percent free, and percent used.
It supports filtering by drive letter and can output results as objects for further processing.
.PARAMETER PassThru
If specified, outputs objects instead of a formatted table, suitable for piping or further processing.
.PARAMETER DriveLetter
Specifies a single drive letter to display (e.g., 'C'). If omitted, all drives are shown.
.EXAMPLE
df.ps1
Displays disk usage for all drives in a formatted table.
.EXAMPLE
df.ps1 -DriveLetter D
Displays disk usage for drive D: only.
.EXAMPLE
df.ps1 -PassThru | Where-Object { $_.PercentFree -lt 10 }
Outputs objects for all drives and filters those with less than 10% free space.
.NOTES
Author: Stéphane BARIZIEN
Public domain script. Latest version: https://gist.github.com/sba923/d0d5ad16f2b12d7785adf830b0395dc2#file-df-ps1
#>
# cSpell: ignore BARIZIEN
[CmdletBinding()]
param([switch]$PassThru, [string]$DriveLetter)
# cSpell: ignore BARIZIEN's tostring logicaldisk Freespace pathnames HKLM
if ($DriveLetter)
{
$DriveLetter = $DriveLetter.TrimEnd(':').ToUpper()
if ($DriveLetter -notmatch '^[A-Z]$')
{
throw "Invalid drive letter '$DriveLetter'"
}
$drive_re = "^${DriveLetter}:"
}
else
{
$drive_re = '^[A-Z]:'
}
function Format-AsKMG
{
param ($bytes, $precision = '0')
foreach ($i in ("", "KB", "MB", "GB", "TB"))
{
if (($bytes -lt 1000) -or ($i -eq "TB"))
{
$bytes = ($bytes).tostring("F0" + "$precision")
if ($i -eq '')
{
return $bytes
}
else
{
return $bytes + " $i"
}
}
else
{
$bytes /= 1KB
}
}
}
$output = Get-CimInstance -ClassName win32_logicaldisk | Where-Object { $null -ne $_.Size -and $_.DeviceID -match $drive_re } | `
Select-Object DeviceID, VolumeName, Size, Freespace | `
ForEach-Object {
$psdrive = Get-PSDrive -name $_.DeviceID.Substring(0, 1) -ErrorAction SilentlyContinue | Where-Object { $_.Name -ceq $_.Name.ToUpper() }
try
{
# work around issue in PowerShell 7.4 where the mount point is not returned correctly for network-mounted drive letters
# see https://github.com/PowerShell/PowerShell/issues/19903
if ($psdrive.DisplayRoot -match 'System\.Span')
{
$uncpath = @((net use) -match ('\s+' + $psdrive.Name + ':\s+.*Microsoft Windows Network'))[0] -replace ('.*' + $psdrive.Name + ':\s+(\\\\.*\S)\s+Microsoft Windows Network'), '$1'
Add-Member -InputObject $_ -MemberType NoteProperty -Name MountedOn -Value $uncpath
}
elseif ($psdrive.DisplayRoot -ne ($psdrive.Name + ':\'))
{
# work around issue in PowerShell 7.4.1 where the DisplayRoot property is "padded with NULs"
# see https://github.com/PowerShell/PowerShell/issues/21064
$displayroot = $psdrive.DisplayRoot
if ($displayroot.Contains([char]0))
{
$displayroot = $displayroot.Substring(0, $displayroot.IndexOf([char]0))
}
Add-Member -InputObject $_ -MemberType NoteProperty -Name MountedOn -Value $displayroot
}
else
{
Add-Member -InputObject $_ -MemberType NoteProperty -Name MountedOn -Value $null
}
}
catch
{
Add-Member -InputObject $_ -MemberType NoteProperty -Name MountedOn -Value $null
}
$_
}
if ($PassThru)
{
$output | Select-Object `
DeviceID, `
VolumeName, `
MountedOn, `
@{Name = 'Size'; Expression = { Format-AsKMG -bytes $_.size -precision 1 } }, `
@{Name = 'Freespace'; Expression = { Format-AsKMG -bytes $_.Freespace -precision 1 } }, `
@{Name = 'PercentFree'; Expression = { "{0:n2}" -f ($_.freespace / $_.size * 100) } }, `
@{Name = 'PercentUsed'; Expression = { "{0:n2}" -f ((1 - ($_.freespace / $_.size)) * 100) } }
}
else
{
$output | `
Format-Table -AutoSize `
DeviceID, `
VolumeName, `
MountedOn, `
@{Name = 'Size'; Expression = { Format-AsKMG -bytes $_.size -precision 1 }; Align = 'right' }, `
@{Name = 'Freespace'; Expression = { Format-AsKMG -bytes $_.Freespace -precision 1 }; Align = 'right' }, `
@{Name = 'PercentFree'; Expression = { "{0:n2}" -f ($_.freespace / $_.size * 100) }; Align = 'right' }, `
@{Name = 'PercentUsed'; Expression = { "{0:n2}" -f ((1 - ($_.freespace / $_.size)) * 100) }; Align = 'right' }
}
# this is one of Stéphane BARIZIEN's public domain scripts
# the most recent version can be found at:
# https://gist.github.com/sba923/d0d5ad16f2b12d7785adf830b0395dc2#file-dirname-ps1
param([string] $literalpath)
try
{
$item = Get-Item -LiteralPath $literalpath -ErrorAction SilentlyContinue
$parent = Split-Path -Path $item.FullName -Parent
$parent
}
catch
{
$null
}
# this is one of Stéphane BARIZIEN's public domain scripts
# the most recent version can be found at:
# https://gist.github.com/sba923/d0d5ad16f2b12d7785adf830b0395dc2#file-whatis-ps1
param([string] $Name, [switch] $ReturnPath, [switch] $Quiet, [switch] $All)
Set-StrictMode -Version Latest
# cSpell: ignore BARIZIEN's
#region Get-ExecutableType
# derived from https://gallery.technet.microsoft.com/scriptcenter/Identify-16-bit-32-bit-and-522eae75
function Get-ExecutableType
{
<#
.Synopsis
Determines whether an executable file is 16-bit, 32-bit or 64-bit.
.DESCRIPTION
Attempts to read the MS-DOS and PE headers from an executable file to determine its type.
The command returns one of four strings (assuming no errors are encountered while reading the
file):
"Unknown", "16-bit", "x86", "x64", "ARM64", "ARM", "IA64" or if another PE machine field value, 0xNNNN
.PARAMETER Path
Path to the file which is to be checked.
.EXAMPLE
Get-ExecutableType -Path C:\Windows\System32\more.com
.INPUTS
None. This command does not accept pipeline input.
.OUTPUTS
String
.LINK
http://msdn.microsoft.com/en-us/magazine/cc301805.aspx
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateScript( { Test-Path -LiteralPath $_ -PathType Leaf })]
[string]
$Path
)
try
{
try
{
$stream = New-Object System.IO.FileStream(
$PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path),
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::Read
)
}
catch
{
throw "Error opening file $Path for Read: $($_.Exception.Message)"
}
$exeType = 'Unknown'
if ([System.IO.Path]::GetExtension($Path) -eq '.COM')
{
# 16-bit .COM files may not have an MS-DOS header. We'll assume that any .COM file with no header
# is a 16-bit executable, even though it may technically be a non-executable file that has been
# given a .COM extension for some reason.
$exeType = '16-bit'
}
$bytes = New-Object byte[](4)
if ($stream.Length -ge 64 -and
$stream.Read($bytes, 0, 2) -eq 2 -and
$bytes[0] -eq 0x4D -and $bytes[1] -eq 0x5A)
{
$exeType = '16-bit'
if ($stream.Seek(0x3C, [System.IO.SeekOrigin]::Begin) -eq 0x3C -and
$stream.Read($bytes, 0, 4) -eq 4)
{
if (-not [System.BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes, 0, 4) }
$peHeaderOffset = [System.BitConverter]::ToUInt32($bytes, 0)
if ($stream.Length -ge $peHeaderOffset + 6 -and
$stream.Seek($peHeaderOffset, [System.IO.SeekOrigin]::Begin) -eq $peHeaderOffset -and
$stream.Read($bytes, 0, 4) -eq 4 -and
$bytes[0] -eq 0x50 -and $bytes[1] -eq 0x45 -and $bytes[2] -eq 0 -and $bytes[3] -eq 0)
{
$exeType = 'Unknown'
if ($stream.Read($bytes, 0, 2) -eq 2)
{
if (-not [System.BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes, 0, 2) }
$machineType = [System.BitConverter]::ToUInt16($bytes, 0)
switch ($machineType) # see https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
{
0x014C { $exeType = 'x86' }
0x0200 { $exeType = 'IA64' }
0x8664 { $exeType = 'x64' }
0xAA64 { $exeType = 'ARM64' }
0x01C4 { $exeType = 'ARM' }
default { $exeType = ('0x{0:X4}' -f $machineType) }
}
}
}
}
}
return $exeType
}
catch
{
throw
}
finally
{
if ($null -ne $stream) { $stream.Dispose() }
}
}
#endregion
function Get-AppxLinkTarget
{
param([Parameter(Mandatory)] [string]$Path)
if (-not ("AppxLinkResolver" -as [type]))
{
$source = @"
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
public class AppxLinkResolver
{
public const uint IO_REPARSE_TAG_APPEXECLINK = 0x8000001B;
public const int FSCTL_GET_REPARSE_POINT = 0x000900A8;
public const int OPEN_EXISTING = 3;
public const int FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
public const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr CreateFile(
string lpFileName,
int dwDesiredAccess,
int dwShareMode,
IntPtr lpSecurityAttributes,
int dwCreationDisposition,
int dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeviceIoControl(
IntPtr hDevice,
int dwIoControlCode,
IntPtr lpInBuffer,
int nInBufferSize,
IntPtr lpOutBuffer,
int nOutBufferSize,
out int lpBytesReturned,
IntPtr lpOverlapped);
public static string GetAppxLinkTarget(string path)
{
IntPtr hFile = CreateFile(path, 0, 3, IntPtr.Zero, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
if (hFile.ToInt64() == -1)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
int outBufferSize = 16 * 1024;
IntPtr outBuffer = Marshal.AllocHGlobal(outBufferSize);
try
{
int bytesReturned;
bool result = DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, IntPtr.Zero, 0,
outBuffer, outBufferSize, out bytesReturned, IntPtr.Zero);
if (!result)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
uint tag = (uint)Marshal.ReadInt32(outBuffer);
if (tag != IO_REPARSE_TAG_APPEXECLINK)
throw new Exception("Not an AppExecLink reparse point.");
// The reparse data starts at offset 8 (skip tag and length fields)
int dataOffset = 8;
int dataLength = Marshal.ReadInt16(IntPtr.Add(outBuffer, 4));
byte[] data = new byte[dataLength];
Marshal.Copy(IntPtr.Add(outBuffer, dataOffset), data, 0, dataLength);
// The target path is a UTF-16 string after three null-terminated UTF-16 strings
// See: https://github.com/libyal/libfwnt/blob/main/documentation/AppExecLink%20reparse%20point.asciidoc
int pos = 0;
int stringCount = 0;
while (stringCount < 3 && pos < data.Length - 1)
{
if (data[pos] == 0 && data[pos + 1] == 0)
stringCount++;
pos += 2;
}
// Now pos is at the start of the target path string
int start = pos;
// Find the end of the target path string (null-terminated)
while (pos < data.Length - 1)
{
if (data[pos] == 0 && data[pos + 1] == 0)
break;
pos += 2;
}
int len = pos - start;
if (len <= 0) return null;
string target = Encoding.Unicode.GetString(data, start, len);
return target;
}
finally
{
CloseHandle(hFile);
Marshal.FreeHGlobal(outBuffer);
}
}
}
"@
Add-Type -TypeDefinition $source -Language CSharp -ErrorAction Stop
}
[AppxLinkResolver]::GetAppxLinkTarget($Path)
}
$gcm_all = $null
try
{
$gcm_all = @(Get-Command -Name $Name -ErrorAction SilentlyContinue -All)
}
catch
{
$gcm_all = $null
}
if ($null -eq $gcm_all -or $gcm_all.Count -eq 0)
{
if (!$Quiet)
{
Write-Error("'{0}' is not a known command" -f $Name)
}
else
{
$null
}
Exit(1)
}
if ((!$All) -and ($gcm_all.Count -gt 1))
{
if (!$Quiet)
{
Write-Warning("There is more than one resolution for '{0}', using the default one as reported by Get-Command" -f $Name)
}
$gcm_all = @(Get-Command -Name $Name -ErrorAction SilentlyContinue)[0]
}
elseif (!$All)
{
$gcm_all = @($gcm_all[0])
}
foreach ($gcm in $gcm_all)
{
$cmdtype = $gcm.CommandType
if (($cmdtype -eq 'Application') -and ($gcm.Path -match '\.cmd$'))
{
$cmdtype = 'CMD script'
}
$cmdtype_s = $cmdtype.ToString().ToLower()
if ($cmdtype_s[0] -in @('a', 'e', 'i', 'o', 'u'))
{
$article = 'an'
}
else
{
$article = 'a'
}
if ($cmdtype -eq 'CMD script')
{
$cmdtype_s = $cmdtype
}
if (!$ReturnPath)
{
Write-Host("'{0}' is {1} {2}" -f $Name, $article, $cmdtype_s)
}
switch ($cmdtype)
{
'Application'
{
$itempath = $gcm.Path
$item = Get-Item -LiteralPath $itempath
if ($item.Length -ne 0)
{
if (!$ReturnPath)
{
Write-Host("Executable path is '{0}'" -f $gcm.Path)
Write-Host("Description: '{0}'" -f $item.VersionInfo.FileDescription)
Write-Host("Product version: '{0}'" -f $item.VersionInfo.ProductVersion)
Write-Host("Executable type: {0}" -f (Get-ExecutableType -Path $itempath))
}
else
{
$gcm.Path
}
}
else
{
if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)
{
if (!$ReturnPath)
{
Write-Host ("'{0}' is an AppX reparse point" -f $gcm.Path)
}
elseif (!$Quiet)
{
Write-Warning ("'{0}' is an AppX reparse point" -f $gcm.Path)
}
$targetpath = $null
$targetitem = $null
# Handle reparse points with Target property (traditional symlinks)
if (($null -ne $item.Target) -and ($item.Target.Count -ne 0))
{
$target = Get-Item -LiteralPath $item.Target -ErrorAction SilentlyContinue
if ($null -ne $target)
{
if ($item.Target.GetType().Name -match '\[\]') # weirdly, this is an array
{
if ($item.Target.Count -eq 1)
{
$targetpath = $item.Target[0]
}
else
{
throw ("Cannot determine target path for {0}: Target property is an array with more than one element" -f $item.FullName)
}
}
else
{
$targetpath = $item.Target
}
$targetitem = $target
}
else
{
Write-Error ("Cannot access reparse point target '{0}'" -f $item.Target)
}
}
# Handle AppX reparse points without Target property
else
{
$target = Get-AppxLinkTarget -Path $item.FullName
if ($null -ne $target)
{
$targetpath = $target
$targetitem = Get-Item -LiteralPath $target -ErrorAction SilentlyContinue
}
else
{
Write-Error ("Cannot access reparse point target '{0}'" -f $item.FullName)
}
}
# Output results based on mode
if ($null -ne $targetpath)
{
if (!$ReturnPath)
{
Write-Host("Executable path is '{0}'" -f $targetpath)
if ($null -ne $targetitem)
{
Write-Host("Description: '{0}'" -f $targetitem.VersionInfo.FileDescription)
Write-Host("Product version: '{0}'" -f $targetitem.VersionInfo.ProductVersion)
Write-Host("Executable type: {0}" -f (Get-ExecutableType -Path $targetpath))
}
}
else
{
if ($null -ne $targetitem)
{
$targetitem.FullName
}
else
{
$targetpath
}
}
}
}
else
{
Write-Host -ForegroundColor Magenta ("'{0}' is a 0-byte file!!!???" -f $gcm.Path)
}
}
}
'Alias'
{
if (!$ReturnPath)
{
Write-Host("Resolves to: '{0}'" -f $gcm.Definition)
}
}
'Cmdlet'
{
if (($gcm.Module -ne '') -and ($gcm.Module -notmatch '^Microsoft\.PowerShell'))
{
if (!$ReturnPath)
{
Write-Host("Implemented in module '{0}'" -f $gcm.Module)
}
}
}
'ExternalScript'
{
if (!$ReturnPath)
{
Write-Host("Path is '{0}'" -f $gcm.Path)
}
else
{
$gcm.Path
}
}
'Function'
{
if ($ReturnPath)
{
"function:{0}" -f $Name
}
}
'CMD script'
{
if (!$Name)
{
Write-Host("Script path is '{0}'" -f $gcm.Path)
}
else
{
$gcm.Path
}
}
}
}
# this is one of Stéphane BARIZIEN's public domain scripts
# the most recent version can be found at:
# https://gist.github.com/sba923/d0d5ad16f2b12d7785adf830b0395dc2#file-whence-ps1
# cSpell: ignore BARIZIEN's whatis
param([string] $Name, [switch] $Quiet, [switch] $All)
. whatis.ps1 -Name $Name -ReturnPath -Quiet:$Quiet -All:$All
@sba923
Copy link
Author

sba923 commented Mar 10, 2022

FWIW: The discussion at PowerShell/PowerShell#15079 didn't end up anywhere 😢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment