Микро хаки

Copyright(С) Олег П. Филон
январь 2001, Гомель, BY
[email protected]

За много лет общения с разными компьютерами и программами у меня накопились всякие мелкие приемы и фокусы, помогающие мне в повседневной работе. Предлагаю их и вам, вдруг когда-нибудь пригодятся.

Очень давно, ещё трудясь в большом коллективе на советской многопроцессорной суперЭВМ "Эльбрус", мы уже были хорошо знакомы с зависанием машины. После ее жёсткого рестарта начиналось своеобразное соревнование - кто первым залогинится и запустит на трансляцию свою программу. Это называлось "выиграть вбрасывание". Часто выигрывать вбрасывание мне помогал нехитрый приём - я называл файлы простыми быстро набираемыми на клавиатуре именами. В современных командных процессорах есть полезная команда, позволяющая использовать этот приём по максимуму. Любую часто используемую команду можно обозвать коротким псевдонимом - alias'ом. Вот некоторые alias'ы, живущие у меня в bash:

alias ds='dirs'
alias l='less'
alias ll='/bin/ls -lF --color'
alias ls='ls --color=auto '
alias m='more'
alias mu='mutt -y'
alias pp='pushd'
alias ss='ps aux'
alias xx='startx -- -bpp 16'

Здесь можно заметить ещё один простой приём. Ваш командный процессор хранит стек каталогов, с которым можно работать с помощью трёх команд: pushd, popd и dirs. Если завести у себя файл .dirs_stack приблизительно вот такого вида:

for sd in /usr/doc/HOWTO ~ftp/pub /usr/src/linux /usr/local/src
do if [ -d $sd ] ; then pushd $sd
   fi
done > /dev/null
pushd -0 > /dev/null

а затем при старте, например, из файла .bashrc его вызывать

if [ -f ~/.dirs_stack ]; then
    source ~/.dirs_stack
fi

, то перемещаться между несколькими любимыми каталогами не составит особого труда. Например, если команда ds выдает

~ ~ftp/pub /usr/src/linux /usr/local/src /usr/doc/HOWTO

, то попасть в каталог HOWTO можно, нажав на клавиши всего 6 раз:

...$ pp -0

Посмотреть, как себя чувствует, к примеру, web-сервер apache, проще всего вот так:

...$ ss|grep [a]pache

А используете ли вы bash, если надо слегка поправить один список и получить другой? Например, найдём юзеров, имеющих в качестве shell этот самый bash, и сделаем соответствующий отчет:

...$ LIST=$(grep /bin/bash /etc/passwd|cut -d: -f1)
...$ LIST=$(echo $LIST|sed 's/ /,/g')
...$ LIST=$(eval echo 'юзер\ \"'{$LIST}'\"\ пользуется\ bash\\n')
...$ echo -en ' '$LIST

Этот пример, пожалуй, не очень удачный для генерации списка. Раз уж мы задействовали sed, надо было ему и весь отчёт поручить. По поводу раскрытия фигурных скобок возможности bash лучше проиллюстрирует вот такое упражнение с русским языком:

...$ echo {в,от,на,про,\ }{реж,пол,вод}{\ ,ит,ка}|fmt

В последних версиях bash появилась возможность использовать прямо в командах регулярные выражения. Часто возникает необходимость выделить из полного имени отдельно имя каталога и имя файла. Для этого исторически были предназначены отдельные команды /usr/bin/{basename,dirname}. Например:

...$ PROBA=$(pwd)/micro-hacks
...$ echo $PROBA
/home/ophil/articles/micro-hacks
...$ basename $PROBA
micro-hacks
...$ dirname $PROBA
/home/ophil/articles

А вот как почти то же самое можно теперь делать прямо в bash'е:

...$ NAME=${PROBA##*/}
...$ DIR=${PROBA%$NAME}
...$ echo $DIR $NAME
/home/ophil/articles/ micro-hacks

Знаете ли вы, что в bash'е, как в настоящем языке программирования, есть переменные целого типа и арифметические операции над ними? Если объявить переменную целой, с ней можно выполнять элементарные арифметический действия:

...$ declare -i n=10
...$ n=n+20
...$ echo $n
30
...$ n=n/3-2
...$ echo $n
8

Если вам вдруг понадобится арифметика с большой точностью, из bash'а легко получить доступ ещё к одной жемчужине из классического наследия UNIX - калькулятору bc. Допустим, мы захотели проверить, действительно ли, как шутили когда-то на физтехе, "Ежды Пи = Пижды Е" хотя бы с точностью 400 знаков:

...$ PI=$(echo 'scale=400;4*a(1)'|bc -l)
...$ PI=$(echo $PI|sed 's/\\ //g')
...$ E=$(echo 'scale=400;e(1)'|bc -l
...$ E=$(echo $E|sed 's/\\ //g')
...$ echo $PI*$E|bc

Bash проявляет свою неисчерпаемую мощь, конечно, не сам по себе, а в качестве соединительной ткани между многочисленными мелкими програмками, утилитами, фильтрами, возникшими и отточеными в процессе эволюции вычислительной техники и программирования. Вот маленькая программка с использованием языка AWK

#! /bin/sh
usage="\n
       Usage: $0 имя_каталога\n
       показывает список файлов, имеющих одинаковую длину\n\a
"
if [ $# != 1 ]
then echo -e $usage ; exit 0
fi

ls -lS $1|awk '
size==$5 { list[$9]=$0      # совпал размер - сохраним в массиве
	   split(line,a)    # вернёмся к предыдущей строке
	   list[a[9]]=line  # обе строки в массиве
	 }
	 { size=$5	    # для каждой строки
	   line=$0
	 }
END	 { for (i in list)
		print list[i]
	 }'|sort -k5n

Здесь bash соединяет воедино простейшую подсказку, показ содержимого каталога, вызов интерпретатора awk и текст программки для него, включая печать файлов с подозрительно одинаковой длиной - кандидатов на замену файла жесткой ссылкой, и в самом конце сортирует список файлов по размеру.

Еще один пример использования языка AWK можно посмотреть в отдельной заметке, рассказывающей о генерации случайного телефонного справочника любого размера из подручных средств: произвольного текстового файла, утилиты tr и интерпретатора awk.

<DEPRECATED> Я обнаружил, что упражняясь с perl, также иногда удобно оборачивать вокруг программы на perl скрипт bash, чтобы иметь в одном файле и текст программки, и обрабатываемые данные:

#! /bin/bash
perl -we '
use strict;
use Data::Dumper;
my(@new);
while(<>){
chomp;
@new=();
push(@new, $+) while m{
    "([^\"\\]*(?:\\.[^\"\\]*)*)",?  # groups the phrase in quotes
    | ([^,]+),?
    | ,
    }gx;                   # пример из perlfaq4 by Jeffrey Friedl
push(@new, undef) if substr($_,-1,1) eq ",";
print "ophil debug:\n", Dumper(\@new);
}'<<\EOT
SAR001,"","Ci, $$","\"Bob@Smith\"","%am",N,8,1,0,7,"Core&&Dumped"
Я12340,,"НЕЧТО-О-О-О","Абвгд'ей Прстуф","проба",Ъ,1,0777,3,4,"да"
EOT

</DEPRECATED>

Предыдущий пример пришлось отменить. Несмотря на то, что он вполне работает, но называться хаком он права не имеет. По хакерской традиции, если есть более изящное или эффективное решение, то хаком является именно оно. Конечно, в perl'е в самом есть конструкции, позволяющие получить ввод прямо из файла. Вот более правильный вариант, не использующий лишний вызов bash, не привязанный вообще к командному процессору:

#! /usr/bin/perl -w
use strict;
use Data::Dumper;
my(@new);
while(<DATA>){
chomp;
@new=();
push(@new, $+) while m{
    "([^\"\\]*(?:\\.[^\"\\]*)*)",?  # groups the phrase in quotes
    | ([^,]+),?
    | ,
    }gx;                   # пример из perlfaq4 by Jeffrey Friedl
push(@new, undef) if substr($_,-1,1) eq ",";
print "ophil debug: ", Dumper(\@new);
}
__DATA__
SAR001,"","Ci, $$","\"Bob@Smith\"","%am",N,8,1,0,7,"Core&&Dumped"
Я12340,,"НЕЧТО-О-О-О","Абвгд'ей Прстуф","проба",Ъ,1,0777,3,4,"да"

Рассмотрим возникающую время от времени реальную задачу - переименовать или подредактировать большое количество файлов, например, пришедших из другой системы. Здесь оказываются очень удобны пара утилит find и xargs. Если ещё под рукой имеются интерпретатор perl, pcregrep, понимающий перловые регулярные выражения, и миниатюра rename, написанная самим Ларри Уолом, то количество файлов перестает играть какую-либо роль, единственная сложность - правильно регулярно выражаться.

Для начала займёмся самими именами файлов - например, заменим в имени файла пробелы на знак подчеркивания "_":

...$ find -name '* *' -print0|xargs -r0 rename 's/ /_/g'

Если пробел встречается в имени каталога, повторите эту команду несколько раз. Если глубина вложения каталогов большая, а вы принципиально против рутинной работы, оберните нужную команду циклом:

...$ for i in $(seq 1 20);do нужные команды;done

Теперь попробуем выполнить какое-нибудь несложное редактирование. Для примера, будем искать фразу 'http://что-угодно/буквыцифры.htm"' и заменим ее на 'http://что-угодно/буквы-00-цифры.html"' во всех файлах типа *.html, содержащих указанный контекст. Регулярные выражения, в отличие от нецензурных, нужно старательно обдумывать, а иногда не грех их и поотлаживать. Например, перед собственно редактированием полезно посмотреть, что же наши конструкции на самом деле нашли

...$ find -type f -a -name \*\.html|
> xargs perl -wne 'print "$1 $2 $3\n"
> if m#(http://(?:[^/]+/)+)([a-zA-Z-_]+)(\d+)\.htm"#'

Если мы нашли действительно то, что искали, поправим все файлы с заданным контекстом, не трогая остальные:

...$ find -type f -a -name \*\.html|
> xargs pcregrep -l 'http://([^/]+/)+[a-zA-Z-_]+\d+\.htm"'|
> xargs perl -i.bak -wpe '
> s#(http://(?:[^/]+/)+)([a-zA-Z-_]+)(\d+)\.htm"
> #$1$2-00-$3.html"#g'

Убедившись, что все в порядке, удаляем старые файлы:

...$ find -name \*\.bak|xargs rm

В заключение ещё один фокус - как перенести установленный Линукс из одного раздела в другой или на новый диск. Допустим, мы успешно загрузили старую систему, создали на новом диске разделы и файловую систему, смонтировали новый раздел и перешли в него. Как и положено, пока он содержит только каталог lost+found. Делаем:

...# tar cp -С / bin boot dev etc lib root sbin usr var|tar xvp
...# mkdir proc mnt tmp;chmod 1777 tmp

Теперь нужно поправить файлы, привязанные к текущему разделу. Иногда достаточно поправить только etc/fstab. Но лучше внимательно проверить весь etc на предмет имен хостов, адресов, создать заранее swap раздел. Не стоит также оставлять старые журнальные файлы в var/log/. Архивы или каталог /home можно копировать избирательно. Например:

...# mkdir home;cd home
...# tar cp -C /home ftp/pub/doc ophil proba|tar xvp

Перед моментом истины - подключением нового диска и загрузки с него, имеет смысл создать аварийную дискету. Если новый корневой раздел будет, к примеру, /dev/hda3, приготовьте дискету и подходящее ядро. После этого можно копировать образ ядра:

...$ /usr/sbin/rdev vmlinuz-2.4.0 /dev/hda3
...$ dd<vmlinuz-2.4.0>/dev/fd0

Настоящим кладезем подобных полезных приемов и программ является книга издательства O'Reilly "Unix power tools, 2nd ed." , авторы Jerry Peek, Tim O'Reilly, Mike Loukides. Эта книга переведена на русский и выпущена издательством BHV.