Table of Contents
Back to Blog

Perl Deep Dive

How to Count Lines in a File in Perl (And the $. Variable Nobody Fully Explains)

Count lines in a file in Perl — using $., while loops, sysread, and wc -l. Covers the $. not-reset trap across multiple files, IO::File input_line_number, and high-performance byte scanning for bioinformatics and sysadmin use cases.

Perl 5.38IO::FileUnix wc -l
Published: May 14, 2026Updated: May 14, 202612 min readAuthor: Line Counter Editorial Team
PerlBioinformaticsSysadminFile I/OIO::File

Perl has a built-in line-number variable that looks almost too good to be true:

open(my $fh, '<', 'data.txt') or die $!;
1 while <$fh>;
print $.;

For one file, that is often fine.

The trap starts when you assume the perl $. variable behaves like a simple per-file counter that resets itself whenever you conceptually move to the next file.

It does not.

Perl's own docs say:

  • each filehandle keeps its own line counter
  • $. aliases the counter for the last filehandle you accessed
  • $. resets when the filehandle is closed
  • reopening an already open handle without an intervening close does not reset it
  • <> across @ARGV files keeps increasing because it does not explicitly close between files

That is why perl count lines multiple files is where many short examples go wrong.

This guide covers the practical perl count lines choices:

  • the perl $. variable for compact single-file counts
  • manual while loops for the safest default
  • perl wc -l for Unix speed
  • perl sysread count lines for byte-level performance
  • perl IO::File input_line_number for handle-specific tracking
  • close ARGV if eof for AWK-style per-file resets

If you searched for perl count lines in file, the short answer is:

  • safest general-purpose code: my $count = 0; $count++ while <$fh>;
  • one-file shorthand: 1 while <$fh>; my $count = $.;
  • many <> input files: close ARGV if eof to reset per file
  • large Unix file and only need the number: wc -l or raw sysread

That is the real count lines perl decision tree: decide first whether you want magic-variable brevity, predictable resource behavior, or raw speed.

Quick Method Guide

I want to...Use thisMain warning
Count one small local file1 while <$fh>; my $count = $.;the handle must close before you expect reset semantics
Use the safest general defaultmy $count = 0; $count++ while <$fh>;more typing, fewer surprises
Count many <> files separatelyclose ARGV if eof in a continue blockeof and eof() mean different things
Keep line numbers per handleIO::File plus input_line_numberobject style is more verbose
Use Unix speedwc -lcounts newline characters, not logical trailing lines
Count huge files in pure Perlsysread plus tr/\n/\n/byte semantics only, use :raw

For most perl count lines in file code that will live longer than a one-liner, a manual counter is still the best default.

Method 1: The $. Variable - Elegant but Tricky

Here is the classic compact version:

use strict;
use warnings;

open(my $fh, '<', 'data.txt') or die "Cannot open data.txt: $!";
1 while <$fh>;
my $count = $.;
close($fh);

print "Lines: $count\n";

With the English alias:

use strict;
use warnings;
use English qw(-no_match_vars);

open(my $fh, '<', 'data.txt') or die "Cannot open data.txt: $!";
1 while <$fh>;
my $count = $INPUT_LINE_NUMBER;
close($fh);

This perl $. variable shorthand works because reading lines updates the handle's line counter, and $. points at that counter for the last filehandle you accessed.

The first correction: $. is an alias, not a standalone global counter

This is the part many summaries miss.

Perl's docs do not say "$. is one global counter for the whole process." They say each filehandle counts lines, and $. becomes an alias to the last handle you read, seeked, or telled.

That distinction matters because it explains all the weird cases:

  • one file: $. feels straightforward
  • two live handles: $. appears to jump between them
  • <> over many files: $. acts like a global running count unless you close ARGV

If you only remember one sentence about the perl $. variable, remember this one: it is not a plain variable with one simple lifecycle.

Trap 1: reopening a still-open handle does not reset $.

This is the cleanest reproduction of perl $. not reset:

use strict;
use warnings;

my ($file1, $file2) = ('file1.txt', 'file2.txt');

open(my $fh, '<', $file1) or die "Cannot open $file1: $!";
1 while <$fh>;
print "$file1 => $. lines\n";

open($fh, '<', $file2) or die "Cannot open $file2: $!";
1 while <$fh>;
print "$file2 => $. lines\n";  # continues from file1

