Utils/cat.ps1

Register-GnuUtil @{
    Name         = 'cat'
    Category     = 'Viewing file contents'
    Synopsis     = 'Concatenate files and print on the standard output.'
    Usage        = @('cat [OPTION]... [FILE]...')
    Description  = @('Concatenate FILE(s) to standard output.', '', 'With no FILE, or when FILE is -, read standard input.')
    FileOperands = $true
    Options      = @(
        @{ Short = 'A'; Long = 'show-all'; Sets = @{ 'show-nonprinting' = 1; 'show-ends' = 1; 'show-tabs' = 1 }; Help = 'equivalent to -vET' }
        @{ Short = 'b'; Long = 'number-nonblank'; Help = 'number nonempty output lines, overrides -n' }
        @{ Short = 'e'; Sets = @{ 'show-nonprinting' = 1; 'show-ends' = 1 }; Help = 'equivalent to -vE' }
        @{ Short = 'E'; Long = 'show-ends'; Help = 'display $ at end of each line' }
        @{ Short = 'n'; Long = 'number'; Help = 'number all output lines' }
        @{ Short = 's'; Long = 'squeeze-blank'; Help = 'suppress repeated empty output lines' }
        @{ Short = 't'; Sets = @{ 'show-nonprinting' = 1; 'show-tabs' = 1 }; Help = 'equivalent to -vT' }
        @{ Short = 'T'; Long = 'show-tabs'; Help = 'display TAB characters as ^I' }
        @{ Short = 'u'; Help = '(ignored)' }
        @{ Short = 'v'; Long = 'show-nonprinting'; Help = 'use ^ and M- notation, except for LFD and TAB' }
    )
    Notes        = @('PowerShell note: pipeline input is read as text lines; files are read as UTF-8.')
}



function script:New-GnuCatState {
    <#
    .SYNOPSIS
    Formatting state of one cat invocation (line counter and squeeze tracking carry across files).
    #>

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

    $state = @{
        NumberAll       = Test-GnuOption -Context $Context -Key 'number'
        NumberNonBlank  = Test-GnuOption -Context $Context -Key 'number-nonblank'
        Squeeze         = Test-GnuOption -Context $Context -Key 'squeeze-blank'
        ShowEnds        = Test-GnuOption -Context $Context -Key 'show-ends'
        ShowTabs        = Test-GnuOption -Context $Context -Key 'show-tabs'
        ShowNonprinting = Test-GnuOption -Context $Context -Key 'show-nonprinting'
        Number          = [long]0
        PreviousBlank   = $false
        Raw             = $false
    }
    $state.Raw = $state.ShowEnds -or $state.ShowNonprinting
    $state
}

function script:Format-GnuCatLine {
    <#
    .SYNOPSIS
    Formats one cat line, or returns nothing if it is squeezed away.
    #>

    param(
        [Parameter(Mandatory)][hashtable]$State,
        [Parameter(Mandatory)][AllowEmptyString()][string]$Line,
        [Parameter(Mandatory)][bool]$Terminated
    )

    $blank = $Line.Length -eq 0
    if ($State.Squeeze -and $blank -and $State.PreviousBlank) { return }
    $State.PreviousBlank = $blank

    $text = $Line
    if ($State.ShowNonprinting -or $State.ShowTabs) {
        $text = ConvertTo-GnuVisibleText -Text $text -Nonprinting:$State.ShowNonprinting -Tabs:$State.ShowTabs
    }
    if ($State.ShowEnds -and $Terminated) { $text += '$' }

    if ($State.NumberNonBlank) {
        if (-not $blank) {
            $State.Number++
            $text = $State.Number.ToString([System.Globalization.CultureInfo]::InvariantCulture).PadLeft(6) + "`t" + $text
        }
    }
    elseif ($State.NumberAll) {
        $State.Number++
        $text = $State.Number.ToString([System.Globalization.CultureInfo]::InvariantCulture).PadLeft(6) + "`t" + $text
    }
    $text
}

function script:ConvertTo-GnuVisibleText {
    <#
    .SYNOPSIS
    cat -v / -T rendering: ^X for control characters, ^? for DEL, M- notation for bytes >= 128, ^I for TAB (with -T).
    #>

    param(
        [Parameter(Mandatory)][AllowEmptyString()][string]$Text,
        [switch]$Nonprinting,
        [switch]$Tabs
    )

    $builder = New-Object System.Text.StringBuilder
    $i = 0
    while ($i -lt $Text.Length) {
        $char = $Text[$i]
        $code = [int]$char
        $i++
        if ($code -eq 9) {
            if ($Tabs) { [void]$builder.Append('^I') } else { [void]$builder.Append($char) }
            continue
        }
        if (-not $Nonprinting) { [void]$builder.Append($char); continue }

        if ($code -lt 32) { [void]$builder.Append('^').Append([char]($code + 64)); continue }
        if ($code -eq 127) { [void]$builder.Append('^?'); continue }
        if ($code -lt 128) { [void]$builder.Append($char); continue }

        $chunk = [string]$char
        if ([char]::IsHighSurrogate($char) -and $i -lt $Text.Length -and [char]::IsLowSurrogate($Text[$i])) {
            $chunk += $Text[$i]
            $i++
        }
        # UTF-8 bytes of non-ASCII characters are 0x80-0xF4, so 'M-^?' (byte 0xFF) cannot occur here.
        foreach ($byte in $script:GnuUtf8.GetBytes($chunk)) {
            $low = $byte - 128
            if ($low -lt 32) { [void]$builder.Append('M-^').Append([char]($low + 64)) }
            else { [void]$builder.Append('M-').Append([char]$low) }
        }
    }
    $builder.ToString()
}