Группы команд и подоболочки
bash поддерживает возможность группировки команд. Воспользоваться ею можно двумя способами: либо путем группировки команд, либо путем применения подоболочки. Ниже приводятся примеры синтаксиса обоих подходов.
Группа команд:
{ команда1; команда2; [команда3; ...] }
Подоболочка:
(команда1; команда2; [команда3;...])
Группа команд заключается в фигурные скобки, а подоболочка оформляется круглыми скобками. Вот и вся разница. Однако обратите внимание, что из-за особенностей реализации группировки команд в bash фигурные скобки должны отделяться от команд пробелами и последняя команда должна завершаться точкой с запятой или символом перевода строки.
Итак, где могут пригодиться группы команд и подоболочки? Даже при том, что между ними имеются важные различия (которые будут раскрыты далее), и те и другие используются в основном для перенаправления. Рассмотрим фрагмент сценария, выполняющий перенаправление вывода множества команд:
ls -l > output.txt
echo "Listing of foo.txt" >> output.txt
cat foo.txt >> output.txt
Выглядит достаточно просто: вывод трех команд перенаправляется в файл с именем output.txt. Воспользовавшись приемом группировки, то же самое можно выразить более кратко:
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
Подоболочка используется аналогично:
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt
Этот прием помог нам сэкономить силы и время на вводе текста сценария, но истинная мощь групп команд и подоболочек проявляется в конвейерах. Создавая конвейеры из команд, мы часто сталкиваемся с необходимостью объединения результатов нескольких команд в общий поток. Группы команд и подоболочки упрощают эту задачу:
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr
Здесь мы объединили вывод трех команд и передали его по конвейеру на вход команды lpr, чтобы напечатать отчет.
Несмотря на внешнее сходство и возможность объединения потоков для последующего перенаправления, между группами команд и подоболочками существуют важные отличия. Все команды, входящие в группу, выполняются в текущей оболочке, подоболочка (как можно догадаться из названия) выполняет свои команды в дочерней копии текущей командной оболочки. Это означает, что в момент запуска подоболочки создается копия текущей оболочки и передается новому экземпляру оболочки. Когда подоболочка завершается, ее копия окружения уничтожается, соответственно теряются любые изменения в окружении подоболочки (включая значения переменных).
Поэтому если нет прямой необходимости в использовании подоболочки, предпочтительнее использовать группы команд. Группы команд выполняются быстрее и требуют меньше памяти.
В главе 28 мы столкнулись с одной из проблем, характерных для подоболочек, когда выяснили, что команда read действует в конвейерах не так, как можно было бы ожидать. Там мы сконструировали следующий конвейер:
echo "foo" | read
echo $REPLY
после выполнения которого переменная REPLY всегда оставалась пустой, потому что команда read выполняется в подоболочке и ее копия REPLY уничтожается по ее завершении.
Так как конвейеры команд всегда выполняются в подоболочке, любые команды, присваивающие значения переменным, будут сталкиваться с этой проблемой. К счастью, командная оболочка поддерживает экзотическую форму подстановки, которая называется подстановкой процессов и может использоваться для преодоления указанных трудностей.
Подстановка процессов оформляется двумя способами: для процессов, отправляющих результаты в стандартный вывод:
<(список)
и для процессов, принимающих данные через стандартный ввод:
>(список)
где список — это список команд.
Ниже показано, как использовать подстановку процессов для решения проблемы с командой read:
read < <(echo "foo")
echo $REPLY
Подстановка процессов позволяет интерпретировать вывод подоболочки как обычный файл и осуществлять его перенаправление. Так как это форма подстановки, всегда можно узнать действительное подставляемое значение:
[ [email protected] ~]$ echo <(echo "foo")
/dev/fd/63
Вывод результата подстановки командой echo показывает, что вывод подоболочки передается через файл с именем /dev/fd/63.
Подстановка процессов часто используется в циклах, содержащих команду read. Ниже приводится пример использования read в цикле, обрабатывающем список файлов в каталоге, созданном подоболочкой:
#!/bin/bash
# pro-sub : демонстрация подстановки процессов
while read attr links owner group size date time filename; do
cat <<- EOF
Filename: $filename
Size: $size
Owner: $owner
Group: $group
Modified: $date $time
Links: $links
Attributes: $attr
EOF
done < <(ls -l | tail -n +2)
Цикл выполняет read для каждой строки в списке с содержимым каталога. Сам список создается последней строкой в сценарии. Здесь вывод подоболочки перенаправляется на стандартный ввод цикла с помощью подстановки процесса. Команда tail включена в конвейер, чтобы устранить первую строку в списке, которая не нужна.
Этот сценарий выведет примерно следующее:
[ [email protected] ~]$ pro_sub | head -n 20
Filename: addresses.ldif
Size: 14540
Owner: me
Group: me
Modified: 2012-04-02 11:12
Links: 1
Attributes: -rw-r--r--
Filename: bin
Size: 4096
Owner: me
Group: me
Modified: 2012-07-10 07:31
Links: 2
Attributes: drwxr-xr-x
Filename: bookmarks.html
Size: 394213
Owner: me
Group: me
В главе 10 мы узнали, что программы могут реагировать на сигналы. Эту возможность можно добавить и в сценарии. Ни в одном из сценариев, написанных нами до сих пор, этого не требовалось (потому что они быстро завершаются и не создают временных файлов), но в больших и сложных сценариях процедура обработки сигналов может оказаться весьма кстати.
Проектируя большие и сложные сценарии, важно предусматривать их реакцию на неожиданный выход пользователя из системы или выключение компьютера во время их выполнения. Если возникают подобные события, всем процессам посылается сигнал. Программы, представляющие эти процессы, могут выполнять некие действия, гарантирующие корректное завершение с сохранением необходимых данных. Допустим, к примеру, что мы написали сценарий, создающий временный файл во время выполнения. При внимательном подходе к проектированию мы могли бы предусмотреть удаление этого файла по завершении сценария. Было бы неплохо также предусмотреть удаление файла в случае получения сценарием сигнала, требующего преждевременного завершения программы.
Для этой цели в bash поддерживается механизм, известный как ловушка (trap). Ловушки реализуются с применением встроенной команды с соответствующим именем trap. Команда trap имеет следующий синтаксис:
trap аргумент сигнал [сигнал...]
где аргумент — это строка, которая будет прочитана и выполнена как команда, а сигнал — идентификатор сигнала, в ответ на который будет выполнена указанная команда.
Рассмотрим простой пример:
#!/bin/bash
# trap-demo : простой пример обработки сигналов
trap "echo 'I am ignoring you.'" SIGINT SIGTERM
for i in {1..5}; do
echo "Iteration $i of 5"
sleep 5
done
Этот сценарий определяет ловушку, которая будет выполнять команду echo в ответ на сигналы SIGINT и SIGTERM, получаемые сценарием во время выполнения. Ниже показано, как выглядят попытки остановить сценарий нажатием комбинации CTRL+C:
[ [email protected] ~]$ trap-demo
Iteration 1 of 5
Iteration 2 of 5
I am ignoring you.
Iteration 3 of 5
I am ignoring you.
Iteration 4 of 5
Iteration 5 of 5
Как видите, каждый раз, когда пользователь пытается прервать работу программы, она вместо этого выводит сообщение.
Иногда бывает непросто сформировать строку с требуемой последовательностью команд, поэтому на практике в качестве команды часто используют функции. Следующий пример демонстрирует применение разных функций для обработки разных сигналов:
#!/bin/bash
# trap-demo2 : простой пример обработки сигналов
exit_on_signal_SIGINT () {
echo "Script interrupted." 2>&1
exit 0
}
exit_on_signal_SIGTERM () {
echo "Script terminated." 2>&1
exit 0
}
trap exit_on_signal_SIGINT SIGINT
trap exit_on_signal_SIGTERM SIGTERM
for i in {1..5}; do
echo "Iteration $i of 5"
sleep 5
done
Этот сценарий дважды использует команду trap, настраивая ловушки для двух сигналов. В каждой ловушке используется своя функция, которая будет вызвана для обработки конкретного сигнала. Обратите внимание на включение команды exit в обе функции обработки сигналов. Без этого сценарий продолжил бы выполняться после завершения функции.
Если во время выполнения этого сценария пользователь нажмет комбинацию CTRL+C, он увидит следующее:
[ [email protected] ~]$ trap-demo2
Iteration 1 of 5
Iteration 2 of 5
Script interrupted.
временные файлы
Одним из побудительных мотивов включения обработчиков сигналов в сценарии является необходимость удаления временных файлов, которые сценарии могут создавать для хранения промежуточных результатов. Выбор имен для временных файлов — целое искусство. Традиционно программы в Unix-подобных системах создают свои временные файлы в каталоге /tmp, общем для всех и предназначенном именно для таких файлов. Однако из-за того что каталог является общим, возникает проблема безопасности, особенно остро проявляющаяся в программах, действующих с привилегиями суперпользователя. Помимо очевидной необходимости установки соответствующих разрешений для файлов, которые могут быть доступны всем пользователям в системе, важно также давать временным файлам непредсказуемые имена. Это поможет избежать атак вида гонка за временными файлами (temp race attack). Ниже показан один из способов создания непредсказуемого (но все еще осмысленного) имени: