File Coverage

bin/sort
Criterion Covered Total %
statement 196 298 65.7
branch 100 228 43.8
condition 19 62 30.6
subroutine 14 21 66.6
pod n/a
total 329 609 54.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =begin metadata
4              
5             Name: sort
6             Description: sort or merge text files
7             Author: Chris Nandor, pudge@pobox.com
8             License: perl
9              
10             =end metadata
11              
12             =cut
13              
14              
15             # perl implementation of sort(1), by pudge@pobox.com
16             # see POD below for more information
17              
18 6     6   32286 use Getopt::Long qw(GetOptions);
  6         94846  
  6         45  
19 6     6   1409 use Fcntl qw(O_RDONLY O_WRONLY O_CREAT O_TRUNC);
  6         11  
  6         423  
20 6     6   2652 use Symbol qw(gensym);
  6         8276  
  6         412  
21 6     6   38 use strict;
  6         12  
  6         157  
22 6     6   2487 use locale;
  6         4962  
  6         42  
23 6     6   339 use vars qw($VERSION *sortsub *sort1 *sort2 *map1 *map2 %fh);
  6         9  
  6         43370  
24              
25 6         930769 $VERSION = '1.01';
26 6         48 Getopt::Long::config('bundling'); # -cmu == -c -m -u
27              
28             {
29 6         264 my(%o, @argv, @pos);
  6         32  
30              
31             # take care of +pos1 -pos2 now instead of in getopts
32 6         45 foreach my $argv (0..$#ARGV) {
33 13         22 $_ = $ARGV[$argv];
34 13         19 my $n;
35 13 50       35 if (/^\+(\d+)(?:\.(\d+))?([bdfinr]+)?/) {
36 0         0 $n = $1 + 1;
37 0 0       0 $n .= '.' . ($2 + 1) if defined $2;
38 0 0       0 $n .= $3 if $3;
39 0         0 push @argv, $argv;
40             } else {
41 13         23 next;
42             }
43              
44 0         0 $_ = $ARGV[$argv + 1];
45 0 0       0 if (/^\-(\d+)(?:\.(\d+))?([bdfinr]+)?/) {
46 0 0       0 $n .= "," . (defined $2 ? ($1 + 1) . ".$2" : $1);
47 0 0       0 $n .= $3 if $3;
48 0         0 push @argv, $argv;
49             }
50              
51 0         0 push @pos, $n;
52             }
53              
54             # delete used elements from @ARGV
55 6         29 for (reverse @argv) {
56 0         0 splice(@ARGV, $_, 1);
57             }
58              
59             usage() unless GetOptions(\%o, 'k=s@', qw(c m u d f i n r b D),
60 6 50       19 map {"$_=s"} qw(X t y F o R));
  36         81  
61 6 50       9476 push @{$o{'k'}}, @pos if @pos; # add pos stuff back
  0         0  
62 6 50       20 @ARGV = '-' unless @ARGV;
63 6         22 $o{I} = [@ARGV]; # input files
64              
65 6         26 my $exit = _sort_file(\%o);
66 6 100       75 warn "Exit status is $exit\n" if $exit != 1;
67 6 50       0 exit($exit == 1 ? $exit ? 0 : $exit : 1);
    100          
68             }
69              
70             sub _sort_file {
71 6     6   29 local $\; # don't mess up our prints
72 6         14 my($opts, @fh, @recs) = shift;
73              
74             # record separator, default to \n
75 6 50       41 local $/ = $opts->{R} ? $opts->{R} : "\n";
76              
77             # get input files into anon array if not already
78 6 50       19 $opts->{I} = [$opts->{I}] unless ref $opts->{I};
79              
80 6 50       10 usage() unless @{$opts->{I}};
  6         21  
81              
82             # "K" == "no k", for later
83 6 100       21 $opts->{K} = $opts->{k} ? 0 : 1;
84 6 50       36 $opts->{k} = $opts->{k} ? [$opts->{k}] : [] if !ref $opts->{k};
    100          
85              
86 6   50     79 $opts->{'y'} ||= $ENV{'MAX_SORT_RECORDS'} || 200000; # default max records
      33        
87 6   50     46 $opts->{'F'} ||= $ENV{'MAX_SORT_FILES'} || 40; # default max files
      33        
88 6 50       31 if (defined $opts->{'F'}) {
89 6 50       32 die "option -F expects a positive number\n" if (int($opts->{'F'}) < 1);
90             }
91 6 50       23 if (defined $opts->{'y'}) {
92 6 50       26 die "option -y expects a positive number\n" if (int($opts->{'y'}) < 1);
93             }
94              
95             # see big ol' mess below
96 6         29 _make_sort_sub($opts);
97              
98             # only check to see if file is sorted
99 6 100       28 if ($opts->{c}) {
    50          
100 2         6 local *F;
101 2         4 my $last;
102              
103 2         6 my $filein = $opts->{I}[0];
104 2 50       7 if ($filein eq '-') {
105 2         27 open F, '<-';
106             } else {
107 0 0       0 die "$0: '$filein' is a directory\n" if -d $filein;
108 0 0       0 sysopen(F, $filein, O_RDONLY)
109             or die "Can't open `$filein' for reading: $!\n";
110             }
111              
112 2         80 while (defined(my $rec = )) {
113             # fail if -u and keys are not unique (assume sorted)
114 6 50 33     28 if ($opts->{u} && $last) {
115 0 0       0 return 0 unless _are_uniq($opts->{K}, $last, $rec);
116             }
117              
118             # fail if records not in proper sort order
119 6 100       14 if ($last) {
120 4         6 my @foo;
121 4 50       15 if ($opts->{K}) {
122 4         43 local $^W;
123 4         89 @foo = sort sort1 ($rec, $last);
124             } else {
125 0         0 local $^W;
126 0         0 @foo = map {$_->[0]} sort sortsub
  0         0  
127             map &map1, ($rec, $last);
128             }
129 4 100 66     30 return 0 if $foo[0] ne $last || $foo[1] ne $rec;
130             }
131              
132             # save value of last record
133 5         62 $last = $rec;
134             }
135              
136             # success, yay
137 1         8 return 1;
138              
139             # if merging sorted files
140             } elsif ($opts->{'m'}) {
141              
142 0         0 foreach my $filein (@{$opts->{I}}) {
  0         0  
143              
144             # just open files and get array of handles
145 0 0       0 die "$0: '$filein' is a directory\n" if -d $filein;
146 0         0 my $sym = gensym();
147              
148 0 0       0 sysopen($sym, $filein, O_RDONLY)
149             or die "Can't open `$filein' for reading: $!";
150              
151 0         0 push @fh, $sym;
152             }
153              
154             # ooo, get ready, get ready
155             } else {
156              
157             # once for each input file
158 4         6 foreach my $filein (@{$opts->{I}}) {
  4         11  
159 4         9 local *F;
160 4         6 my $count = 0;
161              
162 4 50       17 _debug("Sorting file $filein ...\n") if $opts->{D};
163              
164 4 100       14 if ($filein eq '-') {
165 1 50       16 open F, '<-' or die "Could not open '-': $!/$^E";
166             } else {
167 3 50       120 die "$0: '$filein' is a directory\n" if -d $filein;
168 3 50       124 sysopen(F, $filein, O_RDONLY)
169             or die "Can't open `$filein' for reading: $!";
170             }
171              
172 4         183 while (defined(my $rec = )) {
173 118         121 push @recs, $rec;
174 118         79 $count++; # keep track of number of records
175              
176 118 50       137 if ($count >= $opts->{'y'}) { # don't go over record limit
177              
178             _debug("$count records reached in `$filein'\n")
179 0 0       0 if $opts->{D};
180              
181             # save to temp file, add new fh to array
182 0         0 push @fh, _write_temp(\@recs, $opts);
183              
184             # reset record count and record array
185 0         0 ($count, @recs) = (0);
186              
187             # do a merge now if at file limit
188 0 0       0 if (@fh >= $opts->{F}) {
189              
190             # get filehandle and restart array with it
191 0         0 @fh = (_merge_files($opts, \@fh, [], _get_temp()));
192              
193 0 0       0 _debug("\nCreating temp files ...\n") if $opts->{D};
194             }
195             }
196             } continue {
197 118 100       212 close F if eof;
198             }
199              
200 4         20 close F;
201             }
202              
203             # records leftover, didn't reach record limit
204 4 50       37 if (@recs) {
205 4 50       13 _debug("\nSorting leftover records ...\n") if $opts->{D};
206 4         20 _check_last(\@recs);
207 4 100       14 if ($opts->{K}) {
208 3         24 local $^W;
209 3         63 @recs = sort sort1 @recs;
210             } else {
211 1         2 local $^W;
212 1         23 @recs = map {$_->[0]} sort sortsub map &map1, @recs;
  8         9  
213             }
214             }
215             }
216              
217             # do the merge thang, uh huh, do the merge thang
218 4         28 my $close = _merge_files($opts, \@fh, \@recs, $opts->{o});
219 4 50       18 close $close unless fileno($close) == fileno('STDOUT'); # don't close STDOUT
220              
221 4 50       10 _debug("\nDone!\n\n") if $opts->{D};
222 4         27 return 1; # yay
223             }
224              
225             # take optional arrayref of handles of sorted files,
226             # plus optional arrayref of sorted scalars
227             sub _merge_files {
228             # we need the options, filehandles, and output file
229 4     4   15 my($opts, $fh, $recs, $file) = @_;
230 4         8 my($uniq, $first, $o, %oth);
231              
232             # arbitrarily named keys, store handles as values
233 4         10 %oth = map {($o++ => $_)} @$fh;
  0         0  
234              
235             # match handle key in %oth to next record of the handle
236             %fh = map {
237 4         11 my $fh = $oth{$_};
  0         0  
238 0         0 ($_ => scalar <$fh>);
239             } keys %oth;
240              
241             # extra records, special X "handle"
242 4 50       895 $fh{X} = shift @$recs if @$recs;
243              
244 4 50       1070 _debug("\nCreating sorted $file ...\n") if $opts->{D};
245              
246             # output to STDOUT if no output file provided
247 4 50       738 if ($file eq '') {
    0          
248 4         13 $file = \*STDOUT;
249              
250             # if output file is a path, not a reference to a file, open
251             # file and get a reference to it
252             } elsif (!ref $file) {
253 0         0 my $tfh = gensym();
254 0 0       0 sysopen($tfh, $file, O_WRONLY|O_CREAT|O_TRUNC)
255             or die "Can't open `$file' for writing: $!";
256 0         0 $file = $tfh;
257             }
258              
259 4         18 my $oldfh = select $file;
260 4         11 $| = 0; # just in case, use the buffer, you knob
261              
262 4         12 while (keys %fh) {
263             # don't bother sorting keys if only one key remains!
264 4 50 33     24 if (!$opts->{u} && keys %fh == 1) {
265 4         9 ($first) = keys %fh;
266 4         6 my $curr = $oth{$first};
267 4 50       32 my @left = $first eq 'X' ? @$recs : <$curr>;
268 4         18 print $fh{$first}, @left;
269 4         7 delete $fh{$first};
270 4         12 last;
271             }
272              
273             {
274             # $first is arbitrary number assigned to first fh in sort
275 0 0       0 if ($opts->{K}) {
  0         0  
276 0         0 local $^W;
277 0         0 ($first) = (sort sort2 keys %fh);
278             } else {
279 0         0 local $^W;
280 0         0 ($first) = (map {$_->[0]} sort sortsub
  0         0  
281             map &map2, keys %fh);
282             }
283             }
284              
285             # don't print if -u and not unique
286 0 0       0 if ($opts->{u}) {
287             print $fh{$first} if
288 0 0 0     0 (!$uniq || _are_uniq($opts->{K}, $uniq, $fh{$first}));
289 0         0 $uniq = $fh{$first};
290             } else {
291 0         0 print $fh{$first};
292             }
293              
294             # get current filehandle
295 0         0 my $curr = $oth{$first};
296              
297             # use @$recs, not filehandles, if key is X
298 0 0       0 my $rec = $first eq 'X' ? shift @$recs : scalar <$curr>;
299              
300 0 0       0 if (defined $rec) { # bring up next record for this filehandle
301 0         0 $fh{$first} = $rec;
302              
303             } else { # we don't need you anymore
304 0         0 delete $fh{$first};
305             }
306             }
307              
308 4         192 seek $file, 0, 0; # might need to read back from it
309 4         17 select $oldfh;
310 4         17 return $file;
311             }
312              
313             sub _check_last {
314             # add new record separator if not one there
315 4 100   4   5 ${$_[0]}[-1] .= $/ if (${$_[0]}[-1] !~ m|$/$|);
  1         6  
  4         102  
316             }
317              
318             sub _write_temp {
319 0     0   0 my($recs, $opts) = @_;
320 0 0       0 my $temp = _get_temp() or die "Can't get temp file: $!";
321              
322 0         0 _check_last($recs);
323              
324 0 0       0 _debug("New tempfile: $temp\n") if $opts->{D};
325              
326 0 0       0 if ($opts->{K}) {
327 0         0 local $^W;
328 0         0 print $temp sort sort1 @{$recs};
  0         0  
329             } else {
330 0         0 local $^W;
331 0         0 print $temp map {$_->[0]} sort sortsub map &map1, @{$recs};
  0         0  
  0         0  
332             }
333              
334 0         0 seek $temp, 0, 0; # might need to read back from it
335 0         0 return $temp;
336             }
337              
338             sub _parse_keydef {
339 2     2   3 my($k, $topts) = @_;
340              
341             # gurgle
342 2         8 $k =~ /^(\d+)(?:\.(\d+))?([bdfinr]+)?
343             (?:,(\d+)(?:\.(\d+))?([bdfinr]+)?)?$/x;
344              
345             # set defaults at zero or undef
346 2 50 50     29 my %opts = (
      50        
      50        
      50        
      50        
347             %$topts, # get other options
348             ksf => $1 || 0, # start field
349             ksc => $2 || 0, # start field char start
350             kst => $3 || '', # start field type
351             kff => (defined $4 ? $4 : undef), # end field
352             kfc => $5 || 0, # end field char end
353             kft => $6 || '', # end field type
354             );
355              
356             # their idea of 1 is not ours
357 2         3 for (qw(ksf ksc kff)) { # kfc stays same
358 6 100       12 $opts{$_}-- if $opts{$_};
359             }
360              
361             # if nothing in kst or kft, use other flags possibly passed
362 2 50 33     7 if (!$opts{kst} && !$opts{kft}) {
363 2         3 foreach (qw(b d f i n r)) {
364 12 50       14 $opts{kst} .= $_ if $topts->{$_};
365 12 50       12 $opts{kft} .= $_ if $topts->{$_};
366             }
367              
368             # except for b, flags on one apply to the other
369             } else {
370 0         0 foreach (qw(d f i n r)) {
371 0 0 0     0 $opts{kst} .= $_ if ($opts{kst} =~ /$_/ || $opts{kft} =~ /$_/);
372 0 0 0     0 $opts{kft} .= $_ if ($opts{kst} =~ /$_/ || $opts{kft} =~ /$_/);
373             }
374             }
375              
376 2         3 return \%opts;
377             }
378              
379             sub _make_sort_sub {
380 6     6   17 my($topts, @sortsub, @mapsub, @sort1, @sort2) = shift;
381              
382             # if no keydefs set
383 6 100       20 if ($topts->{K}) {
384 5         17 $topts->{kst} = '';
385 5         14 foreach (qw(b d f i n r)) {
386 30 100       63 $topts->{kst} .= $_ if $topts->{$_};
387             }
388              
389             # more complex stuff, act like we had -k defined
390 5 50       22 if ($topts->{kst} =~ /[bdfi]/) {
391 0         0 $topts->{K} = 0;
392 0         0 $topts->{k} = ['K']; # special K ;-)
393             }
394             }
395              
396             # if no keydefs set
397 6 100       21 if ($topts->{K}) {
398 5 50       17 _debug("No keydef set\n") if $topts->{D};
399              
400             # defaults for main sort sub components
401 5         49 my($cmp, $aa, $bb, $fa, $fb) = qw(cmp $a $b $fh{$a} $fh{$b});
402              
403             # reverse sense
404 5 50       20 ($bb, $aa, $fb, $fa) = ($aa, $bb, $fa, $fb) if $topts->{r};
405              
406             # do numeric sort
407 5 100       16 $cmp = '<=>' if $topts->{n};
408              
409             # add finished expression to array
410 5         16 my $sort1 = "sub { $aa $cmp $bb }\n";
411 5         14 my $sort2 = "sub { $fa $cmp $fb }\n";
412              
413 5 50       15 _debug("$sort1\n$sort2\n") if $topts->{D};
414              
415             {
416 5         11 local $^W;
  5         20  
417 5     564   517 *sort1 = eval $sort1;
  564         463  
418 5 50       29 die "Can't create sort sub: $@" if $@;
419 5     0   343 *sort2 = eval $sort2;
  0         0  
420 5 50       39 die "Can't create sort sub: $@" if $@;
421             }
422              
423             } else {
424              
425             # get text separator or use whitespace
426             $topts->{t} =
427             defined $topts->{X} ? $topts->{X} :
428 1 50       4 defined $topts->{t} ? quotemeta($topts->{t}) :
    50          
429             '\s+';
430 1 50       3 $topts->{t} =~ s|/|\\/|g if defined $topts->{X};
431              
432 1         1 foreach my $k (@{$topts->{k}}) {
  1         3  
433 2         2 my($opts, @fil) = ($topts);
434              
435             # defaults for main sort sub components
436 2         4 my($cmp, $ab_, $fab_, $aa, $bb) = qw(cmp $_ $fh{$_} $a $b);
437              
438             # skip stuff if special K
439 2 50       7 $opts = $k eq 'K' ? $topts : _parse_keydef($k, $topts);
440              
441 2 50       3 if ($k ne 'K') {
442             my($tmp1, $tmp2) = ("\$tmp[$opts->{ksf}]",
443 2 50       7 ($opts->{kff} ? "\$tmp[$opts->{kff}]" : ''));
444              
445             # skip leading spaces
446 2 50       4 if ($opts->{kst} =~ /b/) {
447 0         0 $tmp1 = "($tmp1 =~ /(\\S.*)/)[0]";
448             }
449              
450 2 50       3 if ($opts->{kft} =~ /b/) {
451 0         0 $tmp2 = "($tmp2 =~ /(\\S.*)/)[0]";
452             }
453              
454             # simpler if one field, goody for us
455 2 50 33     4 if (! defined $opts->{kff} || $opts->{ksf} == $opts->{kff}) {
456              
457             # simpler if chars are both 0, wicked pissah
458 2 50 33     7 if ($opts->{ksc} == 0 &&
    0 33        
459             (!$opts->{kfc} || $opts->{kfc} == 0)) {
460 2         4 @fil = "\$tmp[$opts->{ksf}]";
461              
462             # hmmmmm
463             } elsif (!$opts->{kfc}) {
464 0         0 @fil = "substr($tmp1, $opts->{ksc})";
465              
466             # getting out of hand now
467             } else {
468             @fil = "substr($tmp1, $opts->{ksc}, ".
469 0         0 ($opts->{kfc} - $opts->{ksc}) . ')';
470             }
471              
472             # try again, shall we?
473             } else {
474              
475             # if spans two fields, but chars are both 0
476             # and neither has -b, alrighty
477 0 0 0     0 if ($opts->{kfc} == 0 && $opts->{ksc} == 0 &&
    0 0        
      0        
478             $opts->{kst} !~ /b/ && $opts->{kft} !~ /b/) {
479 0         0 @fil = "join(''," .
480             "\@tmp[$opts->{ksf} .. $opts->{kff}])";
481              
482             # if only one field away
483             } elsif (($opts->{kff} - $opts->{ksf}) == 1) {
484 0         0 @fil = "join('', substr($tmp1, $opts->{ksc}), " .
485             "substr($tmp2, 0, $opts->{kfc}))";
486              
487             # fine, have it your way! hurt me! love me!
488             } else {
489             @fil = "join('', substr($tmp1, $opts->{ksc}), " .
490             "\@tmp[" . ($opts->{ksf} + 1) . " .. " .
491 0         0 ($opts->{kff} - 1) . "], " .
492             "substr($tmp2, 0, $opts->{kfc}))";
493             }
494             }
495             } else {
496 0 0       0 @fil = $opts->{kst} =~ /b/ ?
497             "(\$tmp[0] =~ /(\\S.*)/)[0]" : "\$tmp[0]";
498             }
499              
500             # fold to upper case
501 2 50       4 if ($opts->{kst} =~ /f/) {
502 0         0 $fil[0] = "uc($fil[0])";
503             }
504              
505             # only alphanumerics and whitespace, override -i
506 2 50       6 if ($opts->{kst} =~ /d/) {
    50          
507 0         0 $topts->{DD}++;
508 0         0 push @fil, "\$tmp =~ s/[^\\w\\s]+//g", '"$tmp"';
509              
510             # only printable characters
511             } elsif ($opts->{kst} =~ /i/) {
512 0         0 $fil[0] = "join '', grep { / ^ [[:print:]]+ $ /x } " .
513             "split //,\n$fil[0]";
514             }
515              
516 2 50       3 $fil[0] = "\$tmp = $fil[0]" if $opts->{kst} =~ /d/;
517              
518              
519             # reverse sense
520 2 50       3 ($bb, $aa) = ($aa, $bb) if ($opts->{kst} =~ /r/);
521              
522             # do numeric sort
523 2 50       3 $cmp = '<=>' if ($opts->{kst} =~ /n/);
524              
525             # add finished expressions to arrays
526 2         3 my $n = @sortsub + 2;
527 2         7 push @sortsub, sprintf "%s->[$n] %s %s->[$n]",
528             $aa, $cmp, $bb;
529              
530 2 50       3 if (@fil > 1) {
531             push @mapsub, " (\n" .
532 0         0 join(",\n", map {s/^/ /mg; $_} @fil),
  0         0  
  0         0  
533             "\n )[-1],\n ";
534             } else {
535 2         8 push @mapsub, " " . $fil[0] . ",\n ";
536             }
537             }
538              
539             # if not -u
540 1 50       6 if (! $topts->{u} ) {
541             # do straight compare if all else is equal
542             push @sortsub, sprintf "%s->[1] %s %s->[1]",
543 1 50       4 $topts->{r} ? qw($b cmp $a) : qw($a cmp $b);
544             }
545              
546 1         2 my(%maps, $sortsub, $mapsub) = (map1 => '$_', map2 => '$fh{$_}');
547              
548 1         691 $sortsub = "sub {\n " . join(" || \n ", @sortsub) . "\n}\n";
549              
550 1         5 for my $m (keys %maps) {
551 2         4 my $k = $maps{$m};
552             $maps{$m} = sprintf "sub {\n my \@tmp = %s;\n",
553 2 50       9 $topts->{k}[0] eq 'K' ? $k : "split(/$topts->{t}/, $k)";
554              
555 2 50       3 $maps{$m} .= " my \$tmp;\n" if $topts->{DD};
556 2         3 $maps{$m} .= "\n [\$_, $k";
557 2 50       5 $maps{$m} .= ",\n " . join('', @mapsub) if @mapsub;
558 2         2 $maps{$m} .= "]\n}\n";
559             }
560              
561 1 50       3 _debug("$sortsub\n$maps{map1}\n$maps{map2}\n") if $topts->{D};
562              
563             {
564 1         1 local $^W;
  1         3  
565 1 50 66 16   89 *sortsub = eval $sortsub;
  16         28  
566 1 50       3 die "Can't create sort sub: $@" if $@;
567 1     8   83 *map1 = eval $maps{map1};
  8         12  
  8         27  
568 1 50       3 die "Can't create sort sub: $@" if $@;
569 1     0   76 *map2 = eval $maps{map2};
  0            
  0            
570 1 50       6 die "Can't create sort sub: $@" if $@;
571             }
572             }
573             }
574              
575              
576             sub _get_temp { # nice and simple
577 0     0   0 require IO::File;
578 0         0 IO::File->new_tmpfile;
579             }
580              
581             sub _are_uniq {
582 0     0   0 my $nok = shift;
583 0         0 local $^W;
584              
585 0 0       0 if ($nok) {
586 0         0 ($a, $b) = @_;
587 0         0 return &sort1;
588             } else {
589 0         0 ($a, $b) = map &map1, @_;
590 0         0 return &sortsub;
591             }
592             }
593              
594             sub _debug {
595 0     0   0 print STDERR @_;
596             }
597              
598             sub usage {
599 0     0   0 require Pod::Usage;
600 0         0 Pod::Usage::pod2usage({ -exitval => 1, -verbose => 0 });
601             }
602              
603             __END__