1 #!/usr/bin/perl
3 # collectd - contrib/rrd_filter.px
4 # Copyright (C) 2007-2008 Florian octo Forster
5 #
6 # This program is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by the
8 # Free Software Foundation; only version 2 of the License is applicable.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 #
19 # Authors:
20 # Florian octo Forster <octo at verplant.org>
22 use strict;
23 use warnings;
25 =head1 NAME
27 rrd_filter.px - Perform same advanced non-standard operations on an RRD file.
29 =head1 SYNOPSYS
31 rrd_filter.px -i input.rrd -o output.rrd [options]
33 =head1 DEPENDENCIES
35 rrd_filter.px requires the RRDTool binary, Perl and the included
36 L<Getopt::Long> module.
38 =cut
40 use Getopt::Long ('GetOptions');
42 our $InFile;
43 our $InDS = [];
44 our $OutFile;
45 our $OutDS = [];
47 our $NewDSes = [];
48 our $NewRRAs = [];
50 our $Step = 0;
52 our $Scale = 1.0;
53 our $Shift = 0.0;
55 our $Debug = 0;
57 =head1 OPTIONS
59 The following options can be passed on the command line:
61 =over 4
63 =item B<--infile> I<file>
65 =item B<-i> I<file>
67 Reads from I<file>. If I<file> ends in C<.rrd>, then C<rrdtool dump> is invoked
68 to create an XML dump of the RRD file. Otherwise the XML dump is expected
69 directly. The special filename C<-> can be used to read from STDIN.
71 =item B<--outfile> I<file>
73 =item B<-o> I<file>
75 Writes output to I<file>. If I<file> ends in C<.rrd>, then C<rrdtool restore>
76 is invoked to create a binary RRD file. Otherwise an XML output is written. The
77 special filename C<-> can be used to write to STDOUT.
79 =item B<--map> I<in_ds>:I<out_ds>
81 =item B<-m> I<in_ds>:I<out_ds>
83 Writes the datasource I<in_ds> to the output and renames it to I<out_ds>. This
84 is useful to extract one DS from an RRD file.
86 =item B<--step> I<seconds>
88 =item B<-s> I<seconds>
90 Changes the step of the output RRD file to be I<seconds>. The new stepsize must
91 be a multiple of the old stepsize of the other way around. When increasing the
92 stepsize the number of PDPs in each RRA must be dividable by the factor by
93 which the stepsize is increased. The length of CDPs and the absolute length of
94 RRAs (and thus the data itself) is not altered.
96 Examples:
98 step = 10, rra_steps = 12 => step = 60, rra_steps = 2
99 step = 300, rra_steps = 1 => step = 10, rra_steps = 30
101 =item B<--rra> B<RRA>:I<CF>:I<XFF>:I<steps>:I<rows>
103 =item B<-a> B<RRA>:I<CF>:I<XFF>:I<steps>:I<rows>
105 Inserts a new RRA in the generated RRD file. This is done B<after> the step has
106 been adjusted, take that into account when specifying I<steps> and I<rows>. For
107 an explanation of the format please see L<rrdcreate(1)>.
109 =item B<--scale> I<factor>
111 Scales the values by the factor I<factor>, i.E<nbsp>e. all values are
112 multiplied by I<factor>.
114 =item B<--shift> I<offset>
116 Shifts all values by I<offset>, i.E<nbsp>e. I<offset> is added to all values.
118 =back
120 =cut
122 GetOptions ("infile|i=s" => \$InFile,
123 "outfile|o=s" => \$OutFile,
124 'map|m=s' => sub
125 {
126 my ($in_ds, $out_ds) = split (':', $_[1]);
127 if (!defined ($in_ds) || !defined ($out_ds))
128 {
129 print STDERR "Argument for `map' incorrect! The format is `--map in_ds:out_ds'\n";
130 exit (1);
131 }
132 push (@$InDS, $in_ds);
133 push (@$OutDS, $out_ds);
134 },
135 'step|s=i' => \$Step,
136 'ds|d=s' => sub
137 {
138 #DS:ds-name:GAUGE | COUNTER | DERIVE | ABSOLUTE:heartbeat:min:max
139 my ($ds, $name, $type, $hb, $min, $max) = split (':', $_[1]);
140 if (($ds ne 'DS') || !defined ($max))
141 {
142 print STDERR "Please use the standard RRDTool syntax when adding DSes. I. e. DS:<name>:<type>:<heartbeat>:<min>:<max>.\n";
143 exit (1);
144 }
145 push (@$NewDSes, {name => $name, type => $type, heartbeat => $hb, min => $min, max => $max});
146 },
147 'rra|a=s' => sub
148 {
149 my ($rra, $cf, $xff, $steps, $rows) = split (':', $_[1]);
150 if (($rra ne 'RRA') || !defined ($rows))
151 {
152 print STDERR "Please use the standard RRDTool syntax when adding RRAs. I. e. RRA:<cf><xff>:<steps>:<rows>.\n";
153 exit (1);
154 }
155 push (@$NewRRAs, {cf => $cf, xff => $xff, steps => $steps, rows => $rows});
156 },
157 'scale=f' => \$Scale,
158 'shift=f' => \$Shift
159 ) or exit (1);
161 if (!$InFile || !$OutFile)
162 {
163 print STDERR "Usage: $0 -i <infile> -m <in_ds>:<out_ds> -s <step>\n";
164 exit (1);
165 }
166 if ((1 + @$InDS) != (1 + @$OutDS))
167 {
168 print STDERR "You need the same amount of in- and out-DSes\n";
169 exit (1);
170 }
171 main ($InFile, $OutFile);
172 exit (0);
174 {
175 my $ds_index;
176 my $current_index;
177 # state 0 == searching for DS index
178 # state 1 == parse RRA header
179 # state 2 == parse values
180 my $state;
181 my $out_cache;
182 sub handle_line_dsmap
183 {
184 my $line = shift;
185 my $index = shift;
186 my $ret = '';
188 if ((@$InDS == 0) || (@$OutDS == 0))
189 {
190 post_line ($line, $index + 1);
191 return;
192 }
194 if (!defined ($state))
195 {
196 $current_index = -1;
197 $state = 0;
198 $out_cache = [];
200 # $ds_index->[new_index] = old_index
201 $ds_index = [];
202 for (my $i = 0; $i < @$InDS; $i++)
203 {
204 print STDOUT "DS map $i: $InDS->[$i] -> $OutDS->[$i]\n" if ($Debug);
205 $ds_index->[$i] = -1;
206 }
207 }
209 if ($state == 0)
210 {
211 if ($line =~ m/<ds>/)
212 {
213 $current_index++;
214 $out_cache->[$current_index] = $line;
215 }
216 elsif ($line =~ m#<name>\s*([^<\s]+)\s*</name>#)
217 {
218 # old_index == $current_index
219 # new_index == $i
220 for (my $i = 0; $i < @$InDS; $i++)
221 {
222 next if ($ds_index->[$i] >= 0);
224 if ($1 eq $InDS->[$i])
225 {
226 $line =~ s#<name>\s*([^<\s]+)\s*</name>#<name> $OutDS->[$i] </name>#;
227 $ds_index->[$i] = $current_index;
228 last;
229 }
230 }
232 $out_cache->[$current_index] .= $line;
233 }
234 elsif ($line =~ m#<last_ds>\s*([^\s>]+)\s*</last_ds>#i)
235 {
236 $out_cache->[$current_index] .= "\t\t<last_ds> NaN </last_ds>\n";
237 }
238 elsif ($line =~ m#<value>\s*([^\s>]+)\s*</value>#i)
239 {
240 $out_cache->[$current_index] .= "\t\t<value> NaN </value>\n";
241 }
242 elsif ($line =~ m#</ds>#)
243 {
244 $out_cache->[$current_index] .= $line;
245 }
246 elsif ($line =~ m#<rra>#)
247 {
248 # Print out all the DS definitions we need
249 for (my $new_index = 0; $new_index < @$InDS; $new_index++)
250 {
251 my $old_index = $ds_index->[$new_index];
252 while ($out_cache->[$old_index] =~ m/^(.*)$/gm)
253 {
254 post_line ("$1\n", $index + 1);
255 }
256 }
258 # Clear the cache - it's used in state1, too.
259 for (my $i = 0; $i <= $current_index; $i++)
260 {
261 $out_cache->[$i] = '';
262 }
264 $ret .= $line;
265 $current_index = -1;
266 $state = 1;
267 }
268 elsif ($current_index == -1)
269 {
270 # Print all the lines before the first DS definition
271 $ret .= $line;
272 }
273 else
274 {
275 # Something belonging to a DS-definition
276 $out_cache->[$current_index] .= $line;
277 }
278 }
279 elsif ($state == 1)
280 {
281 if ($line =~ m#<ds>#)
282 {
283 $current_index++;
284 $out_cache->[$current_index] .= $line;
285 }
286 elsif ($line =~ m#<value>\s*([^\s>]+)\s*</value>#i)
287 {
288 $out_cache->[$current_index] .= "\t\t\t<value> NaN </value>\n";
289 }
290 elsif ($line =~ m#</cdp_prep>#)
291 {
292 # Print out all the DS definitions we need
293 for (my $new_index = 0; $new_index < @$InDS; $new_index++)
294 {
295 my $old_index = $ds_index->[$new_index];
296 while ($out_cache->[$old_index] =~ m/^(.*)$/gm)
297 {
298 post_line ("$1\n", $index + 1);
299 }
300 }
302 # Clear the cache
303 for (my $i = 0; $i <= $current_index; $i++)
304 {
305 $out_cache->[$i] = '';
306 }
308 $ret .= $line;
309 $current_index = -1;
310 }
311 elsif ($line =~ m#<database>#)
312 {
313 $ret .= $line;
314 $state = 2;
315 }
316 elsif ($current_index == -1)
317 {
318 # Print all the lines before the first DS definition
319 # and after cdp_prep
320 $ret .= $line;
321 }
322 else
323 {
324 # Something belonging to a DS-definition
325 $out_cache->[$current_index] .= $line;
326 }
327 }
328 elsif ($state == 2)
329 {
330 if ($line =~ m#</database>#)
331 {
332 $ret .= $line;
333 $current_index = -1;
334 $state = 1;
335 }
336 else
337 {
338 my @values = ();
339 my $i;
341 $ret .= "\t\t";
343 if ($line =~ m#(<!-- .*? -->)#)
344 {
345 $ret .= "$1 ";
346 }
347 $ret .= "<row> ";
349 $i = 0;
350 while ($line =~ m#<v>\s*([^<\s]+)\s*</v>#g)
351 {
352 $values[$i] = $1;
353 $i++;
354 }
356 for (my $new_index = 0; $new_index < @$InDS; $new_index++)
357 {
358 my $old_index = $ds_index->[$new_index];
359 $ret .= '<v> ' . $values[$old_index] . ' </v> ';
360 }
361 $ret .= "</row>\n";
362 }
363 }
364 else
365 {
366 die;
367 }
369 if ($ret)
370 {
371 post_line ($ret, $index + 1);
372 }
373 }} # handle_line_dsmap
375 #
376 # The _step_ handler
377 #
378 {
379 my $step_factor_up;
380 my $step_factor_down;
381 sub handle_line_step
382 {
383 my $line = shift;
384 my $index = shift;
386 if (!$Step)
387 {
388 post_line ($line, $index + 1);
389 return;
390 }
392 if ($Debug && !defined ($step_factor_up))
393 {
394 print STDOUT "New step: $Step\n";
395 }
397 $step_factor_up ||= 0;
398 $step_factor_down ||= 0;
400 if (($step_factor_up == 0) && ($step_factor_down == 0))
401 {
402 if ($line =~ m#<step>\s*(\d+)\s*</step>#i)
403 {
404 my $old_step = 0 + $1;
405 if ($Step < $old_step)
406 {
407 $step_factor_down = int ($old_step / $Step);
408 if (($step_factor_down * $Step) != $old_step)
409 {
410 print STDERR "The old step ($old_step seconds) "
411 . "is not a multiple of the new step "
412 . "($Step seconds).\n";
413 exit (1);
414 }
415 $line = "<step> $Step </step>\n";
416 }
417 elsif ($Step > $old_step)
418 {
419 $step_factor_up = int ($Step / $old_step);
420 if (($step_factor_up * $old_step) != $Step)
421 {
422 print STDERR "The new step ($Step seconds) "
423 . "is not a multiple of the old step "
424 . "($old_step seconds).\n";
425 exit (1);
426 }
427 $line = "<step> $Step </step>\n";
428 }
429 else
430 {
431 $Step = 0;
432 }
433 }
434 }
435 elsif ($line =~ m#<pdp_per_row>\s*(\d+)\s*</pdp_per_row>#i)
436 {
437 my $old_val = 0 + $1;
438 my $new_val;
439 if ($step_factor_up)
440 {
441 $new_val = int ($old_val / $step_factor_up);
442 if (($new_val * $step_factor_up) != $old_val)
443 {
444 print STDERR "Can't divide number of PDPs per row ($old_val) by step-factor ($step_factor_up).\n";
445 exit (1);
446 }
447 }
448 else
449 {
450 $new_val = $step_factor_down * $old_val;
451 }
452 $line = "<pdp_per_row> $new_val </pdp_per_row>\n";
453 }
455 post_line ($line, $index + 1);
456 }} # handle_line_step
458 #
459 # The _add DS_ handler
460 #
461 {
462 my $add_ds_done;
463 sub handle_line_add_ds
464 {
465 my $line = shift;
466 my $index = shift;
468 my $post = sub { for (@_) { post_line ($_, $index + 1); } };
470 if (!@$NewDSes)
471 {
472 $post->($line);
473 return;
474 }
476 if (!$add_ds_done && ($line =~ m#<rra>#i))
477 {
478 for (my $i = 0; $i < @$NewDSes; $i++)
479 {
480 my $ds = $NewDSes->[$i];
481 my $temp;
483 my $min;
484 my $max;
486 if ($Debug)
487 {
488 print STDOUT "Adding DS: name = $ds->{'name'}, type = $ds->{'type'}, heartbeat = $ds->{'heartbeat'}, min = $ds->{'min'}, max = $ds->{'max'}\n";
489 }
491 $min = 'NaN';
492 if (defined ($ds->{'min'}) && ($ds->{'min'} ne 'U'))
493 {
494 $min = sprintf ('%.10e', $ds->{'min'});
495 }
497 $max = 'NaN';
498 if (defined ($ds->{'max'}) && ($ds->{'max'} ne 'U'))
499 {
500 $max = sprintf ('%.10e', $ds->{'max'});
501 }
504 $post->("\t<ds>\n",
505 "\t\t<name> $ds->{'name'} </name>\n",
506 "\t\t<type> $ds->{'type'} </type>\n",
507 "\t\t<minimal_heartbeat> $ds->{'heartbeat'} </minimal_heartbeat>\n",
508 "\t\t<min> $min </min>\n",
509 "\t\t<max> $max </max>\n",
510 "\n",
511 "\t\t<!-- PDP Status -->\n",
512 "\t\t<last_ds> UNKN </last_ds>\n",
513 "\t\t<value> NaN </value>\n",
514 "\t\t<unknown_sec> 0 </unknown_sec>\n",
515 "\t</ds>\n",
516 "\n");
517 }
519 $add_ds_done = 1;
520 }
521 elsif ($add_ds_done && ($line =~ m#</ds>#i)) # inside a cdp_prep block
522 {
523 $post->("\t\t\t</ds>\n",
524 "\t\t\t<ds>\n",
525 "\t\t\t<primary_value> NaN </primary_value>\n",
526 "\t\t\t<secondary_value> NaN </secondary_value>\n",
527 "\t\t\t<value> NaN </value>\n",
528 "\t\t\t<unknown_datapoints> 0 </unknown_datapoints>\n");
529 }
530 elsif ($line =~ m#<row>#i)
531 {
532 my $insert = '<v> NaN </v>' x (0 + @$NewDSes);
533 $line =~ s#</row>#$insert</row>#i;
534 }
536 $post->($line);
537 }} # handle_line_add_ds
539 #
540 # The _add RRA_ handler
541 #
542 {
543 my $add_rra_done;
544 my $num_ds;
545 sub handle_line_add_rra
546 {
547 my $line = shift;
548 my $index = shift;
550 my $post = sub { for (@_) { post_line ($_, $index + 1); } };
552 $num_ds ||= 0;
554 if (!@$NewRRAs || $add_rra_done)
555 {
556 $post->($line);
557 return;
558 }
560 if ($line =~ m#<ds>#i)
561 {
562 $num_ds++;
563 }
564 elsif ($line =~ m#<rra>#i)
565 {
566 for (my $i = 0; $i < @$NewRRAs; $i++)
567 {
568 my $rra = $NewRRAs->[$i];
569 my $temp;
571 if ($Debug)
572 {
573 print STDOUT "Adding RRA: CF = $rra->{'cf'}, xff = $rra->{'xff'}, steps = $rra->{'steps'}, rows = $rra->{'rows'}, num_ds = $num_ds\n";
574 }
576 $post->("\t<rra>\n",
577 "\t\t<cf> $rra->{'cf'} </cf>\n",
578 "\t\t<pdp_per_row> $rra->{'steps'} </pdp_per_row>\n",
579 "\t\t<params>\n",
580 "\t\t\t<xff> $rra->{'xff'} </xff>\n",
581 "\t\t</params>\n",
582 "\t\t<cdp_prep>\n");
584 for (my $j = 0; $j < $num_ds; $j++)
585 {
586 $post->("\t\t\t<ds>\n",
587 "\t\t\t\t<primary_value> NaN </primary_value>\n",
588 "\t\t\t\t<secondary_value> NaN </secondary_value>\n",
589 "\t\t\t\t<value> NaN </value>\n",
590 "\t\t\t\t<unknown_datapoints> 0 </unknown_datapoints>\n",
591 "\t\t\t</ds>\n");
592 }
594 $post->("\t\t</cdp_prep>\n", "\t\t<database>\n");
595 $temp = "\t\t\t<row>" . join ('', map { "<v> NaN </v>" } (1 .. $num_ds)) . "</row>\n";
596 for (my $j = 0; $j < $rra->{'rows'}; $j++)
597 {
598 $post->($temp);
599 }
600 $post->("\t\t</database>\n", "\t</rra>\n");
601 }
603 $add_rra_done = 1;
604 }
606 $post->($line);
607 }} # handle_line_add_rra
609 #
610 # The _scale/shift_ handler
611 #
612 sub calculate_scale_shift
613 {
614 my $value = shift;
615 my $tag = shift;
616 my $scale = shift;
617 my $shift = shift;
619 if (lc ("$value") eq 'nan')
620 {
621 $value = 'NaN';
622 return ("<$tag> NaN </$tag>");
623 }
625 $value = ($scale * (0.0 + $value)) + $shift;
626 return (sprintf ("<%s> %1.10e </%s>", $tag, $value, $tag));
627 }
629 sub handle_line_scale_shift
630 {
631 my $line = shift;
632 my $index = shift;
634 if (($Scale != 1.0) || ($Shift != 0.0))
635 {
636 $line =~ s#<(min|max|last_ds|value|primary_value|secondary_value|v)>\s*([^\s<]+)\s*</[^>]+>#calculate_scale_shift ($2, $1, $Scale, $Shift)#eg;
637 }
639 post_line ($line, $index + 1);
640 }
642 #
643 # The _output_ handler
644 #
645 # This filter is unfinished!
646 #
647 {
648 my $fh;
649 sub set_output
650 {
651 $fh = shift;
652 }
654 {
655 my $previous_values;
656 my $previous_differences;
657 my $pdp_per_row;
658 sub handle_line_peak_detect
659 {
660 my $line = shift;
661 my $index = shift;
663 if (!$previous_values)
664 {
665 $previous_values = [];
666 $previous_differences = [];
667 }
669 if ($line =~ m#</database>#i)
670 {
671 $previous_values = [];
672 $previous_differences = [];
673 print STDERR "==============================================================================\n";
674 }
675 elsif ($line =~ m#<pdp_per_row>\s*([1-9][0-9]*)\s*</pdp_per_row>#)
676 {
677 $pdp_per_row = int ($1);
678 print STDERR "pdp_per_row = $pdp_per_row;\n";
679 }
680 elsif ($line =~ m#<row>#)
681 {
682 my @values = ();
683 while ($line =~ m#<v>\s*([^\s>]+)\s*</v>#ig)
684 {
685 if ($1 eq 'NaN')
686 {
687 push (@values, undef);
688 }
689 else
690 {
691 push (@values, 0.0 + $1);
692 }
693 }
695 for (my $i = 0; $i < @values; $i++)
696 {
697 if (!defined ($values[$i]))
698 {
699 $previous_values->[$i] = undef;
700 }
701 elsif (!defined ($previous_values->[$i]))
702 {
703 $previous_values->[$i] = $values[$i];
704 }
705 elsif (!defined ($previous_differences->[$i]))
706 {
707 $previous_differences->[$i] = abs ($previous_values->[$i] - $values[$i]);
708 }
709 else
710 {
711 my $divisor = ($previous_differences->[$i] < 1.0) ? 1.0 : $previous_differences->[$i];
712 my $difference = abs ($previous_values->[$i] - $values[$i]);
713 my $change = $pdp_per_row * $difference / $divisor;
714 if (($divisor > 10.0) && ($change > 10e5))
715 {
716 print STDERR "i = $i; average difference = " . $previous_differences->[$i]. "; current difference = " . $difference. "; change = $change;\n";
717 }
718 $previous_values->[$i] = $values[$i];
719 $previous_differences->[$i] = (0.95 * $previous_differences->[$i]) + (0.05 * $difference);
720 }
721 }
722 }
724 post_line ($line, $index + 1);
725 }} # handle_line_peak_detect
727 sub handle_line_output
728 {
729 my $line = shift;
730 my $index = shift;
732 if (!defined ($fh))
733 {
734 post_line ($line, $index + 1);
735 return;
736 }
738 print $fh $line;
739 }} # handle_line_output
741 #
742 # Dispatching logic
743 #
744 {
745 my @handlers = ();
746 sub add_handler
747 {
748 my $handler = shift;
750 die unless (ref ($handler) eq 'CODE');
751 push (@handlers, $handler);
752 } # add_handler
754 sub post_line
755 {
756 my $line = shift;
757 my $index = shift;
759 if (0)
760 {
761 my $copy = $line;
762 chomp ($copy);
763 print "DEBUG: post_line ($copy, $index);\n";
764 }
766 if ($index > $#handlers)
767 {
768 return;
769 }
770 $handlers[$index]->($line, $index);
771 }} # post_line
773 sub handle_fh
774 {
775 my $in_fh = shift;
776 my $out_fh = shift;
778 set_output ($out_fh);
780 if (@$InDS)
781 {
782 add_handler (\&handle_line_dsmap);
783 }
785 if ($Step)
786 {
787 add_handler (\&handle_line_step);
788 }
790 if (($Scale != 1.0) || ($Shift != 0.0))
791 {
792 add_handler (\&handle_line_scale_shift);
793 }
795 #add_handler (\&handle_line_peak_detect);
797 if (@$NewDSes)
798 {
799 add_handler (\&handle_line_add_ds);
800 }
802 if (@$NewRRAs)
803 {
804 add_handler (\&handle_line_add_rra);
805 }
807 add_handler (\&handle_line_output);
809 while (my $line = <$in_fh>)
810 {
811 post_line ($line, 0);
812 }
813 } # handle_fh
815 sub main
816 {
817 my $in_file = shift;
818 my $out_file = shift;
820 my $in_fh;
821 my $out_fh;
823 my $in_needs_close = 1;
824 my $out_needs_close = 1;
826 if ($in_file =~ m/\.rrd$/i)
827 {
828 open ($in_fh, '-|', 'rrdtool', 'dump', $in_file) or die ("open (rrdtool): $!");
829 }
830 elsif ($in_file eq '-')
831 {
832 $in_fh = \*STDIN;
833 $in_needs_close = 0;
834 }
835 else
836 {
837 open ($in_fh, '<', $in_file) or die ("open ($in_file): $!");
838 }
840 if ($out_file =~ m/\.rrd$/i)
841 {
842 open ($out_fh, '|-', 'rrdtool', 'restore', '-', $out_file) or die ("open (rrdtool): $!");
843 }
844 elsif ($out_file eq '-')
845 {
846 $out_fh = \*STDOUT;
847 $out_needs_close = 0;
848 }
849 else
850 {
851 open ($out_fh, '>', $out_file) or die ("open ($out_file): $!");
852 }
854 handle_fh ($in_fh, $out_fh);
856 if ($in_needs_close)
857 {
858 close ($in_fh);
859 }
860 if ($out_needs_close)
861 {
862 close ($out_fh);
863 }
864 } # main
866 =head1 LICENSE
868 This script is licensed under the GNU general public license, versionE<nbsp>2
869 (GPLv2).
871 =head1 AUTHOR
873 Florian octo Forster E<lt>octo at verplant.orgE<gt>