ed05f998ab95f844c0cff84ca1c959589022d041
[sitka/sitka-tools.git] / marc_export_custom / marc_export_custom
1 #!/usr/bin/perl
2 # vim:et:sw=4:ts=4:
3 use strict;
4 use warnings;
5 use bytes;
6
7 use OpenSRF::System;
8 use OpenSRF::EX qw/:try/;
9 use OpenSRF::AppSession;
10 use OpenSRF::Utils::JSON;
11 use OpenSRF::Utils::SettingsClient;
12 use OpenILS::Application::AppUtils;
13 use OpenILS::Utils::Fieldmapper;
14 use OpenILS::Utils::CStoreEditor;
15
16 use MARC::Record;
17 use MARC::File::XML;
18 use UNIVERSAL::require;
19
20 use Time::HiRes qw/time/;
21 use Getopt::Long;
22
23 use Config::Simple;
24 use Data::Dumper;
25
26 my @formats = qw/USMARC UNIMARC XML BRE ARE/;
27
28 my ($config,$format,$encoding,$location,$dollarsign,$idl,$help,$holdings,$timeout,$export_mfhd,$type,$all_records) = ('/openils/conf/opensrf_core.xml','USMARC','MARC8','','$',0,undef,undef,0,undef,'biblio',undef);
29 my ($exclusion_ini,$collapse_to_depth, $output_file);
30 my $cfg;
31 my $force901;
32
33 GetOptions(
34         'help'       => \$help,
35         'items'      => \$holdings,
36         'mfhd'       => \$export_mfhd,
37         'all'        => \$all_records,
38         'location=s' => \$location,
39         'money=s'    => \$dollarsign,
40         'config=s'   => \$config,
41         'format=s'   => \$format,
42         'type=s'     => \$type,
43         'xml-idl=s'  => \$idl,
44         'encoding=s' => \$encoding,
45         'timeout=i'  => \$timeout,
46         'force901'  => \$force901,
47         'exclusion_ini=s' => \$exclusion_ini,
48         'collapse_to_depth=i' => \$collapse_to_depth,
49         'output-file=s' => \$output_file,
50 );
51
52 if ($exclusion_ini) {
53         die "exclusion ini file does not exist" unless (-r $exclusion_ini and -s $exclusion_ini);
54         $cfg = new Config::Simple($exclusion_ini) 
55 }
56
57 if ($help) {
58 print <<"HELP";
59 This script exports MARC authority, bibliographic, and serial holdings
60 records from an Evergreen database. 
61
62 Input to this script can consist of a list of record IDs, with one record ID
63 per line, corresponding to the record ID in the Evergreen database table of
64 your requested record type.
65
66 Alternately, passing the --all option will attempt to export all records of
67 the specified type from the Evergreen database. The --all option starts at
68 record ID 1 and increments the ID by 1 until the largest ID in the database
69 is retrieved. This may not be very efficient for databases with large gaps
70 in their ID sequences.
71
72 Usage: $0 [options]
73  --help or -h       This screen.
74  --config or -c     Configuration file [/openils/conf/opensrf_core.xml]
75  --format or -f     Output format (USMARC, UNIMARC, XML, BRE, ARE) [USMARC]
76  --encoding or -e   Output encoding (UTF-8, ISO-8859-?, MARC8) [MARC8]
77  --xml-idl or -x    Location of the IDL XML
78  --timeout          Timeout for exporting a single record; increase if you
79                     are using --holdings and are exporting records that
80                     have a lot of items attached to them.
81  --type or -t       Record type (BIBLIO, AUTHORITY) [BIBLIO]
82  --all or -a        Export all records; ignores input list
83
84  Additional options for type = 'BIBLIO':
85  --items or -i      Include items (holdings) in the output
86  --money            Currency symbol to use in item price field [\$]
87  --mfhd             Export serial MFHD records for associated bib records
88                     Not compatible with --format=BRE
89  --location or -l   MARC Location Code for holdings from
90                     http://www.loc.gov/marc/organizations/orgshome.html
91
92 Examples:
93
94 To export a set of USMARC records in a file named "output_file" based on the
95 IDs contained in a file named "list_of_ids":
96   cat list_of_ids | $0 > output_file
97
98 To export a set of MARC21XML authority records in a file named "output.xml"
99 for all authority records in the database:
100   $0 --format XML --type AUTHORITY --all > output.xml
101
102 HELP
103     exit;
104 }
105
106 $type = lc($type);
107 $format = uc($format);
108 $encoding = uc($encoding);
109
110 my $outfh;
111 my $real_stdout;
112 open($real_stdout, ">&STDOUT") or die "Can't dup STDOUT: $!";
113 if($output_file) {
114     open($outfh, '>', $output_file) or die "Can't open file for output $output_file: $!"; 
115 } else {
116     $outfh = $real_stdout;
117 }
118
119 binmode($outfh, ':raw') if ($encoding ne 'UTF-8');
120 binmode($outfh, ':utf8') if ($encoding eq 'UTF-8');
121
122 if (!grep { $format eq $_ } @formats) {
123     die "Please select a supported format.  ".
124         "Right now that means one of [".
125         join('|',@formats). "]\n";
126 }
127
128 if ($format ne 'XML') {
129     my $type = 'MARC::File::' . $format;
130     $type->require;
131 }
132
133 if ($timeout <= 0) {
134     # set default timeout and/or correct silly user who 
135     # supplied a negative timeout; default timeout of
136     # 300 seconds if exporting items determined empirically.
137     $timeout = $holdings ? 300 : 1;
138 }
139
140 OpenSRF::System->bootstrap_client( config_file => $config );
141
142 if (!$idl) {
143     $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
144 }
145
146 Fieldmapper->import(IDL => $idl);
147
148 my $ses = OpenSRF::AppSession->create('open-ils.cstore');
149 OpenILS::Utils::CStoreEditor::init();
150 my $editor = OpenILS::Utils::CStoreEditor->new();
151
152 print $outfh <<HEADER if ($format eq 'XML');
153 <?xml version="1.0" encoding="$encoding"?>
154 <collection xmlns='http://www.loc.gov/MARC21/slim'>
155 HEADER
156
157 my %orgs;
158 my %shelves;
159 my %statuses;
160 my %outypes;
161
162 my $flesh = {};
163
164 if ($holdings) {
165     get_bib_locations();
166 }
167
168 my $start = time;
169 my $last_time = time;
170 my %count = ('bib' => 0, 'did' => 0);
171 my $speed = 0;
172
173 if ($all_records) {
174     my $top_record = 0;
175     if ($type eq 'biblio') {
176         $top_record = $editor->search_biblio_record_entry([
177             {deleted => 'f'},
178             {order_by => { 'bre' => 'id DESC' }, limit => 1}
179         ])->[0]->id;
180     } elsif ($type eq 'authority') {
181         $top_record = $editor->search_authority_record_entry([
182             {deleted => 'f'},
183             {order_by => { 'are' => 'id DESC' }, limit => 1}
184         ])->[0]->id;
185     }
186     for (my $i = 0; $i++ < $top_record;) {
187         export_record($i);
188     }
189 } else {
190     while ( my $i = <> ) {
191         export_record($i);
192     }
193 }
194
195 print $outfh "</collection>\n" if ($format eq 'XML');
196
197 $speed = $count{did} / (time - $start);
198 my $time = time - $start;
199 print STDERR <<DONE;
200
201 Exports Attempted : $count{bib}
202 Exports Completed : $count{did}
203 Overall Speed     : $speed
204 Total Time Elapsed: $time seconds
205
206 DONE
207
208 sub export_record {
209     my $id = shift;
210
211     my $bib; 
212
213     my $r = $ses->request( "open-ils.cstore.direct.$type.record_entry.retrieve", $id, $flesh );
214     my $s = $r->recv(timeout => $timeout);
215     if (!$s) {
216         warn "\n!!!!! Failed trying to read record $id\n";
217         return;
218     }
219     if ($r->failed) {
220         warn "\n!!!!!! Failed trying to read record $id: " . $r->failed->stringify . "\n";
221         return;
222     }
223     if ($r->timed_out) {
224         warn "\n!!!!!! Timed out trying to read record $id\n";
225         return;
226     }
227     $bib = $s->content;
228     $r->finish;
229
230     $count{bib}++;
231     return unless $bib;
232
233     if ($format eq 'ARE' or $format eq 'BRE') {
234         print $outfh OpenSRF::Utils::JSON->perl2JSON($bib);
235         stats();
236         $count{did}++;
237         return;
238     }
239
240     try {
241
242         my $r = MARC::Record->new_from_xml( $bib->marc, $encoding, $format );
243         if ($type eq 'biblio') {
244             add_bib_holdings($bib, $r);
245         }
246
247         if($force901){
248             $r->delete_field( $r->field('901') );
249             $r->append_fields(
250                 MARC::Field->new(
251                     '901', ' ', ' ',
252                     a => $bib->tcn_value,
253                     b => $bib->tcn_source,
254                     c => $bib->id
255                 )
256             );
257         }
258
259         if ($format eq 'XML') {
260             my $xml = $r->as_xml_record;
261             $xml =~ s/^<\?.+?\?>$//mo;
262             print $outfh $xml;
263         } elsif ($format eq 'UNIMARC') {
264             print $outfh $r->as_usmarc;
265         } elsif ($format eq 'USMARC') {
266             print $outfh $r->as_usmarc;
267         }
268
269         $count{did}++;
270
271     } otherwise {
272         my $e = shift;
273         my $errorid = $id;
274         chomp($errorid);
275         chomp($e);
276         warn "\nERROR ON RECORD $errorid: $e\n";
277         import MARC::File::XML; # reset SAX parser so that one bad record doesn't kill the entire export
278     };
279
280     if ($export_mfhd and $type eq 'biblio') {
281         my $mfhds = $editor->search_serial_record_entry({record => $id, deleted => 'f'});
282         foreach my $mfhd (@$mfhds) {
283             try {
284                 my $r = MARC::Record->new_from_xml( $mfhd->marc, $encoding, $format );
285
286                 if($force901){
287                     $r->delete_field( $r->field('901') );
288                     $r->append_fields(
289                         MARC::Field->new(
290                             '901', ' ', ' ',
291                             a => $bib->tcn_value,
292                             b => $bib->tcn_source,
293                             c => $bib->id
294                         )
295                     );
296                 }
297
298                 if ($format eq 'XML') {
299                     my $xml = $r->as_xml_record;
300                     $xml =~ s/^<\?.+?\?>$//mo;
301                     print $outfh $xml;
302                 } elsif ($format eq 'UNIMARC') {
303                     print $outfh $r->as_usmarc;
304                 } elsif ($format eq 'USMARC') {
305                     print $outfh $r->as_usmarc;
306                 }
307             } otherwise {
308                 my $e = shift;
309                 my $errorid = chomp($id);
310                 chomp($e);
311                 warn "\nERROR ON MFHD RECORD $errorid: $e\n";
312                 import MARC::File::XML; # reset SAX parser so that one bad record doesn't kill the entire export
313             };
314         }
315     }
316
317     stats() if (! ($count{bib} % 50 ));
318 }
319
320 sub stats {
321     try {
322         no warnings;
323
324         $speed = $count{did} / (time - $start);
325
326         my $speed_now = ($count{did} - $count{did_last}) / (time - $count{time_last});
327         my $cn_speed = $count{cn} / (time - $start);
328         my $cp_speed = $count{cp} / (time - $start);
329
330         printf STDERR "\r  $count{did} of $count{bib} @  \%0.4f/s ttl / \%0.4f/s rt ".
331                 "($count{cn} CNs @ \%0.4f/s :: $count{cp} CPs @ \%0.4f/s)\r",
332                 $speed,
333                 $speed_now,
334                 $cn_speed,
335                 $cp_speed;
336     } otherwise {};
337     $count{did_last} = $count{did};
338     $count{time_last} = time;
339 }
340
341 sub get_bib_locations {
342     print STDERR "Retrieving Org Units ... ";
343     my $r = $ses->request( 'open-ils.cstore.direct.actor.org_unit.search', { id => { '!=' => undef } } );
344
345     while (my $o = $r->recv) {
346         die $r->failed->stringify if ($r->failed);
347         $o = $o->content;
348         last unless ($o);
349         $orgs{$o->id} = $o;
350     }
351     $r->finish;
352     print STDERR "OK\n";
353
354     print STDERR "Retrieving Copy statuses ... ";
355     $r = $ses->request( 'open-ils.cstore.direct.config.copy_status.search', { id => { '!=' => undef } } );
356
357     while (my $sta = $r->recv) {
358         die $r->failed->stringify if ($r->failed);
359         $sta = $sta->content;
360         last unless ($sta);
361         $statuses{$sta->id} = $sta;
362     }
363     $r->finish;
364     print STDERR "OK\n";
365
366     print STDERR "Retrieving OU types ... ";
367     $r = $ses->request( 'open-ils.cstore.direct.actor.org_unit_type.search', { id => { '!=' => undef } } );
368
369     while (my $outy = $r->recv) {
370         die $r->failed->stringify if ($r->failed);
371         $outy = $outy->content;
372         last unless ($outy);
373         $outypes{$outy->id} = $outy;
374     }
375     $r->finish;
376     print STDERR "OK\n";
377
378     print STDERR "Retrieving Shelving locations ... ";
379     $r = $ses->request( 'open-ils.cstore.direct.asset.copy_location.search', { id => { '!=' => undef } } );
380
381     while (my $s = $r->recv) {
382         die $r->failed->stringify if ($r->failed);
383         $s = $s->content;
384         last unless ($s);
385         $shelves{$s->id} = $s;
386     }
387     $r->finish;
388     print STDERR "OK\n";
389
390     $flesh = { flesh => 2, flesh_fields => { bre => [ 'call_numbers' ], acn => [ 'copies' ] } };
391 }
392
393 sub add_bib_holdings {
394     my $bib = shift;
395     my $r = shift;
396
397     my $cn_list = $bib->call_numbers;
398     if ($cn_list && @$cn_list) {
399
400         $count{cn} += @$cn_list;
401     
402         my $cp_list = [ map { @{ $_->copies } } @$cn_list ];
403         if ($cp_list && @$cp_list) {
404
405             my %cn_map;
406             push @{$cn_map{$_->call_number}}, $_ for (@$cp_list);
407                             
408             for my $cn ( @$cn_list ) {
409                 my $cn_map_list = $cn_map{$cn->id};
410
411                 COPYMAP: for my $cp ( @$cn_map_list ) {
412                     $count{cp}++;
413
414
415                     my $owninglib = $cn->owning_lib;
416                     my $circlib = $cp->circ_lib;
417                     my $printlib = $cp->circ_lib;
418
419                     if($cfg){
420                         my $thisorg = $orgs{$circlib};
421
422                         if($collapse_to_depth){
423                             while ( $outypes{ $thisorg->ou_type }->depth > $collapse_to_depth ){
424                                 my $localcfg = $cfg->param(-block=> $thisorg->shortname);
425                                 if( $localcfg->{'DontCollapse'} ){
426                                     last;
427                                 }
428                                 if($thisorg->parent_ou){
429                                     $thisorg = $orgs{$thisorg->parent_ou};
430                                     $printlib = $thisorg->id;
431                                 }
432                             }
433                         }
434
435                         $thisorg = $orgs{$circlib};
436
437
438                         while( $thisorg ){
439                             # load the local config from the .ini file for exclusions
440                             my $localcfg = $cfg->param(-block=> $thisorg->shortname);
441
442                             # if we see this setting, just skip that org
443
444                             if( $localcfg->{'ExcludeEntireOrg'} ) 
445                             { skipnote($bib->id,"ExcludeEntireOrg"); next COPYMAP; } 
446
447                             # what follows are exclusion rules
448                     
449                             # Excluded Flags
450                             if($localcfg->{'Flags'}){
451                                 # this little line is just forcing scalars into an array so we can 'use strict' with Config::Simple
452                                 my @flags = ( (ref($localcfg->{'Flags'}) eq "ARRAY") ? @{$localcfg->{'Flags'}} : ($localcfg->{'Flags'}));
453                                 if( grep { $_ eq 'reference' } @flags && $cp->ref eq 't')
454                                 { skipnote($bib->id,"Flags: reference"); next COPYMAP; } 
455                                 elsif( grep { $_ eq 'unholdable' } @flags && $cp->holdable eq 'f')
456                                 { skipnote($bib->id,"Flags: unholdable"); next COPYMAP; } 
457                                 elsif( grep { $_ eq 'circulate' } @flags && $cp->circulate eq 'f')
458                                 { skipnote($bib->id,"Flags: circulate"); next COPYMAP; } 
459                                 elsif( grep { $_ eq 'hidden' } @flags && $cp->opac_visible eq 'f')
460                                 { skipnote($bib->id,"Flags: hidden"); next COPYMAP; } 
461                             }
462                             # Excluded Circ Modifiers
463                             if($localcfg->{'CircMods'}){
464                                 my $circmod = $cp->circ_modifier || "";
465                                 my @circmods = ( (ref($localcfg->{'CircMods'}) eq "ARRAY") ? @{$localcfg->{'CircMods'}} : ($localcfg->{'CircMods'}) );
466                                 if( grep { $_ eq $circmod } @circmods && @circmods)
467                                 { skipnote($bib->id,"CircMods"); next COPYMAP; } 
468                             }
469                             # Inverse rule -- only include specified Circ Mods
470                             if($localcfg->{'OnlyIncludeCircMods'}){
471                                 my $circmod = $cp->circ_modifier || "";
472                                 my @circmods = ( (ref($localcfg->{'CircMods'}) eq "ARRAY") ? @{$localcfg->{'CircMods'}} : ($localcfg->{'CircMods'}) );
473                                 unless( grep { $_ eq $circmod } @circmods && @circmods)
474                                 { skipnote($bib->id,"OnlyIncludeCircMods"); next COPYMAP; } 
475                             }
476                             # Excluded Copy Statuses
477                             if($localcfg->{'Statuses'}){
478                                 my @statuses = ( (ref($localcfg->{'Statuses'}) eq "ARRAY") ? @{$localcfg->{'Statuses'}} : ($localcfg->{'Statuses'}) );
479                                 if( grep { $_ eq $statuses{$cp->status}->name } @statuses && @statuses)
480                                 { skipnote($bib->id,"Statuses"); next COPYMAP; } 
481                             }
482                             # Excluded Locations
483                             if($localcfg->{'Locations'}){
484                                 my @locations = ( (ref($localcfg->{'Locations'}) eq "ARRAY") ? @{$localcfg->{'Locations'}} : ($localcfg->{'Locations'}) );
485                                 if( grep { $_ eq $shelves{$cp->location}->name } @locations && @locations)
486                                 { skipnote($bib->id,"Locations"); next COPYMAP; }
487                             }
488                             # Inverse rule - Only use the specified locations
489                             if($localcfg->{'OnlyIncludeLocations'}){
490                                 my @locations = ( (ref($localcfg->{'OnlyIncludeLocations'}) eq "ARRAY") ? @{$localcfg->{'OnlyIncludeLocations'}} : ($localcfg->{'Locations'}) );
491                                 unless( grep { $_ eq $shelves{$cp->location}->name } @locations && @locations)
492                                 { skipnote($bib->id,"OnlyIncludeLocations"); next COPYMAP; } 
493                             }
494                             # exclude based on a regex match to location names
495                             if($localcfg->{'LocationRegex'}){
496                                 my @locregex = ( (ref($localcfg->{'LocationRegex'}) eq "ARRAY") ? @{$localcfg->{'LocationRegex'}} : ($localcfg->{'LocationRegex'}) );
497                                 my $reg = $localcfg->{'LocationRegex'};
498                                 if( grep { $shelves{$cp->location}->name =~ m/($reg)/ } @locregex && @locregex)
499                                 { skipnote($bib->id,"LocationRegex"); next COPYMAP; }
500                             }
501                             # include based on a regex match to location names
502                             if($localcfg->{'OnlyIncludeLocationRegex'}){
503                                 my @locregex = ( (ref($localcfg->{'OnlyIncludeLocationRegex'}) eq "ARRAY") ? @{$localcfg->{'OnlyIncludeLocationRegex'}} : ($localcfg->{'OnlyIncludeLocationRegex'}) );
504                                 my $reg = $localcfg->{'OnlyIncludeLocationRegex'};
505                                 unless( grep { $shelves{$cp->location}->name =~ m/($reg)/ } @locregex && @locregex)
506                                 { skipnote($bib->id,"OnlyIncludeLocationRegex"); next COPYMAP; } 
507                             }
508                             # Exclude based on a callno regex
509                             if($localcfg->{'CallNoRegex'}){
510                                 my @callnoregex = ( (ref($localcfg->{'CallNoRegex'}) eq "ARRAY") ? @{$localcfg->{'CallNoRegex'}} : ($localcfg->{'CallNoRegex'}) );
511                                 my $reg = $localcfg->{'CallNoRegex'};
512                                 if( grep { $cn->label =~ m/($reg)/ } @callnoregex && @callnoregex)
513                                 { skipnote($bib->id,"CallNoRegex"); next COPYMAP; }
514                             }
515                             # Include based on a callno regex
516                             if($localcfg->{'OnlyIncludeCallNoRegex'}){
517                                 my @callnoregex = ( (ref($localcfg->{'OnlyIncludeCallNoRegex'}) eq "ARRAY") ? @{$localcfg->{'OnlyIncludeCallNoRegex'}} : ($localcfg->{'OnlyIncludeCallNoRegex'}) );
518                                 my $reg = $localcfg->{'OnlyIncludeCallNoRegex'};
519                                 unless( grep { $cn->label =~ m/($reg)/ } @callnoregex && @callnoregex)
520                                 { skipnote($bib->id,"OnlyIncludeCallNoRegex"); next COPYMAP; } 
521                             }
522
523                             # Trim call number to a float and exclude based on Dewey Range
524                             if($localcfg->{'DeweyGT'} || $localcfg->{'DeweyLT'}){
525                                 my $gt = $localcfg->{'DeweyGT'};
526                                 my $lt = $localcfg->{'DeweyLT'};
527
528                                 # FIXME if either config has an array just ditch for now
529                                 if (ref($gt) eq "ARRAY" or ref($lt) eq "ARRAY")
530                                 { skipnote($bib->id,""); next COPYMAP; } 
531                                 $gt =~ s/[^0-9\.]//g if $gt; #trim off anything not deweyish
532                                 $lt =~ s/[^0-9\.]//g if $lt; #trim off anything not deweyish
533
534                                 my $callno = $cn->label;
535                                 $callno =~ s/[^0-9\.]//g; #trim off anything not deweyish
536                                 print STDERR $callno;
537                                 #note that we are making big assumptions about the call numbers in the db 
538
539                                 # we have a range, exclude what's inbetween
540                                 if($lt && $gt){
541                                     if($callno > $gt and $callno < $lt)
542                                     { skipnote($bib->id,"Dewey LTGT"); next COPYMAP; } 
543                                 # we only have a top threshold, exclude everything below it
544                                 } elsif ($lt){
545                                     if($callno < $lt)
546                                     { skipnote($bib->id,"Dewey LT"); next COPYMAP; }
547                                 # we only have a bottom threshold, exclude everything above it
548                                 } elsif ($gt){
549                                     if($callno > $gt)
550                                     { skipnote($bib->id,"Dewey GT"); next COPYMAP; } 
551                                 }
552                             }
553
554                             if($thisorg->parent_ou){
555                                  $thisorg = $orgs{$thisorg->parent_ou}
556                             } else {
557                                 $thisorg = ();
558                             }
559                             
560                         }
561                     }
562
563                     $r->append_fields(
564                         MARC::Field->new(
565                             852, '4', '', 
566                             a => $location,
567                             b => $orgs{$printlib}->shortname,
568                             #b => $orgs{$owninglib}->shortname,
569                             #b => $orgs{$circlib}->shortname,
570                             c => $shelves{$cp->location}->name,
571                             j => $cn->label,
572                             ($cp->circ_modifier ? ( g => $cp->circ_modifier ) : ()),
573                             p => $cp->barcode,
574                             ($cp->price ? ( y => $dollarsign.$cp->price ) : ()),
575                             ($cp->copy_number ? ( t => $cp->copy_number ) : ()),
576                             ($cp->ref eq 't' ? ( x => 'reference' ) : ()),
577                             ($cp->holdable eq 'f' ? ( x => 'unholdable' ) : ()),
578                             ($cp->circulate eq 'f' ? ( x => 'noncirculating' ) : ()),
579                             ($cp->opac_visible eq 'f' ? ( x => 'hidden' ) : ()),
580                             z => $statuses{$cp->status}->name,
581                         )
582                     );
583
584
585
586                     stats() if (! ($count{cp} % 100 ));
587                 } # for cnmap
588             } # for cnlist
589         } # if block
590     } # if block
591 } # sub
592
593 sub skipnote { 
594         my $id = shift;
595         my $note = shift;
596         my $outf = *STDERR;
597         $outf = *STDOUT if($output_file) ;
598         printf($outf "Skipped %s due to config: %s\n",$id,$note); 
599 }