Beginner Friendly · Level 1

Perl Nuts &
Bolts

Every basic building block explained clearly — how it works, why it exists, and what to watch out for.

Variables Strings Numbers Arrays Hashes Control Flow Loops Subs Refs Regex File I/O Strict & Warnings
$
Sigils & Variable Types
#01 The three variable types — scalar, array, hash
basicssigils

Perl has three fundamental variable types, each identified by a sigil — a punctuation character at the start of the name. $ means "one thing" (scalar). @ means "a list of things" (array). % means "a dictionary of key-value pairs" (hash). The sigil is part of the syntax, not the name — the variable named fruit can exist in all three forms at once.

sigils.pl
#!/usr/bin/perl
use strict;
use warnings;

# $ sigil — ONE value (a scalar)
my $name   = "Alice";    # holds a string
my $age    = 30;          # holds a number
my $pi     = 3.14159;     # holds a float
my $empty;                # holds undef (no value)

# @ sigil — an ORDERED LIST (array)
my @colors  = ("red", "green", "blue");
my @numbers = (1, 2, 3, 4, 5);
my @empty_list = ();     # empty array

# % sigil — KEY-VALUE PAIRS (hash / dictionary)
my %person  = (
    name  => "Bob",         # => is the "fat comma" — same as ","
    age   => 25,
    city  => "London",
);

# Accessing values — sigil changes based on WHAT you get back
print $name;              # scalar — stays $
print $colors[0];         # ONE element from array — $ (not @)
print $person{name};      # ONE value from hash  — $ (not %)
The $ sigil rule
When you access a single element from any container — even an array or hash — you use $. You're getting one thing back. $colors[0] not @colors[0].
The fat comma =>
=> is just a comma that auto-quotes the left side. name => "Bob" is identical to "name", "Bob". It makes hash assignments read like key-value pairs.
undef — Perl's "nothing"
An uninitialised variable holds undef. It's false in boolean context, "" in string context, and 0 in numeric context. Check with defined($var).
my = declare locally
my declares a lexical variable — it only exists in the current block { } or file. Always use my. Never use undeclared globals.
"
Strings
#02 Single quotes, double quotes, and interpolation
strings

