Equivalent of *nix fold in PowerShell

2019-02-27 11:03发布

问题:

Today I had a few hundred items (IDs from SQL query) and needed to paste them into another query to be readable by an analyst. I needed *nix fold command. I wanted to take the 300 lines and reformat them as multiple numbers per line seperated by a space. I would have used fold -w 100 -s.

Similar tools on *nix include fmt and par.

On Windows is there an easy way to do this in PowerShell? I expected one of the *-Format commandlets to do it, but I couldn't find it. I'm using PowerShell v4.

See https://unix.stackexchange.com/questions/25173/how-can-i-wrap-text-at-a-certain-column-size

# Input Data
# simulate a set of 300 numeric IDs from 100,000 to 150,000
100001..100330 | 
    Out-File _sql.txt -Encoding ascii

# I want output like:
# 100001,  100002,  100003,  100004,  100005, ... 100010, 100011
# 100012,  100013,  100014,  100015,  100016, ... 100021, 100021
# each line less than 100 characters.

回答1:

Depending on how big the file is you could read it all into memory, join it with spaces and then split on 100* characters or the next space

(Get-Content C:\Temp\test.txt) -join " " -split '(.{100,}?[ |$])' | Where-Object{$_}

That regex looks for 100 characters then the first space after that. That match is then -split but since the pattern is wrapped in parenthesis the match is returned instead of discarded. The Where removes the empty entries that are created in between the matches.

Small sample to prove theory

@"
134
124
1
225
234
4
34
2
42
342
5
5
2
6
"@.split("`n") -join " "  -split '(.{10,}?[ |$])' | Where-Object{$_}

The above splits on 10 characters where possible. If it cannot the numbers are still preserved. Sample is based on me banging on the keyboard with my head.

134 124 1 
225 234 4 
34 2 42 
342 5 5 
2 6

You could then make this into a function to get the simplicity back that you are most likely looking for. It can get better but this isn't really the focus of the answer.

Function Get-Folded{
    Param(
        [string[]]$Strings,
        [int]$Wrap = 50
    )
    $strings  -join " " -split "(.{$wrap,}?[ |$])" | Where-Object{$_}
}

Again with the samples

PS C:\Users\mcameron> Get-Folded -Strings (Get-Content C:\temp\test.txt) -wrap 40
"Lorem ipsum dolor sit amet, consectetur 
adipiscing elit, sed do eiusmod tempor incididunt 
ut labore et dolore magna aliqua. Ut enim 
ad minim veniam, quis nostrud exercitation 
... output truncated...

You can see that it was supposed to split on 40 characters but the second line is longer. It split on the next space after 40 to preserve the word.



回答2:

If it's one item per line, and you want to join every 100 items onto a single line separated by a space you could put all the output into a text file then do this:

gc c:\count.txt -readcount 100 | % {$_ -join " "}


回答3:

When I saw this, the first thing that came to my mind was abusing Format-Table to do this, mostly because it knows how to break the lines properly when you specify a width. After coming up with a function, it seems that the other solutions presented are shorter and probably easier to understand, but I figured I'd still go ahead and post this solution anyway:

function fold {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        $InputObject,
        [Alias('w')]
        [int] $LineWidth = 100,
        [int] $ElementWidth
    )

    begin {
        $SB = New-Object System.Text.StringBuilder

        if ($ElementWidth) {
            $SBFormatter = "{0,$ElementWidth} "
        }
        else {
            $SBFormatter = "{0} "
        }
    }

    process {
        foreach ($CurrentObject in $InputObject) {
            [void] $SB.AppendFormat($SBFormatter, $CurrentObject)
        }
    }

    end {
        # Format-Table wanted some sort of an object assigned to it, so I 
        # picked the first static object that popped in my head:
        ([guid]::Empty | Format-Table -Property @{N="DoesntMatter"; E={$SB.ToString()}; Width = $LineWidth } -Wrap -HideTableHeaders |
            Out-String).Trim("`r`n")
    }
}

Using it gives output like this:

PS C:\> 0..99 | Get-Random -Count 100 | fold
1 73 81 47 54 41 17 87 2 55 30 91 19 50 64 70 51 29 49 46 39 20 85 69 74 43 68 82 76 22 12 35 59 92 
13 3 88 6 72 67 96 31 11 26 80 58 16 60 89 62 27 36 37 18 97 90 40 65 42 15 33 24 23 99 0 32 83 14  
21 8 94 48 10 4 84 78 52 28 63 7 34 86 75 71 53 5 45 66 44 57 77 56 38 79 25 93 9 61 98 95          

PS C:\> 0..99 | Get-Random -Count 100 | fold -ElementWidth 2
74 89 10 42 46 99 21 80 81 82  4 60 33 45 25 57 49  9 86 84 83 44  3 77 34 40 75 50  2 18  6 66 13  
64 78 51 27 71 97 48 58  0 65 36 47 19 31 79 55 56 59 15 53 69 85 26 20 73 52 68 35 93 17  5 54 95  
23 92 90 96 24 22 37 91 87  7 38 39 11 41 14 62 12 32 94 29 67 98 76 70 28 30 16  1 61 88 43  8 63  
72                                                                                                  

PS C:\> 0..99 | Get-Random -Count 100 | fold -ElementWidth 2 -w 40
21 78 64 18 42 15 40 99 29 61  4 95 66  
86  0 69 55 30 67 73  5 44 74 20 68 16  
82 58  3 46 24 54 75 14 11 71 17 22 94  
45 53 28 63  8 90 80 51 52 84 93  6 76  
79 70 31 96 60 27 26  7 19 97  1 59  2  
65 43 81  9 48 56 25 62 13 85 47 98 33  
34 12 50 49 38 57 39 37 35 77 89 88 83  
72 92 10 32 23 91 87 36 41              


回答4:

This is what I ended up using.

# simulate a set of 300 SQL IDs from 100,000 to 150,000
100001..100330 | 
    %{ "$_, " } | # I'll need this decoration in the SQL script
    Out-File _sql.txt -Encoding ascii

gc .\_sql.txt -ReadCount 10 | %{ $_ -join ' ' }

Thanks everyone for the effort and the answers. I'm really surprised there wasn't a way to do this with Format-Table without the use of [guid]::Empty in Rohn Edward's answer.

My IDs are much more consistent than the example I gave, so Noah's use of gc -ReadCount is by far the simplest solution in this particular data set, but in the future I'd probably use Matt's answer or the answers linked to by Emperor in comments.



回答5:

I came up with this:

$array = 
(@'
1
2
3
10
11
100
101
'@).split("`n") |
foreach {$_.trim()}

$array = $array * 40

$SB = New-Object Text.StringBuilder(100,100)

foreach ($item in $array) {

Try { [void]$SB.Append("$item ") }

Catch {
         $SB.ToString()
         [void]$SB.Clear()
         [Void]$SB.Append("$item ")
      }
}    
#don't forget the last line
$SB.ToString()

1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 
1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 
1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 
1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 
1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 
1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 
1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 1 2 3 10 11 100 101 

Maybe not as compact as you were hoping for, and there may be better ways to do it, but it seems to work.