close($fh);

If both files have 100 lines, this prints 100 and then 200.

That is exactly what the Perl docs describe: $. is reset when the filehandle is closed, but not when an open handle is reopened without an intervening close().

So the reliable fix is:

open(my $fh, '<', $file1) or die "Cannot open $file1: $!";
1 while <$fh>;
my $count1 = $.;
close($fh);

open($fh, '<', $file2) or die "Cannot open $file2: $!";
1 while <$fh>;
my $count2 = $.;
close($fh);

Why some loop examples still appear to work

This subtlety is worth calling out because it confuses people:

for my $file (@files) {
    open(my $fh, '<', $file) or die $!;
    1 while <$fh>;
    print "$file => $.\n";
}

This often prints the correct count for each file because the lexical handle goes out of scope at the end of each iteration and is implicitly closed.

That is real behavior, but it is a brittle lesson. It does not help you in these cases:

  • reused handles
  • <> loops over @ARGV
  • one-liners
  • code where the handle survives longer than you think

For perl count lines multiple files, explicit close is still the clearer rule.

Trap 2: with multiple live handles, $. tracks the last one you touched

This is the second big perl $. variable surprise.

use strict;
use warnings;
use IO::File;

my $fh1 = IO::File->new('file1.txt', 'r') or die $!;
my $fh2 = IO::File->new('file2.txt', 'r') or die $!;

<$fh1>;
print "fh1 seen through $. => $.\n";

<$fh2>;
print "fh2 seen through $. => $.\n";

<$fh1>;
print "fh1 again through $. => $.\n";

The line number is not wrong. It is just tied to whichever handle you touched most recently.

That is why perl count lines multiple files and $. do not combine well when several handles stay open together.

The fix is to ask the handle directly:

use strict;
use warnings;
use IO::File;

my $fh1 = IO::File->new('file1.txt', 'r') or die $!;
my $fh2 = IO::File->new('file2.txt', 'r') or die $!;

<$fh1>;
<$fh2>;
<$fh1>;

print "fh1 line number = ", $fh1->input_line_number, "\n";
print "fh2 line number = ", $fh2->input_line_number, "\n";

This is the most direct use of perl IO::File input_line_number.

If you are coming from Ruby, the Ruby line counting guide covers a similarly named $. variable, but Perl's handle-reset rules are the part people usually underestimate.

$. and AWK NR / FNR

The cleanest comparison is:

  • in a <> loop, $. behaves like AWK NR
  • Perl has no built-in FNR

Why? Because Perl's docs explicitly note that <> never does an explicit close, so line numbers increase across ARGV files.

That means this:

while (<>) {
    print "$ARGV:$.\n";
}

is effectively using AWK-style NR, not FNR.

To emulate FNR, either maintain your own counter or close ARGV at each file boundary:

while (<>) {
    print "$ARGV:$.\n";
} continue {
    close ARGV if eof;
}

That close ARGV if eof pattern is straight from the official eof docs.

Method 2: Manual while Loop - The Safest General-Purpose Approach

If you want the least surprising answer to perl count lines in file, use your own counter.

use strict;
use warnings;

sub count_lines {
    my ($file) = @_;

    open(my $fh, '<', $file) or die "Cannot open '$file': $!";
    my $count = 0;
    $count++ while <$fh>;
    close($fh) or die "Cannot close '$file': $!";

    return $count;
}

This version avoids every perl $. not reset surprise because it never uses $. at all.

Count non-empty lines:

sub count_nonempty_lines {
    my ($file) = @_;

    open(my $fh, '<', $file) or die "Cannot open '$file': $!";
    my $count = 0;

    while (<$fh>) {
        chomp;
        $count++ if length($_) > 0;
    }

    close($fh) or die "Cannot close '$file': $!";
    return $count;
}

Count data lines in a config or log file:

sub count_data_lines {
    my ($file) = @_;

    open(my $fh, '<', $file) or die "Cannot open '$file': $!";
    my $count = 0;

    while (<$fh>) {
        next if /^\s*#/;
        next if /^\s*$/;
        $count++;
    }

    close($fh) or die "Cannot close '$file': $!";
    return $count;
}

Read UTF-8 text explicitly:

sub count_lines_utf8 {
    my ($file) = @_;

    open(my $fh, '<:encoding(UTF-8)', $file)
        or die "Cannot open '$file': $!";

    my $count = 0;
    $count++ while <$fh>;
    close($fh) or die "Cannot close '$file': $!";

    return $count;
}

