Regen Configure.
[p5sagit/p5-mst-13.2.git] / Porting / patchls
CommitLineData
08aa1457 1#!/bin/perl -w
2#
3e3baf6d 3# patchls - patch listing utility
08aa1457 4#
5# Input is one or more patchfiles, output is a list of files to be patched.
6#
3e3baf6d 7# Copyright (c) 1997 Tim Bunce. All rights reserved.
8# This program is free software; you can redistribute it and/or
9# modify it under the same terms as Perl itself.
10#
11# With thanks to Tom Horsley for the seed code.
fb73857a 12
08aa1457 13
3e3baf6d 14use Getopt::Std;
08aa1457 15use Text::Wrap qw(wrap $columns);
16use Text::Tabs qw(expand unexpand);
17use strict;
fb73857a 18use vars qw($VERSION);
19
a8710ca1 20$VERSION = 2.10;
08aa1457 21
3e3baf6d 22sub usage {
43051805 23die qq{
3e3baf6d 24 patchls [options] patchfile [ ... ]
25
84902520 26 -h no filename headers (like grep), only the listing.
27 -l no listing (like grep), only the filename headers.
fb73857a 28 -i Invert: for each patched file list which patch files patch it.
84902520 29 -c Categorise the patch and sort by category (perl specific).
30 -m print formatted Meta-information (Subject,From,Msg-ID etc).
31 -p N strip N levels of directory Prefix (like patch), else automatic.
32 -v more verbose (-d for noisy debugging).
b73f5677 33 -n give a count of the number of patches applied to a file if >1.
84902520 34 -f F only list patches which patch files matching regexp F
43051805 35 (F has \$ appended unless it contains a /).
36 -e Expect patched files to Exist (relative to current directory)
37 Will print warnings for files which don't. Also affects -4 option.
fb73857a 38 other options for special uses:
84902520 39 -I just gather and display summary Information about the patches.
fb73857a 40 -4 write to stdout the PerForce commands to prepare for patching.
43051805 41 -5 like -4 but add "|| exit 1" after each command
fb73857a 42 -M T Like -m but only output listed meta tags (eg -M 'Title From')
43 -W N set wrap width to N (defaults to 70, use 0 for no wrap)
b73f5677 44 -X list patchfiles that may clash (i.e. patch the same file)
43051805 45
46 patchls version $VERSION by Tim Bunce
3e3baf6d 47}
48}
49
3e3baf6d 50$::opt_p = undef; # undef != 0
08aa1457 51$::opt_d = 0;
52$::opt_v = 0;
53$::opt_m = 0;
b73f5677 54$::opt_n = 0;
08aa1457 55$::opt_i = 0;
56$::opt_h = 0;
57$::opt_l = 0;
58$::opt_c = 0;
84902520 59$::opt_f = '';
43051805 60$::opt_e = 0;
fb73857a 61
62# special purpose options
84902520 63$::opt_I = 0;
fb73857a 64$::opt_4 = 0; # output PerForce commands to prepare for patching
43051805 65$::opt_5 = 0;
fb73857a 66$::opt_M = ''; # like -m but only output these meta items (-M Title)
67$::opt_W = 70; # set wrap width columns (see Text::Wrap module)
43051805 68$::opt_C = 0; # 'Chip' mode (handle from/tags/article/bug files) undocumented
b73f5677 69$::opt_X = 0; # list patchfiles that patch the same file
08aa1457 70
3e3baf6d 71usage unless @ARGV;
08aa1457 72
b73f5677 73getopts("dmnihlvecC45Xp:f:IM:W:") or usage;
fb73857a 74
75$columns = $::opt_W || 9999999;
76
77$::opt_m = 1 if $::opt_M;
43051805 78$::opt_4 = 1 if $::opt_5;
b73f5677 79$::opt_i = 1 if $::opt_X;
80
81# see get_meta_info()
82my @show_meta = split(' ', $::opt_M || 'Title From Msg-ID Files');
83my %show_meta = map { ($_,1) } @show_meta;
08aa1457 84
3e3baf6d 85my %cat_title = (
84902520 86 'BUILD' => 'BUILD PROCESS',
87 'CORE' => 'CORE LANGUAGE',
3e3baf6d 88 'DOC' => 'DOCUMENTATION',
b73f5677 89 'LIB' => 'LIBRARY',
84902520 90 'PORT1' => 'PORTABILITY - WIN32',
fb73857a 91 'PORT2' => 'PORTABILITY - GENERAL',
84902520 92 'TEST' => 'TESTS',
93 'UTIL' => 'UTILITIES',
94 'OTHER' => 'OTHER CHANGES',
b73f5677 95 'EXT' => 'EXTENSIONS',
a8710ca1 96 'UNKNOWN' => 'UNKNOWN - NO FILES PATCHED',
3e3baf6d 97);
08aa1457 98
43051805 99
100sub get_meta_info {
101 my $ls = shift;
102 local($_) = shift;
b73f5677 103 if (/^From:\s+(.*\S)/i) {;
104 my $from = $1; # temporary measure for Chip Salzenberg
105 $from =~ s/chip\@(atlantic\.net|perlsupport\.com)/chip\@pobox.com/;
106 $from =~ s/\(Tim Bunce\) \(Tim Bunce\)/(Tim Bunce)/;
107 $ls->{From}{$from} = 1
108 }
109 if (/^Subject:\s+(?:Re: )?(.*\S)/i) {
110 my $title = $1;
111 $title =~ s/\[(PATCH|PERL)[\w\. ]*\]:?//g;
112 $title =~ s/\b(PATCH|PERL)[\w\.]*://g;
113 $title =~ s/\bRe:\s+/ /g;
114 $title =~ s/\s+/ /g;
115 $title =~ s/^\s*(.*?)\s*$/$1/g;
116 $ls->{Title}{$title} = 1;
117 }
43051805 118 $ls->{'Msg-ID'}{$1}=1 if /^Message-Id:\s+(.*\S)/i;
119 $ls->{Date}{$1}=1 if /^Date:\s+(.*\S)/i;
120 $ls->{$1}{$2}=1 if $::opt_M && /^([-\w]+):\s+(.*\S)/;
121}
122
08aa1457 123
124# Style 1:
125# *** perl-5.004/embed.h Sat May 10 03:39:32 1997
126# --- perl-5.004.fixed/embed.h Thu May 29 19:48:46 1997
127# ***************
128# *** 308,313 ****
129# --- 308,314 ----
130#
131# Style 2:
132# --- perl5.004001/mg.c Sun Jun 08 12:26:24 1997
133# +++ perl5.004-bc/mg.c Sun Jun 08 11:56:08 1997
a8710ca1 134# @@ .. @@
135# or for deletions
136# --- perl5.004001/mg.c Sun Jun 08 12:26:24 1997
137# +++ /dev/null Sun Jun 08 11:56:08 1997
138# @@ ... @@
08aa1457 139# or (rcs, note the different date format)
140# --- 1.18 1997/05/23 19:22:04
141# +++ ./pod/perlembed.pod 1997/06/03 21:41:38
142#
143# Variation:
144# Index: embed.h
145
43051805 146my %ls;
147
b73f5677 148my $in;
149my $ls;
150my $prevline = '';
43051805 151my $prevtype = '';
a8710ca1 152my (%removed, %added);
fb73857a 153my $prologue = 1; # assume prologue till patch or /^exit\b/ seen
08aa1457 154
43051805 155
08aa1457 156foreach my $argv (@ARGV) {
157 $in = $argv;
a8710ca1 158 if (-d $in) {
159 warn "Ignored directory $in\n";
160 next;
161 }
08aa1457 162 unless (open F, "<$in") {
163 warn "Unable to open $in: $!\n";
164 next;
165 }
166 print "Reading $in...\n" if $::opt_v and @ARGV > 1;
3e3baf6d 167 $ls = $ls{$in} ||= { is_in => 1, in => $in };
08aa1457 168 my $type;
169 while (<F>) {
170 unless (/^([-+*]{3}) / || /^(Index):/) {
fb73857a 171 # not an interesting patch line
172 # but possibly meta-information or prologue
173 if ($prologue) {
a8710ca1 174 $added{$1} = 1 if /^touch\s+(\S+)/;
175 $removed{$1} = 1 if /^rm\s+(?:-f)?\s*(\S+)/;
fb73857a 176 $prologue = 0 if /^exit\b/;
177 }
43051805 178 get_meta_info($ls, $_) if $::opt_m;
08aa1457 179 next;
180 }
181 $type = $1;
182 next if /^--- [0-9,]+ ----$/ || /^\*\*\* [0-9,]+ \*\*\*\*$/;
fb73857a 183 $prologue = 0;
08aa1457 184
b73f5677 185 print "Last: $prevline","This: ${_}Got: $type\n\n" if $::opt_d;
08aa1457 186
187 # Some patches have Index lines but not diff headers
3e3baf6d 188 # Patch copes with this, so must we. It's also handy for
189 # documenting manual changes by simply adding Index: lines
b73f5677 190 # to the file which describes the problem being fixed.
191 if (/^Index:\s+(.*)/) {
192 my $f;
a8710ca1 193 foreach $f (split(/ /, $1)) { add_patched_file($ls, $f) }
b73f5677 194 next;
195 }
08aa1457 196
197 if ( ($type eq '---' and $prevtype eq '***') # Style 1
198 or ($type eq '+++' and $prevtype eq '---') # Style 2
199 ) {
fb73857a 200 if (/^[-+*]{3} (\S+)\s*(.*?\d\d:\d\d:\d\d)?/) { # double check
a8710ca1 201 if ($1 eq "/dev/null") {
202 $prevline =~ /^[-+*]{3} (\S+)\s*/;
203 add_deleted_file($ls, $1);
204 }
205 else {
206 add_patched_file($ls, $1);
207 }
08aa1457 208 }
209 else {
210 warn "$in $.: parse error (prev $prevtype, type $type)\n$prevline$_";
211 }
212 }
213 }
214 continue {
215 $prevline = $_;
b73f5677 216 $prevtype = $type || '';
08aa1457 217 $type = '';
218 }
43051805 219
220 # special mode for patch sets from Chip
b73f5677 221 if ($in =~ m:[\\/]patch$:) {
222 my $is_chip;
43051805 223 my $chip;
224 my $dir; ($dir = $in) =~ s:[\\/]patch$::;
225 if (!$ls->{From} && (open(CHIP,"$dir/article") || open(CHIP,"$dir/bug"))) {
226 get_meta_info($ls, $_) while (<CHIP>);
b73f5677 227 $is_chip = 1;
43051805 228 }
229 if (open CHIP,"<$dir/from") {
230 chop($chip = <CHIP>);
231 $ls->{From} = { $chip => 1 };
b73f5677 232 $is_chip = 1;
43051805 233 }
234 if (open CHIP,"<$dir/tag") {
235 chop($chip = <CHIP>);
236 $ls->{Title} = { $chip => 1 };
b73f5677 237 $is_chip = 1;
43051805 238 }
b73f5677 239 $ls->{From} = { "Chip Salzenberg" => 1 } if $is_chip && !$ls->{From};
43051805 240 }
241
3e3baf6d 242 # if we don't have a title for -m then use the file name
a8710ca1 243 $ls->{Title}{"Untitled: $in"}=1 if $::opt_m
3e3baf6d 244 and !$ls->{Title} and $ls->{out};
245
246 $ls->{category} = $::opt_c
247 ? categorize_files([keys %{ $ls->{out} }], $::opt_v) : '';
08aa1457 248}
3e3baf6d 249print scalar(@ARGV)." files read.\n" if $::opt_v and @ARGV > 1;
250
251
fb73857a 252# --- Firstly we filter and sort as needed ---
253
254my @ls = values %ls;
08aa1457 255
84902520 256if ($::opt_f) { # filter out patches based on -f <regexp>
84902520 257 $::opt_f .= '$' unless $::opt_f =~ m:/:;
258 @ls = grep {
84902520 259 my $match = 0;
b73f5677 260 if ($_->{is_in}) {
261 my @out = keys %{ $_->{out} };
262 $match=1 if grep { m/$::opt_f/o } @out;
263 }
264 else {
265 $match=1 if $_->{in} =~ m/$::opt_f/o;
84902520 266 }
267 $match;
268 } @ls;
269}
270
fb73857a 271@ls = sort {
272 $a->{category} cmp $b->{category} || $a->{in} cmp $b->{in}
273} @ls;
274
275
276# --- Handle special modes ---
277
278if ($::opt_4) {
43051805 279 my $tail = ($::opt_5) ? "|| exit 1" : "";
a8710ca1 280 print map { "p4 delete $_$tail\n" } sort keys %removed if %removed;
281 print map { "p4 add $_$tail\n" } sort keys %added if %added;
b73f5677 282 my @patches = sort grep { $_->{is_in} } @ls;
283 my @no_outs = grep { keys %{$_->{out}} == 0 } @patches;
284 warn "Warning: Some files contain no patches:",
285 join("\n\t", '', map { $_->{in} } @no_outs), "\n" if @no_outs;
a8710ca1 286
fb73857a 287 my %patched = map { ($_, 1) } map { keys %{$_->{out}} } @patches;
a8710ca1 288 delete @patched{keys %added};
fb73857a 289 my @patched = sort keys %patched;
b73f5677 290 foreach(@patched) {
a8710ca1 291 next if $removed{$_};
43051805 292 my $edit = ($::opt_e && !-f $_) ? "add " : "edit";
b73f5677 293 print "p4 $edit $_$tail\n";
294 }
43051805 295 exit 0 unless $::opt_C;
fb73857a 296}
297
b73f5677 298
84902520 299if ($::opt_I) {
300 my $n_patches = 0;
301 my($in,$out);
302 my %all_out;
b73f5677 303 my @no_outs;
84902520 304 foreach $in (@ls) {
305 next unless $in->{is_in};
306 ++$n_patches;
307 my @outs = keys %{$in->{out}};
b73f5677 308 push @no_outs, $in unless @outs;
84902520 309 @all_out{@outs} = ($in->{in}) x @outs;
310 }
311 my @all_out = sort keys %all_out;
312 my @missing = grep { ! -f $_ } @all_out;
313 print "$n_patches patch files patch ".@all_out." files (".@missing." missing)\n";
b73f5677 314 print @no_outs." patch files don't contain patches.\n" if @no_outs;
fb73857a 315 print "(use -v to list patches which patch 'missing' files)\n"
b73f5677 316 if (@missing || @no_outs) && !$::opt_v;
317 if ($::opt_v and @no_outs) {
318 print "Patch files which don't contain patches:\n";
319 foreach $out (@no_outs) {
320 printf " %-20s\n", $out->{in};
321 }
322 }
84902520 323 if ($::opt_v and @missing) {
324 print "Missing files:\n";
325 foreach $out (@missing) {
b73f5677 326 printf " %-20s\t", $out unless $::opt_h;
327 print $all_out{$out} unless $::opt_l;
328 print "\n";
84902520 329 }
330 }
a8710ca1 331 print "Added files: ".join(" ",sort keys %added )."\n" if %added;
332 print "Removed files: ".join(" ",sort keys %removed)."\n" if %removed;
84902520 333 exit 0+@missing;
334}
335
08aa1457 336unless ($::opt_c and $::opt_m) {
3e3baf6d 337 foreach $ls (@ls) {
338 next unless ($::opt_i) ? $ls->{is_out} : $ls->{is_in};
b73f5677 339 next if $::opt_X and keys %{$ls->{out}} <= 1;
08aa1457 340 list_files_by_patch($ls);
341 }
342}
343else {
344 my $c = '';
3e3baf6d 345 foreach $ls (@ls) {
346 next unless ($::opt_i) ? $ls->{is_out} : $ls->{is_in};
84902520 347 print "\n ------ $cat_title{$ls->{category}} ------\n"
348 if $ls->{category} ne $c;
08aa1457 349 $c = $ls->{category};
3e3baf6d 350 unless ($::opt_i) {
351 list_files_by_patch($ls);
352 }
353 else {
354 my $out = $ls->{in};
355 print "\n$out patched by:\n";
356 # find all the patches which patch $out and list them
357 my @p = grep { $_->{out}->{$out} } values %ls;
358 foreach $ls (@p) {
359 list_files_by_patch($ls, '');
360 }
361 }
08aa1457 362 }
363 print "\n";
364}
365
3e3baf6d 366exit 0;
367
368
369# ---
370
08aa1457 371
a8710ca1 372sub add_patched_file {
08aa1457 373 my $ls = shift;
a8710ca1 374 my $raw_name = shift;
375 my $action = shift || 1; # 1==patched, 2==deleted
3e3baf6d 376
a8710ca1 377 my $out = trim_name($raw_name);
378 print "add_patched_file '$out' ($raw_name, $action)\n" if $::opt_d;
379
380 $ls->{out}->{$out} = $action;
3e3baf6d 381
43051805 382 warn "$out patched but not present\n" if $::opt_e && !-f $out;
383
3e3baf6d 384 # do the -i inverse as well, even if we're not doing -i
385 my $i = $ls{$out} ||= {
386 is_out => 1,
387 in => $out,
388 category => $::opt_c ? categorize_files([ $out ], $::opt_v) : '',
389 };
390 $i->{out}->{$in} = 1;
08aa1457 391}
392
a8710ca1 393sub add_deleted_file {
394 my $ls = shift;
395 my $raw_name = shift;
396 my $out = trim_name($raw_name);
397 print "add_deleted_file '$out' ($raw_name)\n" if $::opt_d;
398 $removed{$out} = 1;
399 #add_patched_file(@_[0,1], 2);
400}
401
08aa1457 402
403sub trim_name { # reduce/tidy file paths from diff lines
404 my $name = shift;
84902520 405 $name =~ s:\\:/:g; # adjust windows paths
406 $name =~ s://:/:g; # simplify (and make win \\share into absolute path)
a8710ca1 407 if ($name eq "/dev/null") {
408 # do nothing (XXX but we need a way to record deletions)
409 }
410 elsif (defined $::opt_p) {
08aa1457 411 # strip on -p levels of directory prefix
412 my $dc = $::opt_p;
413 $name =~ s:^[^/]+/(.+)$:$1: while $dc-- > 0;
414 }
415 else { # try to strip off leading path to perl directory
416 # if absolute path, strip down to any *perl* directory first
417 $name =~ s:^/.*?perl.*?/::i;
a8710ca1 418 $name =~ s:.*(perl|maint)[-_]?5?[._]?[-_a-z0-9.+]*/::i;
08aa1457 419 $name =~ s:^\./::;
420 }
421 return $name;
422}
423
424
425sub list_files_by_patch {
3e3baf6d 426 my($ls, $name) = @_;
427 $name = $ls->{in} unless defined $name;
08aa1457 428 my @meta;
429 if ($::opt_m) {
fb73857a 430 my $meta;
431 foreach $meta (@show_meta) {
432 next unless $ls->{$meta};
433 my @list = sort keys %{$ls->{$meta}};
434 push @meta, sprintf "%7s: ", $meta;
435 if ($meta eq 'Title') {
b73f5677 436 @list = map { "\"$_\""; } @list;
43051805 437 push @list, "#$1" if $::opt_C && $ls->{in} =~ m:\b(\w\d+)/patch$:;
fb73857a 438 }
439 elsif ($meta eq 'From') {
440 # fix-up bizzare addresses from japan and ibm :-)
441 foreach(@list) {
442 s:\W+=?iso.*?<: <:;
443 s/\d\d-\w\w\w-\d{4}\s+\d\d:\S+\s*//;
444 }
445 }
446 elsif ($meta eq 'Msg-ID') {
447 my %from; # limit long threads to one msg-id per site
448 @list = map {
449 $from{(/@(.*?)>/ ? $1 : $_)}++ ? () : ($_);
450 } @list;
451 }
08aa1457 452 push @meta, my_wrap(""," ", join(", ",@list)."\n");
453 }
3e3baf6d 454 $name = "\n$name" if @meta and $name;
08aa1457 455 }
456 # don't print the header unless the file contains something interesting
b73f5677 457 return if !@meta and !$ls->{out} and !$::opt_v;
458 if ($::opt_l) { # -l = no listing, just names
459 print "$ls->{in}";
460 my $n = keys %{ $ls->{out} };
461 print " ($n patches)" if $::opt_n and $n>1;
462 print "\n";
463 return;
464 }
08aa1457 465
3e3baf6d 466 # a twisty maze of little options
467 my $cat = ($ls->{category} and !$::opt_m) ? "\t$ls->{category}" : "";
468 print "$name$cat: " unless ($::opt_h and !$::opt_v) or !"$name$cat";
a8710ca1 469 my $sep = "\n";
470 $sep = "" if @show_meta==1 && $::opt_c && $::opt_h;
471 print join('', $sep, @meta) if @meta;
08aa1457 472
b73f5677 473 return if $::opt_m && !$show_meta{Files};
3e3baf6d 474 my @v = sort PATORDER keys %{ $ls->{out} };
b73f5677 475 my $n = @v;
476 my $v = "@v";
08aa1457 477 print $::opt_m ? " Files: ".my_wrap(""," ",$v) : $v;
b73f5677 478 print " ($n patches)" if $::opt_n and $n>1;
479 print "\n";
08aa1457 480}
481
482
483sub my_wrap {
84902520 484 my $txt = eval { expand(wrap(@_)) }; # die's on long lines!
485 return $txt unless $@;
486 return expand("@_");
08aa1457 487}
488
489
490
3e3baf6d 491sub categorize_files {
492 my($files, $verb) = @_;
08aa1457 493 my(%c, $refine);
3e3baf6d 494
495 foreach (@$files) { # assign a score to a file path
496 # the order of some of the tests is important
497 $c{TEST} += 5,next if m:^t/:;
498 $c{DOC} += 5,next if m:^pod/:;
499 $c{UTIL} += 10,next if m:^(utils|x2p|h2pl)/:;
84902520 500 $c{PORT1}+= 15,next if m:^win32:;
501 $c{PORT2} += 15,next
2c2d71f5 502 if m:^(cygwin|os2|plan9|qnx|vms)/:
08aa1457 503 or m:^(hints|Porting|ext/DynaLoader)/:
504 or m:^README\.:;
b73f5677 505 $c{EXT} += 10,next
506 if m:^(ext|lib/ExtUtils)/:;
3e3baf6d 507 $c{LIB} += 10,next
b73f5677 508 if m:^(lib)/:;
3e3baf6d 509 $c{'CORE'} += 15,next
84902520 510 if m:^[^/]+[\._]([chH]|sym|pl)$:;
3e3baf6d 511 $c{BUILD} += 10,next
08aa1457 512 if m:^[A-Z]+$: or m:^[^/]+\.SH$:
84902520 513 or m:^(install|configure|configpm):i;
08aa1457 514 print "Couldn't categorise $_\n" if $::opt_v;
3e3baf6d 515 $c{OTHER} += 1;
516 }
517 if (keys %c > 1) { # sort to find category with highest score
518 refine:
519 ++$refine;
520 my @c = sort { $c{$b} <=> $c{$a} || $a cmp $b } keys %c;
521 my @v = map { $c{$_} } @c;
522 if (@v > 1 and $refine <= 1 and "@v" =~ /^(\d) \1/
523 and $c[0] =~ m/^(DOC|TESTS|OTHER)/) { # rare
524 print "Tie, promoting $c[1] over $c[0]\n" if $::opt_d;
525 ++$c{$c[1]};
526 goto refine;
527 }
528 print " ".@$files." patches: ", join(", ", map { "$_: $c{$_}" } @c),".\n"
529 if $verb;
530 return $c[0] || 'OTHER';
08aa1457 531 }
3e3baf6d 532 else {
533 my($c, $v) = %c;
b73f5677 534 $c ||= 'UNKNOWN'; $v ||= 0;
3e3baf6d 535 print " ".@$files." patches: $c: $v\n" if $verb;
536 return $c;
08aa1457 537 }
08aa1457 538}
539
540
541sub PATORDER { # PATORDER sort by Chip Salzenberg
542 my ($i, $j);
543
544 $i = ($a =~ m#^[A-Z]+$#);
545 $j = ($b =~ m#^[A-Z]+$#);
546 return $j - $i if $i != $j;
547
548 $i = ($a =~ m#configure|hint#i) || ($a =~ m#[S_]H$#);
549 $j = ($b =~ m#configure|hint#i) || ($b =~ m#[S_]H$#);
550 return $j - $i if $i != $j;
551
552 $i = ($a =~ m#\.pod$#);
553 $j = ($b =~ m#\.pod$#);
554 return $j - $i if $i != $j;
555
556 $i = ($a =~ m#include/#);
557 $j = ($b =~ m#include/#);
558 return $j - $i if $i != $j;
559
560 if ((($i = $a) =~ s#/+[^/]*$##)
561 && (($j = $b) =~ s#/+[^/]*$##)) {
562 return $i cmp $j if $i ne $j;
563 }
564
565 $i = ($a =~ m#\.h$#);
566 $j = ($b =~ m#\.h$#);
567 return $j - $i if $i != $j;
568
569 return $a cmp $b;
570}
571