Utils/shuf.ps1
|
Register-GnuUtil @{ Name = 'shuf' Category = 'Text processing' Synopsis = 'Generate random permutations.' Usage = @('shuf [OPTION]... [FILE]', 'shuf -e [OPTION]... [ARG]...', 'shuf -i LO-HI [OPTION]...') Description = @( 'Write a random permutation of the input lines to standard output.', '', 'With no FILE, or when FILE is -, read standard input.' ) Options = @( @{ Short = 'e'; Long = 'echo'; Help = 'treat each ARG as an input line' } @{ Short = 'i'; Long = 'input-range'; Arg = 'LO-HI'; Help = 'treat each number LO through HI as an input line' } @{ Short = 'n'; Long = 'head-count'; Arg = 'COUNT'; Help = 'output at most COUNT lines' } @{ Short = 'o'; Long = 'output'; Arg = 'FILE'; Help = 'write result to FILE instead of standard output' } @{ Long = 'random-source'; Arg = 'FILE'; Help = 'get random bytes from FILE' } @{ Short = 'r'; Long = 'repeat'; Help = 'output lines can be repeated' } @{ Short = 'z'; Long = 'zero-terminated'; Help = 'line delimiter is NUL, not newline' } ) Notes = @( 'PowerShell note: --random-source seeds the generator from the file content, so the same file gives the same order (not the same order as GNU).', 'PowerShell note: -r with -o needs -n (GNU would write to the file forever).' ) } function script:Invoke-GnuShuffle { <# .SYNOPSIS Implementation of shuf. #> param([Parameter(Mandatory)][hashtable]$Context) $operands = @($Context.Operands) $zero = Test-GnuOption -Context $Context -Key 'zero-terminated' $items = New-Object 'System.Collections.Generic.List[string]' if (Test-GnuOption -Context $Context -Key 'input-range') { $range = [string](Get-GnuOptionValue -Context $Context -Key 'input-range') if ($range -notmatch '^(\d+)-(\d+)$' -or [long]$Matches[2] -lt [long]$Matches[1] - 1) { Write-GnuError -Context $Context -Message "invalid input range: '$range'" return } if ($operands.Count -gt 0) { Write-GnuUsageError -Context $Context -Message "extra operand '$($operands[0])'"; return } for ($n = [long]$Matches[1]; $n -le [long]$Matches[2]; $n++) { $items.Add([string]$n) } } elseif (Test-GnuOption -Context $Context -Key 'echo') { foreach ($operand in $operands) { $items.Add($operand) } } else { if ($operands.Count -gt 1) { Write-GnuUsageError -Context $Context -Message "extra operand '$($operands[1])'"; return } $operand = '-' if ($operands.Count -eq 1) { $operand = $operands[0] } $text = Get-GnuInputText -Context $Context -Operand $operand if ($null -eq $text) { return } $separator = "`n" if ($zero) { $separator = "`0" } foreach ($record in @(Split-GnuRecord -Text $text.Replace("`r`n", "`n") -Separator $separator)) { $items.Add($record) } } $count = [long]::MaxValue if (Test-GnuOption -Context $Context -Key 'head-count') { $countText = [string](Get-GnuOptionValue -Context $Context -Key 'head-count') if ($countText -notmatch '^\d+$') { Write-GnuError -Context $Context -Message "invalid line count: '$countText'"; return } $count = ConvertTo-GnuInt64 -Value (ConvertFrom-GnuSize -Text $countText) } $random = New-Object System.Random if (Test-GnuOption -Context $Context -Key 'random-source') { $sourceName = [string](Get-GnuOptionValue -Context $Context -Key 'random-source') $bytes = Get-GnuInputBytes -Context $Context -Operand $sourceName if ($null -eq $bytes) { return } $hasher = [System.Security.Cryptography.SHA256]::Create() $seed = [System.BitConverter]::ToInt32($hasher.ComputeHash([byte[]]$bytes), 0) $hasher.Dispose() $random = New-Object System.Random -ArgumentList $seed } $output = New-Object 'System.Collections.Generic.List[string]' if (Test-GnuOption -Context $Context -Key 'repeat') { if ($items.Count -eq 0) { if ($count -gt 0) { Write-GnuError -Context $Context -Message 'no lines to repeat' } return } if ($count -eq [long]::MaxValue) { if (Test-GnuOption -Context $Context -Key 'output') { Write-GnuUsageError -Context $Context -Message '--repeat with --output needs --head-count (the file would grow forever)' return } # GNU repeats forever; stop it with Ctrl+C or Select-Object -First. while ($true) { $items[$random.Next($items.Count)] } } for ($i = 0; $i -lt $count; $i++) { $output.Add($items[$random.Next($items.Count)]) } } else { # Fisher-Yates shuffle. $array = $items.ToArray() for ($i = $array.Length - 1; $i -gt 0; $i--) { $j = $random.Next($i + 1) $swap = $array[$i]; $array[$i] = $array[$j]; $array[$j] = $swap } $take = [long][Math]::Min([long]$array.Length, $count) for ($i = 0; $i -lt $take; $i++) { $output.Add($array[$i]) } } if (Test-GnuOption -Context $Context -Key 'output') { $target = [string](Get-GnuOptionValue -Context $Context -Key 'output') $terminator = "`n" if ($zero) { $terminator = "`0" } $content = '' if ($output.Count -gt 0) { $content = ($output -join $terminator) + $terminator } try { [System.IO.File]::WriteAllText((Resolve-GnuPath -Path $target), $content, $script:GnuUtf8) } catch { Write-GnuError -Context $Context -Message "$($target): $(Get-GnuErrorReason -Exception $_.Exception)" } return } ConvertTo-GnuRecordOutput -Record $output.ToArray() -Zero:$zero } |