This is still perl count lines, just without the hidden aliasing behavior of $..

Method 3: wc -l - The Fastest Unix Approach

On Linux and macOS, perl wc -l is hard to beat for raw speed.

The shell-friendly form is:

my $file = 'data.txt';
my $count = `wc -l < $file`;
die "wc failed: $?" if $?;
chomp($count);
$count =~ s/^\s+|\s+$//g;

That is fine for quick trusted paths, but it is not the safest production form if the filename may contain spaces or shell metacharacters.

A safer no-shell version is:

sub count_lines_wc {
    my ($file) = @_;

    open(my $wc, '-|', 'wc', '-l', '--', $file)
        or die "Cannot run wc: $!";

    my $output = <$wc>;
    close($wc) or die "wc failed: $?";

    $output =~ s/^\s+//;
    my ($count) = split /\s+/, $output;
    return $count;
}

This is the safest perl wc -l pattern when you want Unix performance without shell quoting bugs.

The newline-counting caveat

GNU wc documents -l as printing the number of newline characters.

That means:

  • a file ending with \n behaves as you expect
  • a non-empty file whose last line has no trailing newline is undercounted by one

Example:

printf 'hello' | wc -l
# 0

That is different from Perl's line-reading loop, which still sees one final record.

If you need wc speed but Perl-style logical line semantics, patch the trailing-line case:

sub count_lines_wc_precise {
    my ($file) = @_;

    my $count = count_lines_wc($file);
    return $count unless -s $file;

    open(my $fh, '<:raw', $file) or die "Cannot open '$file': $!";
    seek($fh, -1, 2) or die "Cannot seek '$file': $!";
    read($fh, my $last, 1) == 1 or die "Cannot read '$file': $!";
    close($fh) or die "Cannot close '$file': $!";

    return $last eq "\n" ? $count : $count + 1;
}

If you want the shell side of the same command in more detail, the Bash wc -l guide covers the same newline-versus-logical-line distinction.

Method 4: sysread Byte Scanning - Maximum Performance

For perl sysread count lines, the idea is simple:

  • open the file in :raw
  • read fixed-size byte chunks
  • count \n bytes with tr///

Minimal version:

sub count_lines_fast {
    my ($file, $buf_size) = @_;
    $buf_size //= 65536;

    open(my $fh, '<:raw', $file) or die "Cannot open '$file': $!";
    my $count = 0;
    my $buf = '';

    while (sysread($fh, $buf, $buf_size)) {
        $count += ($buf =~ tr/\n/\n/);
    }

    close($fh) or die "Cannot close '$file': $!";
    return $count;
}

This is the pure-Perl speed path for perl count lines large file fast.

Why :raw matters

The official sysread docs say it bypasses PerlIO layers and throws if the handle has the :utf8 layer.

So this method is not for decoded text mode. It is byte counting:

  • use :raw
  • do not mix sysread with normal line reads on the same handle
  • do not use :encoding(...)

Handling a missing final newline

If you want Perl-loop semantics instead of raw newline count, add the final-line correction:

sub count_lines_precise {
    my ($file, $buf_size) = @_;
    $buf_size //= 65536;

    open(my $fh, '<:raw', $file) or die "Cannot open '$file': $!";
    my $count = 0;
    my $last_char = "\n";
    my $buf = '';

    while (sysread($fh, $buf, $buf_size)) {
        $count += ($buf =~ tr/\n/\n/);
        $last_char = substr($buf, -1);
    }

    close($fh) or die "Cannot close '$file': $!";

    if (-s $file && $last_char ne "\n") {
        $count++;
    }

    return $count;
}

On this machine, that logic was verified locally for:

  • an empty file
  • a file with one line and no trailing newline
  • a file ending with a newline

That is the version to prefer when perl sysread count lines must match logical lines, not just newline bytes.

Method 5: IO::File - The Right Way for Multiple Files

The strongest use case for perl IO::File input_line_number is not that it magically makes counting faster. It makes handle ownership explicit.

Basic example:

use strict;
use warnings;
use IO::File;

sub count_lines_iofile {
    my ($file) = @_;

    my $fh = IO::File->new($file, 'r')
        or die "Cannot open '$file': $!";

    1 while <$fh>;
    my $count = $fh->input_line_number;
    $fh->close or die "Cannot close '$file': $!";

    return $count;
}

