Perl parsing with regular expressions
last modified October 20, 2026
Regular expressions are often used to extract structure from text. This article
applies the main features, such as named captures, /x,
/g and \G, to practical parsing tasks: server logs,
CSV, INI files, URLs and input validation.
Parsing a web server log
Log files have a regular structure, which makes them a natural fit for regular
expressions. Named captures turn each field into a hash entry in
%+, so the code reads like the log format.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $log = <<~'LOG';
192.168.1.5 - - [20/Oct/2026:10:15:32 +0000] "GET /index.html HTTP/1.1" 200 5120
10.0.0.7 - bob [20/Oct/2026:10:16:01 +0000] "POST /login HTTP/1.1" 302 0
192.168.1.5 - - [20/Oct/2026:10:17:45 +0000] "GET /missing HTTP/1.1" 404 312
LOG
my $line_re = qr/
^(?<ip>\S+)\s+\S+\s+(?<user>\S+)\s+
\[(?<time>[^\]]+)\]\s+
"(?<method>[A-Z]+)\s+(?<path>\S+)\s+[^"]*"\s+
(?<status>\d{3})\s+(?<bytes>\d+)$
/x;
my %by_status;
for my $line (split /\n/, $log) {
next unless $line =~ $line_re;
my %r = %+;
$by_status{ $r{status} }++;
say "$r{ip} $r{method} $r{path} -> $r{status}";
}
say "$_: $by_status{$_}" for sort keys %by_status;
The pattern is written with /x, so it can be split over several
lines. Each group has a name, such as ip or status,
and the fields of one match are copied from %+ into a regular hash,
because %+ is overwritten by the next match. Lines that do not
match are skipped. The heredoc with <<~'LOG' keeps the sample
data inside the script.
$ perl log.pl 192.168.1.5 GET /index.html -> 200 10.0.0.7 POST /login -> 302 192.168.1.5 GET /missing -> 404 200: 1 302: 1 404: 1
Parsing CSV with quoted fields
Splitting on commas fails when a field contains a comma inside quotes. A pattern
that handles quoted fields, including doubled quotes, can be used with
/g and the \G anchor to walk through the line.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $line = 'id,"Smith, John",42,"say ""hi""",,end';
my @fields;
while ($line =~ /\G(?:"((?:[^"]|"")*)"|([^,]*))(?:,|$)/g) {
my $f = defined $1 ? $1 =~ s/""/"/gr : $2;
push @fields, $f;
last if pos($line) >= length $line;
}
say scalar(@fields), " fields";
say "[$_]" for @fields;
Each pass of the loop matches either a quoted field or a plain one, followed by
a comma or the end of the line. The anchor \G forces each match to
start where the previous one stopped, so nothing is skipped silently. A doubled
quote inside a quoted field is turned back into a single one. Empty fields are
kept. The loop stops when pos reaches the end of the string. For
real CSV data with embedded newlines, use the Text::CSV module.
$ perl csv.pl 6 fields [id] [Smith, John] [42] [say "hi"] [] [end]
Parsing an INI file
Configuration files of the INI type consist of sections in brackets and
key = value lines. Three small patterns handle comments, sections
and settings.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $ini = <<~'INI';
; global comment
name = demo
[database]
host = localhost
port = 5432 # default port
[paths]
root = /var/www
INI
my (%config, $section);
$section = 'main';
for my $line (split /\n/, $ini) {
$line =~ s/\s*[;#].*$//;
next if $line =~ /^\s*$/;
if ($line =~ /^\s*\[([^\]]+)\]\s*$/) {
$section = $1;
} elsif ($line =~ /^\s*([^=\s]+)\s*=\s*(.*?)\s*$/) {
$config{$section}{$1} = $2;
}
}
for my $s (sort keys %config) {
say "[$s]";
say " $_ = $config{$s}{$_}" for sort keys %{ $config{$s} };
}
The first substitution strips comments that start with a semicolon or hash mark,
and blank lines are skipped. A line in brackets selects the current section,
while a key = value line is stored in a hash of hashes under that
section. Keys before the first section end up in main. The
non-greedy (.*?)\s*$ trims trailing whitespace from the value.
$ perl ini.pl [database] host = localhost port = 5432 [main] name = demo [paths] root = /var/www
Parsing a URL
A URL has a fixed structure: scheme, host, optional port, path, query and fragment. A pattern with named groups and optional parts splits it into components.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $url = 'https://example.com:8080/path/to/page.html?q=perl&lang=en#top';
my $re = qr{
^(?<scheme>[a-z][a-z0-9+.-]*)://
(?<host>[^/:?\#]+)
(?::(?<port>\d+))?
(?<path>/[^?\#]*)?
(?:\?(?<query>[^\#]*))?
(?:\#(?<fragment>.*))?$
}xi;
if ($url =~ $re) {
say "$_: ", $+{$_} // '(none)' for qw(scheme host port path query fragment);
my %params = map { split /=/, $_, 2 } split /&/, $+{query};
say "param $_ = $params{$_}" for sort keys %params;
}
Each optional part is wrapped in a group followed by ?, and the
missing ones are reported through the defined-or operator. The #
inside the pattern is escaped, because with /x an unescaped one
starts a comment. The query string is then split on & and on
the first = to build a hash of parameters. In production code the
URI module also handles percent-encoding and edge cases.
$ perl url.pl scheme: https host: example.com port: 8080 path: /path/to/page.html query: q=perl&lang=en fragment: top param lang = en param q = perl
Validating input
Patterns anchored with ^ and $ validate that a whole
value has the expected shape. A table of patterns keeps the rules together.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my %patterns = (
email => qr/^[\w.+-]+@[\w-]+(?:\.[\w-]+)+$/,
date => qr/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/,
phone => qr/^\+?\d{1,3}[ -]?\(?\d{3}\)?[ -]?\d{3}[ -]?\d{4}$/,
hex => qr/^#?(?:[0-9a-f]{3}){1,2}$/i,
);
my @tests = (
[ email => 'jan@example.com' ],
[ email => 'not an email' ],
[ date => '2026-10-20' ],
[ date => '2026-13-01' ],
[ phone => '+1 555-123-4567' ],
[ hex => '#1A2b3C' ],
[ hex => '#12345' ],
);
for my $t (@tests) {
my ($type, $value) = @$t;
printf "%-6s %-18s %s\n", $type, $value, $value =~ $patterns{$type} ? 'ok' : 'rejected';
}
The date pattern checks the ranges of month and day, but not the length of the
month, so a value such as 2026-02-31 would still pass. The e-mail and phone
patterns are deliberately simple. Regular expressions can reject clearly wrong
input; they cannot prove that an address exists or that a date is real. Use a
module such as Time::Piece to check dates and send a confirmation
message to verify an address.
$ perl validate.pl email jan@example.com ok email not an email rejected date 2026-10-20 ok date 2026-13-01 rejected phone +1 555-123-4567 ok hex #1A2b3C ok hex #12345 rejected
Best Practices
- Anchor patterns: use
^and$so that the whole line has to match. - Name your captures:
%+is clearer than$1,$2,$3and survives changes to the pattern. - Use /x: spread long patterns over lines and add comments.
- Prefer a module for complex formats:
Text::CSV,URIandConfig::Tinyhandle the corner cases. - Keep the validation simple: a regex can check the shape of a value, not whether it is true.
In this article we have used regular expressions to parse log lines, CSV, INI files and URLs, and to validate input.
Source
perlre: Perl regular expressions
Author
List all Perl tutorials.