File Coverage

bufralter.pl
Criterion Covered Total %
statement 141 168 83.9
branch 63 110 57.2
condition 4 9 44.4
subroutine 11 12 91.6
pod n/a
total 219 299 73.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             # (C) Copyright 2010-2025 MET Norway
4             #
5             # This program is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by
7             # the Free Software Foundation; either version 2 of the License, or
8             # (at your option) any later version.
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
16             # along with this program; if not, write to the Free Software
17             # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18             # 02110-1301, USA.
19              
20             # pod included at end of file
21              
22 3     3   14902 use strict;
  3         5  
  3         116  
23 3     3   12 use warnings;
  3         12  
  3         161  
24 3     3   2201 use Getopt::Long;
  3         46696  
  3         18  
25 3     3   1806 use Pod::Usage qw(pod2usage);
  3         269863  
  3         259  
26 3     3   4431 use Geo::BUFR;
  3         16  
  3         232  
27              
28             # This is actually default in BUFR.pm, but provided here to make it
29             # easier for users to change to 'ECCODES' if preferred
30 3     3   27 use constant DEFAULT_TABLE_FORMAT => 'BUFRDC';
  3         4  
  3         251  
31              
32             # Will be used if neither --tablepath nor $ENV{BUFR_TABLES} is set
33 3     3   17 use constant DEFAULT_TABLE_PATH_BUFRDC => '/usr/local/lib/bufrtables';
  3         4  
  3         155  
34 3     3   15 use constant DEFAULT_TABLE_PATH_ECCODES => '/usr/local/share/eccodes/definitions/bufr/tables';
  3         4  
  3         10800  
