26 September 2009

Однострочные программы на Perl

Perl

Введение


Я собираюсь рассказать об однострочных программах на Perl. Если вы овладете однострочным Perl`ом, то можете сэкономить кучу времени (я экономлю).

Цель поста — показать как Perl можно использовать заместо find, grep, awk, sed. В конце поста будет написано зачем это надо.

Ну обо всём по порядку.

Флаги


Флаг -e
Флаг позволяет запускать перловый код прямо в консоли, эту возможность я использую для проверки какого-нибудь тестового кода.
Допустим, я хочу узнать десятичное значение шестнадцатеричного числа 0xFA23B:
perl -e "print 0xFA23B"

Примечание. Когда однострочный перловый код запускаем из под Винды, то код нужно заключать в двойные кавычки:
perl -e "print 0xFA23B"
, в случае Линукса/Юникса код может быть как в двойных кавычках, так и в одинарных, но в Юниксе/Линуксе случае двойных кавычек приходится экранировать знаки "$":
perl -e "\$i = 0;print \$i"

Примечание. После флага -e должен следовать сразу код.

Флаг -l
Это флаг делает инициализирует переменные $/ и $\ значением "\n";
Переменная $/ задаёт разделитель входных полей.
Переменная $\ задаёт, что будет выводиться после команды print.

Программа:
perl -le "print 1"

Эквивалентна следующей:
BEGIN { $/ = "\n"; $\ = "\n"; }
print 1;

Таким образом в конце не приходится писать print "\n";

Флаг -n
Отсюда начинается самое интересное.

Следующий код:
perl -ne 'print 1'

эквивалентен этому:
LINE: while (defined($_ = <ARGV>)) {
   print 1;
}

Где же это можно использовать?
А вот, например, нам надо добавить к именам файлов, имена которых начинаются с цифр, расширение «bak»:

Вуаля:
ls | perl -lne 'rename $_, "$_.bak" if /^\d+/'

А для Windows? Пожалуйста:
dir /b | perl 	-lne "rename $_, \"$_.bak\" if /^\d+/"

Посмотрим на получившуюся программу:
BEGIN { $/ = "\n"; $\ = "\n"; }
LINE: while (defined($_ = <ARGV>)) {
    chomp $_;
    rename $_, "$_.bak" if /^\d+/;
}

chomp $_; взялся от флага -l: он совместно с -n даёт добавляет ещё и chomp $_;, а не только BEGIN { $/ = "\n"; $\ = "\n"; }

Флаг -a
Флаг -a позволяет использовать Perl как awk.

Cледующий код:
perl -nae "print 1"

эквивалентен:
LINE: while (defined($_ = <ARGV>)) {
    our(@F) = split(" ", $_, 0);
    print 1;
}

То есть каждая строка расщепляется split`ом по пробелам, и получившиеся поля кладутся в массив @F.
Разделитель полей можно поменять с помощью флага -F.

Допустим, надо вывести из файла /etc/passwd имена пользователей с их домашними директориями:
less /etc/passwd | perl -F: -nlae 'print "$F[0]:$F[4]"'

А Винде я, например, хочу узнать имена файлов в папке, которые последний раз изменял в сентябре 2009:
dir /TW | perl -nale "print $F[$#F] if $F[0] =~ /\.09\.2009/"

Флаг -p
Этот флаг делает тоже, что -n только добавляет ещё блок continue c «print $_».

Следующий код:
perl -pe "print 1"

эквивалентен:
LINE: while (defined($_ = <ARGV>)) {
    print 1;
}
continue {
    print $_;
}

Допустим, мы выводим файл /etc/passwd, попутно заменяя 3 на 6.

Вместо такого кода:
less /etc/passwd | perl -ne "s/3/6/;print \$_"

Мы можем написать:
less /etc/passwd | perl -pe "s/3/6/"

Флаг -i
Флаг i позволяет изменять файлы.

Следующая программа:
perl -i.bak -pe "s/foo/bar/"

эквивалентна этой:
BEGIN { $^I = ".bak"; }
LINE: while (defined($_ = <ARGV>)) {
    s/foo/bar/;
}
continue {
    print $_;
}

которая в свою очередь эквивалентна этой:
$extension = '.bak';
LINE: while (<>) {
if ($ARGV ne $oldargv) {
	if ($extension !~ /\*/) {
		$backup = $ARGV . $extension;
	}
	else {
		($backup = $extension) =~ s/\*/$ARGV/g;
	}
	rename($ARGV, $backup);
	open(ARGVOUT, ">$ARGV");
	select(ARGVOUT);
	$oldargv = $ARGV;
	}
	s/foo/bar/;
}
continue {
	print;  # this prints to original filename
}
select(STDOUT);