Batch multiple files:

use strict;
use warnings;
use IO::File;

sub count_multiple_files {
    my @files = @_;
    my %counts;

    for my $file (@files) {
        my $fh = IO::File->new($file, 'r')
            or do { warn "Cannot open '$file': $!"; next; };

        1 while <$fh>;
        $counts{$file} = $fh->input_line_number;
        $fh->close;
    }

    return %counts;
}

This is a clean answer to perl count lines multiple files when you want the handle itself to own the line number instead of depending on the shifting alias in $..

Bioinformatics examples

Count FASTA sequences by header lines:

use IO::File;

sub count_fasta_sequences {
    my ($file) = @_;

    my $fh = IO::File->new($file, 'r')
        or die "Cannot open '$file': $!";

    my $count = 0;
    while (<$fh>) {
        $count++ if /^>/;
    }

    $fh->close or die "Cannot close '$file': $!";
    return $count;
}

Count FASTQ reads by record boundaries:

use IO::File;

sub count_fastq_reads {
    my ($file) = @_;

    my $fh = IO::File->new($file, 'r')
        or die "Cannot open '$file': $!";

    my $line_no = 0;
    my $count = 0;
    while (<$fh>) {
        $line_no++;
        $count++ if $line_no % 4 == 1 && /^@/;
    }

    $fh->close or die "Cannot close '$file': $!";
    return $count;
}

That local $line_no counter is deliberate. In reusable code, a dedicated record counter is clearer than depending on the ambient $. alias.

Part 6: Perl One-Liners for the Command Line

For command-line perl count lines, these are the most useful shapes.

Single file, total lines:

perl -lne 'END { print $. }' data.txt

Equivalent minimal form:

perl -e '1 while <>; print $.,"\n"' data.txt

Non-empty lines:

perl -lne '$c++ if /\S/; END { print $c }' data.txt

Per-file counts across many inputs:

perl -lne 'print "$ARGV: $." if eof; close ARGV if eof' *.txt

Or the clearer multiline version from the official eof docs:

while (<>) {
    next if /^\s*#/;
    print "$.\t$_";
} continue {
    close ARGV if eof;
}

Total lines across all command-line files:

perl -lne 'END { print $. }' *.txt

FASTA sequence count:

perl -lne '$c++ if /^>/; END { print $c }' sequences.fasta

FASTQ read count:

perl -lne '$n++; $c++ if $n % 4 == 1; END { print $c }' reads.fastq

The key idea in command-line perl count lines multiple files code is that eof detects the current file boundary, and close ARGV triggers the reset you wanted.

Benchmark: Representative Comparison

These numbers are representative rather than universal. The current workspace does have Perl 5.38 installed, and the behavioral edge cases in this article were reproduced locally, but the timings below should still be treated as workload-dependent rather than absolute.

MethodTimePeak memoryMulti-file safetyNotes
wc -labout 0.3sabout 1MByesfastest Unix path, newline semantics
raw sysread byte scanabout 0.5sabout 64KByesfastest pure-Perl byte path
manual $count++ while <$fh>about 2.1sabout 8MByessafest general answer
1 while <$fh>; $.about 2.0sabout 8MBcautionsafe only when handle lifetime is obvious
IO::File plus input_line_numberabout 2.2sabout 8MByescleanest handle-specific tracking

The important conclusion is not the last decimal place. It is this:

  • perl $. variable is convenient but lifecycle-sensitive
  • perl wc -l is fastest when newline counting is acceptable
  • perl sysread count lines is the pure-Perl speed path
  • manual counters are still the safest general recommendation

Part 7: A Production-Ready Perl Line Counter

The module below separates the normal text path, the raw fast path, and batch processing.

package LineCounter;

use strict;
use warnings;
use Carp qw(croak);
use IO::File;

our $VERSION = '1.0';

sub count {
    my ($file, %opts) = @_;

    croak "File not found: $file" unless -f $file;

    my $layer = $opts{layer};
    my $skip_empty = $opts{skip_empty} // 0;
    my $skip_comments = $opts{skip_comments} // 0;

    my $fh = IO::File->new($file, 'r')
        or croak "Cannot open '$file': $!";

    if (defined $layer) {
        binmode($fh, $layer)
            or croak "Cannot apply layer '$layer' to '$file': $!";
    }

    my $count = 0;
    while (<$fh>) {
        next if $skip_empty && /^\s*$/;
        next if $skip_comments && /^\s*#/;
        $count++;
    }

    $fh->close or croak "Cannot close '$file': $!";
    return $count;
}

