В настоящее время Perl обделяется вниманием: о нём мало что и где можно услышать и увидеть. При этом Perl действительно уникальный язык программирования, который может предложить что-то новое, и особенности которого сильно выделяют его среди других. И сегодня я вам о нём поведаю, а также расскажу о его фичах с примерами его примения.

Perl
Perl

Что из себя представляет Perl?

Perl — это скриптовый язык (как Bash или Python), разработанный ещё в 1987 году Лэри Волом. Perl — динамически типизированный. По синтаксису код на нём выглядит как что‑то между Python, C, и чем‑то своим, при этом одним из его девизов является — «Здесь больше одного способа это сделать», что отражается в исключительной гибкости языка.

Пример вывода “Hello, World!”:

my $var = "World"; # Инициализация локальной переменной $var

print ("Hello, $var!\n");       # Стандартный пример вывода "Hello, World!"
print "Hello, $var!\n";         # Без скобочек
print "Hello, ", $var, "!\n";   # В виде листа аргументов, разделённых запятой
print "Hello, " . $var . "!\n"; # В виде строки, которая соединяется в одну точками
printf "Hello, %s!\n", $var;    # printf
say "Hello, $var!"; # say - тоже самое, что и print, только с переносом строки на конце
                    # Есть в Perl 5.10 и позднее

Пример отсчёта от 10 до 1:

# Стандартный для C for loop
for (my $i = 10; $i > 0; --$i) {
    say $i;
}

# foreach по перевёрнутому массиву от 1 до 10
foreach my $i (reverse 1..10) {
    say $i;
}

# Никакой разницы между foreach и for нет
for my $i (reverse 1..10) {
    say $i;
}

# Запись и вывод из дефолтного значения $_
for (reverse 1..10) {
    # То же самое как если бы мы записали
    # for my $_ (reverse 1..10)
    say $_;
}

# В одну строчку
say $_ for reverse 1..10;

Кроссплатформенность и простота работы с системой.

Так как Perl изначально создавался как скриптовый язык общего назначения для Unix, на многих Unix системах он стоит по умолчанию. Можете сами проверить есть ли он у вас whereis perl.

Конечно для пользователей прекрасной и неповторимой Windows не всё так просто, но в отличие от Bash, не нужны никакие танцы с бубном для эмуляции Linux и достаточно просто cкачать интерпретатор Perl.

Perl позволяет очень просто работать с файловой системой: читать, изменять, удалять, файлы и папки, свой простой аналог функции cat или sort в Perl можно написать всего в одну строку:

# cat в одну строку, который принимает в себя любое число файлов и выводит их содержание.
# Даже точка с запятой на конце не нужны потому, что в конце блока её можно не ставить.

print <> # Алмазный оператор <> - это оператор ввода пользовательского выбора

# Этот оператор работает следующим образом: если пользователь передал в программу файлы,
# то он возвращает их содержание, если нет, то принимает ввод с консоли до тех пор пока
# пользователь не вернёт end-of-file код (Сtrl-D) и возвращает введённое
print sort <> # Сортирует все строки файла и выводит их в консоль
# Аналог 'nl -b a' который нумерует все строки файла и выводит их в консоль

printf "%6d  $_", $. while <> # $. возвращает номер строки
Что-то подобное алмазному оператору <> на языке Perl можно написатьследующим образом:
# Сабрутина (то же самое что и функция в других языках) subr
sub subr {
    my $ret; # Инициализация локальной переменной $ret

    # Если массив входных параметров программы, @ARGV, пустой, то принимать
    # ввод с консоли и построчно объединять его с переменной $ret
    if (!@ARGV)
    {
        while (my $line = <STDIN>) {
            $ret .= $line;
        }
    }

    # Если в программу передавались параметры, то поочереди открывать каждый
    # файл в режиме чтения и построчно записывать его содержание в $ret
    else
    {
        foreach my $ARG (@ARGV)
        {
            open my $fd, "<", $ARG;

            while (my $line = <$fd>) {
                $ret .= $line;
            }
        }
    }
    return $ret;
}

# Вызов функции
print &subr

а

Можете сами попробовать создать main.pl вставить туда код и запустить:

perl main.pl PATH_TO_FILE

