Utils/paste.ps1

Register-GnuUtil @{
    Name         = 'paste'
    Category     = 'Text processing'
    Synopsis     = 'Merge lines of files.'
    Usage        = @('paste [OPTION]... [FILE]...')
    Description  = @(
        'Write lines consisting of the sequentially corresponding lines from',
        'each FILE, separated by TABs, to standard output.',
        '',
        'With no FILE, or when FILE is -, read standard input.'
    )
    FileOperands = $true
    Options      = @(
        @{ Short = 'd'; Long = 'delimiters'; Arg = 'LIST'; Help = 'reuse characters from LIST instead of TABs' }
        @{ Short = 's'; Long = 'serial'; Help = 'paste one file at a time instead of in parallel' }
        @{ Short = 'z'; Long = 'zero-terminated'; Help = 'line delimiter is NUL, not newline' }
    )
    Notes        = @('LIST may contain the escapes \n (newline), \t (tab), \\ (backslash) and \0 (no delimiter).')
}



function script:ConvertFrom-GnuPasteDelimiter {
    <#
    .SYNOPSIS
    Expands the escapes of a paste -d LIST into a list of delimiters ('' for \0).
    #>

    param([Parameter(Mandatory)][AllowEmptyString()][string]$List)

    $delimiters = New-Object 'System.Collections.Generic.List[string]'
    for ($i = 0; $i -lt $List.Length; $i++) {
        $char = $List[$i]
        if ($char -eq '\' -and $i + 1 -lt $List.Length) {
            $i++
            switch -CaseSensitive ([string]$List[$i]) {
                'n' { $delimiters.Add("`n") }
                't' { $delimiters.Add("`t") }
                '0' { $delimiters.Add('') }
                default { $delimiters.Add([string]$List[$i]) }
            }
        }
        else {
            $delimiters.Add([string]$char)
        }
    }
    , $delimiters.ToArray()
}

function script:Invoke-GnuPasteMerge {
    <#
    .SYNOPSIS
    Implementation of paste.
    #>

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

    $delimiters = [string[]]@("`t")
    if (Test-GnuOption -Context $Context -Key 'delimiters') {
        $delimiters = ConvertFrom-GnuPasteDelimiter -List ([string](Get-GnuOptionValue -Context $Context -Key 'delimiters'))
        if ($delimiters.Length -eq 0) { $delimiters = [string[]]@('') }
    }
    $zero = Test-GnuOption -Context $Context -Key 'zero-terminated'
    $operands = @($Context.Operands)
    if ($operands.Count -eq 0) { $operands = @('-') }

    # Every operand becomes a queue of records; all '-' operands share the same queue (GNU reads stdin in turn).
    $stdin = $null
    $sources = New-Object 'System.Collections.Generic.List[object]'
    foreach ($operand in $operands) {
        if ($operand -eq '-' -and $null -ne $stdin) { $sources.Add($stdin); continue }
        $records = @()
        if ($zero) {
            $text = Get-GnuInputText -Context $Context -Operand $operand
            if ($null -eq $text) { return }
            $records = @(Split-GnuRecord -Text $text -Separator "`0")
        }
        else {
            # GNU: paste stops without output when an input cannot be opened.
            $errorsBefore = $Context.ErrorCount
            $records = @(Get-GnuInputLine -Context $Context -Operand $operand)
            if ($Context.ErrorCount -gt $errorsBefore) { return }
        }
        $queue = New-Object 'System.Collections.Generic.Queue[string]'
        foreach ($record in $records) { $queue.Enqueue($record) }
        if ($operand -eq '-') { $stdin = $queue }
        $sources.Add($queue)
    }

    $output = New-Object 'System.Collections.Generic.List[string]'
    if (Test-GnuOption -Context $Context -Key 'serial') {
        # GNU: one output line per operand, even for empty input (a later '-' finds stdin already consumed).
        foreach ($queue in $sources) {
            $builder = New-Object System.Text.StringBuilder
            $index = 0
            while ($queue.Count -gt 0) {
                if ($index -gt 0) { [void]$builder.Append($delimiters[($index - 1) % $delimiters.Length]) }
                [void]$builder.Append($queue.Dequeue())
                $index++
            }
            $output.Add($builder.ToString())
        }
    }
    else {
        while ($true) {
            $any = $false
            foreach ($queue in $sources) { if ($queue.Count -gt 0) { $any = $true; break } }
            if (-not $any) { break }
            $builder = New-Object System.Text.StringBuilder
            for ($i = 0; $i -lt $sources.Count; $i++) {
                if ($i -gt 0) { [void]$builder.Append($delimiters[($i - 1) % $delimiters.Length]) }
                if ($sources[$i].Count -gt 0) { [void]$builder.Append($sources[$i].Dequeue()) }
            }
            $output.Add($builder.ToString())
        }
    }

    if ($zero) {
        ConvertTo-GnuRecordOutput -Record $output.ToArray() -Zero
        return
    }
    # Delimiters may contain newlines (-d '\n'), so split the result into lines.
    foreach ($line in $output) { ConvertTo-GnuOutputLine -Text ($line + "`n") }
}