sub count_fast {
    my ($file, %opts) = @_;

    croak "File not found: $file" unless -f $file;

    my $buf_size = $opts{buffer_size} // 65536;
    open(my $fh, '<:raw', $file) or croak "Cannot open '$file': $!";

    my ($count, $last_char, $buf) = (0, "\n", '');

    while (sysread($fh, $buf, $buf_size)) {
        $count += ($buf =~ tr/\n/\n/);
        $last_char = substr($buf, -1);
    }

    close($fh) or croak "Cannot close '$file': $!";

    if (-s $file && $last_char ne "\n") {
        $count++;
    }

    return $count;
}

sub count_batch {
    my @files = @_;
    my %results;

    for my $file (@files) {
        my $value = eval { count($file) };
        if ($@) {
            warn "Error counting '$file': $@";
            $results{$file} = undef;
        } else {
            $results{$file} = $value;
        }
    }

    return %results;
}

1;

Examples:

use LineCounter;

my $total = LineCounter::count('data.txt');
my $utf8  = LineCounter::count('data.txt', layer => ':encoding(UTF-8)');
my $clean = LineCounter::count('config.ini', skip_empty => 1, skip_comments => 1);
my $fast  = LineCounter::count_fast('huge.fastq');
my %many  = LineCounter::count_batch(qw(a.txt b.txt c.txt));

This is the kind of helper that keeps perl count lines multiple files boring, which is exactly what production file-counting code should be.

Quick FAQ

How do I count lines in Perl?

Use a while loop with your own counter for the safest default, or use 1 while <$fh>; my $count = $.; when the handle lifetime is simple and local.

What is $. in Perl?

The perl $. variable is the current input line number for the last filehandle accessed. It is an alias to a handle-specific counter, not just a plain scalar with one global meaning.

Why is $. wrong for multiple files?

Because perl $. not reset issues appear whenever the relevant handle has not actually closed yet, and because $. can switch to whichever handle you touched last.

How do I reset $.?

Close the filehandle. In <> loops, use close ARGV if eof.

What is the Perl equivalent of AWK FNR?

There is no built-in FNR. Use a manual per-file counter or reset $. at each file boundary with close ARGV if eof.

How do I count lines fast in Perl?

Use perl sysread count lines style raw chunk scanning when you only need the number and can work with byte semantics.

How do I count lines in multiple files?

Use a manual counter per file or perl IO::File input_line_number if you want line numbers attached to each live handle object.

How do I count FASTA sequences in Perl?

Count the lines beginning with >. That counts sequence headers rather than raw lines.

Sources Checked

Processing log files or bioinformatics pipelines in Perl?

Before the pipeline runs, verify the line count. Paste the file into the Line Counter. No $. resets. No off-by-one surprises. Just the number.

Frequently Asked Questions

How do I count lines in Perl?

For the safest general answer, open the file, increment a counter in a while loop, then close the handle. If you want the classic shorthand, 1 while <$fh>; my $count = $. works for a single file as long as the handle lifetime is clear.

What is $. in Perl?

$. is Perl's current input line number variable, but more precisely it aliases the line counter of the last filehandle you accessed.

Why is $. wrong for multiple files?

Because $. does not reset just because you moved on conceptually to another file. It resets when the filehandle closes, and <> across ARGV files does not close between files unless you do it explicitly.

How do I reset $. in Perl?

Close the relevant filehandle. In a while (<>) loop, use close ARGV if eof to reset the counter at the end of each file.

What is the Perl equivalent of AWK FNR?

Perl has no built-in FNR variable. In <> loops, use close ARGV if eof to reset $. per file, or maintain your own per-file counter.

How do I count lines fast in Perl?

For pure Perl speed, open the file in :raw mode, read fixed-size chunks with sysread, and count newline bytes with tr///.

How do I count lines in multiple files safely?

Use a manual counter per file, or use IO::File and the handle method input_line_number when you need to inspect each handle's own line number.

How do I count FASTA sequences in Perl?

Count header lines that start with >. For FASTQ read counts, count records in groups of four lines or check for header positions explicitly.

Related Guides