Perl — зря забытый язык программирования?

от автора

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

Perl

Perl

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

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

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

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

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

# Стандартный для C for loopfor (my $i = 10; $i > 0; --$i) {    say $i;}# foreach по перевёрнутому массиву от 1 до 10foreach 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 можно написатьследующим образом:
# Сабрутина (то же самое что и функция в других языках) subrsub 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 classstruct 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 и warningsuse v5.42;# Для корректной работы untf8use 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; [  ];inpkgs.mkShell {  nativeBuildInputs = perll ++ perl_modules;}

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

#!/usr/bin/env perluse 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_listmy $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 SYNOPSISmain.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.plUsage:    main.pl [options...] <urls...>% ./main.pl -hUsage:    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.com3408..1.499812 - socks5://72.49.49.11:310343407..3406..1.611254 - socks5://69.61.200.104:361813405..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). У языка есть свои особенности, он по-своему необычен, интересен и крут, но есть ли сейчас много толка в его изучении? Ну насколько много, это вопрос хороший, но толк в этом для меня точно был.

ссылка на оригинал статьи https://habr.com/ru/articles/1025824/