NexusCS

Perl

Scripting languages
Quick reference for Perl - a high-level, interpreted language renowned for text processing, regular expressions, and system administration.
featured

Getting started

Introduction

Perl is a high-level, general-purpose, interpreted, dynamic programming language known for text processing and system administration.

Hello World

#!/usr/bin/env perl
use strict;
use warnings;

print "Hello, World!\n";

Variables

# Scalar (single value)
my $name = "John";
my $age = 30;
my $price = 19.99;

# Array (ordered list)
my @colors = ("red", "green", "blue");
my @numbers = (1, 2, 3, 4, 5);

# Hash (key-value pairs)
my %person = (
    name => "Alice",
    age => 25,
    city => "NYC"
);

Basic operators

# String concatenation
my $full = $first . " " . $last;

# String repetition
my $line = "-" x 40;  # 40 dashes

# Numeric comparison
$a == $b    # equal
$a != $b    # not equal
$a < $b     # less than

# String comparison
$a eq $b    # equal
$a ne $b    # not equal
$a lt $b    # less than

Variables

Scalars

my $string = "Hello";
my $number = 42;
my $float = 3.14;
my $ref = \@array;  # Reference

# Access
print $string;
print "$string world";  # Interpolation

Arrays

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

# Access by index (0-based)
print $fruits[0];     # apple
print $fruits[-1];    # cherry (last)

# Size
my $size = scalar @fruits;
my $last_idx = $#fruits;  # Last index

# Slice
my @some = @fruits[0, 2];  # apple, cherry

Hashes

my %ages = (
    Alice => 30,
    Bob => 25,
    Carol => 35
);

# Access
print $ages{Alice};   # 30

# Keys and values
my @names = keys %ages;
my @values = values %ages;

# Check existence
if (exists $ages{Alice}) { ... }

# Delete
delete $ages{Bob};

Special variables

$_      # Default variable
@_      # Subroutine parameters
$!      # System error message
$?      # Child process exit status
$$      # Process ID
$0      # Program name
@ARGV   # Command line arguments
%ENV    # Environment variables

Control structures

If statements

if ($age >= 18) {
    print "Adult";
} elsif ($age >= 13) {
    print "Teenager";
} else {
    print "Child";
}

# Postfix form
print "Adult" if $age >= 18;

# Unless (negated if)
unless ($error) {
    process_data();
}

print "OK" unless $error;

Ternary operator

my $status = $age >= 18 ? "Adult" : "Minor";

Loops

# For loop
for (my $i = 0; $i < 10; $i++) {
    print "$i\n";
}

# Foreach loop
foreach my $item (@items) {
    print "$item\n";
}

# Iterate with index
for my $i (0 .. $#items) {
    print "$i: $items[$i]\n";
}

# While loop
while ($count < 10) {
    $count++;
}

# Until loop
until ($done) {
    process();
}

Loop controls

# Next (continue)
foreach my $num (@numbers) {
    next if $num % 2 == 0;  # Skip even
    print "$num\n";
}

# Last (break)
foreach my $item (@items) {
    last if $item eq "stop";
    print "$item\n";
}

# Redo (restart iteration)
while ($tries < 3) {
    get_input();
    redo if invalid($input);
    $tries++;
}

Subroutines

Basic subroutines

sub greet {
    my ($name) = @_;
    return "Hello, $name!";
}

my $msg = greet("Alice");

Multiple parameters

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

my $sum = add(5, 3);

Variable arguments

sub sum_all {
    my $total = 0;
    foreach my $num (@_) {
        $total += $num;
    }
    return $total;
}

my $result = sum_all(1, 2, 3, 4, 5);

Default parameters

sub greet {
    my ($name, $greeting) = @_;
    $greeting //= "Hello";  # Default value
    return "$greeting, $name!";
}

print greet("Bob");              # Hello, Bob!
print greet("Bob", "Hi");        # Hi, Bob!

Named parameters

sub create_user {
    my %args = @_;
    my $name = $args{name};
    my $email = $args{email};
    my $role = $args{role} // "user";
    # ...
}

create_user(
    name => "Alice",
    email => "alice@example.com",
    role => "admin"
);

Return values

# Single value
sub square {
    my ($n) = @_;
    return $n * $n;
}

# Multiple values
sub min_max {
    my @nums = @_;
    my $min = (sort {$a <=> $b} @nums)[0];
    my $max = (sort {$a <=> $b} @nums)[-1];
    return ($min, $max);
}

