Dang. I wrote this whole big answer with an elaborate method of parsing a tar
archive - it was cool. But I got to the end and I realized none of it was necessary at all. All you need is sed
and a little shell math:
set ./file[1-5];i=1 n=;eval "${n:=
} sed -n \"$(grep -c '.\|' "$@"|
sed 's|\(.*\):\(.*\)|\
$i,$(((\2/5)+(i+=\2)-\2))w \1|
')\" <<!$n"'$(cat "$@")'"$n!$n"
There grep -c
counts lines in whatever files you've globbed - I globbed file[1-5]
- and hands the count to sed
which then - with a little help from the shell - writes its own script. cat
provides the input via here-document. This is is because I'm iffy on what might happen if sed
does an open on and starts writing to one of the files cat
is trying to read out to it - also I suspect it would be a little better at handling buffers than a pipe would be depending on size - but I'm not too clear on that part.
So that reads all the files in a single stream and w
rites the output accordingly. A little setup is neccessary to increment the file numbers correctly - hence grep
and eval
- nothing terrible. Here's some set -x
output to show what it is doing:
+ set ./file1 ./file2 ./file3 ./file4 ./file5
+ i=1 n=
+ + grep -c .\| ./file1 ./file2 ./file3 ./file4 ./file5
sed s|\(.*\):\(.*\)|\
$i,$(((\2/5)+(i+=\2)-\2))w \1|
+ eval
sed -n "
$i,$(((18400/5)+(i+=18400)-18400))w ./file1
$i,$(((18411/5)+(i+=18411)-18411))w ./file2
$i,$(((18415/5)+(i+=18415)-18415))w ./file3
$i,$(((18418/5)+(i+=18418)-18418))w ./file4
$i,$(((18421/5)+(i+=18421)-18421))w ./file5" <<!
$(cat "$@")
!
+ cat ./file1 ./file2 ./file3 ./file4 ./file5
+ sed -n
1,3681w ./file1
18401,22083w ./file2
36812,40495w ./file3
55227,58910w ./file4
73645,77329w ./file5
As you can see, the lines are addressed based on each file's position in the stream, and are w
ritten as they are read to their respective filenames. Importantly though, this makes no attempt to handle any non-portable characters in a pathname - in particular, newlines in path names are a non-starter in this case as the sed
w
rite command delimits filename arguments on newlines. The situation is easily worked around if necessary with ln
if you require it though.
I should also mention that there is a limit to the number of w
rite file descriptors sed
can support in a single script. The spec says:
[sed
is required] to support at least ten distinct w
files, matching historical practice on many implementations. Implementations are encouraged to support more, but conforming applications should not exceed this limit.
So the command as written above should be portable to any POSIX system for up to 10 concurrent read/write files. If this sort of thing were incorporated into a published script or app in which more might be wanted it could be worth running a few checks before processing real data in /tmp
. Like:
: & set '"" "" "" "" "" "" "" "" "" "" ';n='
' f=/tmp/$$$!'_$((i+=1))' MAXw=[num]
while eval "set '$1$1' $1;exec <<!$n\$(((i=0)+\$#))$n!$n
i=\$(sed \"$(IFS=\ ;printf "\nw $f%.0s" $1)\")"
[ "$(($#==i?(_i=i-1):(MAXw=_i)))" -lt "$MAXw" ]
do :;done; rm "/tmp/$$$!"*; unset _i i f n
...which should fairly portably guage sed
's capability in that area. GNU sed
stalled out at 4093 concurrently opened w
files for me in about a second, but that is likely my system's max, and can be affected w/ ulimit
as well. When it was through - because the check doubles $i
's value for each try - $_i
was left at 2560 and $i
at 5120. I default to setting $MAXw
to the safer $_i
above at loop close - mostly because I'm unsure if all sed
s will properly set their return if they cannot open a w
file - but the reader can do with it what they will.
Note that the initial [num]
value for $MAXw
should be an actual number - whatever your maximum wanted w
files might be - and not literally [num]
.
About the here-document again - I consider it - or something like it - a good idea in this case. sed
must maintain its write descriptors while it reads and so what it might do with identical in/out names I do not know - but I don't guess it is a chance worth taking when alternatives are so readily available to us.
My test files were generated like:
for n in 1 2 3 4 5
do : & seq -s "$(printf "%015s--$n--%015s\n\t")" "$!" >"file$n"
done
...which gets fairly sequential pseudo-random numbers from the kernel in abandoned process PIDs. The file contents were purposely designed to indicate a mismatch in the split. Here's what a sample set looks like before and after:
Before:
for f in file[1-5]; do
nl -ba "$f" | sed -n '$p;$=;1,3p
'; done
1 1 --1--
2 2 --1--
3 3 --1--
3681 3681 --1--
3681
1 1 --2--
2 2 --2--
3 3 --2--
3683 3683 --2--
3683
1 1 --3--
2 2 --3--
3 3 --3--
3684 3684 --3--
3684
1 1 --4--
2 2 --4--
3 3 --4--
3684 3684 --4--
3684
1 1 --5--
2 2 --5--
3 3 --5--
3685 3685 --5--
3685
If the formatting looks a little funky this is probably because seq
doesn't insert the -s
eparator string before the first output line. The important thing is that sed
, seq
and nl
all appear to agree on the line numbers. Anyway...
After:
...
sed -n
1,737w ./file1
3682,4418w ./file2
7365,8101w ./file3
11049,11785w ./file4
14733,15470w ./file5
...
1 1 --1--
2 2 --1--
3 3 --1--
737 737 --1--
737
1 1 --2--
2 2 --2--
3 3 --2--
737 737 --2--
737
1 1 --3--
2 2 --3--
3 3 --3--
737 737 --3--
737
1 1 --4--
2 2 --4--
3 3 --4--
737 737 --4--
737
1 1 --5--
2 2 --5--
3 3 --5--
738 738 --5--
738
And there it is - simple, efficient, and streamed.