В Perl также просто можно вызывать системные комманды и обрабатывать их вывод, что в сочетании с непревзойдённой работой с текстом (о которой будет следующий параграф) может сильно упростить жизнь.

# Оператор `` исполняет команду и возвращает результат её вывода, когда она завершится
my $ping = `ping google.com -w 2`;
print $ping
# -| открывает программу на чтение вывода
open( my $ping, '-|', 'ping google.com -w 2' );

# Будет выводить в консоль результат вывода команды по ходу её выполнения
while my $line (<$ping>) {
    print $line
}

Таким образом я считаю, что Perl куда проще, мощнее и гибче чем Bash, но конечно наверное болшинство знает Bash, да и его использование более распространено. Но тем не менее, если есть возможность писать скрипт на Perl, вместо того, чтобы делать это на Bash, то почему бы и нет, особенно если вы хотите, чтобы этот скрипт работал не только на Unix, но и на Windows, хотя возможно для такого бы также хорошо подошёл Python. Всё зависит от ваших целей, желаний и ограничений.

Работа с текстом

Работа с текстом - это пожалуй самая сильная сторона Perl и главный фактор послуживший его популярности, ну и причина почему я сам начал его изучать. Из‑за глубокой интеграции с regexp и тем как страшно и непонятно код на regexp может выглядеть, иногда можно услышать, что о Perl отзываются как «write only language», но это глубоко не так и даже наоборот и сейчас я докажу это вам на своём примере.

Так вот, когда я делал 3D игру с raylib, у меня появилась потребность в использовании 3D редактора карт и я нашёл TrenchBroom. Для энтити, которых можно добавлять на карту TrenchBroom использует формат .FGD, а этот формат очень специфичный, пеприятный и с ним довольно таки неудобно работать, ну и мне в любом случае прийдётся этих энтити прописывать на C/C++, поэтому зачем вообще к этому формату прикасаться? Вместо этого можно написать программу, которая принимает в себя C/C++ код в виде классов/структур и переводит его в .FGD.

То есть нужна программа которая переводит файл такого формата:

#ifndef EXAMPLE_H
#define EXAMPLE_H

// Example class
struct Example {
    const char* name;
    int hp; // Character's health
    void spawn();
    // Substracts damage from hp
    void takeDamage(int damage);
};

struct Example2 {
    int ammount = 0; // Ammount of stuff
    Example2();
    // Returns true if ammount is even
    bool isEven();
};

#endif //EXAMPLE_H

Вот в это:

@PointClass size(-16 -16 -32, 16 16 32) = Example : "Example class"
[
    name(string): :  : ""
    hp(integer): :  : "Character's health"
]
@PointClass size(-16 -16 -32, 16 16 32) = Example2 : ""
[
    ammount(integer): : 0 : "Ammount of stuff"
]

Ну для начала определим как оно вообще должно из первого сделать второе:

  1. Структуры типа // COMMENT\n struct NAME {...}; переводятся в .FGD классы формата @PointClass size(-16 -16 -32, 16 16 32) = NAME : "COMMENT" [...] при том, что комментарии опциональны.

  2. Поля структуры типа TYPE NAME = DEFINITION; // COMMENT переводятся в поля .FGD класса формата NAME(TYPE): : DEFINITION : "COMMENT", при этом определения полей и комментарии опциональны.

  3. Типы int и const char* переводятся в integer и string.

  4. Все лишние методы класса, лишние комментарии, макросы и инклюды полностью удаляются.

Перед тем как перейти к решению на Perl давайте посмотрим как можно добится подобного результата на другом языке. Тут я уточню, что кроме C/C++ больше ничего то толком и не знаю, поэтому для меня вариантов не много. Очевидно, что для текущей задачи нужно использовать регулярные выражения, а то в противном случае решение получится чрезмерно большим, некрасивым и сложным.

regex.h простой и подойдёт для валидации небольшого набора данных, к примеру IP адрес, пароль, имя пользавателя и тп., но если нужно что-то побольше, то тут его функционала уже начинает не хватать, и получайются очень длинные однострочные write only регекспы. std::regex уже получше, но с ним всё та же проблема.

Есть PCRE2 (Perl Compatable Regular Expressions), который имеет очень мощный функционал, но который требует вызова функций с кучей параметров, что создаёт много шума из-за чего его труднее воспринимать, и для маленькой програмки с одной единственной целью это уже перебор.

