Range and flip-flop operators#

Two operators with two completely different jobs depending on context: in list context they generate ranges, and in scalar context they are stateful «flip-flop» boolean expressions for matching line ranges in stream-processing.

Operator

List-context job

Scalar-context job

..

inclusive range

flip-flop (false-tested entry)

...

inclusive range

flip-flop (true-tested entry)

.. in list context — ranges#

In list context, .. produces the inclusive sequence between its endpoints:

0..9                      # (0,1,2,3,4,5,6,7,8,9)
'a'..'z'                  # ('a','b',...,'y','z')      26 elements
'aa'..'zz'                # ('aa','ab',...,'zy','zz')  676 elements
'A'..'F'                  # ('A','B','C','D','E','F')

for my $i (1..100) { ... }                       # numeric for-loop
for my $name ('alpha'..'omega') { ... }          # alphabetic, magic-incremented

For numeric endpoints the result is the integer sequence (with strict ordering — 5..1 is the empty list, never reverse). For string endpoints the magic-increment from ++ is used, which gives you the natural alphabetic spread (and on 'aa' through 'zz' the expected 676 two-letter combinations).

A common Perl idiom:

my @hex = ('0'..'9', 'a'..'f');           # 16 hex digits as strings
my @rev = reverse 1..10;                   # (10,9,8,...,1)
@arr[ 5..9 ]                               # array slice, indexes 5..9
@arr[ -3..-1 ]                             # last three elements

The all-integer range can be very large — 1..1_000_000_000 is a billion-element list. Perl 5 lazily generates loop ranges in for, so for my $i (1..1_000_000_000) is safe, but my @x = 1..1_000_000_000 allocates the whole array. PetaPerl follows the same rule.

Reversed endpoints#

5..1 is empty. There is no «reverse range» form; if you want descending, generate ascending and reverse:

1..5                    # (1,2,3,4,5)
5..1                    # ()        — empty, no warning
reverse 1..5            # (5,4,3,2,1)

Mixed-type endpoints#

If either endpoint looks numeric, .. does numeric ranging:

'1'..'5'                # (1,2,3,4,5)            — numeric
'1'..'10'               # (1,2,3,4,5,6,7,8,9,10) — numeric

'a'..'5'                # ('a')                  — magic-increment from "a",
                        #                         stops because it can't reach "5"

Make the type explicit to avoid confusion:

0+'1' .. 0+'5'          # numeric, no doubt
sprintf("%d", 1) .. sprintf("%d", 5)

In practice, write integer ranges with bare integer literals and character ranges with bare quoted-string letters; the magic-vs- arithmetic distinction follows automatically.

.. and ... in scalar context — flip-flops#

Inside a boolean context (an if, a while, a ?:, the right of &&/||///), .. becomes a stateful expression that remembers whether it is currently between a left-side match and a right-side match.

while (<$fh>) {
    if (/^BEGIN/ .. /^END/) {     # true from BEGIN line through END line
        print;                     # print everything in between, inclusive
    }
}

The semantics:

  1. The flip-flop is initially false.

  2. When the left condition (/^BEGIN/) becomes true, the flip-flop transitions to true, and the right side is evaluated immediately on the same iteration.

  3. The flip-flop stays true on subsequent iterations until the right condition (/^END/) becomes true. On that iteration it returns true and then transitions back to false.

  4. So both the BEGIN line and the END line are included.

When the flip-flop is true, it returns a sequence number (1 on the entering iteration, 2 on the next, …). On the closing iteration it returns the count followed by E0 — so the value is true (non-empty string, not the false "0") but stringifies as the number 0 for cosmetic clarity. You can usually treat it as plain boolean.

... differs from .. in one detail:

  • .. (two dots) — when the left side becomes true, the right side is checked on the same iteration. So the same line can open and close the range.

  • ... (three dots) — when the left side becomes true, the right side is not checked until the next iteration. Always a multi-line range.

while (<$fh>) {
    print if /<head>/ .. /<\/head>/;     # one-line <head>...</head> works
    print if /<head>/ ... /<\/head>/;    # forces multi-line; same-line ignored
}

When you’d use a flip-flop#

The flip-flop shines in awk-style line-range filtering — pulling sections out of a log file, isolating a fenced code block from markdown, processing a configuration block delimited by keywords. It is famously un-loved by code reviewers because its statefulness is implicit.

Two practical alternatives that some prefer:

# Explicit state variable — equivalent semantics, easier to grep for:
my $in_block = 0;
while (<$fh>) {
    $in_block = 1 if /^BEGIN/;
    print if $in_block;
    $in_block = 0 if /^END/;
}

# A simple two-step iterator:
while (<$fh>) {
    if (/^BEGIN/ .. /^END/) { print }
}

The flip-flop wins on conciseness; the explicit form wins on clarity at the cost of three more lines. Pick by audience.

See also#

  • reverse, grep, map — list operators commonly composed with ranges.

  • Subscript — array slices @arr[1..3] rely on range-in-list-context.

  • Precedence.. and ... sit at row 17, between ||/// and ?:. Almost always parenthesise when combining with logic.