В настоящее время 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"]
Ну для начала определим как оно вообще должно из первого сделать второе:
-
Структуры типа
// COMMENT\n struct NAME {...};переводятся в .FGD классы формата@PointClass size(-16 -16 -32, 16 16 32) = NAME : "COMMENT" [...]при том, что комментарии опциональны. -
Поля структуры типа
TYPE NAME = DEFINITION; // COMMENTпереводятся в поля .FGD класса форматаNAME(TYPE): : DEFINITION : "COMMENT", при этом определения полей и комментарии опциональны. -
Типы int и const char* переводятся в integer и string.
-
Все лишние методы класса, лишние комментарии, макросы и инклюды полностью удаляются.
Перед тем как перейти к решению на 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/