Commit | Line | Data |
---|---|---|
86530b38 AT |
1 | #!/usr/bin/perl |
2 | # | |
3 | # `Diff' program in Perl | |
4 | # Copyright 1998 M-J. Dominus. (mjd-perl-diff@plover.com) | |
5 | # | |
6 | # This program is free software; you can redistribute it and/or modify it | |
7 | # under the same terms as Perl itself. | |
8 | # | |
9 | # Altered to output in `context diff' format (but without context) | |
10 | # September 1998 Christian Murphy (cpm@muc.de) | |
11 | # | |
12 | # Context lines feature added | |
13 | # Unified, "Old" (Standard UNIX), Ed diff added September 1998 | |
14 | # Reverse_Ed (-f option) added March 1999 | |
15 | # Amir D. Karger (karger@bead.aecom.yu.edu) | |
16 | # | |
17 | # Modular functions integrated into program | |
18 | # February 1999 M-J. Dominus (mjd-perl-diff@plover.com) | |
19 | # | |
20 | # In this file, "item" usually means "line of text", and "item number" usually | |
21 | # means "line number". But theoretically the code could be used more generally | |
22 | use strict; | |
23 | use Algorithm::Diff qw(diff); | |
24 | ||
25 | # GLOBAL VARIABLES #### | |
26 | # After we've read up to a certain point in each file, the number of items | |
27 | # we've read from each file will differ by $FLD (could be 0) | |
28 | my $File_Length_Difference = 0; | |
29 | ||
30 | #ed diff outputs hunks *backwards*, so we need to save hunks when doing ed diff | |
31 | my @Ed_Hunks = (); | |
32 | ######################## | |
33 | ||
34 | my $usage = << "ENDUSAGE"; | |
35 | Usage: $0 [{-c | -C lines -e | -f | -u | -U lines}] oldfile newfile | |
36 | -c do a context diff with 3 lines of context | |
37 | -C do a context diff with 'lines' lines of context (implies -c) | |
38 | -e create a script for the ed editor to change oldfile to newfile | |
39 | -f like -e but in reverse order | |
40 | -u do a unified diff with 3 lines of context | |
41 | -U do a unified diff with 'lines' lines of context (implies -u) | |
42 | -q report only whether or not the files differ | |
43 | ||
44 | By default it will do an "old-style" diff, with output like UNIX diff | |
45 | ENDUSAGE | |
46 | ||
47 | my $Context_Lines = 0; # lines of context to print. 0 for old-style diff | |
48 | my $Diff_Type = "OLD"; # by default, do standard UNIX diff | |
49 | my ($opt_c, $opt_u, $opt_e, $opt_f, $opt_q); | |
50 | while ($ARGV[0] =~ /^-/) { | |
51 | my $opt = shift; | |
52 | last if $opt eq '--'; | |
53 | if ($opt =~ /^-C(.*)/) { | |
54 | $Context_Lines = $1 || shift; | |
55 | $opt_c = 1; | |
56 | $Diff_Type = "CONTEXT"; | |
57 | } elsif ($opt =~ /^-c$/) { | |
58 | $Context_Lines = 3; | |
59 | $opt_c = 1; | |
60 | $Diff_Type = "CONTEXT"; | |
61 | } elsif ($opt =~ /^-e$/) { | |
62 | $opt_e = 1; | |
63 | $Diff_Type = "ED"; | |
64 | } elsif ($opt =~ /^-f$/) { | |
65 | $opt_f = 1; | |
66 | $Diff_Type = "REVERSE_ED"; | |
67 | } elsif ($opt =~ /^-U(.*)$/) { | |
68 | $Context_Lines = $1 || shift; | |
69 | $opt_u = 1; | |
70 | $Diff_Type = "UNIFIED"; | |
71 | } elsif ($opt =~ /^-u$/) { | |
72 | $Context_Lines = 3; | |
73 | $opt_u = 1; | |
74 | $Diff_Type = "UNIFIED"; | |
75 | } elsif ($opt =~ /^-q$/) { | |
76 | $Context_Lines = 0; | |
77 | $opt_q = 1; | |
78 | $opt_e = 1; | |
79 | $Diff_Type = "ED"; | |
80 | } else { | |
81 | $opt =~ s/^-//; | |
82 | bag("Illegal option -- $opt"); | |
83 | } | |
84 | } | |
85 | ||
86 | if ($opt_q and grep($_,($opt_c, $opt_f, $opt_u)) > 1) { | |
87 | bag("Combining -q with other options is nonsensical"); | |
88 | } | |
89 | ||
90 | if (grep($_,($opt_c, $opt_e, $opt_f, $opt_u)) > 1) { | |
91 | bag("Only one of -c, -u, -f, -e are allowed"); | |
92 | } | |
93 | ||
94 | bag($usage) unless @ARGV == 2; | |
95 | ||
96 | ######## DO THE DIFF! | |
97 | my ($file1, $file2) = @ARGV; | |
98 | ||
99 | my ($char1, $char2); # string to print before file names | |
100 | if ($Diff_Type eq "CONTEXT") { | |
101 | $char1 = '*' x 3; $char2 = '-' x 3; | |
102 | } elsif ($Diff_Type eq "UNIFIED") { | |
103 | $char1 = '-' x 3; $char2 = '+' x 3; | |
104 | } | |
105 | ||
106 | open (F1, $file1) or bag("Couldn't open $file1: $!"); | |
107 | open (F2, $file2) or bag("Couldn't open $file2: $!"); | |
108 | my (@f1, @f2); | |
109 | chomp(@f1 = <F1>); | |
110 | close F1; | |
111 | chomp(@f2 = <F2>); | |
112 | close F2; | |
113 | ||
114 | # diff yields lots of pieces, each of which is basically a Block object | |
115 | my $diffs = diff(\@f1, \@f2); | |
116 | exit 0 unless @$diffs; | |
117 | ||
118 | if ($opt_q and @$diffs) { | |
119 | print "Files $file1 and $file2 differ\n"; | |
120 | exit 1; | |
121 | } | |
122 | ||
123 | if ($Diff_Type =~ /UNIFIED|CONTEXT/) { | |
124 | my @st = stat($file1); | |
125 | my $MTIME = 9; | |
126 | print "$char1 $file1\t", scalar localtime($st[$MTIME]), "\n"; | |
127 | @st = stat($file2); | |
128 | print "$char2 $file2\t", scalar localtime($st[$MTIME]), "\n"; | |
129 | } | |
130 | ||
131 | my ($hunk,$oldhunk); | |
132 | # Loop over hunks. If a hunk overlaps with the last hunk, join them. | |
133 | # Otherwise, print out the old one. | |
134 | foreach my $piece (@$diffs) { | |
135 | $hunk = new Hunk ($piece, $Context_Lines); | |
136 | next unless $oldhunk; # first time through | |
137 | ||
138 | # Don't need to check for overlap if blocks have no context lines | |
139 | if ($Context_Lines && $hunk->does_overlap($oldhunk)) { | |
140 | $hunk->prepend_hunk($oldhunk); | |
141 | } else { | |
142 | $oldhunk->output_diff(\@f1, \@f2, $Diff_Type); | |
143 | } | |
144 | ||
145 | } continue { | |
146 | $oldhunk = $hunk; | |
147 | } | |
148 | ||
149 | # print the last hunk | |
150 | $oldhunk->output_diff(\@f1, \@f2, $Diff_Type); | |
151 | ||
152 | # Print hunks backwards if we're doing an ed diff | |
153 | map {$_->output_ed_diff(\@f1, \@f2, $Diff_Type)} @Ed_Hunks if @Ed_Hunks; | |
154 | ||
155 | exit 1; | |
156 | # END MAIN PROGRAM | |
157 | ||
158 | sub bag { | |
159 | my $msg = shift; | |
160 | $msg .= "\n"; | |
161 | warn $msg; | |
162 | exit 2; | |
163 | } | |
164 | ||
165 | ######## | |
166 | # Package Hunk. A Hunk is a group of Blocks which overlap because of the | |
167 | # context surrounding each block. (So if we're not using context, every | |
168 | # hunk will contain one block.) | |
169 | { | |
170 | package Hunk; | |
171 | ||
172 | sub new { | |
173 | # Arg1 is output from &LCS::diff (which corresponds to one Block) | |
174 | # Arg2 is the number of items (lines, e.g.,) of context around each block | |
175 | # | |
176 | # This subroutine changes $File_Length_Difference | |
177 | # | |
178 | # Fields in a Hunk: | |
179 | # blocks - a list of Block objects | |
180 | # start - index in file 1 where first block of the hunk starts | |
181 | # end - index in file 1 where last block of the hunk ends | |
182 | # | |
183 | # Variables: | |
184 | # before_diff - how much longer file 2 is than file 1 due to all hunks | |
185 | # until but NOT including this one | |
186 | # after_diff - difference due to all hunks including this one | |
187 | my ($class, $piece, $context_items) = @_; | |
188 | ||
189 | my $block = new Block ($piece); # this modifies $FLD! | |
190 | ||
191 | my $before_diff = $File_Length_Difference; # BEFORE this hunk | |
192 | my $after_diff = $before_diff + $block->{"length_diff"}; | |
193 | $File_Length_Difference += $block->{"length_diff"}; | |
194 | ||
195 | # @remove_array and @insert_array hold the items to insert and remove | |
196 | # Save the start & beginning of each array. If the array doesn't exist | |
197 | # though (e.g., we're only adding items in this block), then figure | |
198 | # out the line number based on the line number of the other file and | |
199 | # the current difference in file lenghts | |
200 | my @remove_array = $block->remove; | |
201 | my @insert_array = $block->insert; | |
202 | my ($a1, $a2, $b1, $b2, $start1, $start2, $end1, $end2); | |
203 | $a1 = @remove_array ? $remove_array[0 ]->{"item_no"} : -1; | |
204 | $a2 = @remove_array ? $remove_array[-1]->{"item_no"} : -1; | |
205 | $b1 = @insert_array ? $insert_array[0 ]->{"item_no"} : -1; | |
206 | $b2 = @insert_array ? $insert_array[-1]->{"item_no"} : -1; | |
207 | ||
208 | $start1 = $a1 == -1 ? $b1 - $before_diff : $a1; | |
209 | $end1 = $a2 == -1 ? $b2 - $after_diff : $a2; | |
210 | $start2 = $b1 == -1 ? $a1 + $before_diff : $b1; | |
211 | $end2 = $b2 == -1 ? $a2 + $after_diff : $b2; | |
212 | ||
213 | # At first, a hunk will have just one Block in it | |
214 | my $hunk = { | |
215 | "start1" => $start1, | |
216 | "start2" => $start2, | |
217 | "end1" => $end1, | |
218 | "end2" => $end2, | |
219 | "blocks" => [$block], | |
220 | }; | |
221 | bless $hunk, $class; | |
222 | ||
223 | $hunk->flag_context($context_items); | |
224 | ||
225 | return $hunk; | |
226 | } | |
227 | ||
228 | # Change the "start" and "end" fields to note that context should be added | |
229 | # to this hunk | |
230 | sub flag_context { | |
231 | my ($hunk, $context_items) = @_; | |
232 | return unless $context_items; # no context | |
233 | ||
234 | # add context before | |
235 | my $start1 = $hunk->{"start1"}; | |
236 | my $num_added = $context_items > $start1 ? $start1 : $context_items; | |
237 | $hunk->{"start1"} -= $num_added; | |
238 | $hunk->{"start2"} -= $num_added; | |
239 | ||
240 | # context after | |
241 | my $end1 = $hunk->{"end1"}; | |
242 | $num_added = ($end1+$context_items > $#f1) ? | |
243 | $#f1 - $end1 : | |
244 | $context_items; | |
245 | $hunk->{"end1"} += $num_added; | |
246 | $hunk->{"end2"} += $num_added; | |
247 | } | |
248 | ||
249 | # Is there an overlap between hunk arg0 and old hunk arg1? | |
250 | # Note: if end of old hunk is one less than beginning of second, they overlap | |
251 | sub does_overlap { | |
252 | my ($hunk, $oldhunk) = @_; | |
253 | return "" unless $oldhunk; # first time through, $oldhunk is empty | |
254 | ||
255 | # Do I actually need to test both? | |
256 | return ($hunk->{"start1"} - $oldhunk->{"end1"} <= 1 || | |
257 | $hunk->{"start2"} - $oldhunk->{"end2"} <= 1); | |
258 | } | |
259 | ||
260 | # Prepend hunk arg1 to hunk arg0 | |
261 | # Note that arg1 isn't updated! Only arg0 is. | |
262 | sub prepend_hunk { | |
263 | my ($hunk, $oldhunk) = @_; | |
264 | ||
265 | $hunk->{"start1"} = $oldhunk->{"start1"}; | |
266 | $hunk->{"start2"} = $oldhunk->{"start2"}; | |
267 | ||
268 | unshift (@{$hunk->{"blocks"}}, @{$oldhunk->{"blocks"}}); | |
269 | } | |
270 | ||
271 | ||
272 | # DIFF OUTPUT ROUTINES. THESE ROUTINES CONTAIN DIFF FORMATTING INFO... | |
273 | sub output_diff { | |
274 | # First arg is the current hunk of course | |
275 | # Next args are refs to the files | |
276 | # last arg is type of diff | |
277 | my $diff_type = $_[-1]; | |
278 | my %funchash = ("OLD" => \&output_old_diff, | |
279 | "CONTEXT" => \&output_context_diff, | |
280 | "ED" => \&store_ed_diff, | |
281 | "REVERSE_ED" => \&output_ed_diff, | |
282 | "UNIFIED" => \&output_unified_diff, | |
283 | ); | |
284 | if (exists $funchash{$diff_type}) { | |
285 | &{$funchash{$diff_type}}(@_); # pass in all args | |
286 | } else {die "unknown diff type $diff_type"} | |
287 | } | |
288 | ||
289 | sub output_old_diff { | |
290 | # Note that an old diff can't have any context. Therefore, we know that | |
291 | # there's only one block in the hunk. | |
292 | my ($hunk, $fileref1, $fileref2) = @_; | |
293 | my %op_hash = ('+' => 'a', '-' => 'd', '!' => 'c'); | |
294 | ||
295 | my @blocklist = @{$hunk->{"blocks"}}; | |
296 | warn ("Expecting one block in an old diff hunk!") if scalar @blocklist != 1; | |
297 | my $block = $blocklist[0]; | |
298 | my $op = $block->op; # +, -, or ! | |
299 | ||
300 | # Calculate item number range. | |
301 | # old diff range is just like a context diff range, except the ranges | |
302 | # are on one line with the action between them. | |
303 | my $range1 = $hunk->context_range(1); | |
304 | my $range2 = $hunk->context_range(2); | |
305 | my $action = $op_hash{$op} || warn "unknown op $op"; | |
306 | print "$range1$action$range2\n"; | |
307 | ||
308 | # If removing anything, just print out all the remove lines in the hunk | |
309 | # which is just all the remove lines in the block | |
310 | if ($block->remove) { | |
311 | my @outlist = @$fileref1[$hunk->{"start1"}..$hunk->{"end1"}]; | |
312 | map {$_ = "< $_\n"} @outlist; # all lines will be '< text\n' | |
313 | print @outlist; | |
314 | } | |
315 | ||
316 | print "---\n" if $op eq '!'; # only if inserting and removing | |
317 | if ($block->insert) { | |
318 | my @outlist = @$fileref2[$hunk->{"start2"}..$hunk->{"end2"}]; | |
319 | map {$_ = "> $_\n"} @outlist; # all lines will be '> text\n' | |
320 | print @outlist; | |
321 | } | |
322 | } | |
323 | ||
324 | sub output_unified_diff { | |
325 | my ($hunk, $fileref1, $fileref2) = @_; | |
326 | my @blocklist; | |
327 | ||
328 | # Calculate item number range. | |
329 | my $range1 = $hunk->unified_range(1); | |
330 | my $range2 = $hunk->unified_range(2); | |
331 | print "@@ -$range1 +$range2 @@\n"; | |
332 | ||
333 | # Outlist starts containing the hunk of file 1. | |
334 | # Removing an item just means putting a '-' in front of it. | |
335 | # Inserting an item requires getting it from file2 and splicing it in. | |
336 | # We splice in $num_added items. Remove blocks use $num_added because | |
337 | # splicing changed the length of outlist. | |
338 | # We remove $num_removed items. Insert blocks use $num_removed because | |
339 | # their item numbers---corresponding to positions in file *2*--- don't take | |
340 | # removed items into account. | |
341 | my $low = $hunk->{"start1"}; | |
342 | my $hi = $hunk->{"end1"}; | |
343 | my ($num_added, $num_removed) = (0,0); | |
344 | my @outlist = @$fileref1[$low..$hi]; | |
345 | map {s/^/ /} @outlist; # assume it's just context | |
346 | ||
347 | foreach my $block (@{$hunk->{"blocks"}}) { | |
348 | foreach my $item ($block->remove) { | |
349 | my $op = $item->{"sign"}; # - | |
350 | my $offset = $item->{"item_no"} - $low + $num_added; | |
351 | $outlist[$offset] =~ s/^ /$op/; | |
352 | $num_removed++; | |
353 | } | |
354 | foreach my $item ($block->insert) { | |
355 | my $op = $item->{"sign"}; # + | |
356 | my $i = $item->{"item_no"}; | |
357 | my $offset = $i - $hunk->{"start2"} + $num_removed; | |
358 | splice(@outlist,$offset,0,"$op$$fileref2[$i]"); | |
359 | $num_added++; | |
360 | } | |
361 | } | |
362 | ||
363 | map {s/$/\n/} @outlist; # add \n's | |
364 | print @outlist; | |
365 | ||
366 | } | |
367 | ||
368 | sub output_context_diff { | |
369 | my ($hunk, $fileref1, $fileref2) = @_; | |
370 | my @blocklist; | |
371 | ||
372 | print "***************\n"; | |
373 | # Calculate item number range. | |
374 | my $range1 = $hunk->context_range(1); | |
375 | my $range2 = $hunk->context_range(2); | |
376 | ||
377 | # Print out file 1 part for each block in context diff format if there are | |
378 | # any blocks that remove items | |
379 | print "*** $range1 ****\n"; | |
380 | my $low = $hunk->{"start1"}; | |
381 | my $hi = $hunk->{"end1"}; | |
382 | if (@blocklist = grep {$_->remove} @{$hunk->{"blocks"}}) { | |
383 | my @outlist = @$fileref1[$low..$hi]; | |
384 | map {s/^/ /} @outlist; # assume it's just context | |
385 | foreach my $block (@blocklist) { | |
386 | my $op = $block->op; # - or ! | |
387 | foreach my $item ($block->remove) { | |
388 | $outlist[$item->{"item_no"} - $low] =~ s/^ /$op/; | |
389 | } | |
390 | } | |
391 | map {s/$/\n/} @outlist; # add \n's | |
392 | print @outlist; | |
393 | } | |
394 | ||
395 | print "--- $range2 ----\n"; | |
396 | $low = $hunk->{"start2"}; | |
397 | $hi = $hunk->{"end2"}; | |
398 | if (@blocklist = grep {$_->insert} @{$hunk->{"blocks"}}) { | |
399 | my @outlist = @$fileref2[$low..$hi]; | |
400 | map {s/^/ /} @outlist; # assume it's just context | |
401 | foreach my $block (@blocklist) { | |
402 | my $op = $block->op; # + or ! | |
403 | foreach my $item ($block->insert) { | |
404 | $outlist[$item->{"item_no"} - $low] =~ s/^ /$op/; | |
405 | } | |
406 | } | |
407 | map {s/$/\n/} @outlist; # add \n's | |
408 | print @outlist; | |
409 | } | |
410 | } | |
411 | ||
412 | sub store_ed_diff { | |
413 | # ed diff prints out diffs *backwards*. So save them while we're generating | |
414 | # them, then print them out at the end | |
415 | my $hunk = shift; | |
416 | unshift @Ed_Hunks, $hunk; | |
417 | } | |
418 | ||
419 | sub output_ed_diff { | |
420 | # This sub is used for ed ('diff -e') OR reverse_ed ('diff -f'). | |
421 | # last arg is type of diff | |
422 | my $diff_type = $_[-1]; | |
423 | my ($hunk, $fileref1, $fileref2) = @_; | |
424 | my %op_hash = ('+' => 'a', '-' => 'd', '!' => 'c'); | |
425 | ||
426 | # Can't be any context for this kind of diff, so each hunk has one block | |
427 | my @blocklist = @{$hunk->{"blocks"}}; | |
428 | warn ("Expecting one block in an ed diff hunk!") if scalar @blocklist != 1; | |
429 | my $block = $blocklist[0]; | |
430 | my $op = $block->op; # +, -, or ! | |
431 | ||
432 | # Calculate item number range. | |
433 | # old diff range is just like a context diff range, except the ranges | |
434 | # are on one line with the action between them. | |
435 | my $range1 = $hunk->context_range(1); | |
436 | $range1 =~ s/,/ / if $diff_type eq "REVERSE_ED"; | |
437 | my $action = $op_hash{$op} || warn "unknown op $op"; | |
438 | print ($diff_type eq "ED" ? "$range1$action\n" : "$action$range1\n"); | |
439 | ||
440 | if ($block->insert) { | |
441 | my @outlist = @$fileref2[$hunk->{"start2"}..$hunk->{"end2"}]; | |
442 | map {s/$/\n/} @outlist; # add \n's | |
443 | print @outlist; | |
444 | print ".\n"; # end of ed 'c' or 'a' command | |
445 | } | |
446 | } | |
447 | ||
448 | sub context_range { | |
449 | # Generate a range of item numbers to print. Only print 1 number if the range | |
450 | # has only one item in it. Otherwise, it's 'start,end' | |
451 | # Flag is the number of the file (1 or 2) | |
452 | my ($hunk, $flag) = @_; | |
453 | my ($start, $end) = ($hunk->{"start$flag"},$hunk->{"end$flag"}); | |
454 | $start++; $end++; # index from 1, not zero | |
455 | my $range = ($start < $end) ? "$start,$end" : $end; | |
456 | return $range; | |
457 | } | |
458 | ||
459 | sub unified_range { | |
460 | # Generate a range of item numbers to print for unified diff | |
461 | # Print number where block starts, followed by number of lines in the block | |
462 | # (don't print number of lines if it's 1) | |
463 | my ($hunk, $flag) = @_; | |
464 | my ($start, $end) = ($hunk->{"start$flag"},$hunk->{"end$flag"}); | |
465 | $start++; $end++; # index from 1, not zero | |
466 | my $length = $end - $start + 1; | |
467 | my $first = $length < 2 ? $end : $start; # strange, but correct... | |
468 | my $range = $length== 1 ? $first : "$first,$length"; | |
469 | return $range; | |
470 | } | |
471 | } # end Package Hunk | |
472 | ||
473 | ######## | |
474 | # Package Block. A block is an operation removing, adding, or changing | |
475 | # a group of items. Basically, this is just a list of changes, where each | |
476 | # change adds or deletes a single item. | |
477 | # (Change could be a separate class, but it didn't seem worth it) | |
478 | { | |
479 | package Block; | |
480 | sub new { | |
481 | # Input is a chunk from &Algorithm::LCS::diff | |
482 | # Fields in a block: | |
483 | # length_diff - how much longer file 2 is than file 1 due to this block | |
484 | # Each change has: | |
485 | # sign - '+' for insert, '-' for remove | |
486 | # item_no - number of the item in the file (e.g., line number) | |
487 | # We don't bother storing the text of the item | |
488 | # | |
489 | my ($class,$chunk) = @_; | |
490 | my @changes = (); | |
491 | ||
492 | # This just turns each change into a hash. | |
493 | foreach my $item (@$chunk) { | |
494 | my ($sign, $item_no, $text) = @$item; | |
495 | my $hashref = {"sign" => $sign, "item_no" => $item_no}; | |
496 | push @changes, $hashref; | |
497 | } | |
498 | ||
499 | my $block = { "changes" => \@changes }; | |
500 | bless $block, $class; | |
501 | ||
502 | $block->{"length_diff"} = $block->insert - $block->remove; | |
503 | return $block; | |
504 | } | |
505 | ||
506 | ||
507 | # LOW LEVEL FUNCTIONS | |
508 | sub op { | |
509 | # what kind of block is this? | |
510 | my $block = shift; | |
511 | my $insert = $block->insert; | |
512 | my $remove = $block->remove; | |
513 | ||
514 | $remove && $insert and return '!'; | |
515 | $remove and return '-'; | |
516 | $insert and return '+'; | |
517 | warn "unknown block type"; | |
518 | return '^'; # context block | |
519 | } | |
520 | ||
521 | # Returns a list of the changes in this block that remove items | |
522 | # (or the number of removals if called in scalar context) | |
523 | sub remove { return grep {$_->{"sign"} eq '-'} @{shift->{"changes"}}; } | |
524 | ||
525 | # Returns a list of the changes in this block that insert items | |
526 | sub insert { return grep {$_->{"sign"} eq '+'} @{shift->{"changes"}}; } | |
527 | ||
528 | } # end of package Block |