В кратце поясню, что происходит, когда вызываются строки perl -i.bak -pe «код» <имя файла>. Например, мы вызываем:
perl -i.bak -pe "s/foo/bar/" test.txt

Файл test.txt переименовывается в файл test.txt.bak, и создаётся новый файл test.txt. Потом в каждой строке исходного файла заменяется foo на bar, которые записываются в новый фалй test.txt (по-видимому, хоть файл и переименовали, мы всё равно имеем доступ к его строкам?)

Допустим, нужно в файле заменить \r\n на \n:
perl -i.bak -pe 's/\r\n/\n/' test.txt

В результате этого кода получатся два файла: один — test.txt.bak, который является копией исходного, другой — test.txt, где \r\n заменено на \n.

Примечание. Если вы посмотрите внимательно на программу выше ( $extension = '.bak'; ...), то увидите, что если вызывать вот так: perl -ibak_*…, то бэкапный файл будет называться «bak_test.txt», то есть если есть звёздочка в значении параметра i, то это это значение расматривается не как расширение, а как шаблон, где звёздочка обозначает имя файла.

Флаг -M

Флаг -M позволяет подключать модули

Например, я хочу узнать где лежит модуль CGI:

для Windows:
perl -MCGI -le "print $INC{'CGI.pm'}"

для Linux:
perl -MCGI -le "print \$INC{'CGI.pm'}"

Недавно мне понадобилось сделать chmod a+x всем файлам с расширением ".cgi",
но на сервере флаг -R для chmod почему-то не работал, так вот что я сделал, что-то подобное:
perl -MFile::Find -e 'finddepth(sub {print $File::Find::name . "\n"}, "."})' | grep -P '\.cgi$' | perl -nle '`chmod a+x $_`'

Этим кодом «perl -MFile::Find -e 'finddepth(sub {print $File::Find::name. „\n“}, »."})'" Я вызвал функцию finddepth модуля File::Find, которая рекурсивно обошла текущую директорию и вывела полные пути файлов.

Потом грепом я взял только те файлы, которые оканчиваются на '.cgi' (-P означает, что используются перловые регулярные выражения), а следующей программой «perl -nle '`chmod a+x $_`'» я сделал права на выполнение найденым файлам.

Хотя этот код я мог бы записать так:
perl -MFile::Find -e 'finddepth(sub {$n = $File::Find::name;`chmod a+x $n` if $n =~ /\.cgi$/}, ".")'

Заметьте, что надо использовать флаг -l, чтобы в $_, попало имя файла без "\n"

А что если надо подключить некоторые переменные или подпрограммы из подключаемого пакета в пакет main?
Тогда надо писать:
perl -MModule=foo,bar -e '...';

или
perl '-Mmodule qw(foo bar)' -e '...';

BEGIN и END


Можно использовать BEGIN и END, для действий, которые должны происходить в начале и в конце, аналогично как у awk.
perl -e 'BEGIN{<начальные действия>};<действие>;END{конечные действия}';

Например выведем линии, состоящие из 40 знаков "=" в начале и конце отчёта:
dir /b | perl -pe "sub line {print '=' x 40 . \"\n\"};BEGIN{line();};END{line()}"

Дебаг


Чтобы дебажить однострочные программы надо подключать модуль B::Deparse,

Если вы запустите:
perl -MO=Deparse -ne "print 1"

То получите вывод:
LINE: while (defined($_ = <ARGV>)) {
    print 1;
}
-e syntax OK

модуль B::Deparse нужно подключать так: "-MO=Deparse", а не так: "-MB::Deparse". Видимо это сделано для того, чтобы чётко определить, что мы хотим использовать этот модуль для вывода исходного кода программы, а не просто для использования каких его либо методов в программе.

Вот так модуль B::Deparse будет использоваться как обычный модуль, вывода кода не будет:
perl -MB::Deparse  -e "print 1"

В примерах выше я использовал MO=Deparse для вывода кода программ.

Примеры однострочных программ


Вывод количества строк в файле (аналог Юниксовского wc -l)
perl -ne '}{ print $.' abc.txt

Эквивалентная программа:
LINE: while (defined($_ = <ARGV>)) {
    ();
}
{
    print $.;
}

