perl.gg / secret-operators

<!-- category: secret-operators -->

Bare Number Flip-Flop

2026-03-30

Print lines 5 through 10 of a file:
perl -ne 'print if 5 .. 10'
No regex. No comparison operator. Just two bare numbers and a .. between them. And somehow Perl knows you mean line numbers.

This is one of Perl's quietest tricks. When the flip-flop operator .. sees a bare integer on either side, it silently compares that integer against $., the current input line number. The expression 5 .. 10 is really $. == 5 .. $. == 10.

You never asked for it. You never wrote it. Perl just does it.

Part 1: THE FLIP-FLOP BASICS

In list context, .. is the range operator:
my @nums = (1 .. 5); # (1, 2, 3, 4, 5)
In scalar context (like inside an if condition), it becomes the flip-flop operator. It remembers state between calls. It starts in the OFF state. When the left condition becomes true, it flips ON. When the right condition becomes true, it flops back OFF.
while (<DATA>) { print if m~START~ .. m~END~; }
That prints everything between START and END, inclusive. The flip-flop turns on at START and off at END.

Part 2: THE BARE NUMBER MAGIC

Now here's the trick. When you put a bare number where the flip-flop expects a condition, Perl doesn't treat it as a boolean. It compares it to $..
while (<>) { print if 5 .. 10; }
Perl reads that as:
while (<>) { print if $. == 5 .. $. == 10; }
On line 5, $. == 5 is true, so the flip-flop turns ON. Lines 5, 6, 7, 8, 9 are printed (flip-flop stays ON). On line 10, $. == 10 is true, so the flip-flop turns OFF after printing line 10.

You get lines 5 through 10. Exactly like sed -n '5,10p'.

Part 3: WHY THIS WORKS

The $. variable is the current line number. It increments every time you read from any filehandle with <>.
while (<>) { print "Line $.: $_"; }
The flip-flop has a special case baked into the Perl parser. From perlop:

> If either operand of scalar .. is a constant expression, that operand is considered true if it is equal (==) to the current input line number (the $. variable).

"Constant expression" means a literal number. Not a variable. Not a calculation. Just a plain integer sitting there. When Perl sees it, the implicit $. == comparison kicks in.

This isn't a feature anyone designed by committee. It's inherited from sed and awk, where line-number ranges were first-class citizens. Larry Wall carried the idea into Perl and made it work through the flip-flop.

Part 4: ONE-LINER RECIPES

Print lines 5 through 10:
perl -ne 'print if 5 .. 10' file.txt
Print the first 20 lines (like head -20):
perl -ne 'print if 1 .. 20' file.txt
Skip the first line (header) and print the rest:
perl -ne 'print if 2 .. eof' file.txt
Print a single line (line 42):
perl -ne 'print if 42 .. 42' file.txt
That last one is silly. You'd normally write perl -ne 'print if $. == 42'. But it shows that the flip-flop works with identical endpoints.

Print lines 10 to the end of the file:

perl -ne 'print if 10 .. eof' file.txt
The eof function returns true when there's no more input. So the flip-flop turns on at line 10 and never turns off.

Part 5: MIXING WITH REGEX

You can mix bare numbers with regex patterns on either side of the flip-flop:
# from line 1 until a blank line perl -ne 'print if 1 .. m~^$~' file.txt
# from a pattern to line 50 perl -ne 'print if m~BEGIN~ .. 50' file.txt
# from line 10 to a pattern perl -ne 'print if 10 .. m~END~' file.txt
The left and right sides of .. are independent. One can be a number (implicitly compared to $.), the other can be a regex, a function call, or any boolean expression.
while (<>) { print if 5 .. m~^---~; }
Start printing at line 5, stop when you hit a line that's just ---. The bare 5 triggers the $. == magic. The regex is evaluated normally.

Part 6: THE THREE-DOT VARIANT

There's also ... (three dots). The difference is subtle.