my ($min, $max) = min_max(3, 7, 1, 9);

Regular expressions

Matching

my $text = "Hello World";

# Match operator
if ($text =~ /World/) {
    print "Found!\n";
}

# Negated match
if ($text !~ /Goodbye/) {
    print "Not found!\n";
}

# Case insensitive
if ($text =~ /world/i) {
    print "Found!\n";
}

Capturing

my $date = "2026-02-06";

if ($date =~ /(\d{4})-(\d{2})-(\d{2})/) {
    my $year = $1;
    my $month = $2;
    my $day = $3;
    print "Year: $year\n";
}

# Named captures (Perl 5.10+)
if ($date =~ /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/) {
    print "Year: $+{year}\n";
}

Substitution

my $text = "Hello World";

# Replace first occurrence
$text =~ s/World/Perl/;

# Replace all (global)
$text =~ s/o/0/g;

# Case insensitive replace
$text =~ s/hello/Hi/i;

# With backreferences
$text =~ s/(\w+) (\w+)/$2 $1/;  # Swap words

Modifiers

/pattern/i   # Case insensitive
/pattern/g   # Global (all matches)
/pattern/m   # Multiline (^ and $ match line boundaries)
/pattern/s   # Single line (. matches newline)
/pattern/x   # Extended (ignore whitespace, allow comments)

# Combined
$text =~ s/pattern/replacement/gi;

Common patterns

# Email
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/

# URL
/https?:\/\/[\w\-\.]+\.\w{2,}/

# IP address
/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/

# Phone (US)
/\d{3}-\d{3}-\d{4}/

# Whitespace
/\s+/      # One or more
/^\s+/     # Leading
/\s+$/     # Trailing

Array operations

Adding elements

my @stack = (1, 2, 3);

# Add to end
push @stack, 4, 5;        # (1,2,3,4,5)

# Add to beginning
unshift @stack, 0;        # (0,1,2,3,4,5)

Removing elements

my @stack = (1, 2, 3, 4, 5);

# Remove from end
my $last = pop @stack;    # Returns 5

# Remove from beginning
my $first = shift @stack; # Returns 1

# Remove by index
splice @stack, 1, 1;      # Remove element at index 1

Slicing

my @nums = (0, 1, 2, 3, 4, 5);

# Get range
my @slice = @nums[1..3];  # (1, 2, 3)

# Get specific elements
my @some = @nums[0, 2, 4]; # (0, 2, 4)

# Replace slice
@nums[1..3] = (10, 20, 30);

Sorting

my @nums = (5, 2, 8, 1, 9);

# Numeric sort
my @sorted = sort {$a <=> $b} @nums;

# String sort (default)
my @words = sort @words;

# Reverse sort
my @reversed = sort {$b <=> $a} @nums;

# Reverse
my @rev = reverse @nums;

Transforming

# Map (transform each element)
my @squared = map { $_ * $_ } @nums;
my @upper = map { uc $_ } @words;

# Grep (filter)
my @evens = grep { $_ % 2 == 0 } @nums;
my @long = grep { length $_ > 5 } @words;

# Join
my $str = join(", ", @items);

# Split
my @parts = split(/,/, $str);
my @words = split(/\s+/, $text);

File operations

Reading files

# Three-argument open (recommended)
open(my $fh, '<', 'input.txt') or die "Cannot open: $!";

while (my $line = <$fh>) {
    chomp $line;  # Remove newline
    print "$line\n";
}

close $fh;

# Read all lines
open(my $fh, '<', 'input.txt') or die $!;
my @lines = <$fh>;
close $fh;

Writing files

# Write mode (overwrites)
open(my $fh, '>', 'output.txt') or die $!;
print $fh "Hello\n";
print $fh "World\n";
close $fh;

# Append mode
open(my $fh, '>>', 'output.txt') or die $!;
print $fh "Appended line\n";
close $fh;

File tests

if (-e $file) { }     # Exists
if (-f $file) { }     # Regular file
if (-d $file) { }     # Directory
if (-r $file) { }     # Readable
if (-w $file) { }     # Writable
if (-x $file) { }     # Executable
if (-z $file) { }     # Zero size
if (-s $file) { }     # Size in bytes

my $size = -s $file;
my $age = -M $file;   # Age in days

Directory operations

