File Coverage

blib/lib/FASTX/Reader.pm
Criterion Covered Total %
statement 229 273 83.8
branch 89 128 69.5
condition 38 62 61.2
subroutine 17 17 100.0
pod 7 7 100.0
total 380 487 78.0


line stmt bran cond sub pod time code
1             package FASTX::Reader;
2 39     39   2527724 use 5.012;
  39         461  
3 39     39   209 use warnings;
  39         72  
  39         1367  
4 39     39   230 use Carp qw(confess);
  39         81  
  39         1800  
5 39     39   18899 use Data::Dumper;
  39         199211  
  39         2137  
6 39     39   17469 use PerlIO::encoding;
  39         433435  
  39         1616  
7             $Data::Dumper::Sortkeys = 1;
8 39     39   17707 use FASTX::Seq;
  39         158  
  39         1888  
9 39     39   284 use File::Basename;
  39         85  
  39         3708  
10             $FASTX::Reader::VERSION = '1.11.0';
11             require Exporter;
12             our @ISA = qw(Exporter);
13              
14             #ABSTRACT: A simple module to parse FASTA and FASTQ files, supporting compressed files and paired-ends.
15              
16 39     39   246 use constant GZIP_SIGNATURE => pack('C3', 0x1f, 0x8b, 0x08);
  39         85  
  39         115694  