Пример кода из их репозитория
/* Set PCRE2_CODE_UNIT_WIDTH to indicate we will use 8-bit input. */
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>

#include <string.h> /* for strlen */
#include <stdio.h>  /* for printf */

int main(int argc, char* argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <pattern> <subject>\n", argv[0]);
        return 1;
    }

    const char *pattern = argv[1];
    const char *subject = argv[2];

    /* Compile the pattern. */
    int error_number;
    PCRE2_SIZE error_offset;
    pcre2_code *re = pcre2_compile(
        pattern,               /* the pattern */
        PCRE2_ZERO_TERMINATED, /* indicates pattern is zero-terminated */
        0,                     /* default options */
        &error_number,         /* for error number */
        &error_offset,         /* for error offset */
        NULL);                 /* use default compile context */
    if (re == NULL) {
        fprintf(stderr, "Invalid pattern: %s\n", pattern);
        return 1;
    }

    /* Match the pattern against the subject text. */
    pcre2_match_data *match_data =
        pcre2_match_data_create_from_pattern(re, NULL);
    int rc = pcre2_match(
        re,                   /* the compiled pattern */
        subject,              /* the subject text */
        strlen(subject),      /* the length of the subject */
        0,                    /* start at offset 0 in the subject */
        0,                    /* default options */
        match_data,           /* block for storing the result */
        NULL);                /* use default match context */

    /* Print the match result. */
    if (rc == PCRE2_ERROR_NOMATCH) {
        printf("No match\n");
    } else if (rc < 0) {
        fprintf(stderr, "Matching error\n");
    } else {
        PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
        printf("Found match: '%.*s'\n", (int)(ovector[1] - ovector[0]),
               subject + ovector[0]);
    }

    pcre2_match_data_free(match_data);   /* Free resources */
    pcre2_code_free(re);
    return 0;
}

А вот boost/regex.hpp - это пожалуй самый лучший вариант из всех, тк совмещает в себе простоту и мощь регекспа Пёрла, никак при этом не давя на глаза.

Пример того как первый шаг конвертации мог бы выглядеть с boost/regex.hpp
#include <boost/regex.hpp>
#include <fstream>
#include <iterator>
#include <string>

int main(int argc, char **argv)
{
    std::ifstream ifs(argv[1], std::ios::binary);
    std::string source((std::istreambuf_iterator<char>(ifs))
                      , std::istreambuf_iterator<char>());

    const boost::regex re(R"((?xs)
    (?: \s* //\s* (?<struct_comment>[^\n]*) )?

    (?<struct>
        \s* struct\s+ (?<struct_name>\w+\d?)\s* \{
            (?<struct_content>.*?)
        \};
    )
    )", boost::regex::perl);

    const std::string repl = R"(
@PointClass size(-16 -16 -32, 16 16 32) = $+{struct_name} : "$+{struct_comment}"
[
    $+{struct_content}
]
)";

    source = boost::regex_replace( source, re, repl
                                 , boost::match_default
                                 | boost::format_perl );

    std::ofstream ofs(argv[2], std::ios::binary);
    ofs << source;
    return 0;
}

Результат конвертации:

#ifndef EXAMPLE_H
#define EXAMPLE_H

@PointClass size(-16 -16 -32, 16 16 32) = Example : "Example class"
[

    const char* name;
    int hp; // Character's health
    void spawn();
    // Substracts damage from hp
    void takeDamage(int damage);

]

@PointClass size(-16 -16 -32, 16 16 32) = Example2 : ""
[

    int ammount = 0; // Ammount of stuff
    Example2();
    // Returns true if ammount is even
    bool isEven();

]


#endif //EXAMPLE_H

У питона есть модуль re тоже способный на написание понятного regex кода:

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

Ну а как бы этот конвертер выглядел на самом Perl? Как-то так, но если вкратце и с пояснениями, то так:

#!/usr/bin/env perl

# Используем самую последнюю версию Perl она по деволту включает strict и warnings
use v5.42;
# Для корректной работы untf8
use utf8;
binmode STDOUT, ':encoding(UTF-8)';
binmode STDIN, ':encoding(UTF-8)';