# List directory
opendir(my $dh, '.') or die $!;
my @files = readdir($dh);
closedir $dh;

# Filter out . and ..
@files = grep { !/^\.\.?$/ } @files;

# Create directory
mkdir('newdir', 0755) or die $!;

# Remove directory
rmdir('olddir') or die $!;

# Change directory
chdir('/tmp') or die $!;

Path operations

use File::Basename;
use File::Spec;

my $path = "/path/to/file.txt";

my $dir = dirname($path);      # /path/to
my $file = basename($path);    # file.txt
my ($name, $ext) = split(/\./, $file);

# Build path
my $full = File::Spec->catfile($dir, $file);

String operations

Basic manipulation

my $str = "Hello World";

# Length
my $len = length($str);

# Substring
my $sub = substr($str, 0, 5);     # "Hello"
my $sub = substr($str, 6);        # "World"
my $sub = substr($str, -5);       # "World"

# Replace substring
substr($str, 0, 5) = "Hi";        # "Hi World"

# Case conversion
my $upper = uc($str);             # HELLO WORLD
my $lower = lc($str);             # hello world
my $title = ucfirst($str);        # Hello world

Trimming

my $str = "  Hello World  ";

# Remove leading whitespace
$str =~ s/^\s+//;

# Remove trailing whitespace
$str =~ s/\s+$//;

# Remove both (trim)
$str =~ s/^\s+|\s+$//g;

# Using a subroutine
sub trim {
    my $s = shift;
    $s =~ s/^\s+|\s+$//g;
    return $s;
}

Searching

my $str = "Hello World";

# Index (position of substring)
my $pos = index($str, "World");      # 6
my $pos = index($str, "Foo");        # -1 (not found)

# Reverse index
my $pos = rindex($str, "o");         # 7 (last o)

# Contains
if (index($str, "World") != -1) { }

# Starts with
if ($str =~ /^Hello/) { }

# Ends with
if ($str =~ /World$/) { }

Splitting and joining

# Split string
my $csv = "a,b,c,d";
my @parts = split(/,/, $csv);

# Limit splits
my @parts = split(/,/, $csv, 2);  # ("a", "b,c,d")

# Split on whitespace
my @words = split(/\s+/, $text);

# Join array
my $str = join(", ", @items);
my $str = join("\n", @lines);

Packages and modules

Using modules

# Load module
use strict;
use warnings;
use Data::Dumper;
use File::Basename qw(dirname basename);

# Runtime loading
require Some::Module;

# Version requirement
use Modern::Perl 5.10;

Creating packages

# lib/MyModule.pm
package MyModule;
use strict;
use warnings;

sub new {
    my ($class, %args) = @_;
    my $self = \%args;
    bless $self, $class;
    return $self;
}

sub method {
    my ($self, $arg) = @_;
    return $self->{value};
}

1;  # Must return true

Importing functions

# Exporter pattern
package MyUtils;
use strict;
use warnings;
use Exporter 'import';

our @EXPORT_OK = qw(func1 func2);

sub func1 { ... }
sub func2 { ... }

1;
# Usage
use MyUtils qw(func1 func2);
func1();
func2();

References

Creating references

my $scalar = 42;
my @array = (1, 2, 3);
my %hash = (a => 1, b => 2);

# References
my $scalar_ref = \$scalar;
my $array_ref = \@array;
my $hash_ref = \%hash;

# Anonymous structures
my $arr_ref = [1, 2, 3];
my $hash_ref = {a => 1, b => 2};

Dereferencing

# Explicit dereferencing
my $val = ${$scalar_ref};
my @arr = @{$array_ref};
my %hsh = %{$hash_ref};

# Arrow operator
my $first = $array_ref->[0];
my $value = $hash_ref->{key};

# Method call
my $result = $object_ref->method();

Nested structures

# Array of arrays
my $matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

print $matrix->[0][0];  # 1
print $matrix->[1][2];  # 6

# Hash of hashes
my $users = {
    alice => {age => 30, city => "NYC"},
    bob => {age => 25, city => "LA"}
};

print $users->{alice}{age};  # 30

Object-oriented Perl

Basic class

package Person;
use strict;
use warnings;

sub new {
    my ($class, %args) = @_;
    my $self = {
        name => $args{name},
        age => $args{age}
    };
    bless $self, $class;
    return $self;
}

sub get_name {
    my ($self) = @_;
    return $self->{name};
}

