ZetCode

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.

log.pl
#!/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.

csv.pl
#!/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.

ini.pl
#!/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.

url.pl
#!/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.

validate.pl
#!/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

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Perl tutorials.