Здесь использован хитрый приём "}{". Мы сами закрыли цикл.

Вывод двоичного числа
perl -e "printf '%b', shift" 200

Замена \r\n на \n в файле
perl -i.bak -pe 's/\r\n/\n/' file.txt

Примечание. Почему-то подобный код не работает в Винде: она упорно добавляет \r\n, я делал binmode ARGV,
binmode $ARGV, binmode *ARG{FILEHANDLE}, но ничего не помогало, буду биться дальше. Буду благодарен вам, если напишите как заменить \r\n на \n в Винде.

Преобразование IP адреса из формы «цифры-точки» в число:
perl -e "print unpack('N', pack('C4', split /\./, shift))" 127.0.0.1

Удаление папок .svn в текущей папке и её подпапках (рекурсивно)
perl -MFile::Find -MCwd -e '$path = getcwd;finddepth(sub {print $File::Find::name."\n"}, "$path")' | grep '\.svn$' | perl -ne 'system("rm -rf $_")';

тоже самое для Windows:
perl -MFile::Find -e "finddepth(sub{ print $File::Find::name . \"\n\"; }, '.')" | perl -ne "print if /.svn$/" | perl -pe "s|/|\\|g" | perl -ne "system(\"rd /s /q $_\");"

Вывод IP-aдреса в шестнадцатеричной форме
perl -e "printf '%02x' x 4, split /\./, shift" 127.0.0.1

Добавление строки "#!/usr/bin/perl" в начало файла
perl -i.bak -pe "print \"#!/usr/bin/perl\n\" if $. == 1" abc.pl 

Для Линукса/Юникса:
perl -i.bak -pe 'print "#!/usr/bin/perl\n" if $. == 1' abc.pl

Зачем это надо?


Как и обещал напишу, зачем это всё надо. Вы можете сказать, что есть find, awk, grep, sed, зачем однострочный Perl?

Ну во-первых, в Винде, по-умолчанию, грепа и awk нету. Да конечно быстрее пользоваться grep для отбора строк, но что если надо сделать чуть по-больше, например переименовать файл? Вы скажете есть ведь find, да есть. Так что же я скажу в защиту однострочного перла?

А вот что:

Во-первых, если программируешь на Perl, то Perl помнишь очень хорошо и можешь сразу начать писать однострочную программу, не заглядывая в man. (Поначалу, правда, может быть немного не привычно, но когда втянешься, будет легко)

Во-вторых, бывает удобно использовать именно Perl. Например, когда я хочу иметь аналог awk (смотри флаг -a) с мощью Perl (например хочу использовать в однострочной программе функции pack, unpack или регулярные выражение Perl)

В-третьих,
Perl — мощный язык. Однострочная программа на perl — это обычная программа на Perl, только в командной строке. А значит однострочные программы на перл можно использовать для самых разных задач! (Но думаю, длинные однострочные программы, наверно, лучше не писать, лучше сделать обычный перловый скрипт).

Заключение


Не подумайте только, что я призываю отказаться от grep, find, sed или awk. Не призываю! Я сам продолжаю использовать grep, find. Просто я хотел рассказать о ещё одном полезном инструменте как «однострочный перл», который удобен для тех кто программирует на Perl, ибо: 1) не надо читать man (ты и так всё помнишь), 2) используется мощь Perl

Спасибо за внимание. Тех кто заинтересовался отсылаю сюда:

perldoc.perl.org/perlrun.html — perldoc, описание всех флагов.
sial.org/howto/perl/one-liner — разные примеры

Советую гуглить перловые однострочные программы с словом: one-liners

обн:
Чтобы заменить \r\n на \n в Винде, надо просто написать:
perl -i.bak -ple "s/\r|\n//g;binmode ARGVOUT" file.txt

Респект AntonShcherbinin

обн:
Добавил в код от AntonShcherbinin, "s/\r|\n//g;", а то в Линуксе просто binmode не прокатывает, теперь этот код универсален: работает и в Винде, и в Линуксе.

обн (26.08.2012):
Переписал примеры кода под теги <source lang=«Perl»></source>
Tags:perlperl -eone-linersоднострочныйоднострочники
Hubs: Perl
+86
31.3k 185
Comments 45
Popular right now
SEO-специалист
December 7, 202064,900 ₽Нетология
UX-дизайнер
December 7, 202047,940 ₽Нетология
iOS-разработчик с нуля
December 7, 202070,740 ₽Нетология
Python для работы с данными
December 7, 202031,500 ₽Нетология