sub set_age {
    my ($self, $age) = @_;
    $self->{age} = $age;
}

1;

Using objects

use Person;

my $person = Person->new(
    name => "Alice",
    age => 30
);

my $name = $person->get_name();
$person->set_age(31);

Inheritance

package Employee;
use strict;
use warnings;
use parent 'Person';  # Inherits from Person

sub new {
    my ($class, %args) = @_;
    my $self = $class->SUPER::new(%args);
    $self->{employee_id} = $args{employee_id};
    return $self;
}

1;

Common idioms

Defined-or operator

# Default value if undef
my $name = $user_input // "Anonymous";

# In-place
$config{timeout} //= 30;

Safe navigation

# Check existence before access
my $value = exists $hash{key} ? $hash{key} : undef;

# With defined
my $upper = defined $str ? uc($str) : undef;

Loop labels

OUTER: for my $i (1..10) {
    INNER: for my $j (1..10) {
        last OUTER if $i * $j > 50;
        next INNER if $i == $j;
        print "$i x $j\n";
    }
}

Schwartzian transform

# Sort by complex criteria
my @sorted = map { $_->[0] }
             sort { $a->[1] <=> $b->[1] }
             map { [$_, compute($_)] }
             @items;

Slurp mode

# Read entire file
{
    local $/;  # Undefine record separator
    open my $fh, '<', 'file.txt' or die $!;
    my $content = <$fh>;
    close $fh;
}

# Or with Path::Tiny
use Path::Tiny;
my $content = path('file.txt')->slurp_utf8;

Error handling

Die and warn

# Die (fatal error)
die "Error: file not found" unless -e $file;
open my $fh, '<', $file or die "Cannot open $file: $!";

# Warn (non-fatal)
warn "Warning: deprecated function\n";

Eval (try-catch)

# Catch exceptions
eval {
    risky_operation();
};
if ($@) {
    print "Error: $@\n";
}

# With Try::Tiny (recommended)
use Try::Tiny;

try {
    risky_operation();
} catch {
    print "Error: $_\n";
};

Custom exceptions

package MyException;
use strict;
use warnings;
use overload '""' => 'as_string';

sub new {
    my ($class, $message) = @_;
    return bless {message => $message}, $class;
}

sub as_string {
    my ($self) = @_;
    return $self->{message};
}

1;
# Throw
die MyException->new("Something went wrong");

Gotchas and best practices

Always use strict and warnings

use strict;
use warnings;

Catches common errors like typos and undefined variables.

Numeric vs string comparisons

# WRONG
if ("10" == "9") { }   # False (numeric)

# RIGHT
if ("10" eq "9") { }   # True (string comparison)

Use ==, !=, <, > for numbers. Use eq, ne, lt, gt for strings.

Three-argument open

# WRONG (security risk)
open my $fh, $filename or die $!;

# RIGHT
open my $fh, '<', $filename or die $!;

Prevents filename injection attacks.

Lexical filehandles

# WRONG (global)
open FILE, '<', 'data.txt' or die $!;

# RIGHT (lexical, auto-closes)
open my $fh, '<', 'data.txt' or die $!;

Lexical filehandles close automatically when out of scope.

Array in scalar context

my @items = (1, 2, 3, 4, 5);
my $count = @items;  # 5 (size)

# Use scalar() to be explicit
my $count = scalar @items;

Hash in list context

my %hash = (a => 1, b => 2);

# Flattens to (a, 1, b, 2)
my @list = %hash;

# Get keys or values explicitly
my @keys = keys %hash;
my @values = values %hash;

Postfix conditionals

# These are readable
print "Found\n" if $found;
next unless $valid;

# These are less readable
do_complex_operation() if some_complex_condition();

Use postfix form for simple one-liners only.

Regular expression gotchas

# Greedy by default
"<tag>content</tag>" =~ /<.*>/;  # Matches entire string

# Use non-greedy
"<tag>content</tag>" =~ /<.*?>/; # Matches <tag>

# Use \Q..\E for literal strings
my $literal = "a.b";
$text =~ /\Q$literal\E/;  # Matches "a.b" not "a<any>b"

Undefined values

# Check for defined
if (defined $var) { }

# Don't compare with undef
if ($var == undef) { }    # WRONG
if (!defined $var) { }    # RIGHT

# Use defined-or operator
my $val = $config{key} // $default;

Also see