Utils/comm.ps1

Register-GnuUtil @{
    Name         = 'comm'
    Category     = 'Text processing'
    Synopsis     = 'Compare two sorted files line by line.'
    Usage        = @('comm [OPTION]... FILE1 FILE2')
    Description  = @(
        'Compare sorted files FILE1 and FILE2 line by line.',
        '',
        'When FILE1 or FILE2 (not both) is -, read standard input.',
        '',
        'With no options, produce three-column output. Column one contains',
        'lines unique to FILE1, column two contains lines unique to FILE2,',
        'and column three contains lines common to both files.'
    )
    FileOperands = $true
    Options      = @(
        @{ Short = '1'; Key = 'hide1'; Help = 'suppress column 1 (lines unique to FILE1)' }
        @{ Short = '2'; Key = 'hide2'; Help = 'suppress column 2 (lines unique to FILE2)' }
        @{ Short = '3'; Key = 'hide3'; Help = 'suppress column 3 (lines that appear in both files)' }
        @{ Long = 'check-order'; Key = 'order'; Value = 'check'; Help = 'check that the input is correctly sorted, even if all input lines are pairable' }
        @{ Long = 'nocheck-order'; Key = 'order'; Value = 'none'; Help = 'do not check that the input is correctly sorted' }
        @{ Long = 'output-delimiter'; Arg = 'STR'; Help = 'separate columns with STR' }
        @{ Long = 'total'; Help = 'output a summary' }
        @{ Short = 'z'; Long = 'zero-terminated'; Help = 'line delimiter is NUL, not newline' }
    )
    Notes        = @('PowerShell note: lines are compared ordinally (like LC_ALL=C), not with locale collation.')
}



function script:Invoke-GnuCommCompare {
    <#
    .SYNOPSIS
    Implementation of comm.
    #>

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

    $operands = @($Context.Operands)
    if ($operands.Count -eq 0) { Write-GnuUsageError -Context $Context -Message 'missing operand'; return }
    if ($operands.Count -eq 1) { Write-GnuUsageError -Context $Context -Message "missing operand after '$($operands[0])'"; return }
    if ($operands.Count -gt 2) { Write-GnuUsageError -Context $Context -Message "extra operand '$($operands[2])'"; return }

    $zero = Test-GnuOption -Context $Context -Key 'zero-terminated'
    $files = @()
    foreach ($operand in $operands) {
        $errorsBefore = $Context.ErrorCount
        if ($zero) {
            $text = Get-GnuInputText -Context $Context -Operand $operand
            $records = @()
            if ($null -ne $text) { $records = @(Split-GnuRecord -Text $text -Separator "`0") }
        }
        else {
            $records = @(Get-GnuInputLine -Context $Context -Operand $operand)
        }
        if ($Context.ErrorCount -gt $errorsBefore) { return }
        $files += , $records
    }

    $delimiter = "`t"
    if (Test-GnuOption -Context $Context -Key 'output-delimiter') {
        $delimiter = [string](Get-GnuOptionValue -Context $Context -Key 'output-delimiter')
        if ($delimiter.Length -eq 0) { $delimiter = [string][char]0 }   # GNU: an empty delimiter means NUL
    }
    $show = @(
        -not (Test-GnuOption -Context $Context -Key 'hide1'),
        -not (Test-GnuOption -Context $Context -Key 'hide2'),
        -not (Test-GnuOption -Context $Context -Key 'hide3')
    )
    $prefix = @('', '', '')
    if ($show[0]) { $prefix[1] = $delimiter; $prefix[2] = $delimiter }
    if ($show[1]) { $prefix[2] += $delimiter }

    $orderMode = [string](Get-GnuOptionValue -Context $Context -Key 'order' -Default 'default')
    $unsorted = @($false, $false)
    $totals = @([long]0, [long]0, [long]0)
    $output = New-Object 'System.Collections.Generic.List[string]'
    $left = $files[0]
    $right = $files[1]
    $i = 0
    $j = 0

    while ($i -lt $left.Count -or $j -lt $right.Count) {
        $column = 0
        if ($i -ge $left.Count) { $column = 1 }
        elseif ($j -ge $right.Count) { $column = 0 }
        else {
            $order = [string]::CompareOrdinal($left[$i], $right[$j])
            if ($order -lt 0) { $column = 0 } elseif ($order -gt 0) { $column = 1 } else { $column = 2 }
        }

        # Order check: each file must be non-decreasing (only reported for unpairable lines unless --check-order).
        foreach ($side in @(0, 1)) {
            $lines = $files[$side]
            $index = $i
            if ($side -eq 1) { $index = $j }
            $consumes = ($column -eq $side) -or ($column -eq 2)
            if ($orderMode -ne 'none' -and $consumes -and $index -gt 0 -and -not $unsorted[$side] -and
                [string]::CompareOrdinal($lines[$index - 1], $lines[$index]) -gt 0 -and ($column -ne 2 -or $orderMode -eq 'check')) {
                $unsorted[$side] = $true
                Write-GnuError -Context $Context -Message "file $($side + 1) is not in sorted order"
                if ($orderMode -eq 'check') { return }
            }
        }

        $text = $null
        switch ($column) {
            0 { $text = $left[$i]; $i++ }
            1 { $text = $right[$j]; $j++ }
            2 { $text = $left[$i]; $i++; $j++ }
        }
        $totals[$column]++
        if ($show[$column]) { $output.Add($prefix[$column] + $text) }
    }

    if (Test-GnuOption -Context $Context -Key 'total') {
        $output.Add(($totals -join $delimiter) + $delimiter + 'total')
    }
    if ($unsorted[0] -or $unsorted[1]) {
        # GNU prints this after the output; the errors above already set the exit code.
        Write-GnuError -Context $Context -Message 'input is not in sorted order'
    }
    ConvertTo-GnuRecordOutput -Record $output.ToArray() -Zero:$zero
}