Last active
October 13, 2025 19:03
-
-
Save sba923/d0d5ad16f2b12d7785adf830b0395dc2 to your computer and use it in GitHub Desktop.
*nix work-alike's for PowerShell: df, dirname, whatis, whence
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi everyone,
Coming from a long Korn Shell history (with a lot of (using, beta-testing, advocacy for) MKS in it @BrucePay 😋), I used to miss some key tools everyday, so I wrote crude versions (as scripts, not yet modules or anything fancy).
For now I have:
dirname: enables oneliners such asii (dirname (dirname (gmo oh-my-posh).Path))basenamewhence: for instance, makes it a snap to edit a script you've just run and had an issue with:code (whence Set-WindowsTerminalProgressState)(where
coderesolves to my owncode.ps1, but that's another discussion altogether)whatis: outputs information about a command in a human-readable format, adding value on top ofGet-Commandin the process, such as version information for AppX reparse points:you get the idea
df: outputs human-readable information about local and network-mounted drives: