Utils/nl.ps1

Register-GnuUtil @{
    Name         = 'nl'
    Category     = 'Viewing file contents'
    Synopsis     = 'Number lines of files.'
    Usage        = @('nl [OPTION]... [FILE]...')
    Description  = @('Write each FILE to standard output, with line numbers added.', '', 'With no FILE, or when FILE is -, read standard input.')
    FileOperands = $true
    Options      = @(
        @{ Short = 'b'; Long = 'body-numbering'; Arg = 'STYLE'; Help = 'use STYLE for numbering body lines' }
        @{ Short = 'd'; Long = 'section-delimiter'; Arg = 'CC'; Help = 'use CC for logical page delimiters' }
        @{ Short = 'f'; Long = 'footer-numbering'; Arg = 'STYLE'; Help = 'use STYLE for numbering footer lines' }
        @{ Short = 'h'; Long = 'header-numbering'; Arg = 'STYLE'; Help = 'use STYLE for numbering header lines' }
        @{ Short = 'i'; Long = 'line-increment'; Arg = 'NUMBER'; Help = 'line number increment at each line' }
        @{ Short = 'l'; Long = 'join-blank-lines'; Arg = 'NUMBER'; Help = 'group of NUMBER empty lines counted as one' }
        @{ Short = 'n'; Long = 'number-format'; Arg = 'FORMAT'; Help = 'insert line numbers according to FORMAT' }
        @{ Short = 'p'; Long = 'no-renumber'; Help = 'do not reset line numbers for each section' }
        @{ Short = 's'; Long = 'number-separator'; Arg = 'STRING'; Help = 'add STRING after (possible) line number' }
        @{ Short = 'v'; Long = 'starting-line-number'; Arg = 'NUMBER'; Help = 'first line number for each section' }
        @{ Short = 'w'; Long = 'number-width'; Arg = 'NUMBER'; Help = 'use NUMBER columns for line numbers' }
    )
    Notes        = @(
        'Default options are: -bt -d''\:'' -fn -hn -i1 -l1 -n''rn'' -s<TAB> -v1 -w6',
        '',
        'CC are two delimiter characters used to construct logical page delimiters;',
        'a missing second character implies '':''.',
        '',
        'STYLE is one of:',
        '',
        ' a number all lines',
        ' t number only nonempty lines',
        ' n number no lines',
        ' pBRE number only lines that contain a match for the basic regular',
        ' expression, BRE',
        '',
        'FORMAT is one of:',
        '',
        ' ln left justified, no leading zeros',
        ' rn right justified, no leading zeros',
        ' rz right justified, leading zeros',
        '',
        'PowerShell note: pBRE uses .NET regular expression syntax.'
    )
}



function script:Get-GnuNlSetting {
    <#
    .SYNOPSIS
    Validates the nl options. Returns the settings hashtable, or nothing after reporting an error.
    #>

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

    $settings = @{}
    foreach ($section in @(@{ Key = 'body-numbering'; Name = 'body'; Default = 't' }, @{ Key = 'header-numbering'; Name = 'header'; Default = 'n' }, @{ Key = 'footer-numbering'; Name = 'footer'; Default = 'n' })) {
        $style = [string](Get-GnuOptionValue -Context $Context -Key $section.Key -Default $section.Default)
        if ($style -cnotmatch '^([atn]|p.*)$') {
            Write-GnuUsageError -Context $Context -Message "invalid $($section.Name) numbering style: '$style'"
            return
        }
        $settings[$section.Name] = $style
    }

    $numbers = @{ 'line-increment' = 1; 'join-blank-lines' = 1; 'starting-line-number' = 1; 'number-width' = 6 }
    $labels = @{ 'line-increment' = 'line number increment'; 'join-blank-lines' = 'line number of blank lines'; 'starting-line-number' = 'starting line number'; 'number-width' = 'line number field width' }
    foreach ($key in @($numbers.Keys)) {
        if (-not (Test-GnuOption -Context $Context -Key $key)) { continue }
        $text = [string](Get-GnuOptionValue -Context $Context -Key $key)
        $value = [long]0
        $valid = [long]::TryParse($text, [System.Globalization.NumberStyles]::AllowLeadingSign, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$value)
        if ($valid -and ($key -eq 'join-blank-lines' -or $key -eq 'number-width') -and $value -lt 1) { $valid = $false }
        if (-not $valid) {
            Write-GnuUsageError -Context $Context -Message "invalid $($labels[$key]): '$text'"
            return
        }
        $numbers[$key] = $value
    }

    $format = [string](Get-GnuOptionValue -Context $Context -Key 'number-format' -Default 'rn')
    if (@('ln', 'rn', 'rz') -cnotcontains $format) {
        Write-GnuUsageError -Context $Context -Message "invalid line numbering format: '$format'"
        return
    }

    $delimiter = [string](Get-GnuOptionValue -Context $Context -Key 'section-delimiter' -Default '\:')
    if ($delimiter.Length -eq 1) { $delimiter += ':' }

    $settings['Increment'] = $numbers['line-increment']
    $settings['JoinBlank'] = $numbers['join-blank-lines']
    $settings['Start'] = $numbers['starting-line-number']
    $settings['Width'] = [int]$numbers['number-width']
    $settings['Format'] = $format
    $settings['Separator'] = [string](Get-GnuOptionValue -Context $Context -Key 'number-separator' -Default "`t")
    $settings['Delimiter'] = $delimiter
    $settings['Renumber'] = -not (Test-GnuOption -Context $Context -Key 'no-renumber')
    $settings
}

function script:Format-GnuNlLine {
    <#
    .SYNOPSIS
    Numbers (or not) one input line and handles section delimiter lines.
    #>

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

    $delimiter = $Settings.Delimiter
    $section = $null
    if ($delimiter.Length -gt 0) {
        if ([string]::Equals($Line, $delimiter * 3, [System.StringComparison]::Ordinal)) { $section = 'header' }
        elseif ([string]::Equals($Line, $delimiter * 2, [System.StringComparison]::Ordinal)) { $section = 'body' }
        elseif ([string]::Equals($Line, $delimiter, [System.StringComparison]::Ordinal)) { $section = 'footer' }
    }
    if ($null -ne $section) {
        # GNU: a delimiter line starts a section, prints as an empty line, and a header resets the numbers.
        $State.Section = $section
        $State.BlankRun = 0
        if ($section -eq 'header' -and $Settings.Renumber) { $State.Number = $Settings.Start }
        return ''
    }

    $style = $Settings[$State.Section]
    $number = $false
    switch -Regex ($style) {
        '^a$' {
            if ($Line.Length -eq 0) {
                $State.BlankRun++
                if ($State.BlankRun -ge $Settings.JoinBlank) { $number = $true; $State.BlankRun = 0 }
            }
            else {
                $number = $true
                $State.BlankRun = 0
            }
        }
        '^t$' { $number = $Line.Length -gt 0 }
        '^p' { $number = [regex]::IsMatch($Line, $style.Substring(1)) }
    }

    if (-not $number) {
        return (' ' * ($Settings.Width + $Settings.Separator.Length)) + $Line
    }

    $text = $State.Number.ToString([System.Globalization.CultureInfo]::InvariantCulture)
    switch ($Settings.Format) {
        'ln' { $text = $text.PadRight($Settings.Width) }
        'rn' { $text = $text.PadLeft($Settings.Width) }
        'rz' {
            if ($State.Number -lt 0) { $text = '-' + $text.Substring(1).PadLeft($Settings.Width - 1, '0') }
            else { $text = $text.PadLeft($Settings.Width, '0') }
        }
    }
    $State.Number += $Settings.Increment
    $text + $Settings.Separator + $Line
}