17              
18              
19             sub new {
20             # Instantiate object
21 73     73 1 28231 my $class = shift @_;
22 73         243 my $self = bless {} => $class;
23 73         161 my $args = {};
24            
25             # Named parameters: undefined $_[0] will read STDIN!
26 73 100 100     813 if (defined $_[0] and substr($_[0], 0, 1) eq '-') {
27 9         34 my %data = @_;
28             # Try parsing
29 9         32 for my $i (keys %data) {
30 14 100       97 if ($i =~ /^-(file|filename)/i) {
    100          
31 9         40 $args->{filename} = $data{$i};
32             } elsif ($i =~ /^-(loadseqs)/i) {
33 4         14 $args->{loadseqs} = $data{$i};
34             } else {
35 1         264 confess "[FASTX::Reader]: Unknown parameter `$i`\n";
36             }
37             }
38             } else {
39             # Legacy instantiation
40 64         207 ($args) = @_;
41             }
42            
43            
44            
45 72 100       276 if (defined $args->{loadseqs}) {
46 6 100 66     42 if ($args->{loadseqs} eq 'name' or $args->{loadseqs} eq 'names' ) {
    100 66        
    50 66        
47 4         12 $args->{loadseqs} = 'name';
48             } elsif ($args->{loadseqs} eq 'seq' or $args->{loadseqs} eq 'seqs' or $args->{loadseqs} eq "1") {
49 1         3 $args->{loadseqs} = 'seq';
50             } elsif ($args->{loadseqs} eq 'records') {
51 1         3 $args->{loadseqs} = 'records';
52             } else {
53 0         0 confess("attribute should be 'name' or 'seq' to specify the key of the hash.");
54             }
55             }
56            
57 72         365 $self->{filename} = $args->{filename};
58 72         199 $self->{loadseqs} = $args->{loadseqs};
59 72         231 $self->{aux} = [undef];
60 72         176 $self->{compressed} = 0;
61 72         135 $self->{fh} = undef;
62              
63              
64             # Check if a filename was provided and not {{STDIN}}
65             # uncoverable branch false
66              
67 72 50 66     1481 if ( defined $self->{filename} and -d $self->{filename} ) {
68 0         0 confess __PACKAGE__, " Directory provide where file was expected\n";
69             }
70              
71 72 100 66     595 if (defined $self->{filename} and $self->{filename} ne '{{STDIN}}') {
72 70 100       4241 open my $initial_fh, '<', $self->{filename} or confess "Unable to read file ", $self->{filename}, "\n";
73 66         4817 read( $initial_fh, my $magic_byte, 4 );
74 66         990 close $initial_fh;
75              
76             # See: __BioX::Seq::Stream__ for GZIP (and other) compressed file reader
77 66 100       1894 if (substr($magic_byte,0,3) eq GZIP_SIGNATURE) {
    50          
78 33         90 $self->{compressed} = 1;
79 33         188 our $GZIP_BIN = _which('pigz', 'gzip');
80             #close $fh;
81 33 50       11798 if (! defined $GZIP_BIN) {
82 0         0 $self->{decompressor} = 'IO::Uncompress::Gunzip';
83 0         0 require IO::Uncompress::Gunzip;
84 0         0 my $fh = IO::Uncompress::Gunzip->new($self->{filename}, MultiStream => 1);
85 0         0 $self->{fh} = $fh;
86             } else {
87 33         218 $self->{decompressor} = $GZIP_BIN;
88 33 50       79453 open my $fh, '-|', "$GZIP_BIN -dc $self->{filename}" or confess "Error opening gzip file ", $self->{filename}, ": $!\n";
89 33         1525 $self->{fh} = $fh;
90             }
91             } elsif (-B $self->{filename}) {
92              
93             # BINARY FILE NOT SUPPORTED?
94             #close $fh;
95 0         0 $self->{fh} = undef;
96 0         0 $self->{status} = 1;
97 0         0 $self->{message} = 'Binary file not supported';
98             } else {
99              
100             #close $fh;
101 33 50       1362 open (my $fh, '<:encoding(utf8):crlf', $self->{filename}) or confess "Unable to read file ", $self->{filename}, ": ", $!, "\n";
102 33         2703 $self->{fh} = $fh;
103             }
104              
105              
106             } else {
107             # Filename not provided, use STDIN
108 2         6 $self->{fh} = \*STDIN;
109 2 50       9 if ($self->{loadseqs}) {
110 0         0 confess("Load sequences not supported for STDIN");
111             }
112             }
113              
114              
115              
116              
117 68 100       475 if ($self->{loadseqs}) {
118 6         35 _load_seqs($self);
119             }
120              
121 68         1529 return $self;
122              
123             }
124              
125              
126             sub records {
127 1     1 1 1365 my $self = shift;
128 1 50       6 confess "No records loaded with -loadseqs => records!\n" unless $self->{loadseqs} eq 'records';
129 1         2 return $self->{seqs};
130             }
131              
132              
133             sub getRead {
134 181     181 1 17484 my $self = shift;
135              
136             #@::::::: :::
137              
138              
139 181 50 33     632 return if (defined $self->{status} and $self->{status} == 0);
140              
141             #my $aux = $self->{aux};
142 181         326 my $sequence_data;
143 181 50       259 @{ $self->{aux} } = [undef, 0] if (!(@{ $self->{aux} }));
  0         0  
  181         541  
144              
145              
146 181 100       497 if ($self->{aux}->[1]) {
147 15         46 $self->{return_0}++;
148             }
149              
150 181 100       441 if (!defined($self->{aux}->[0])) {
151 117         12214 while ($self->{line} = readline($self->{fh})) {
152              
153 101         763 chomp($self->{line});
154 101 100 100     869 if (substr($self->{line}, 0, 1) eq '>' || substr($self->{line}, 0, 1) eq '@') {
155 95         320 $self->{aux}->[0] = $self->{line};
156 95         251 last;
157             }
158             }
159 117 100       365 if (!defined($self->{aux}->[0])) {
160 22         69 $self->{aux}->[1] = 1;
161 22         46 $self->{return_1}++;
162 22         155 return;
163             }
164             }
165              
166              
167             # Comments can have more spaces:
168 159 50       368 return unless defined $self->{line};
169             my ($name, $comm) = $self->{line}=~/^.(\S+)(?:\s+)(.+)/ ? ($1, $2) :
170 159 50       2239 $self->{line}=~/^.(\S+)/ ? ($1, '') : ('?', '');
    100          
171 159         412 my $seq = '';
172 159         238 my $c;
173 159         317 $self->{aux}->[0] = undef;
174 159         743 while ($self->{line} = readline($self->{fh})) {
175             # PARSE SEQx
176 361         648 chomp($self->{line});
177 361         763 $c = substr($self->{line}, 0, 1);
178 361 100 66     1822 last if ($c eq '>' || $c eq '@' || $c eq '+');
      100        
179 220         1089 $seq .= $self->{line};
180             }
181 159         382 $self->{aux}->[0] = $self->{line};
182 159 100       479 $self->{aux}->[1] = 1 if (!defined($self->{aux}->[0]));
183 159         544 $sequence_data->{name} = $name;
184 159         406 $sequence_data->{comment} = $comm;
185 159         403 $sequence_data->{seq} = $seq;
186 159         516 $self->{counter}++;
187             # Return FASTA
188 159 100       397 if ($c ne '+') {
189 94         1251 $self->{return_fasta1}++;
190 94         303 return $sequence_data;
191             }
192 65         288 my $qual = '';
193              
194              
195 65         294 while ($self->{line} = readline($self->{fh})) {
196             # PARSE QUALITY
197 65         154 chomp($self->{line});
198 65         185 $qual .= $self->{line};
199 65 50       244 if (length($qual) >= length($seq)) {
200 65         139 $self->{aux}->[0] = undef;
201 65         131 $sequence_data->{name} = $name;
202 65         129 $sequence_data->{seq} = $seq;
203 65         218 $sequence_data->{comment} = $comm;
204 65         282 $sequence_data->{qual} = $qual;
205             # return FASTQ
206 65         253 $self->{return_fastq}++;
207 65         282 return $sequence_data;
208             }
209             }
210             # PROCH
211 0         0 close $self->{fh};
212              
213 0         0 $self->{aux}->[1] = 1;
214 0         0 $sequence_data->{name} = $name;
215 0         0 $sequence_data->{seq} = $seq;
216 0         0 $sequence_data->{comment} = $comm;
217 0         0 $self->{counter}++;
218             # return FASTA
219 0         0 $self->{return_fasta2}++;
220 0         0 return $sequence_data;
221              
222             }
223              
224              
225             sub next {
226 47     47 1 11576 my $self = shift;
227 47         171 my $scalar_read = $self->getRead();
228 47 100       118 return unless defined $scalar_read;
229             return FASTX::Seq->new( $scalar_read->{seq} // '',
230             $scalar_read->{name} // undef,
231             $scalar_read->{comment} // undef,
232 42   50     811 $scalar_read->{qual} // undef);
      50        
      50        
      100        
233            
234             }
235              
236              
237             sub getFastqRead {
238 7     7 1 2874 my $self = shift;
239 7         20 my $seq_object = undef;
240              
241 7 50 66     86 return if (defined $self->{status} and $self->{status} == 0);
242              
243 7         22 $self->{status} = 1;
244 7         632 my $header = readline($self->{fh});
245 7         84 my $seq = readline($self->{fh});
246 7         25 my $check = readline($self->{fh});
247 7         27 my $qual = readline($self->{fh});
248              
249              
250             # Check 4 lines were found (FASTQ)
251              
252 7 100       32 unless (defined $qual) {
253 1 50       5 if (defined $header) {
254 0         0 $self->{message} = "Unknown format: FASTQ truncated at " . $header . "?";
255 0         0 $self->{status} = 0;
256             }
257 1         4 return;
258             }
259              
260             # Fast format control: header and separator
261 6 100 66     59 if ( (substr($header, 0, 1) eq '@') and (substr($check, 0, 1) eq '+') ) {
262 5         26 chomp($header);
263 5         12 chomp($seq);
264 5         15 chomp($qual);
265             # Also control sequence integrity
266 5 100 66     90 if ($seq=~/^[ACGTNacgtn]+$/ and length($seq) == length($qual) ) {
267 4         37 my ($name, $comments) = split /\s+/, substr($header, 1);
268 4         20 $seq_object->{name} = $name;
269 4         8 $seq_object->{comments} = $comments;
270 4         12 $seq_object->{seq} = $seq;
271 4         20 $seq_object->{qual} = $qual;
272 4         15 $self->{counter}++;
273              
274             } else {
275             # Return error (corrupted FASTQ)
276 1         5 $self->{message} = "Unknown format: expecting FASTQ (corrupted?)";
277 1         3 $self->{status} = 0;
278              
279             }
280             } else {
281             # Return error (not a FASTQ)
282 1         3 $self->{message} = "Unknown format: expecting FASTQ but @ header not found";
283              
284 1 50       4 if (substr($header, 0,1 ) eq '>' ) {
285             # HINT: is FASTA?
286 1         4 $self->{message} .= " (might be FASTA instead)";
287             }
288 1         3 $self->{status} = 0;
289             }
290              
291 6         29 return $seq_object;
292             }
293              
294              
295             sub getIlluminaRead {
296 16     16 1 18259 my $self = shift;
297 16         35 my $seq_object = undef;
298              
299 16 50 66     83 return if (defined $self->{status} and $self->{status} == 0);
300              
301 16         37 $self->{status} = 1;
302 16         756 my $header = readline($self->{fh});
303 16         121 my $seq = readline($self->{fh});
304 16         62 my $check = readline($self->{fh});
305 16         62 my $qual = readline($self->{fh});
306              
307              
308             # Check 4 lines were found (FASTQ)
309              
310 16 100       57 unless (defined $qual) {
311 2 50       9 if (defined $header) {
312 0         0 $self->{message} = "Unknown format: FASTQ truncated at " . $header . "?";
313 0         0 $self->{status} = 0;
314             }
315 2         9 return;
316             }
317              
318             # Fast format control: header and separator
319 14 50 33     93 if ( (substr($header, 0, 1) eq '@') and (substr($check, 0, 1) eq '+') ) {
320 14         38 chomp($header);
321 14         41 chomp($seq);
322 14         21 chomp($qual);
323             # Also control sequence integrity
324 14 50 33     145 if ($seq=~/^[ACGTNacgtn]+$/ and length($seq) == length($qual) ) {
325 14         82 my ($name, $comments) = split /\s+/, substr($header, 1);
326             #@::::::: :::
327 14         66 my ($instrument, $run, $flowcell, $lane, $tile, $x, $y, $umi) = split /:/, $name;
328 14         37 my ($read, $filtered, $ctrl, $index1, $index2);
329              
330 14 50       29 if (not defined $y) {
331 0         0 $self->{message} = "Unknown format: not Illumina naming: ::::::";
332 0         0 $self->{status} = 0;
333             }
334              
335 14         38 $seq_object->{name} = $name;
336 14         26 $seq_object->{comments} = $comments;
337 14         28 $seq_object->{seq} = $seq;
338 14         25 $seq_object->{qual} = $qual;
339              
340 14         29 $seq_object->{instrument} = $instrument;
341 14         27 $seq_object->{run} = $run;
342 14         32 $seq_object->{flowcell} = $flowcell;
343 14         33 $seq_object->{lane} = $lane;
344 14         25 $seq_object->{tile} = $tile;
345 14         25 $seq_object->{x} = $x;
346 14         30 $seq_object->{y} = $y;
347 14         39 $seq_object->{umi} = $umi;
348 14         21 $seq_object->{instrument} = $instrument;
349 14         22 $seq_object->{run} = $run;
350 14         18 $seq_object->{flowcell} = $flowcell;
351              
352 14 100       30 if (defined $comments) {
353 7         39 ($read, $filtered, $ctrl, $index1, $index2) = split /[:+]/, $comments;
354 7 50       16 if ( defined $ctrl ) {
355 7         18 $seq_object->{read} = $read;
356 7 50       18 $filtered eq 'N' ? $seq_object->{filtered} = 0 : $seq_object->{filtered} = 1;
357 7         17 $seq_object->{control} = $ctrl;
358 7 50       11 if ($read eq '1') {
359 7         23 $seq_object->{index} = $index1;
360 7         15 $seq_object->{paired_index} = $index2;
361             } else {
362 0         0 $seq_object->{paired_index} = $index1;
363 0         0 $seq_object->{index} = $index2;
364             }
365             }
366             }
367 14         34 $self->{counter}++;
368              
369             } else {
370             # Return error (corrupted FASTQ)
371 0         0 $self->{message} = "Unknown format: expecting FASTQ (corrupted?)";
372 0         0 $self->{status} = 0;
373              
374             }
375             } else {
376             # Return error (not a FASTQ)
377 0         0 $self->{message} = "Unknown format: expecting FASTQ but @ header not found";
378              
379 0 0       0 if (substr($header, 0,1 ) eq '>' ) {
380             # HINT: is FASTA?
381 0         0 $self->{message} .= " (might be FASTA instead)";
382             }
383 0         0 $self->{status} = 0;
384             }
385              
386 14         53 return $seq_object;
387             }
388              
389             sub getFileFormat {
390 4     4 1 1290 my $self = shift;
391 4         10 my ($filename) = shift;
392 4 50       13 return 0 if (not defined $filename);
393              
394 4 50       157 open my $fh, '<', $filename or confess "Unable to read file ", $filename, "\n";
395 4         77 read( $fh, my $magic_byte, 4 );
396 4         54 close $fh;
397              
398 4 50       22 if (substr($magic_byte,0,3) eq GZIP_SIGNATURE) {
399             # GZIPPED FILE
400 0 0       0 if (! defined $self->{GZIP_BIN}) {
401 0         0 require IO::Uncompress::Gunzip;
402 0         0 $fh = IO::Uncompress::Gunzip->new($filename, MultiStream => 1);
403             } else {
404 0 0       0 open $fh, '-|', "$self->{GZIP_BIN} -dc $filename" or confess "Error opening gzip file ", $filename, ": $!\n";
405             }
406             } else {
407             # NOT COMPRESSED
408 4   33     141 open $fh, '<:encoding(utf8)', "$filename" || confess "Unable to read $filename\n$!\n";
409             }
410 4         334 my $first = readline($fh);
411 4 100       67 if (substr($first, 0,1) eq '>') {
    100          
412             #should be FASTA
413 1         20 return 'fasta';
414             } elsif (substr($first, 0, 1) eq '@') {
415             #should be fastq
416 2         7 readline($fh);
417 2         5 my $sep = readline($fh);
418 2 50       10 if ( substr($sep, 0, 1) eq '+' ) {
419             #second check for fastq
420 2         36 return 'fastq';
421             }
422             } else {
423             #unknown format
424 1         19 return;
425             }
426             }
427             sub _load_seqs {
428 6     6   26 my ($self) = @_;
429 6 50       22 return 0 unless (defined $self->{loadseqs});
430              
431 6         17 my $seqs = undef;
432 6         25 while (my $s = $self->getRead() ) {
433 22         61 my ($name, $seq) = ($s->{name}, $s->{seq});
434 22 100       52 if ($self->{loadseqs} eq 'name') {
    100          
435 12         64 $seqs->{$name} = $seq;
436             } elsif ($self->{loadseqs} eq 'seq') {
437 3         16 $seqs->{$seq} = $name;
438             } else {
439             my $r = FASTX::Seq->new(
440             -name => $name,
441             -seq => $seq,
442             -comment => $s->{comments},
443             -qual => $s->{qual},
444 7         74 );
445 7         18 push(@{$seqs}, $r);
  7         32  
446             }
447              
448             }
449 6         18 $self->{seqs} = $seqs;
450             }
451              
452              
453             sub _which {
454 33 50   33   332 return if ($^O eq 'MSWin32');
455 33         104 my $has_which = eval { require File::Which; File::Which->import(); 1};
  33         7980  
  33         18680  
  33         120  
456 33 50       97 if ($has_which) {
457 33         129 foreach my $cmd (@_) {
458 66 100       8093 return which($cmd) if (which($cmd));
459             }
460             } else {
461 0           foreach my $cmd (@_) {
462 0           my $exit;
463 0           eval {
464 0           `which $cmd 2> /dev/null`;
465 0           $exit = $?;
466             };
467 0 0 0       return $cmd if ($exit == 0 and not $@);
468             }
469             }
470 0           return;
471             }
472             1;
473              
474             __END__