В настоящее время 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"
]
Ну для начала определим как оно вообще должно из первого сделать второе:
-
Структуры типа
// COMMENTn 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: %sn", 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 matchn");
} else if (rc < 0) {
fprintf(stderr, "Matching errorn");
} 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* structs+ (?<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* structs+ (?<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>[ws*::<>]+?)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). У языка есть свои особенности, он по-своему необычен, интересен и крут, но есть ли сейчас много толка в его изучении? Ну насколько много, это вопрос хороший, но толк в этом для меня точно был.
Автор: infgotoinf
