Utils/df.ps1

Register-GnuUtil @{
    Name         = 'df'
    Category     = 'File info and disk usage'
    Synopsis     = 'Report file system disk space usage.'
    Usage        = @('df [OPTION]... [FILE]...')
    Description  = @(
        'Show information about the file system on which each FILE resides,',
        'or all file systems by default.'
    )
    FileOperands = $true
    Options      = @(
        @{ Short = 'a'; Long = 'all'; Help = 'include pseudo, duplicate, inaccessible file systems' }
        @{ Short = 'B'; Long = 'block-size'; Key = 'scale'; Arg = 'SIZE'; Help = 'scale sizes by SIZE before printing them; e.g., ''-BM'' prints sizes in units of 1,048,576 bytes; see SIZE format below' }
        @{ Short = 'h'; Long = 'human-readable'; Key = 'scale'; Value = 'human:1024'; Help = 'print sizes in powers of 1024 (e.g., 1023M)' }
        @{ Short = 'H'; Long = 'si'; Key = 'scale'; Value = 'human:1000'; Help = 'print sizes in powers of 1000 (e.g., 1.1G)' }
        @{ Short = 'i'; Long = 'inodes'; Help = 'list inode information instead of block usage' }
        @{ Short = 'k'; Key = 'scale'; Value = '1K'; Help = 'like --block-size=1K' }
        @{ Short = 'l'; Long = 'local'; Help = 'limit listing to local file systems' }
        @{ Short = 'm'; Key = 'scale'; Value = '1M'; Help = 'like --block-size=1M (undocumented in GNU, accepted for compatibility)' }
        @{ Long = 'no-sync'; Key = 'sync'; Value = 'no'; Help = 'do not invoke sync before getting usage info (default)' }
        @{ Long = 'output'; Arg = 'FIELD_LIST'; OptionalArg = $true; Help = 'use the output format defined by FIELD_LIST, or print all fields if FIELD_LIST is omitted.' }
        @{ Short = 'P'; Long = 'portability'; Help = 'use the POSIX output format' }
        @{ Long = 'sync'; Key = 'sync'; Value = 'yes'; Help = 'invoke sync before getting usage info' }
        @{ Long = 'total'; Help = 'elide all entries insignificant to available space, and produce a grand total' }
        @{ Short = 't'; Long = 'type'; Arg = 'TYPE'; Multi = $true; Help = 'limit listing to file systems of type TYPE' }
        @{ Short = 'T'; Long = 'print-type'; Help = 'print file system type' }
        @{ Short = 'x'; Long = 'exclude-type'; Arg = 'TYPE'; Multi = $true; Help = 'limit listing to file systems not of type TYPE' }
    )
    Notes        = @(
        'Display values are in units of the first available SIZE from --block-size,',
        'and the DF_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables.',
        'Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set).',
        '',
        'FIELD_LIST is a comma-separated list of columns to be included. Valid',
        'field names are: ''source'', ''fstype'', ''itotal'', ''iused'', ''iavail'', ''ipcent'',',
        '''size'', ''used'', ''avail'', ''pcent'', ''file'' and ''target'' (see info page).',
        '',
        'PowerShell note: file systems come from .NET DriveInfo (Windows drive letters are the',
        'source and the mount point); inode counts are not available (-); the environment',
        'variables are ignored; --sync has no effect; SIZE does not take the '' grouping prefix.'
    )
}

# name = @(header, minimum width, right aligned)
$script:GnuDfFields = [ordered]@{
    source = @('Filesystem', 14, $false); fstype = @('Type', 4, $false); itotal = @('Inodes', 5, $true); iused = @('IUsed', 5, $true)
    iavail = @('IFree', 5, $true); ipcent = @('IUse%', 4, $true); size = @('Size', 5, $true); used = @('Used', 5, $true)
    avail = @('Avail', 5, $true); pcent = @('Use%', 4, $true); file = @('File', 0, $false); target = @('Mounted on', 0, $false)
}



function script:Get-GnuDfFileSystem {
    <#
    .SYNOPSIS
    The file systems: @{ Source; Type; Total; Free; Available; Target; Local } (bytes), mockable in tests.
    #>

    param([string]$MountsPath = '/proc/mounts')
    $sources = @{}
    if (-not (Test-GnuWindows) -and [System.IO.File]::Exists($MountsPath)) {
        foreach ($line in [System.IO.File]::ReadAllLines($MountsPath)) {
            $parts = $line.Split(' ')
            if ($parts.Count -ge 2 -and -not $sources.ContainsKey($parts[1])) { $sources[$parts[1]] = $parts[0] }
        }
    }
    foreach ($drive in [System.IO.DriveInfo]::GetDrives()) {
        $entry = @{ Source = $drive.Name.TrimEnd('\'); Type = '-'; Total = [double]0; Free = [double]0; Available = [double]0; Target = $drive.Name; Local = $drive.DriveType -ne [System.IO.DriveType]::Network; Ready = $drive.IsReady }
        if ($sources.ContainsKey($drive.Name)) { $entry.Source = $sources[$drive.Name] }
        if ($drive.IsReady) {
            $entry.Type = ([string]$drive.DriveFormat).ToLowerInvariant()
            $entry.Total = [double]$drive.TotalSize
            $entry.Free = [double]$drive.TotalFreeSpace
            $entry.Available = [double]$drive.AvailableFreeSpace
        }
        $entry
    }
}

function script:Invoke-GnuDfReport {
    <#
    .SYNOPSIS
    Implementation of df.
    #>

    param([Parameter(Mandatory)][hashtable]$Context)

    $scale = Get-GnuSizeScale -Context $Context
    if ($null -eq $scale) { return }
    $portable = Test-GnuOption -Context $Context -Key 'portability'
    $inodes = Test-GnuOption -Context $Context -Key 'inodes'

    $fields = @('source', 'size', 'used', 'avail', 'pcent', 'target')
    if ($inodes) { $fields = @('source', 'itotal', 'iused', 'iavail', 'ipcent', 'target') }
    if (Test-GnuOption -Context $Context -Key 'print-type') { $fields = @($fields[0], 'fstype') + $fields[1..($fields.Count - 1)] }
    $custom = Test-GnuOption -Context $Context -Key 'output'
    if ($custom) {
        foreach ($conflict in @(@('portability', '-P'), @('inodes', '-i'), @('print-type', '-T'))) {
            if (Test-GnuOption -Context $Context -Key $conflict[0]) { Write-GnuUsageError -Context $Context -Message "options $($conflict[1]) and --output are mutually exclusive"; return }
        }
        $list =[string](Get-GnuOptionValue -Context $Context -Key 'output')
        if ([string]::IsNullOrEmpty($list)) { $fields = @($script:GnuDfFields.Keys) }
        else {
            $fields = @($list.Split(','))
            foreach ($field in $fields) {
                if (-not $script:GnuDfFields.Contains($field)) { Write-GnuUsageError -Context $Context -Message "option --output: field '$field' unknown"; return }
            }
        }
    }

    $include = @(Get-GnuOptionValue -Context $Context -Key 'type' -Default @())
    $exclude = @(Get-GnuOptionValue -Context $Context -Key 'exclude-type' -Default @())
    $contradictions = @($include | Where-Object { $exclude -ccontains $_ })
    foreach ($type in $contradictions) { Write-GnuError -Context $Context -Message "file system type '$type' both selected and excluded" }
    if ($contradictions.Count -gt 0) { return }

    $all = @(Get-GnuDfFileSystem)
    $rows = New-Object 'System.Collections.Generic.List[hashtable]'
    $operands = @($Context.Operands)
    if ($operands.Count -gt 0) {
        foreach ($operand in $operands) {
            $path = Resolve-GnuPath -Path $operand
            if (-not (Test-Path -LiteralPath $path)) { Write-GnuError -Context $Context -Message "$($operand): No such file or directory"; continue }
            $full = [System.IO.Path]::GetFullPath($path)
            $best = $null
            foreach ($fs in $all) {
                if ($full.StartsWith($fs.Target, [System.StringComparison]::OrdinalIgnoreCase) -and ($null -eq $best -or $fs.Target.Length -gt $best.Target.Length)) { $best = $fs }
            }
            if ($null -ne $best) { $copy = $best.Clone(); $copy.File = $operand; $rows.Add($copy) }
        }
    }
    else {
        foreach ($fs in $all) {
            if (-not (Test-GnuOption -Context $Context -Key 'all') -and (-not $fs.Ready -or $fs.Total -eq 0)) { continue }
            $copy = $fs.Clone(); $copy.File = '-'; $rows.Add($copy)
        }
    }
    $local = Test-GnuOption -Context $Context -Key 'local'
    $selected = @($rows | Where-Object { ($include.Count -eq 0 -or $include -ccontains $_.Type) -and $exclude -cnotcontains $_.Type -and (-not $local -or $_.Local) })
    if ($selected.Count -eq 0) {
        if ($operands.Count -eq 0 -or $rows.Count -gt 0) { Write-GnuError -Context $Context -Message 'no file systems processed' }
        return
    }
    if (Test-GnuOption -Context $Context -Key 'total') {
        $sum = @{ Source = 'total'; Type = '-'; Target = '-'; File = '-'; Total = [double]0; Free = [double]0; Available = [double]0 }
        foreach ($row in $selected) { $sum.Total += $row.Total; $sum.Free += $row.Free; $sum.Available += $row.Available }
        $selected += $sum
    }

    # Header texts depend on the size mode, as in GNU.
    $headers = @{}
    foreach ($field in $fields) { $headers[$field] = $script:GnuDfFields[$field][0] }
    if ($scale.Human -eq 0) {
        $headers['size'] = "$($scale.Label)-blocks"
        if (-not $custom) { $headers['avail'] = 'Available' }
        if ($portable -and -not $custom) {
            $headers['size'] = "$($scale.Bytes.ToString('0', [System.Globalization.CultureInfo]::InvariantCulture))-blocks"
            $headers['pcent'] = 'Capacity'
        }
    }

    $table = New-Object 'System.Collections.Generic.List[string[]]'
    $table.Add([string[]]@(foreach ($field in $fields) { $headers[$field] }))
    foreach ($row in $selected) {
        $used = $row.Total - $row.Free
        $cells = foreach ($field in $fields) {
            switch ($field) {
                'source' { $row.Source }
                'fstype' { $row.Type }
                { $_ -eq 'size' -or $_ -eq 'used' -or $_ -eq 'avail' } {
                    $bytes = @{ size = $row.Total; used = $used; avail = $row.Available }[$field]
                    Format-GnuScaledSize -Bytes $bytes -Scale $scale
                }
                'pcent' {
                    if ($used + $row.Available -le 0) { '-' } else { [string][Math]::Ceiling($used * 100 / ($used + $row.Available)) + '%' }
                }
                'file' { $row.File }
                'target' { $row.Target }
                default { '-' }
            }
        }
        $table.Add([string[]]@($cells))
    }

    $widths = @(for ($c = 0; $c -lt $fields.Count; $c++) {
            $width = $script:GnuDfFields[$fields[$c]][1]
            foreach ($cells in $table) { $width = [Math]::Max($width, $cells[$c].Length) }
            $width
        })
    foreach ($cells in $table) {
        $parts = for ($c = 0; $c -lt $fields.Count; $c++) {
            $text = $cells[$c]
            if ($c -eq $fields.Count - 1 -and -not $script:GnuDfFields[$fields[$c]][2]) { $text }
            elseif ($script:GnuDfFields[$fields[$c]][2]) { $text.PadLeft($widths[$c]) }
            else { $text.PadRight($widths[$c]) }
        }
        ($parts -join ' ').TrimEnd()
    }
}