Текст книги "UNIX — универсальная среда программирования"
Автор книги: Брайан Керниган
Соавторы: Роб Пайк
сообщить о нарушении
Текущая страница: 13 (всего у книги 31 страниц) [доступный отрывок для чтения: 13 страниц]
5.3 Циклы while и until: контроль входа в систему
В гл. 3 цикл for использовался для нескольких итеративных программ. Обычно цикл for охватывает множество имен файлов, как в 'for i in * .с', или все аргументы командного файла, как в 'for i in $*'. Но циклы в языке shell могут быть более общими, чем в этих идиомах, например цикл for в команде which.
Имеются три вида циклов: for, while и until. Чаще всего используется цикл for. В нем выполняется последовательность команд (тело цикла) для каждого элемента из множества слов. В большинстве случаев множество образуют просто имена файлов. В циклах while и until контроль над выполнением тела цикла осуществляется с помощью кода завершения команды. Тело цикла выполняется до тех пор, пока команда условия не вернет ненулевой код для while или нуль для until. Циклы while и until идентичны, за исключением кода завершения команды.
Ниже приведены основные формы каждого цикла:
for i in список слов
do
тело цикла, $i последовательно получает значения элементов
done
for i (явно перечисляются аргументы командного файла, т.е. $*)
do
тело цикла, $i последовательно получает значения аргументов
done
while команда
do
тело цикла выполняется, пока команда возвращает истина
done
until команда
do
тело цикла выполняется, пока команда возвращает ложь
done
Вторая форма цикла for, в которой пустой список обозначается как $*, является удобным сокращением записи для наиболее типичного использования.
Командой условия, управляющей циклами while или until, может быть любая команда. Очевидным примером служит цикл while, в котором осуществляется контроль входа (пусть Мэри) в систему:
while sleep 60
do
who | grep mary
done
Команда sleep, устанавливающая паузу на 60 с, всегда выполняется нормально (если ее не прервали) и, значит, всегда возвращает код "успех", поэтому в цикле раз в минуту будет проверяться, находится ли Мэри в системе. Недостаток такого решения состоит в том, что если Мэри уже вошла в систему, то нужно ждать 60 с, чтобы узнать об этом. О продолжении же работы Мэри в системе каждую минуту будет поступать сообщение. Цикл можно перевернуть и записать с помощью until, чтобы получать информацию сразу без задержки, если Мэри в данный момент работает в системе:
until who | grep mary do
sleep 60
done
Теперь условие представляется более интересным. Если Мэри вошла в систему, то 'who | grep mary' выдаст запись о ней из списка команды who и вернет код "истина". Это связано с тем, что grep выдает код завершения, показывающий, удалось ли ей найти что-нибудь, а код завершения конвейера есть код завершения последней команды.
В заключение мы можем оформить команду, дав ей имя и установив в системе:
$ cat watchfor
# watchfor: watch for someone to log in
PATH=/bin:/usr/bin case $# in
0) echo 'Usage: watchfor person' 1>&2; exit 1
esac
until who | egrep "$1"
do
sleep 60
done
$ cx watchfor
$ watchfor you
you tty0 Oct 1 08:01 Работает
$ mv watchfor /usr/you/bin Установим в системе
$
Мы заменили grep на egrep, чтобы было можно задавать
$ watchfor 'joe | mary'
и следить за несколькими пользователями.
Более сложный пример: можно контролировать вход в систему и выход из нее всех пользователей и сообщать обо всех фактах входа или выхода. Это можно рассматривать как некоторое дополнение к команде who. Основная идея проста: раз в минуту запускать команду who и сравнивать результат ее действия с результатом, полученным минутой ранее, сообщая обо всех различиях. Вывод команды who хранится в файле, и мы можем записывать его в каталог /tmp. Чтобы отличить свои файлы от файлов, принадлежащих другим процессам, в имена файлов вставляется переменная интерпретатора $$ (номер процесса команды интерпретатора), что является обычной практикой. Имя команды упоминается во временных файлах главным образом для администратора системы. Часто команды (включая данную версию watchfor) оставляют после себя файлы в /tmp, и полезно знать, какая команда это сделала. Здесь ":" – встроенная команда, которая
$ cat watchwho
# watchwho: watch who logs in and out
PATH=/bin:/usr/bin
new=/tmp/wwho1.$$
old=/tmp/wwho2.$$
> $old # create an empty file
while : # loop forever
do
who >$new
diff $old $new
mv $new $old
sleep 60
done | awk '/>/ { $1 = "in: "; print }
/ { $1 = "out: "; print }'
$
только обрабатывает свои аргументы и возвращает код "истина". Мы могли бы заменить ее командой true, просто передающей код завершения "истина" (есть также команда false), но команда ':' более эффективна, поскольку не нужно выполнять эту команду, выбирая ее из файловой системы.
В выводе команды diff используются символы < и > для разделения данных из двух файлов. Программа, написанная на языке awk, обрабатывает результаты, чтобы сообщить об изменениях в более понятном формате. Обратите внимание на то, что весь цикл передает результаты работы по конвейеру awk программе, вместо того, чтобы запускать заново awk программу каждую минуту. Для такой обработки редактор sed не подходит, поскольку его вывод всегда задерживается по сравнению с входным потоком на одну строку: всегда есть одна входная строка, которая уже обработана, но не напечатана, а это приводит к ненужной задержке.
Поскольку файл old создается пустым, первый вывод команды watchfor содержит весь список пользователей, находящихся в системе в данный момент. Замена команды, которая создает файл old, на who > $old приведет к тому, что watchfor выдаст только изменения, но это уже – дело вкуса.
Другая программа в цикле следит за содержимым вашего почтового ящика: как только оно изменяется, программа выдает сообщение: "You have a mail" ("У вас есть почта"). Такая программа является полезной альтернативой встроенному в интерпретатор механизму, использующему переменную MAIL. Чтобы показать другой стиль программирования, мы реализовали ее с помощью переменных интерпретатора, а не файлов:
$ cat checkmail
# checkmail: watch mailbox for growth
PATH=/bin:/usr/bin
MAIL=/usr/spool/mail/`getname` # system dependent
t=${1-60}
x="`ls -l $MAIL`"
while :
do
y="`ls -l $MAIL`"
echo $x $y
x="$y"
sleep $t
done | awk '$4 < $12 { print "You have mail" }'
$
Мы опять воспользовались awk программой, на этот раз – чтобы добиться вывода сообщения только в тех случаях, когда почтовый ящик пополняется, а не просто изменяется. Иначе вы получите сообщение сразу после исключения письма. (Версия, встроенная в интерпретатор, имеет такой недостаток.)
Обычно интервал времени устанавливается равным 60 с, но если командная строка содержит параметр, например
$ chekmail 30
то интервал задается им. Переменная интерпретатора принимает в качестве значения заданное параметрами время или 60 с, если время не задано, с помощью присваивания
t=${1-60}
Это еще одна возможность языка shell. ${var} эквивалентно $var и может использоваться для преодоления трудностей, связанных с появлением переменных внутри буквенно-цифровых строк:
$ var=hello
$ varx=goodbye
$ echo $var
hello
$ echo ${var}x
hellox
$
Определенные символы внутри фигурных скобок задают специальную обработку переменной. В том случае, когда переменная не определена и за ее именем идет знак вопроса, выдается строка, следующая за символом ?, и интерпретатор прекращает работу (если только он не работает в диалоговом режиме). При отсутствии строки печатается стандартное сообщение:
$ echo ${var?}
hello все в порядке, var определено
$ echo ${junk}
junk: parameter not set стандартное сообщение
$ echo ${junk?error!}
junk: error! строка задана
$
Отметим, что в сообщении, выдаваемом интерпретатором, всегда указывается имя неопределенной переменной.
В другой конструкции ${var-thing} выбирается $var, если оно определено, и thing – в противном случае. В подобной конструкции ${var-thing} значение $var также устанавливается равным thing:
$ echo ${junk-'Hi there'}
Hi there
$ echo ${junk?)
junk: parameter not set значение junk не изменилось
$ echo {junk='Hi there'}
Hi there
$ echo ${junk?}
Hi there junk принял значение Hi there
$
Правила получения значений переменных приведены в табл. 5.3. Возвращаясь к нашему исходному примеру
t=${1-60}
видим, что t присваивается $1 или 60, если аргумент не задан.
$var | Значение var; ничего, если var не определено |
${var} | То же; полезно, если за именем переменной следует буквенно-цифровая строка |
${var-thing} | Значение var, если оно определено; в противном случае – thing; $var не изменяется |
${var=thing} | Значение var, если оно определено; в противном случае – thing. Если var не определено, то $var присваивается thing |
${var?строка} | Если var определено – $var; в противном случае выводится строка и интерпретатор прекращает работу. При пустой строке выводится: var: parameter not set |
${var+thing} | thing, если $var определено; в противном случае – ничего |
Таблица 5.3: Получение значений переменных в языке
Упражнение 5.9
Обратите внимание на реализацию команд
trueиfalseв/usr/binили/bin. (Как бы вы определили, где они находятся?)
Упражнение 5.10
Измените команду
watchforтак, чтобы пользователь мог задавать несколько имен, а не вводить'joe|mary'.
Упражнение 5.11
Напишите версию команды
watchwho, которая использует командуcommвместоawkдля сравнения новой и старой информации. Какая версия вам больше нравится?
Упражнение 5.12
Напишите версию команды
watchwho, в которой вывод командыwhoхранится в переменных языкаshell, а не в файлах. Какая версия лучше? Какая версия быстрее работает? Следует ли в командахwatchwhoиcheckmailавтоматически использовать операцию&?
Упражнение 5.13
В чем состоит различие между пустой командой языка
shell: и символом примечания#? Нужны ли они?
5.4 Команда trap: обработка прерываний
Если во время выполнения команды watchwho нажать клавишу DEL (УДЛ) или отключить компьютер от сети, то один или несколько временных файлов останутся в каталоге /tmp. Команда watchwho удаляет временные файлы перед окончанием своей работы. Необходимы средства обнаружения таких ситуаций и восстановления после прерывания.
При нажатии клавиши DEL всем процессам, запущенным с этого терминала, посылается сигнал прерывания. Аналогично в случае отключения посылается сигнал отбоя. Существуют и другие сигналы. Если в программе не предусмотрены специальные действия по обработке сигналов, то указанные сигналы прекращают ее выполнение. Интерпретатор защищает программы, запущенные с помощью &, от прерываний, но не от отключений.
В гл. 7 сигналы рассматриваются подробнее, но для работы с ними на языке shell глубоких знаний не требуется. Встроенная команда интерпретатора trap устанавливает последовательность команд, которая должна выполняться при возникновении сигнала:
trap последовательность_команд список_номеров_сигналов
Последовательность команд – единый аргумент, поэтому его почти всегда нужно брать в кавычки. Номера сигналов обозначаются небольшими целыми числами, например, 2 соответствует сигналу, возникающему при нажатии клавиши DEL, а 1 – сигналу, возникающему при отключении от сети. Номера сигналов, наиболее часто используемых в shell-программах, приведены в табл. 5.4.
| 0 | Выход из интерпретатора (по любой причине, включая конец файла) |
| 1 | Отбой |
| 2 | Прерывание (клавиша DEL) |
| 3 | Останов (ctl-; вызывает распечатку содержимого памяти программы) |
| 9 | Уничтожение (нельзя перехватить или игнорировать) |
| 15 | Окончание выполнения; сигнал по умолчанию, производимый kill(1) |
Таблица 5.4: Номера сигналов в интерпретаторе
Для удаления временных файлов в команде watchwho вызов команды trap должен указываться перед циклом, чтобы перехватить сигналы прерывания, отбоя и окончания выполнения:
...
trap 'rm -f $new $old; exit 1' 1 2 15
while:
...
Последовательность команд, образующих первый аргумент команды trap, подобна вызову подпрограммы, который происходит сразу по возникновении сигнала. Когда эта последовательность окончится, прерванная программа возобновляется с места прерывания, если только сигнал не уничтожит ее. Таким образом, последовательность команд в trap должна явно вызывать exit, иначе shell-программа продолжит свое выполнение после прерывания. Кроме того, последовательность команд будет читаться дважды: при установке команды trap и при обращении к ней. Поэтому последовательность команд лучше защищать апострофами, чтобы значения переменных вычислялись только при выполнении программ, указанных в команде trap. В данном случае это не имеет значения, но позднее вы столкнетесь с ситуацией, когда это важно. Кстати, флаг -f предписывает команде rm не задавать вопросов.
Иногда команду trap полезно применять в диалоговом режиме, чаще всего для того, чтобы не допустить уничтожения программы сигналом отбоя, возникшим при обрыве телефонной связи:
$ (trap "" 1; долго_выполняемая команда) &
2134
$
Для процесса и его потомков пустая последовательность означает, что нужно игнорировать прерывания. При наличии скобок команда trap и долго_выполняемая_команда выполняются порожденным интерпретатором вместе и как фоновые; без них команда trap действовала бы на исходный интерпретатор, так же как и на долго_выполняемую_команду.
Команда nohup(1) – небольшая shell-программа, обеспечивающая непрерывное выполнение команд. Ниже полностью приведен ее вариант из седьмой версии:
$ cat 'which nohup'
trap "" 1 15
if test -t 2>&1
then
echo "Sending output to 'nohup.out'"
exec nice -5 $* >>nohup.out 2>&1
else
exec nice -5 $* 2>&1
fi
$
Команда test -t проверяет, направлен ли стандартный выходной поток на терминал, чтобы вы могли решить, следует ли его сохранять. Фоновая программа выполняется с помощью команды nice, что снижает ее приоритет по сравнению с диалоговыми программами. (Обратите внимание, что команда nohup не устанавливает значение PATH. А может быть, это нужно?)
Команда exec использована только для повышения эффективности; команда nice может выполняться и без нее. Exec – встроенная команда интерпретаторов, которая заменяет процесс, играющий роль текущего интерпретатора, на указанную программу. Таким образом она избавляется от одного процесса, а именно от интерпретатора, обычно ожидающего завершения программы. Мы могли бы применять exec и в некоторых других программах, например в конце обобщенной программы cal, когда происходит обращение к /usr/bin/cal.
Кстати, сигнал 9 – это тот сигнал, который нельзя перехватить или игнорировать: он всегда уничтожает процесс. На языке shell его посылка задается с помощью
$ kill -9 номер_процесса
Обращение kill -9 не является стандартным, поскольку процессу, уничтоженному таким способом, не дается время для приведения в порядок своих дел перед "смертью".
Упражнение 5.14
В приведенной выше версии команды
nohupстандартный поток диагностики команды соединяется со стандартным выходным потоком. Хорошее ли это решение? Если нет, то как бы вы разделили их явно?
Упражнение 5.15
Найдите встроенную команду
timesи добавьте к вашему файлу строку.profile, чтобы при вашем выходе из системы интерпретатор выдавал использованное вами процессорное время.
Упражнение 5.16
Напишите программу, находящую следующий свободный идентификатор пользователя в файле
/etc/passwd. Если у вас есть энтузиазм (и право доступа), сделайте из нее команду, устанавливающую нового пользователя системы. Какие нужны для нее права доступа? Как следует ей обращаться с прерываниями?
5.5 Команда overwrite: замена файла
В команде sort есть флаг -о для замены файла:
$ sort файл1 -о файл2
Ее эквивалент:
$ sort файл1 > файл2
Если файл1 и файл2 – это один и тот же файл, то после операции переключения > входной файл станет пустым перед сортировкой. Но с флагом -о команда выполняется правильно, потому что входной файл сортируется и сохраняется во временном файле перед созданием выходного файла.
Могут использовать флаг -о и другие команды. Например, редактор sed может редактировать файл с заменой:
$ sed 's/UNIX/UNIX (TM)/g' -o ch2 Так не получится!
Непрактично изменять все подобные команды, вводя флаг – это не лучшее решение. Более целесообразным представляется централизованное выполнение функций, как в случае операции > интерпретатора, для чего мы создадим программу overwrite. Первый ее вариант выглядит так:
$ sed 's/UNIX/UNIX (TM)/g' гл2 | overwrite гл2
В основном алгоритм программы очевиден: нужно только сохранить где-нибудь весь входной поток вплоть до конца файла, а затем копировать его в файл, указанный как аргумент:
# overwrite: copy standard input to output after EOF
# version 1. BUG here
PATH=/bin:/usr/bin
case $# in
1) ;;
*) echo 'Usage: overwrite file' 1>&2; exit 2
esac
new=/tmp/overwr.$$
trap 'rm -f $new; exit 1' 1 2 15
cat >$new # collect the input
cp $new $1 # overwrite the input file
rm -f $new
Команда cp используется вместо команды mv, чтобы не изменились права доступа и остался прежним владелец выходного файла, если он уже существует. Хотя этот вариант и чрезвычайно прост, здесь возможна "фатальная" ошибка: если пользователь нажмет клавишу DEL (УДЛ) во время выполнения команды cp, первоначальный выходной файл будет уничтожен. Необходимо соблюдать осторожность, поскольку прерывание может остановить замену входного файла:
# overwrite: copy standard input to output after EOF
# version 2. BUG here too
PATH=/bin:/usr/bin
case $# in 1) ;;
*) echo 'Usage: overwrite file' 1>&2; exit 2
esac
new=/tmp/overwr1.$$
old=/tmp/overwr2.$$
trap 'rm -f $new $old; exit 1' 1 2 15
cat >$new # collect the input
cp $1 $old # save original file
trap '' 1 2 15 # we are committed; ignore signals
cp $new $1 # overwrite the input file
rm -f $new $old
Если клавиша DEL будет нажата прежде, чем начнется работа с исходным файлом, то произойдет удаление временных файлов и файл останется один. После сохранения файла сигналы игнорируются, поэтому выполнение последней команды cp не прервется. Если команда cp начала выполняться, команда overwrite обязана заменить исходный файл.
Здесь есть некоторая тонкость. Рассмотрим последовательность:
$ sed 's/UNIX/UNIX(TM)g' special | overwrite special
command garbled: s/UNIX(TM)g
$ ls -l special
-rw-rw-rw– 1 you 0 Oct 1 09:02 special #$%@*!
$
Если в программе, поставляющей входной поток для команды overwrite, произойдет ошибка, то выходной поток будет пустым, и overwrite обязательно (с сознанием выполненного долга) уничтожит файл, заданный в качестве аргумента.
Во избежание такого финала можно предложить несколько решений. Команда overwrite могла бы запрашивать подтверждение перед заменой файла, но, сделав команду диалоговой, мы потеряем большую часть ее достоинств. Она могла бы проверять, что ее входной поток не пуст (с помощью test -2), но это некрасиво и к тому же неверно: выходной поток мог быть создан до обнаружения ошибки.
Наилучшее решение заключается в том, чтобы выполнять программу, поставляющую данные, под контролем команды overwrite, чтобы можно было проверить ее код завершения. Это, правда, противоречит традициям и здравому смыслу: ведь в конвейере команда overwrite обычно должна быть последней, но для правильной работы она должна идти первой. Однако overwrite ничего не выдает в стандартный выходной поток, поэтому можно считать, что не происходит потери общности. Более того, ее синтаксис не является каким-то необычным: time, nice, nohup представляют собой команды, аргументами которых служат другие команды. Ниже приведен безопасный вариант:
# overwrite: copy standard input to output after EOF
# final version
opath=$PATH
PATH=/bin:/usr/bin
case $# in
0|1) echo 'Usage: overwrite file cmd [args]' 1>&2; exit 2
esac
file=$1; shift
new=/tmp/overwr1.$$; old=/tmp/overwr2.$$
trap 'rm -f $new $old; exit 1' 1 2 15 # clean up files
if PATH=$opath "$@" >$new # collect input
then
cp $file $old # save original file
trap '' 1 2 15 # we are committed; ignore signals
cp $new $file
else
echo "overwrite: $1 failed, $file unchanged" 1>&2 exit 1
fi
rm -f $new $old
Встроенная команда интерпретатора shift сдвигает весь список аргументов на одну позицию влево: $2 становится $1, $3 становится $2 и т.д. Строка обозначает все аргументы (после shift), как и $*, но без интерпретации; мы вернемся к ее рассмотрению в разд. 5.7.
Заметьте, что значение PATH нужно восстановить перед выполнением команды пользователя; если этого не сделать, то команды, не находящиеся в /bin или /usr/bin, будут недоступны для overwrite.
Теперь команда overwrite выполняется верно (хотя и она получилась несколько громоздкой):
$ cat notice
Unix is a Trademark of Bell Laboratories
$ overwrite notice sed 's/UNIXUNIX(TM)/g' notice
command garbled: s/UNIXUNIX(TM)/g
overwrite: sed failed, notice unchanged
$ cat notice
UNIX is a Trademark of Bell Laboratories He изменился
$ overwrite notice sed 's/UNIX/UNIX(TM)/g' notice
$ cat notice
UNIX(TM) is a Trademark of Bell Laboratories
$
Типичной задачей является использование редактора sed для замены всех вхождений одного слова на другое слово. Имея под рукой команду overwrite, легко написать программу на языке shell для ее решения:
$ cat replace
# replace: replace str1 in files with str2, in place
PATH=/bin:/usr/bin
case $# in
0|1|2) echo 'Usage: replace str1 str2 files' 1>&2; exit 1
esac
left="$1"; right="$2"; shift; shift
for i do
overwrite $i sed "s@$left@$right@g" $i
done
$ cat footnote
UNIX is not an acronym
$ replace UNIX Unix footnote
$ cat footnote
Unix is not an acronym
$
(Вспомните: если список в цикле for пуст, то по умолчанию он равен $*.) Мы использовали @ вместо / для разбиения в команде подстановки, поскольку менее вероятно, что @ вступит в конфликт с входной строкой. Команда replace устанавливает PATH равным /bin:/usr/bin, исключая $HOME/bin. Это означает, что overwrite должна находиться в /usr/bin, чтобы команда replace сработала. Мы сделали такое предположение для простоты; если вы не можете поместить overwrite в /usr/bin, вам придется добавить $HOME/bin к PATH в команде replace или явно задать полное имя overwrite. В дальнейшем будем полагать, что команды, которые мы создаем, находятся в /usr/bin, где им и следует быть.
Упражнение 5.17
Почему команда
overwriteне использует сигнал 0 в командеtrap, чтобы файлы удалялись при выходе из нее? Подсказка: попробуйте нажать клавишу DEL во время выполнения следующей программы:
trap "echo exiting; exit 1" 0 2
sleep 10
Упражнение 5.18
Добавьте флаг
-vк командеreplaceдля вывода всех измененных строк на/dev/tty.Подсказка:
s/$left/$right/g $vflag.
Упражнение 5.19
Увеличьте надежность команды
replace, чтобы ее выполнение не зависело от символов в строке замены.
Упражнение 5.20
Можно ли использовать
replaceдля заменыiнаindexвсюду в программе? Какие вы внесли бы изменения, чтобы добиться этого?
Упражнение 5.21
Достаточно ли команда
replaceэффективна и удобна, чтобы находиться в каталоге/usr/bin? Не лучше ли вводить по мере необходимости подходящие команды редактораsed(да или нет)? Обоснуйте свой ответ.
Упражнение 5.22
(Усложненное.) Команда
$ overwrite файл 'who | sort'не выполняется. Объясните причину этого и исправьте ее. Подсказка: посмотрите
evalв справочном руководстве поsh(1). Как ваше решение повлияет на интерпретацию специальных символов в команде?




