With .. (two dots), if the left side and right side both become true on the same line, the flip-flop turns on AND off in one step. With ... (three dots), when the left side becomes true, the right side isn't checked until the NEXT iteration.

# two dots: if line 5 also matches END, it turns on and off print if 5 .. m~END~; # three dots: if line 5 matches END, it stays on until NEXT END print if 5 ... m~END~;
In practice this rarely matters for bare number ranges, because 5 .. 10 can't have both sides true on the same line (a line number can't be 5 and 10 simultaneously). But it matters when you mix a number with a regex:
# file where line 5 happens to say "END" # two dots: prints only line 5 (turns on and off on same line) # three dots: prints from line 5 until the NEXT "END"
When in doubt, two dots is the default. Use three dots when you know the off-condition might match the same line as the on-condition and you want to keep going.

Part 7: WHAT COUNTS AS "BARE"

The implicit $. comparison triggers only for constant expressions. These work:
print if 5 .. 10; # bare integers, magic applies print if 1 .. 100; # bare integers, magic applies
These do NOT trigger the magic:
my $start = 5; print if $start .. 10; # variable, no magic, boolean eval print if (2+3) .. 10; # expression, no magic (actually, this # IS constant-folded, so it depends on # the Perl version)
The safest rule: if it's a literal integer typed directly into the code, the magic happens. If it's anything else, it doesn't.

Part 8: MULTIPLE RANGES

The flip-flop resets after turning off, so you can catch multiple ranges:
while (<>) { print if 3 .. 5; print "---\n" if $. == 5; }
But wait. Can you have multiple different flip-flop ranges in the same loop? Yes. Each .. expression has its own independent state:
while (<>) { if (1 .. 3) { print "HEADER: $_"; } if (10 .. 15) { print "DATA: $_"; } }
Lines 1-3 get the HEADER prefix. Lines 10-15 get the DATA prefix. Two independent flip-flops, each tracking its own state.

Part 9: PRACTICAL USES

Extract a function from source code by line numbers (you got them from grep -n):
grep -n 'sub process_data' code.pl # output: 142:sub process_data perl -ne 'print if 142 .. m~^}~' code.pl
Start at line 142 (the bare number magic), stop at the first closing brace at the start of a line. You just extracted the function.

Print the 5 lines around line 200 (context):

perl -ne 'print if 195 .. 205' huge.log
Skip a known header in a data file:
# CSV with 3 header lines perl -ne 'print if 4 .. eof' data.csv
Print every other block of 10 lines:
perl -ne 'print if int(($.-1)/10) % 2 == 0' file.txt
OK, that last one doesn't use the flip-flop. But it shows that once you know about $., a whole world of line-number tricks opens up.

Part 10: THE $. VARIABLE

Since bare numbers in flip-flops compare against $., it's worth knowing $. well.
$. Current line number of last filehandle read Increments with each <> read Resets when you explicitly close a filehandle Does NOT auto-reset between files in @ARGV
That last point is important. If you process multiple files with <>, $. keeps counting across files unless you close ARGV:
# file1.txt has 10 lines, file2.txt has 10 lines perl -ne 'print "$.: $_"' file1.txt file2.txt # lines 1-10 from file1, lines 11-20 from file2
To reset per file:
perl -ne 'print "$.: $_"; close ARGV if eof' file1.txt file2.txt # lines 1-10 from file1, lines 1-10 from file2
This affects bare-number flip-flops too. print if 1 .. 5 will only trigger once across all files unless you reset $..
.----. | $. | '------' || [1][2][3]...[N] || 5 .. 10 means $. == 5 .. $. == 10 Invisible magic. Two numbers. One variable. Perl reads your mind. .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
The bare number flip-flop is a relic of Perl's sed/awk heritage. It saves a few keystrokes and looks like magic to anyone who hasn't read perlop carefully. But once you know the rule (bare integer + .. = implicit $. comparison), it stops being magic and starts being a tool.

A really convenient tool.

perl.gg