Perl has two main string delimiters with completely different behaviour. Double quotes " " interpret escape sequences and interpolate variables — variables inside them are expanded to their values. Single quotes ' ' are completely literal — no escapes (except \' and \\), no interpolation. This is the most important beginner distinction in Perl.

strings.pl
my $name = "Alice";
my $age  = 30;

# Double quotes: variables ARE expanded, \n IS a newline
print "Hello, $name! You are $age years old.\n";
# Output: Hello, Alice! You are 30 years old.

# Single quotes: everything is LITERAL — \ is just a backslash
print 'Hello, $name! \n';
# Output: Hello, $name! \n   (literally)

# String concatenation uses the . (dot) operator
my $greeting = "Hello" . ", " . $name;    # "Hello, Alice"

# String repetition uses the x operator
my $line  = "-" x 20;    # "--------------------"
my $stars = "*" x 5;    # "*****"

# Common string functions
my $s = "  Hello, World!  ";

length($s);             # 18  — number of characters
uc($s);                # "  HELLO, WORLD!  " — uppercase
lc($s);                # "  hello, world!  " — lowercase
ucfirst("hello");       # "Hello"
index($s, "World");    # 9   — position (0-based), -1 if not found
substr($s, 2, 5);      # "Hello" — start at 2, length 5

# Trim whitespace (no built-in — use a substitution)
$s =~ s/^\s+|\s+$//g;  # now "Hello, World!"

# Heredoc — multiline string (useful for templates)
my $block = <<END;
Line one
Line two: $name
Line three
END
Interpolation in detail
Inside double quotes, $var expands to its value. @arr expands to the array joined by spaces. To avoid interpolation: use single quotes, or escape with backslash: \$name.
Common escape sequences
\n newline · \t tab · \r carriage return · \\ literal backslash · \" literal double quote · \0 null byte
. vs + for concatenation
Perl uses . (dot) for string joining — NOT + like JavaScript. "Hello" + " World" gives 0 (numeric addition of two non-numeric strings).
Heredoc styles
<<END interpolates variables (like double quotes). <<'END' is literal (like single quotes). The closing word must be at column 1, alone on its line.
Watch Out "$hash{key}" works fine in strings, but "@array" joins all elements with spaces. To embed a single element use "$array[0]". To embed a hash value use "$hash{key}".
7
Numbers & Operators
#03 Numeric literals, arithmetic, and comparison
numbersoperators

Perl stores numbers internally as integers or floating-point and converts automatically. There's no separate int/float/double distinction — a scalar holds whatever the number is. Perl also has two completely separate sets of comparison operators: one for numbers and one for strings. Using the wrong one is a common bug.

numbers.pl
# Numeric literals
my $int   = 42;
my $float = 3.14;
my $neg   = -7;
my $sci   = 1.5e6;       # 1,500,000
my $big   = 1_000_000;   # _ separators: ignored, for readability
my $hex   = 0xFF;         # 255  (0x prefix = hexadecimal)
my $oct   = 0755;         # 493  (0 prefix  = octal!)
my $bin   = 0b1010;      # 10   (0b prefix = binary)

# Arithmetic operators
my $sum  = 10 +  3;   # 13   addition
my $diff = 10 -  3;   # 7    subtraction
my $prod = 10 *  3;   # 30   multiplication
my $quot = 10 /  3;   # 3.33 division (float)
my $mod  = 10 %  3;   # 1    modulo (remainder)
my $pow  =  2 ** 8;   # 256  exponentiation (power)

# Increment / Decrement
$sum++;    # $sum += 1 — post-increment
$sum--;    # $sum -= 1 — post-decrement
$sum += 5; # compound assignment: $sum = $sum + 5

# NUMERIC comparison (returns true/false)
10 == 10;   # equal
10 !=  5;   # not equal
10 >   5;   # greater than
10 <  20;   # less than
10 >= 10;   # greater than or equal

# STRING comparison (uses alphabetical/ASCII order)
"abc" eq "abc";   # equal
"abc" ne "xyz";   # not equal
"abc" lt "xyz";   # less than (alphabetically)
"abc" gt "aaa";   # greater than

# Spaceship operators — return -1, 0, or 1 (for sorting)
5 <=> 3;          # 1  (numeric spaceship)
"b" cmp "a";       # 1  (string spaceship)
Number vs String ops
Use == != < > for numbers. Use eq ne lt gt for strings. "10" == "10.0" is true (numeric). "10" eq "10.0" is false (string).
** is exponentiation
Unlike many languages, Perl uses ** for powers: 2**10 is 1024. Not the caret ^ — which is bitwise XOR.
0 prefix = octal!
A leading zero makes a number octal. 010 == 8, not 10. This surprises everyone. Use oct("010") to convert strings, oct("0x1f") for hex strings.
NumericString equivalentMeaning
==eqequal
!=nenot equal
<ltless than
>gtgreater than
<=leless than or equal
>=gegreater than or equal
<=>cmpthree-way compare (for sort)
@
Arrays
#04 Creating, accessing, and manipulating arrays
arrayslists

Arrays hold an ordered list of scalars, indexed from 0. They grow and shrink automatically. Perl provides built-in functions like push, pop, shift, unshift, splice, sort, reverse, grep, and map — mastering these eliminates most hand-written loops.

arrays.pl
my @fruits = ("apple", "banana", "cherry");

# Access by index — ALWAYS use $ because you get one thing back
print $fruits[0];      # "apple"  (first element)
print $fruits[-1];     # "cherry" (last element — negative indexes)
print $fruits[-2];     # "banana" (second-to-last)

# Length — use scalar() or $# (last index)
my $count      = scalar(@fruits);  # 3 — number of elements
my $last_index = $#fruits;         # 2 — last valid index (count - 1)

# push/pop — add/remove from the END
push @fruits, "date";      # ("apple","banana","cherry","date")
my $last = pop @fruits;   # removes and returns "date"

# shift/unshift — add/remove from the FRONT
unshift @fruits, "avocado";  # ("avocado","apple","banana","cherry")
my $first = shift @fruits;  # removes and returns "avocado"

# sort and reverse
my @sorted  = sort @fruits;            # alphabetical
my @nums    = sort { $a <=> $b } (5,2,8,1);  # numeric sort
my @rev     = reverse @sorted;

# grep — filter (like WHERE in SQL)
my @long = grep { length($_) > 5 } @fruits;

# map — transform (like SELECT in SQL)
my @upper = map { uc($_) } @fruits;

# join — turn array into a single string
my $csv    = join(",",  @fruits);  # "apple,banana,cherry"
my $spaced = join(" ", @fruits);  # "apple banana cherry"

# split — turn a string into an array
my @parts  = split(/,/, "a,b,c");  # ("a","b","c")
my @words  = split(/\s+/, "hello world foo");

# Array slices — multiple elements at once
my @two    = @fruits[0, 2];   # elements 0 and 2  — note @ not $
my @range  = @fruits[0..1];  # elements 0 to 1 (range operator)
$_ — the default variable
Inside grep { } and map { }, the current element is in $_. Many Perl functions use $_ as their implicit argument. You'll see $_ everywhere in Perl code.
push/pop vs shift/unshift
push/pop work on the right end (fast). shift/unshift work on the left end (slower — shifts all indices). Use push/pop for stacks.
$a and $b in sort
Inside sort { }, Perl provides $a and $b — the two elements being compared. Return negative, 0, or positive. $a <=> $b sorts ascending numerically.
Array slice uses @
When taking multiple elements from an array, use @array[0, 2] (with @). You're getting a list back, so the sigil changes to @.
%
Hashes
#05 Key-value stores — creating, reading, modifying
hashesdicts

A hash is Perl's key-value dictionary. Keys are always strings; values are any scalar. Hash order is not guaranteed — don't rely on insertion order. Use keys, values, and each to iterate. The exists and delete functions test and remove entries.

hashes.pl
my %person = (
    name  => "Carol",
    age   => 28,
    city  => "Paris",
);

# Read a value — use $ because you get ONE thing back
print $person{name};    # "Carol"
print $person{age};     # 28

# Add a new key or modify existing
$person{email} = "carol@example.com";
$person{age}   = 29;              # update

# Check if a key exists (don't just test the value!)
if (exists $person{email}) {
    print "email is set\n";
}

# Delete a key
delete $person{city};

# List all keys (order is RANDOM each run)
my @k = keys   %person;   # ("name","age","email") in some order
my @v = values %person;   # ("Carol",29,"carol@...") same order as keys

# Iterate with for — sort keys for consistent order
for my $key (sort keys %person) {
    print "$key: $person{$key}\n";
}

# Iterate with while/each (key-value at once)
while (my ($k, $v) = each %person) {
    print "$k = $v\n";
}

# Count entries
my $count = scalar(keys %person);  # 3

# Hash slice — get multiple values at once
my ($name, $age) = @person{qw(name age)};  # @ sigil for multiple
exists vs defined
exists $h{k} asks "is there a key called k?" — even if value is undef. defined $h{k} asks "is the value not undef?" Use exists to check key presence.
qw() — quote words
qw(name age city) is shorthand for ("name", "age", "city") — splits on whitespace, no commas needed. Great for lists of strings.
Hash order is random
Perl intentionally randomises hash key order (security against hash collision attacks). Always sort keys %hash if you need consistent output.
Auto-vivification
Assigning to a nested key that doesn't exist creates it: $h{a}{b} = 1 creates %h, the inner hashref $h{a}, and the key b — all at once.
~
Context: scalar vs list
#06 How context changes what you get back
contextcore

Context is one of Perl's most distinctive features. The same expression returns different things depending on where it's used. An array in scalar context returns its element count. An array in list context returns all its elements. Many functions also behave differently based on context. Understanding this is essential for reading Perl code correctly.

context.pl
my @arr = ("a", "b", "c");

# SCALAR context — @arr evaluates to its COUNT
my $n    = @arr;           # $n = 3
my $n2   = scalar(@arr);  # explicit scalar context
if (@arr) { ... }          # boolean (true if array not empty)
print "count: " . @arr;   # . forces scalar: "count: 3"

# LIST context — @arr returns all elements
my ($first, $second) = @arr;  # $first="a", $second="b"
my @copy             = @arr;  # @copy = ("a","b","c")

# localtime in scalar context = formatted string
my $time_str = localtime;    # "Mon Jan  1 12:00:00 2024"

# localtime in list context = individual components
my ($sec, $min, $hour, $day, $mon, $year) = localtime;

# wantarray() — test which context YOUR sub was called in
sub flexible {
    if (wantarray) {
        return (1, 2, 3);   # list context: return list
    } else {
        return "summary";    # scalar context: return string
    }
}
my $s  = flexible();        # "summary"
my @l  = flexible();        # (1, 2, 3)
Mental Model
Think of context like asking a question. Scalar context asks "how many?" — the array answers with a count. List context asks "what do you have?" — the array gives you everything. The same entity (@arr), different questions, different answers.
What forces scalar context
Assigning to $scalar =, using in if()/while(), using with . concatenation, calling scalar() explicitly, or any arithmetic.
What forces list context
Assigning to @array = or my ($a, $b) =, passing to a function that expects a list, using in for/foreach.
?
Control Flow
#07 if / elsif / else / unless and postfix forms
controllogic

Perl's conditionals are very readable. Besides the standard if/elsif/else, Perl has unless (if-not), postfix forms (condition after statement), and the ternary operator. Truth in Perl: false is 0, "0", "", undef. Everything else — including "0.0", "false", and " " — is true.

control_flow.pl
my $score = 75;

# Standard if / elsif / else
if    ($score >= 90) { print "A\n" }
elsif ($score >= 80) { print "B\n" }
elsif ($score >= 70) { print "C\n" }  # fires: score is 75
else                  { print "F\n" }

# unless = "if NOT" — reads naturally in English
unless ($score < 60) {
    print "Passed!\n";
}

# Ternary operator: condition ? if_true : if_false
my $label = ($score >= 60) ? "pass" : "fail";

# Postfix forms — condition AFTER the statement (very Perlish)
print "High score!\n"  if     $score >  90;
print "Not perfect\n"  unless $score == 100;

# Defined-or operator // — use default if value is undef
my $name  = undef;
my $label2 = $name // "Guest";    # "Guest" (because $name is undef)
$name //= "Guest";                # assign only if currently undef

# Boolean operators: && and || (or 'and' / 'or' in words)
my $valid = ($score > 0) && ($score <= 100);
my $ok    = ($score >= 90) || ($score == 42);

# 'or' and 'and' are the same but lower precedence (for flow control)
open(my $fh, '<', 'file.txt') or die "Can't open: $!\n";
What is FALSE in Perl
Only these 5 are false: 0 · "0" · "" (empty string) · undef · empty list (). The string "false" is TRUE. "0.0" is TRUE. " " (space) is TRUE.
// vs ||
// (defined-or) tests if the value is defined. || tests if it's truthy. Use // when 0 or "" are valid values you want to keep.
Postfix form
do_thing() if $condition is idiomatic Perl for simple one-liners. It reads like English. Use it for guards: return if $error, next if $skip.
or die pattern
open(...) or die "..." is the standard error-checking idiom. If open returns false (failure), die runs. The $! variable holds the system error message.
Loops
#08 for, foreach, while, until — and loop control
loopsiteration

Perl has four loop types. foreach (or for — they're identical) iterates over a list. while loops while a condition is true. until loops until a condition becomes true. C-style for gives fine-grained control. last, next, and redo control loop flow — Perl's equivalents of break, continue, and restart.

loops.pl
# foreach — iterate over a list (most common)
foreach my $fruit ("apple", "banana", "cherry") {
    print "$fruit\n";
}

# for and foreach are interchangeable in Perl
for my $i (1..5) {    # 1..5 is the range operator
    print "$i\n";       # prints 1 2 3 4 5
}

# Default variable $_ — no explicit loop variable needed
for (qw(red green blue)) {
    print "$_\n";    # $_ holds the current element
}

# C-style for loop
for (my $i = 0; $i < 5; $i++) {
    print "$i\n";      # init; condition; increment
}

# while — loop while condition is true
my $count = 3;
while ($count > 0) {
    print "$count\n";
    $count--;
}

# until — loop UNTIL condition becomes true (while NOT)
$count = 0;
until ($count >= 3) {
    print "$count\n";
    $count++;
}

# Loop control
for my $n (1..10) {
    next if $n % 2 == 0;  # skip even numbers (next = continue)
    last if $n > 7;        # stop when n > 7 (last = break)
    print "$n\n";           # prints 1, 3, 5, 7
}

# Postfix for — one-liner iteration
print "$_\n" for (1..3);     # prints 1, 2, 3

# Reading a file line by line (canonical while loop pattern)
while (my $line = <$fh>) {
    chomp $line;            # remove trailing newline
    print $line;
}
last / next / redo
last = break out of the loop. next = skip to next iteration. redo = restart current iteration without re-testing the condition. All work with postfix conditions too.
Range operator ..
1..10 creates a list (1,2,3...10). Works in for loops, array assignment, and slices. Also works for strings: 'a'..'z'.
$_ is an alias
Inside a loop, $_ is an alias to the actual list element — modifying $_ modifies the original. To avoid this, use for my $item (@list).
ƒ
Subroutines
#09 Defining and calling subroutines — @_, return, defaults
subsfunctions

Subroutines (functions) are defined with sub. All arguments arrive in the special array @_ — always unpack them at the top of the sub. The last evaluated expression is returned automatically, but explicit return is clearer. Perl subs can return multiple values naturally as a list.

subs.pl
# Basic sub definition
sub greet {
    my ($name) = @_;   # @_ holds ALL arguments
    return "Hello, $name!";
}
print greet("World");   # "Hello, World!"

# Multiple arguments
sub add {
    my ($a, $b) = @_;
    return $a + $b;
}

# Default parameter values using //=
sub greet2 {
    my ($name, $title) = @_;
    $title //= "friend";    # use "friend" if $title is undef
    return "Hello, $title $name!";
}
print greet2("Alice");          # "Hello, friend Alice!"
print greet2("Bob", "Dr.");    # "Hello, Dr. Bob!"

# Returning multiple values (as a list)
sub min_max {
    my @nums = @_;
    my $min = $nums[0];
    my $max = $nums[0];
    for my $n (@nums) {
        $min = $n if $n < $min;
        $max = $n if $n > $max;
    }
    return ($min, $max);   # return TWO values
}
my ($lo, $hi) = min_max(5,2,9,1);  # $lo=1, $hi=9

# Named parameters via a hash (useful for many args)
sub create_user {
    my (%args) = @_;        # unpack @_ as a hash
    $args{role} //= "user";
    return \%args;
}
my $u = create_user(name=>"Eve", age=>22);
@_ is always a flat list
When you call f(1, 2, 3), inside f, @_ = (1,2,3). If you pass an array f(@arr), its elements are flattened into @_. To pass an array without flattening, pass a reference: f(\@arr).
Implicit return
Without return, a Perl sub returns the value of the last expression evaluated. This is fine for simple getters but use explicit return in complex subs — it's clearer and prevents surprises.
Named parameters pattern
Using my (%args) = @_ turns argument pairs into a hash. Call it as f(key=>"val", key2=>"val2"). This is the cleanest way to handle subs with many optional parameters.
Declare before use
Perl compiles the whole file before running it, so you can call a sub defined later in the file. However with use strict, it's good practice to define subs before using them or forward-declare them.
\
References
#10 Creating and using references — the \ and -> operators
refspointers

A reference is a scalar that points to another variable. References are how Perl builds complex data structures (arrays of hashes, hashes of arrays) and how you pass arrays without flattening them. Use \ to create a reference, use -> to dereference it.

refs.pl
# Creating references with backslash \
my @arr   = (1,2,3);
my $aref  = \@arr;         # ref to existing array

my %hash  = (a=>1, b=>2);
my $href  = \%hash;         # ref to existing hash

# Anonymous refs — create without naming the original
my $aref2 = [10, 20, 30];  # [ ] creates anonymous arrayref
my $href2 = { x=>1, y=>2 }; # { } creates anonymous hashref

# Accessing via -> (arrow/dereference operator)
print $aref2->[0];         # 10 — element 0
print $href2->{x};         # 1  — key "x"

# Nested structures — array of hashes (common pattern)
my @people = (
    { name => "Alice", age => 30 },  # hashref
    { name => "Bob",   age => 25 },
);

# Access nested: $array[index]->{key}
print $people[0]->{name};    # "Alice"
print $people[1]->{age};     # 25

# Iterate array of hashes
for my $p (@people) {
    print "$p->{name} is $p->{age}\n";
}

# ref() tells you what kind of reference something is
print ref($aref);   # "ARRAY"
print ref($href);   # "HASH"
print ref(\$aref);  # "REF"  (reference to a reference)
[ ] vs ( ) for arrays
( ) creates a list — context-dependent. [ ] creates an anonymous arrayref — always returns a scalar reference. Use [ ] when building nested structures.
{ } ambiguity
{ } is either a hash ref or a code block depending on context. To force hash ref: +{ key => val }. To force block: put a semicolon first: {; key => val }.
Arrow -> is optional between brackets
Between consecutive brackets, the arrow is optional: $a->[0]{key} is the same as $a->[0]->{key}. The first arrow is always required.
ref() for type checking
Always check what kind of reference you have before dereferencing it. ref($x) eq 'ARRAY' ensures it's safe to use @{$x}. On non-references, ref() returns "".
/
Regular Expressions
#11 Matching, capturing, and substituting with regex
regexpatterns

Perl's regex support is legendary — it's where the =~ operator, capture groups, global matches, and the s/// substitution all live. The pattern is delimited by / slashes. Capture groups use ( ). Results land in $1, $2, etc. The /g modifier finds all matches.

regex.pl
my $text = "The price is $42.50 today";

# =~ tests if a string matches a pattern
if ($text =~ /price/) {
    print "Found 'price'\n";
}

# Capture groups ( ) — results in $1, $2, $3...
if ($text =~ /\$(\d+\.\d+)/) {
    print "Amount: $1\n";    # "42.50"
}

# !~ tests if something does NOT match
if ($text !~ /banana/) {
    print "No banana here\n";
}

# Common modifiers
# /i  — case-insensitive
# /g  — global (find all matches)
# /m  — multiline (^ and $ match line boundaries)
# /x  — extended (ignore whitespace, allow comments)
$text =~ /PRICE/i;          # matches "price" case-insensitively

# Global match — capture all occurrences into an array
my $html   = "<b>bold</b> and <i>italic</i>";
my @tags   = ($html =~ /<(\w+)>/g);
# @tags = ("b", "b", "i", "i") — all tag names

# Substitution: s/pattern/replacement/
my $str = "Hello World";
$str =~ s/World/Perl/;    # "Hello Perl"  (first match)

# Global substitution /g — replace all occurrences
my $s2 = "aaa bbb aaa";
$s2 =~ s/aaa/xxx/g;      # "xxx bbb xxx"

# Common metacharacters
# .    any character except newline
# \d   digit [0-9]        \D  non-digit
# \w   word char [a-z0-9_]\W  non-word
# \s   whitespace         \S  non-whitespace
# ^    start of string    $   end of string
# +    one or more        *   zero or more
# ?    zero or one        {n} exactly n times
# |    alternation (or)   [abc] character class
Tip Use the /x modifier to write readable regex with whitespace and comments: s/ ^\s+ | \s+$ //gx is much clearer than s/^\s+|\s+$//g for trimming.
$1, $2 are temporary
Capture variables $1, $2... are only valid until the next regex match. Always copy them immediately: my $val = $1.
Modifying a copy
To substitute without changing the original: (my $copy = $str) =~ s/old/new/g. The parentheses copy first, then apply the substitution.
📄
File I/O
#12 Reading and writing files safely
file i/oio

Always use the three-argument form of open and always check for errors with or die. The mode strings are: '<' read, '>' write (create/overwrite), '>>' append. The <$fh> diamond operator reads one line at a time. chomp removes the trailing newline.

file_io.pl
# Write a file — '>' creates/overwrites
open(my $out, '>', 'myfile.txt')
    or die "Cannot open for writing: $!\n";
# $! holds the OS error message on failure

print $out "First line\n";
print $out "Second line\n";
close($out);    # always close when done

# Read a file line by line — most memory-efficient
open(my $in, '<', 'myfile.txt')
    or die "Cannot open: $!\n";

while (my $line = <$in>) {
    chomp $line;         # remove trailing \n
    print "Line: $line\n";
}
close($in);

# Read entire file into an array (one line per element)
open(my $fh, '<', 'myfile.txt') or die $!;
my @lines = <$fh>;   # list context: reads all lines
close($fh);
chomp @lines;          # chomp all elements at once

# Append to a file — '>>' keeps existing content
open(my $app, '>>', 'myfile.txt') or die $!;
print $app "Third line\n";
close($app);

# Test operators — check file attributes
if (-e 'myfile.txt')  { print "exists\n"      }
if (-f 'myfile.txt')  { print "is a file\n"   }
if (-d '/tmp')        { print "is a dir\n"    }
if (-r 'myfile.txt')  { print "is readable\n" }
my $sz = -s 'myfile.txt';  # file size in bytes
3-arg open is mandatory
Old-style open(FH, ">file") is unsafe — if a filename starts with >, the mode is ambiguous. Always use 3-arg: open($fh, '>', $filename). This separates mode from filename cleanly.
$! — system error
$! holds the operating system error string (like "No such file or directory") after a failed system call. Always include it in die messages: die "Can't open: $!".
chomp vs chop
chomp removes the trailing newline (if present) — safe to call even if there's no newline. chop removes the last character unconditionally — rarely what you want.
Filehandle scoping
Lexical filehandles (my $fh) close automatically when they go out of scope. Old-style bareword handles (FH) are global and don't auto-close. Always use my $fh.
!
strict & warnings
#13 Why use strict and use warnings are non-negotiable
essentialsafety

Every Perl file should start with these two lines. No exceptions. use strict requires all variables to be declared with my (catching typos before they become bugs). use warnings enables helpful diagnostic messages about suspicious operations. Together they catch a huge class of beginner and expert bugs at compile time.

strict_warnings.pl
#!/usr/bin/perl
use strict;     # ALWAYS — require declared variables
use warnings;  # ALWAYS — enable diagnostic messages

# Without strict, this silently creates a new global $nme (typo!):
# $nme = "Alice";  # oops! $name was intended

# With strict, this fails at compile time with "Global symbol...
# requires explicit package name" — you catch the bug immediately

# With strict, you MUST use my (or our for globals, local for dynamic)
my $name = "Alice";  # correct

# use warnings catches these common problems:

# 1. Using undef in a numeric context
my $x;
my $y = $x + 1;   # warns: "Use of uninitialized value"

# 2. Comparing string with == (numeric operator)
if ("hello" == "world") {}  # warns: both treated as 0

# 3. Useless use of a variable in void context
$name;   # warns: "Useless use of $name in void context"

# Suppressing a specific warning when you know it's safe:
{
    no warnings 'uninitialized';
    my $val = $x . "suffix";  # no warning in this block
}

# For modern Perl (5.10+), add these too:
use feature 'say';     # enables say() — print with auto-newline
use feature 'state';   # enables state variables (persistent in subs)

# Or all in one:
use 5.010;  # enables all features from Perl 5.10 onwards
use 5.020;  # enables all features from Perl 5.20 onwards
Golden Rule The first three lines of every Perl script should be: #!/usr/bin/perl · use strict; · use warnings;. If you see Perl code without these, treat it with suspicion — it may have hidden bugs.
strict vars
Requires my, our, or local for every variable. Typos in variable names become compile-time errors, not silent wrong-value bugs.
strict refs
Prevents using a string as a variable name (symbolic reference). This closes a footgun: $$name where $name = "x" would access $x without strict.
say vs print
say "hello" is print "hello\n" with an automatic newline appended. Available from Perl 5.10 with use feature 'say' or use 5.010.
$$
Special Variables
#14 The most important built-in Perl variables
special varsbuilt-in

Perl has dozens of special punctuation variables. Most have long English aliases via use English. Here are the ones you'll encounter constantly. They're documented fully in perlvar.

special_vars.pl
# $_ — the default variable (used by loops, grep, map, etc.)
for (qw(a b c)) {
    print;        # print $_ — print without argument uses $_
}

# @_ — arguments to the current subroutine
sub show_args {
    print "Got: " . join(", ", @_) . "\n";
}

# $! — OS error string after failed system call
open(my $fh, '<', 'missing.txt') or print "Error: $!\n";
# Error: No such file or directory

# $@ — error from last eval { } block
eval { die "something failed\n" };
print "Caught: $@" if $@;

# $0 — name of the current script
print "Running: $0\n";

# $$ — process ID of current Perl process
print "PID: $$\n";

# @ARGV — command-line arguments to the script
# perl script.pl arg1 arg2  -->  @ARGV = ("arg1","arg2")
if (@ARGV) {
    print "First arg: $ARGV[0]\n";
}

# %ENV — environment variables
print "Home: $ENV{HOME}\n";
print "Path: $ENV{PATH}\n";

# $/ — input record separator (default: newline)
# Setting $/ to undef makes <$fh> read entire file at once (slurp mode)
my $entire_file;
{
    local $/ = undef;   # temporarily unset (local = restore after block)
    open(my $f, '<', 'myfile.txt') or die $!;
    $entire_file = <$f>; # reads entire file
}

# $, — output field separator (between print args, default: none)
# $\ — output record separator (after each print, default: none)
{
    local $, = ", ";   # print "a","b","c"  ==>  "a, b, c"
    local $\ = "\n";   # auto-append newline to every print
    print "x", "y", "z";
}
VariableLong name (use English)What it holds
$_$ARGDefault input / loop variable
@_Sub argument list
$!$OS_ERROROS error from last failed syscall
$@$EVAL_ERRORError from last eval block
$0$PROGRAM_NAMEScript filename
$$$PID / $PROCESS_IDCurrent process ID
@ARGVCommand-line arguments
%ENVEnvironment variables
$/$INPUT_RECORD_SEPARATORLine ending for reading
$,$OUTPUT_FIELD_SEPARATORSeparator between print args
$\$OUTPUT_RECORD_SEPARATORAppended after each print
$1 $2...Regex capture groups
#fruitsLast index of @fruits
Tip Use local — not my — when temporarily changing special variables like $/, $,, $\. local saves the old value and restores it when the block exits, even if an error occurs. my creates a new variable that shadows the global without restoring it.