Введение
Я собираюсь рассказать об однострочных программах на 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>