# Первый аргумент - C стркутура, второй - путь куда будет экспортирован конвертированный файл
my $source = $ARGV[0];
my $dest = $ARGV[1];

# Чтение открытого файла
open (my $source_file, '<', $source)
    or die "Could not open source file: $!\n";

# Соединяем все строки в одну
my $source_code = join '', <$source_file>;


# Заменяем структуру на .FGD
$source_code =~ s{
# Необязательный комментарий к структуре
(?: ^\s* //\s* (?<struct_comment>[^\n]*) )?

# Блок структуры
(?<struct>
    \s* struct\s+ (?<struct_name>\w+\d?)\s* \{
        (?<struct_content>.*?)
    \};
)

}{\@PointClass size(-16 -16 -32, 16 16 32) = $+{struct_name} : "$+{struct_comment}"
[
    $+{struct_content}
]

}mgxs; # m для того, чтобы $^\R были для каждой строки, а не всего файла
       # g для замены всех совпадений, а не только первого
       # x для того, чтобы regexp игнорировал пробельные символы и всё можно
       #   было читаемо разместить
       # s для того, чтобы любой символ . также принимал в себя \n

# Замена полей структуры на поля .FGD класса
$source_code =~ s{
# Парс типа, имени, определения и комментария поля
\s* (?<variable_type>[\w\s*::<>]+?)\s+
    (?<var_name>\w+)
    (?:\s* =\s* (?<default_value>\w+?))?;
    (?:\s* //\s* (?<var_comment>[^\n]+))?
}{
    $+{var_name}($+{variable_type}): : $+{default_value} : "$+{var_comment}"
}mgxs;

# Удаление всех лишних строк
$source_code =~ s{
^\s*
# Строки начинающиеся с #, //, заканчивающиеся с ; с возможным комментарием
# и просто пустые строки
( \#.+ | //.*? | .*?; (\s*//.*)? | )
\R
}{}mgx;

# Переименование типов данных
$source_code =~ s/\((const )?int\)/(integer)/g;
$source_code =~ s/\((const )?char\*\)/(string)/g;

# Запись файла
open (my $dest_file, '>', $dest)
    or die "Could not create dest file: $!\n";

Как вы видите с Perl проще, чем в любом другом языке, писать сложные регулярные выражения, но стоит ли оно того?.. ?

Пользовательские модули CPAN

Для Perl написано очень много пользовательских модулей для самых разных задач (прямо как и для Python), но пожалуй самыми полезными из них являются Getopt::Long и Pod::Usage, которые позволяют очень просто передавать параметры в программу и документировать код. Многие стандартные модули идут сразу вместе с Perl (включая те, про которые я только что рассказал), а те, что и не идут можно достаточно просто скачать. Для этого я рекомендую использовать cpanm, тогда процесс установки любого модуля упрощается до sudo cpanm MODULE. На NixOS я просто пишу такой простой шелл конфиг:

shell.nix
{ pkgs ? import <nixpkgs>{}}:

let
  perll = with pkgs; [
    perl
    perlnavigator
  ];

  # А здесь сами модули
  perl_modules = with pkgs.perl5Packages; [
  ];

in
pkgs.mkShell {
  nativeBuildInputs = perll ++ perl_modules;
}

Ну и на последок давайте я вам покажу как можно использовать всё, о чём я только что рассказал, в одном едином скрипте, который берёт список прокси серверов и пингует через них сайты:

#!/usr/bin/env perl

use v5.42;
use utf8;
binmode STDOUT, ':encoding(UTF-8)';
binmode STDIN, ':encoding(UTF-8)';
# Подключаем пользовательске модули
use Furl;
use Getopt::Long;
use Pod::Usage;

# Задаём дефолтные значения
my $help = 0;
my $proxy_list =
'https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/all/data.txt';
my $timeout = 1;

# Обрабатываем входные параметры
GetOptions( 'help|?' => \$help
          , 'link-to-proxy|p=s' => \$proxy_list
          , 'timeout|t=i' => \$timeout);
pod2usage(1) if $help;
pod2usage(2) unless $ARGV[0];

# Открываем сайт со списком прокси и записываем все прокси в массив @proxy_list
my $furl = Furl->new(timeout => 5);
my @proxy_list = split( /\n/, $furl->get($proxy_list)->content );

# Записываем аргументы переданные в программу в список cайтов, которые мы будем
# пинговать
my @link_list = @ARGV;


my %proxy_hash;
my $counter = @proxy_list;
foreach my $proxy (@proxy_list)
{
    say $counter--, '..'; # Отcчёт того сколько прокси осталось

    foreach my $link (@link_list)
    {
        # Делаем curl запрос
        open my $fh, '-|',
        "curl -s -x GET -o /dev/null --write-out '\%{exitcode} \%{time_total}' --proxy $proxy -m $timeout $link";

        while (my $line = <$fh>)
        {
            # Проверяем что запрос удался и если да, то выводим результат и
            # записываем его в %proxy_hash
            if ($line =~ /^(?<exit_code>\d+) (?<time>.+)/
            and $+{exit_code} == 0 && $+{time} < $timeout)
            {
                say "$+{time} - $proxy";
                $proxy_hash{$proxy} .= $+{time};
            }
        }
    }
}

# Сортируем все прокси по времени и выводим
foreach (sort {$proxy_hash{$a} <=> $proxy_hash{$b}} keys %proxy_hash) {
    say "$proxy_hash{$_} - $_";
}

# perlpod документация
__END__

=head1 SYNOPSIS

main.pl [options...] <urls...>

=head1 OPTIONS

=over 4

=item B<-h, --help>

Prints this message.

=item B<-p, --link-to-proxy> <url>

Link to a site from that to fetch proxy server links.

Default is: https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/all/data.txt

=item B<-t, --timeout> <seconds>

Max time to wait for a responce before switching to next proxy.

Default is: 1

=back

=cut
Результат работы программы
% ./main.pl
Usage:
    main.pl [options...] <urls...>


% ./main.pl -h
Usage:
    main.pl [options...] <urls...>

Options:
    -h, --help
        Prints this message.

    -p, --link-to-proxy <url>
        Link to a site from that to fetch proxy server links.

        Default is:
        https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/al
        l/data.txt

    -t, --timeout <seconds>
        Max time to wait for a responce before switching to next proxy.

        Default is: 1


% ./main.pl -t 2 google.com
3408..
1.499812 - socks5://72.49.49.11:31034
3407..
3406..
1.611254 - socks5://69.61.200.104:36181
3405..
3404..
3403..
3402..
3401..
3400..
3399..
^C

Ресурсы для дальнейшего изучения

Если вас заинтересовал Perl, то я рекомендую следующие ресурсы к ознакомлению:

  • perldoc/perlintro - краткое введение в Perl и его возможности.

  • perldoc/perl - документация всего Perl.

  • perldoc/perlvar - $_, $., $!, $$ и тд.

  • perldoc/perlre - о регулярках в Perl.

  • PerlTutorial - простые туториалы для начинающих.

  • pelmaven/perl-tutuorial - блог на разные темы, знать которые может пригодиться.

  • metacpan - сайт с пользовательскими модулями для Perl.

Ну а также крайне особо сильно настоятельно рекомендую прочитать Learning Perl 8th Edition, очень хорошая книга 2021-ого года. Если к тому времени, как вы читаете эту статью выйдет новое издание — читайте его. Удачи!

Выводы

В процессе написания этой статьи я понял, что уникальность и особенность языка Perl, к сожалению не делает его незаменимым инструментом, а очень даже заменимым (заменимым на Python). У языка есть свои особенности, он по-своему необычен, интересен и крут, но есть ли сейчас много толка в его изучении? Ну насколько много, это вопрос хороший, но толк в этом для меня точно был.

Комментарии (1)


  1. oliva80
    20.04.2026 20:56

    "Забытый” — это сильное преувеличение. Скорее, Perl перешёл из мейнстрима в нишевый инструмент для конкретных задач. Если 15 лет назад для задач на HP-UX на PA-RISC ты выбрал Perl и теперь у тебя куча легаси, он всё ещё твой лучший друг! Без проблем перенесёшь на любую современную платформу, сэкономив немало сил и средств.

    Куча критической инфраструктуры (банки, заводы, энергетика) построена на этом "железе". Можно переписать на Python, но это X-Y месяцев работы команды, тесты, риски. А старый Perl-скрипт просто работает. И теперь твоё мудрое, принятое 15 лет назад решение, экономит компании миллионы.