35              
36             # Parse command line options
37 3         482096 our %option = ();
38 3 50       33 GetOptions(
39             \%option,
40             'bufr_edition=i',
41             'category=i',
42             'centre=i',
43             'compress=i',
44             'data=s%',
45             'day=i',
46             'help',
47             'hour=i',
48             'int_subcategory=i',
49             'loc_subcategory=i',
50             'local_table_version=i',
51             'master_table_version=i',
52             'minute=i',
53             'month=i',
54             'observed=i',
55             'outfile=s',
56             'remove_qc',
57             'remove_sec2',
58             'second=i',
59             'strict_checking=i',
60             'subcategory=i',
61             'subcentre=i',
62             'tableformat=s',
63             'tablepath=s',
64             'update_number=i',
65             'verbose=i',
66             'year=i',
67             'year_of_century=i',
68             ) or pod2usage(-verbose => 0);
69              
70             # User asked for help
71 3 50       14793 pod2usage(-verbose => 1) if $option{help};
72              
73             # Make sure there is an input file
74 3 50       11 pod2usage(-verbose => 0) unless @ARGV == 1;
75              
76 3         7 my $infile = $ARGV[0];
77 3 50       234 open(my $IN, '<',$infile)
78             or die "Cannot open $infile: $!";
79              
80             # Default is to ignore 'recoverable' errors found in decoded or
81             # encoded BUFR format. This can be changed by setting strict_checking,
82             # which will then apply both to decoding and encoding.
83             my $strict_checking = defined $option{strict_checking}
84 3 50       18 ? $option{strict_checking} : 0;
85 3         33 Geo::BUFR->set_strict_checking($strict_checking);
86              
87             # Set verbosity level
88 3 50       9 Geo::BUFR->set_verbose($option{verbose}) if $option{verbose};
89              
90             # Set BUFR table format
91 3 100       11 my $tableformat = (defined $option{tableformat}) ? uc $option{tableformat} : DEFAULT_TABLE_FORMAT;
92 3         18 Geo::BUFR->set_tableformat($tableformat);
93              
94             # Set BUFR table path
95 3 50       8 if ($option{tablepath}) {
    0          
96             # Command line option --tablepath overrides all
97 3         17 Geo::BUFR->set_tablepath($option{tablepath});
98             } elsif ($ENV{BUFR_TABLES}) {
99             # If no --tablepath option, use the BUFR_TABLES environment variable
100 0         0 Geo::BUFR->set_tablepath($ENV{BUFR_TABLES});
101             } else {
102             # If all else fails, use the default tablepath in BUFRDC/ECCODES
103 0 0       0 if ($tableformat eq 'BUFRDC') {
    0          
104 0         0 Geo::BUFR->set_tablepath(DEFAULT_TABLE_PATH_BUFRDC);
105             } elsif ($tableformat eq 'ECCODES') {
106 0         0 Geo::BUFR->set_tablepath(DEFAULT_TABLE_PATH_ECCODES);
107             }
108             }
109              
110             # Where to print the altered BUFR message(s)
111 3         5 my $OUT;
112 3 50       9 if ($option{outfile}) {
113             open($OUT, '>', $option{outfile})
114 3 50       856 or die "Cannot open $option{outfile} for writing: $!";
115             } else {
116 0         0 $OUT = *STDOUT;
117             }
118 3         15 binmode($OUT);
119              
120             # Change input separator to 'BUFR'
121 3         10 my $oldeol = $/;
122 3         12 $/ = 'BUFR';
123              
124             # Read in everything before first 'BUFR'
125 3         47 my $out = <$IN>;
126              
127 3         23 while (my $msg = <$IN>) {
128             # Leave input unaltered if 'BUFR' is not start of a BUFR message
129 3 50       10 if (length($msg) < 4) {
130 0         0 $out .= $msg;
131 0         0 next;
132             }
133 3         15 my $len = unpack 'N', "\0$msg";
134 3 50 33     21 if ($len < 8 || $len > length($msg) + 4) {
135 0         0 $out .= $msg;
136 0         0 next;
137             }
138 3 50       16 if (substr($msg,$len-8,4) != '7777') {
139 0         0 $out .= $msg;
140 0         0 next;
141             }
142              
143             # 'BUFR' is quite probably start of a valid BUFR message, so
144             # transfer 'BUFR' from $out to $msg, transfer text following BUFR
145             # message from $msg to $out, and try to alter $msg. Input
146             # separator must be reverted before calling Geo::BUFR routines
147 3         7 chomp $out;
148 3         8 my $rest = substr($msg,$len-4);
149 3         8 $msg = 'BUFR' . substr($msg,0,$len-4);
150              
151 3         7 $/ = $oldeol;
152 3         23 my $bufr = Geo::BUFR->new($msg);
153              
154 3         14 $out .= alter($bufr);
155 3         8 $out .= $rest;
156              
157 3         18 $bufr->fclose();
158 3         140 $/ = 'BUFR';
159             }
160              
161 3 50       0 print $OUT $out if $out;
162              
163              
164             # Extract data from BUFR file, possibly alter the data, and write the
165             # new messages to STDOUT.
166             sub alter {
167 3     3   4 my $bufr = shift; # BUFR object
168              
169 3 50       10 if ($option{remove_qc}) {
170 0         0 Geo::BUFR->set_noqc();
171             }
172              
173 3         9 my $new_bufr = Geo::BUFR->new();
174 3         7 my @subset_data; # Will contain data values for subset 1,2...
175             my @subset_desc; # Will contain the set of descriptors for subset 1,2...
176              
177             READLOOP:
178 3         15 while (not $bufr->eof()) {
179              
180             # Read (and decode) next observation
181 5         23 my ($data, $descriptors) = $bufr->next_observation();
182 5         24 my $isub = $bufr->get_current_subset_number();
183 5         16 my $nsubsets = $bufr->get_number_of_subsets();
184              
185 5 100       12 if ($isub == 1) {
186 3         16 $new_bufr->copy_from($bufr,'metadata');
187 3         31 @subset_data = ();
188 3         7 @subset_desc = ();
189              
190 3         21 set_section1_data($bufr, $new_bufr);
191              
192 3 100       35 if (defined $option{observed}) {
193 1         6 $new_bufr->set_observed_data($option{observed});
194             }
195 3 100       23 if (defined $option{compress}) {
196 1         6 $new_bufr->set_compressed_data($option{compress});
197             }
198 3 50       9 if ($option{remove_sec2}) {
199 0         0 $new_bufr->set_optional_section(0);
200             }
201 3 50       10 if ($option{remove_qc}) {
202 0         0 remove_qc_from_unexpanded($new_bufr);
203             }
204             }
205              
206 5 50       13 if (defined $option{data}) {
207 5         56 DESCRIPTOR: while (my ($desc, $value) = each %{$option{data}}) {
  15         53  
208 10         24 for (my $i=0; $i < @$descriptors; $i++) {
209 115 100       251 if ($descriptors->[$i] == $desc) {
210 10 50       108 if ($value =~ /(.*)\+$/) {
    100          
211 0         0 $data->[$i] += $1;
212             } elsif ($value eq 'missing') {
213 5         11 $data->[$i] = undef;
214             } else {
215 5         13 $data->[$i] = $value;
216             }
217 10         43 next DESCRIPTOR;
218             }
219             }
220             }
221             }
222 5         11 $subset_data[$isub] = $data;
223 5         8 $subset_desc[$isub] = $descriptors;
224              
225 5 100       27 if ($isub == $nsubsets) {
226 3         16 return $new_bufr->encode_message(\@subset_data, \@subset_desc);
227             }
228             }
229             }
230              
231             sub set_section1_data {
232 3     3   8 my ($bufr, $new_bufr) = @_;
233              
234 3 50       15 if (defined $option{centre}) {
235 3         17 $new_bufr->set_centre($option{centre});
236             }
237 3 50       10 if (defined $option{subcentre}) {
238 3         14 $new_bufr->set_subcentre($option{subcentre});
239             }
240 3 50       24 if (defined $option{update_number}) {
241 3 100       15 if ($option{update_number} >= 0) {
242 1         7 $new_bufr->set_update_sequence_number($option{update_number});
243             } else {
244 2         6 my $old_number = $bufr->get_update_sequence_number();
245 2         3 my $update_number = $option{update_number};
246 2 50       11 if ($option{update_number} == -1) {
    0          
247 2         7 $new_bufr->set_update_sequence_number($old_number + 1);
248             } elsif ($option{update_number} == -2) {
249 0         0 $new_bufr->set_update_sequence_number($old_number - 1);
250             } else {
251 0         0 pod2usage(-verbose => 1);
252             }
253             }
254             }
255 3 50       21 if (defined $option{category}) {
256 3         34 $new_bufr->set_data_category($option{category});
257             }
258 3 50       10 if (defined $option{subcategory}) {
259 3         14 $new_bufr->set_data_subcategory($option{subcategory});
260             }
261 3 50       15 if (defined $option{int_subcategory}) {
262 0         0 $new_bufr->set_int_data_subcategory($option{int_subcategory});
263             }
264 3 50       12 if (defined $option{loc_subcategory}) {
265 0         0 $new_bufr->set_loc_data_subcategory($option{loc_subcategory});
266             }
267 3 100       21 if (defined $option{master_table_version}) {
268 2         9 $new_bufr->set_master_table_version($option{master_table_version});
269             }
270 3 100       10 if (defined $option{local_table_version}) {
271 2         9 $new_bufr->set_local_table_version($option{local_table_version});
272             }
273 3 100       8 if (defined $option{year}) {
274 2         11 $new_bufr->set_year($option{year});
275             }
276 3 50       9 if (defined $option{year_of_century}) {
277 0         0 $new_bufr->set_year_of_century($option{year_of_century});
278             }
279 3 50       8 if (defined $option{month}) {
280 3         15 $new_bufr->set_month($option{month});
281             }
282 3 50       6 if (defined $option{day}) {
283 3         16 $new_bufr->set_day($option{day});
284             }
285 3 50       9 if (defined $option{hour}) {
286 3         13 $new_bufr->set_hour($option{hour});
287             }
288 3 50       8 if (defined $option{minute}) {
289 3         14 $new_bufr->set_minute($option{minute});
290             }
291 3 50       10 if (defined $option{second}) {
292 0         0 $new_bufr->set_second($option{second});
293             }
294             # Should be processed last of the change metadata options,
295             # because setting of BUFR edition may depend on other
296             # metadata which user has opted to set
297 3 50       7 if (defined $option{bufr_edition}) {
298 3         12 set_bufr_edition($option{bufr_edition}, $bufr, $new_bufr);
299             }
300 3         7 return;
301             }
302              
303             sub remove_qc_from_unexpanded {
304 0     0   0 my $bufr = shift;
305 0         0 my $desc = $bufr->get_descriptors_unexpanded();
306 0         0 $desc =~ s/ 222000.*//;
307 0         0 $bufr->set_descriptors_unexpanded($desc);
308             }
309              
310             # If user hasn't provided the new metadata required for the new bufr
311             # edition, we make some educated guesses of these new metadata.
312             sub set_bufr_edition {
313 3     3   8 my ($new_bufr_edition, $bufr, $new_bufr) = @_;
314              
315 3         12 my $old_bufr_edition = $bufr->get_bufr_edition();
316              
317 3 100 66     23 if ($old_bufr_edition == 4 and $new_bufr_edition < 4) {
    50 33        
318 2 50       5 if (!defined $new_bufr->get_data_subcategory()) {
319 0         0 $new_bufr->set_data_subcategory($bufr->get_loc_data_subcategory());
320             }
321             # get_year_of_century() fetches from YEAR if YEAR_OF_CENTURY isn't set
322 2         7 $new_bufr->set_year_of_century($new_bufr->get_year_of_century());
323             } elsif ($old_bufr_edition < 4 and $new_bufr_edition == 4) {
324 1 50       68 if (!defined $new_bufr->get_loc_data_subcategory()) {
325 1         5 $new_bufr->set_loc_data_subcategory($bufr->get_data_subcategory());
326             }
327 1 50       5 if (!defined $new_bufr->get_int_data_subcategory()) {
328 1         5 $new_bufr->set_int_data_subcategory(255); # Undefined value
329             }
330 1 50       5 if (!defined $new_bufr->get_year()) {
331             # Should work most of the time
332 0         0 $new_bufr->set_year($bufr->get_year_of_century() + 2000);
333             }
334 1 50       7 if (!defined $new_bufr->get_second()) {
335 1         35 $new_bufr->set_second(0);
336             }
337             }
338              
339 3         23 $new_bufr->set_bufr_edition($new_bufr_edition);
340             }
341              
342             =pod
343              
344             =encoding utf8
345              
346             =head1 SYNOPSIS
347              
348             bufralter.pl
349             [--data ]
350             [--bufr_edition ]
351             [--centre ]
352             [--subcentre ]
353             [--update_number ]
354             [--category ]
355             [--subcategory ]
356             [--int_subcategory ]
357             [--loc_subcategory ]
358             [--master_table_version ]
359             [--local_table_version ]
360             [--year ]
361             [--year_of_century ]
362             [--month ]
363             [--day ]
364             [--hour ]
365             [--minute ]
366             [--second ]
367             [--observed 0|1]
368             [--compress 0|1]
369             [--remove_sec2]
370             [--remove_qc]
371             [--outfile ]
372             [--strict_checking n]
373             [--tableformat ]
374             [--tablepath ]
375             [--verbose n]
376             [--help]
377              
378             =head1 DESCRIPTION
379              
380             Will alter the BUFR messages in according to what is
381             specified by the options provided. The modified file (text surrounding
382             the BUFR messages will not be affected) will be printed to STDOUT
383             (unless C<--outfile> is set).
384              
385             Execute without arguments for Usage, with option C<--help> for some
386             additional info.
387              
388             =head1 OPTIONS
389              
390             --data Set (first) data value in section 4 for
391             descriptor. A trailing '+' means that the value
392             should be added to existing value. Use 'missing'
393             to set a missing value. Repeat the option if more
394             sequence descriptors are to be set. Example:
395             --data 004004=-1+ --data 004005=50 --data
396             012101=missing This will set the data value for
397             first (and only first!) occurrence of these 3
398             descriptors in every subset and every message in
399             to the given value (subtracting 1 from
400             the existing value for 004004)
401             --bufr_edition Set BUFR edition to . If the new edition
402             involves some metadata not present in the old edition,
403             some educated guesses for these new metadata are made,
404             but you should also consider setting these new metadata
405             explicitely
406             --centre Set originating centre to
407             --subcentre
408             Set originating subcentre to
409             --update_number
410             Set update sequence number to . Use the special
411             value -1 to increment existing update sequence number,
412             -2 to decrement it
413             --category Set data category to
414             --subcategory Set data sub-category to
415             --int_subcategory Set international data sub-category to
416             --loc_subcategory Set local data sub-category to
417             --master_table_version
418             Set master table version number to
419             --local_table_version
420             Set local table version number to
421             -- Set (= year | year_of_century | month |
422             day | hour | minute | second) in section 1 to
423             --observed 0|1 Set observed data in section 3 to 0 or 1
424             --compress 0|1 Set compression in section 3 to 0 or 1
425             --remove_sec2 Remove optional section 2 if present
426             --remove_qc Remove all quality control information,
427             i.e. remove all descriptors from 222000 on
428             --outfile
429             Will print to instead of STDOUT
430             --strict_checking n n=0 (default) Disable strict checking of BUFR format
431             n=1 Issue warning if (recoverable) error in
432             BUFR format
433             n=2 Croak if (recoverable) error in BUFR format.
434             Nothing more in this message will be
435             decoded/encoded.
436             --tableformat Currently supported are BUFRDC and ECCODES (default is BUFRDC)
437             --tablepath
438             Set path to BUFR tables (overrides $ENV{BUFR_TABLES})
439             --verbose n Set verbose level to n, 0<=n<=6 (default 0). Verbose
440             output is sent to STDOUT, so ought to be combined with
441             option --outfile
442             --help Display Usage and explain the options used. Almost
443             the same as consulting perldoc bufralter.pl
444              
445             Options may be abbreviated, e.g. C<--he> or C<-he> for C<--help>.
446              
447             To avoid having to use the C<--tablepath> option, you are adviced to
448             set the environment variable BUFR_TABLES to the directory where your
449             BUFR tables are located (unless the default path provided by
450             bufralter.pl works for you). For tableformat ECCODES, se
451             L
452             for more info on how to set C<--tablepath> (or BUFR_TABLES).
453              
454             =head1 AUTHOR
455              
456             Pål Sannes Epal.sannes@met.noE
457              
458             =head1 COPYRIGHT
459              
460             Copyright (C) 2010-2025 MET Norway
461              
462             =cut