Текст книги "UNIX — универсальная среда программирования"
Автор книги: Брайан Керниган
Соавторы: Роб Пайк
сообщить о нарушении
Текущая страница: 9 (всего у книги 31 страниц) [доступный отрывок для чтения: 12 страниц]
3.8 Циклы в shell-программах
Язык shell
– действительно язык программирования: в нем есть переменные, циклы, ветвления и т.п. Здесь мы обсудим основные циклы, а структуры управления рассмотрим более подробно в гл. 5.
Типичным считается цикл по последовательности имен файлов, и оператор for
языка shell
является единственной структурой управления, которую обычно задают с терминала, а не помещают в файл для последующего выполнения. Синтаксис оператора for
таков:
for перем in список_слов
do
команды
done
Например, для получения эха имен файлов по одному на строке достаточно задать:
$ for i in *
> do
> echo $i
> done
Вместо i
можно применять любую переменную языка shell
, но это обозначение традиционно. Заметьте, что значение переменной получается с помощью $i
, однако в заголовке цикла переменную указывают как i
. Мы задействовали *
для выбора всех файлов текущего каталога, но можно использовать и любой другой список аргументов. Обычно нужно сделать что-нибудь более интересное, чем печать имен файлов. Нам часто приходилось сравнивать набор файлов с их предыдущими версиями, например старую версию гл. 2 (хранимую в каталоге old
) с текущей:
$ ls ch2. * | 5
ch2.1 ch2.2 ch2.3 ch2.4 ch2.5
ch2.6 ch2.7
$ for i in ch2.*
> do
> echo $i
> diff -b old/$i $i
> echo
Добавим пустую строку для красоты
> done | pr -h "diff `pwd`/old `pwd` | lpr &
3712
Номер процесса
$
Выходной поток направлен по конвейеру через команды pr
и lpr
просто для того, чтобы показать, что это возможно: стандартный выходной поток программ, находящихся внутри цикла for
, попадает в стандартный выходной поток самой команды for
. С помощью флага -h
в команде pr мы поместили в выходной поток заголовок с "архитектурными излишествами", используя два вложенных обращения к pwd
. Вся последовательность команд запущена асинхронно (&
), так что не нужно ждать ее окончания; &
применяется ко всякому циклу и конвейеру.
Мы предпочитаем указанный формат для цикла for
, но вы можете сократить его. Единственное ограничение заключается в том, что do
и done
распознаются как ключевые слова, только если они появляются сразу после перевода строки или точки с запятой. В зависимости от размера цикла for
иногда лучше помещать все на одной строке:
for i in список; do команды; done
Следует использовать цикл for
для обработки составных команд или в тех случаях, когда не подходит встроенная обработка отдельных команд. Но не применяйте его там, где в отдельной команде есть цикл по именам файлов:
# Плохая идея:
for i in $*
do
chmod +x $i
done
Предпочтительнее сделать так:
chmod +x $*
поскольку в цикле for
отдельная команда chmod
выполняется для каждого файла, что требует больших вычислительных ресурсов. (Убедитесь в том, что вы понимаете разницу между командами
for i in *
в которой цикл выполняется по всем именам файлов текущего каталога, и
for i in $*
в которой цикл выполняется по всем аргументам командного файла.)
Список аргументов для цикла for
часто получают путем выбора имен файлов по шаблону, но можно получать и любым другим способом, в частности:
for i in `cat ...`
или просто вводом аргументов. Например, ранее в этой главе мы создали ряд программ для печати в несколько столбцов под именами 2
, 3
и т.д. Они являются связями с одним файлом, которые можно установить следующим образом (при условии, что программа 2
написана):
$ for i in 3 4 5 6; do ln 2 $i; done
$
Цикл for
имеет и более интересное назначение. Выберем с помощью команды pick
те файлы, которые будут сравниваться с файлами из каталога старых версий:
$ for i in `pick ch2.*`
> do
> echo $i:
> diff old/$i $i
> done | pr | lpr
ch2.1? y
ch2.2
ch2.3
ch2.4? y
ch2.5? y
ch2.6?
ch2.7?
$
Очевидно, данный цикл следует поместить в командный файл, чтобы уменьшить ввод в следующий раз (ведь если вы что-то сделали дважды, вероятно, вы сделаете это и в третий раз).
Упражнение 3.15
Если цикл с командой
diff
хранится в командном файле, поместите ли вы туда команду pick? Объясните, почему.
Упражнение 3.16
Что произойдет, если последняя строка приведенного цикла будет иметь вид:
> done | pr | lpr &
т.е. кончаться амперсандом? Попробуйте сделать прогноз, а затем проверьте его.
3.9 Программа bundle
: соберем все воедино
Чтобы лучше понять, как создаются командные файлы, обратимся к такому примеру. Предположим, вы получили почту от приятеля с другой машины: "где-то!боб"
(Существует несколько вариантов обозначений для адресата на другой машине. Наиболее общим является следующее: машина!пользователь[10] 10
Это старая адресация для UUNET сетей
[Закрыть]. См. справочное руководство по mail(1)
), и он хотел бы скопировать командные файлы из вашего каталога bin
. Самый простой способ их пересылки заключается в ответной почте, так что вы могли бы начать вводить:
$ cd /usr/you/bin
$ for i in `pick *`
> do
> echo ============== Это файл $i ==============
> cat $i
> done | mail где-то!боб
$
Однако посмотрим на это с точки зрения адресата "где-то!боб"
: он должен получить почту, в которой все файлы четко разделены, но ему придется воспользоваться редактором для разбивки сообщений на отдельные файлы. Для того чтобы адресату ничего не надо было делать, почтовое сообщение, построенное подходящим образом, должно автоматически распаковать себя, а значит, оно должно быть командным файлом, содержащим и сами файлы, и операции по их распаковке. Вторая идея заключается в том, что конструкция языка shell
"документ здесь" является удобным способом задания информации для команды при ее запуске. Тогда остальная часть задачи сводится к тому, чтобы правильно расставить кавычки. Ниже приведена работающая программа bundle, которая группирует файлы в выходной поток самодокументированного командного файла:
$ cat bundle
# bundle: группирует файлы в распределенный пакет
echo '# Для разбиения на файлы вызовите sh с этим файлом'
for i
do
echo "echo $i 1>&2"
echo "cat >$i <<'End of $i'"
cat $i
echo "End of $i"
done
$
Поскольку мы взяли в кавычки "End of $i"
, любые метасимволы из файлов будут игнорироваться.
Естественно, что вам следует выполнить пробный запуск программы, чтобы не нанести ущерб адресату "где-то!боб"
:
$ bundle cx lc >junk
Пробный запуск bundle
$ cat junk
# Для разбиения на файлы вызовите sh с этим файлом
echo cx 1>&2
cat >cx <<'End of cx'
chmod +x сх
End of cx
echo lc 1>&2
cat >lc <<'End of lc'
# lc: подсчет числа строк в файлах
wc -l $*
End of lc
$ mkdir test
$ sh ../junk
Попробуем
cx
lc
$ ls
cx
lc
$ cat cx
chmod +x $*
$ cat lc
# lc: подсчет числа строк в файлах
wc -l $*
Похоже верно
$ cd ..
$ rm junk test/*; rmdir test
Удалим ненужное
$ pwd
/usr/you/bin
$ bundle `pick *` | mail где-то!боб
Посылка файлов
$
Здесь могут возникнуть трудности, если окажется, что один из посылаемых файлов содержит строку вида
End of имя_файла
но это маловероятное событие. Для обеспечения полной надежности программы нам потребуются некоторые из описываемых в последующих главах средства, однако и в таком виде она удивительно полезна и удобна.
Программа bundle
является хорошим примером приспособляемости программного мира UNIX: в ней используются циклы языка shell
, переключение ввода-вывода, конструкция "документ здесь" и командные файлы. Она непосредственно обращается к команде mail
, и, что особенно интересно, порождает программу. Это одна из самых "красивых" среди известных вам shell
-программ: файл в несколько строк предлагает простое и элегантное решение.
Упражнение 3.17
Как бы вы использовали
bundle
для посылки всех файлов с учетом вложенных каталогов? Подсказка: командные файлы могут быть рекурсивными.
Упражнение 3.18
Модифицируйте программу
bundle
так, чтобы к каждому файлу она добавляла информацию, выведенную командойls -l
, в частности права доступа и время его последнего изменения. Сравните возможностиbundle
и архивной программыar(1)
.
3.10 Для чего нужно программировать на языке shell!
Программа shell
системы UNIX не относится к типичным интерпретаторам команд, хотя и дает возможность запускать команды обычным способом. Тем не менее это язык программирования, который позволяет достичь большего. Имеет смысл сделать ретроспективный обзор данной главы, поскольку здесь приведен довольно обширный материал, и, кроме того (что является главной причиной), мы обещали вам обсудить "средства общего пользования", а затем увлеклись примерами программирования на языке shell
. Дело в том, что используя язык shell
, вы все время пишите маленькие, практически однострочные программы, в частности конвейер – это программа, равноценная фразе "Чай готов". Однако вы выполняете свою работу так легко и естественно (если умеете), что даже не считаете ее программированием.
Интерпретатор дает вам такие средства, как циклы, переключение ввода-вывода с помощью <
и >
, порождение имен файлов с помощью *
, причем применение этих средств единообразно во всех программах. Некоторые средства, например командные файлы и программные каналы, на самом деле обеспечиваются ядром, но язык shell
предоставляет естественную запись для их создания. Они не только удобны, но и увеличивают мощность системы в целом.
Базой для интерпретатора служит ядро системы UNIX, например, хотя интерпретатор и определяет конвейеры, именно ядро осуществляет передачу данных по ним. Способ, которым система обрабатывает выполняемые файлы, позволяет программировать командные файлы так, чтобы они выполнялись подобно оттранслированным программам. Пользователь не должен думать о том, что это командные файлы, – для передачи их на выполнение не требуется специальная команда типа RUN
. Сам интерпретатор является программой, а не частью ядра. Его можно настраивать, расширять и обращаться с ним, как с любой другой программой. Такой подход не является уникальным, но здесь он реализован полнее, чем где бы то ни было.
В гл. 5 мы вернемся к теме программирования на языке shell
, а пока запомните: вне зависимости от того, как вы работаете с интерпретатором, вы программируете на его языке (чем в основном и объясняются его достоинства).
Историческая и библиографическая справка
На языке интерпретатора программируют с незапамятных времен. Сперва были отдельные команды для
if
,goto
и меток, а командаgoto
выполнялась путем просмотра входного файла от его начала до нужной метки. (Поскольку невозможно читать заново через программный канал, нельзя было и передавать по программному каналу в командный файл, использующий структуры управления.)Седьмая версия интерпретатора была создана С. Боурном, которому оказал помощь и идейную поддержку Д. Мэшей. Как вы увидите в гл. 5, здесь есть все необходимое для программирования. Кроме того, реорганизована работа с входным и выходным потоками: теперь можно без ограничения переключать ввод-вывод из командных файлов и в них. Неотъемлемым свойством интерпретатора является обработка метасимволов в именах файлов; в ранних версиях, которые остались лишь на очень маленьких машинах, она реализовывалась отдельной программой.
Другой вариант интерпретатора, с которым вы могли встречаться (а может быть, вы предпочитаете с ним работать) –
csh
, так называемый Си-shell
, созданный Б. Джоем на базе интерпретатора шестой версии. По сравнению с интерпретатором Боурна этот интерпретатор лучше обеспечивает диалог. Он предоставляет средство "история", позволяющее повторять в сокращенной записи (возможно, с небольшим редактированием) предварительно введенные команды. Отличается также и синтаксис команд интерпретатора Джоя. Но, поскольку Си-shell
, базируется на интерпретаторе ранней версии, в нем содержится меньше средств для программирования; это скорее диалоговый интерпретатор команд, чем язык программирования. В частности, исключена передача по программному каналу из (или в) командного файла со структурами управления.Команда
pick
предложена Т. Даффом, а командаbundle
– независимо А. Хьитом и Д. Гослингом.
Глава 4
Фильтры
Существует большое число программ UNIX, которые читают входной поток, выполняют простые операции над ним и записывают результат в выходной поток. Примерами могут служить программы grep
и tail
, выбирающие часть входного потока, sort
, сортирующая его, wc
, производящая подсчет в нем, и т.д. Такие программы называются фильтрами.
В настоящей главе обсуждаются наиболее часто используемые фильтры. Первой мы рассмотрим программу grep
, сосредоточившись на более сложных шаблонах, чем описанные в гл. 1, а затем две другие родственные программы – egrep
и fgrep
. Далее вы познакомитесь с еще несколькими полезными фильтрами, включая tr, который предназначен для транслитерации символов, dd
, предназначенный для работы с данными, полученными из других систем, и uniq
– для обнаружения повторяющихся строк. Приводится дополнительная информация и о программе sort
.
Конец главы посвящен двум преобразователям данных общего назначения, или программируемым фильтрам. Они называются так потому, что конкретное преобразование записывается как программа на некотором простом языке программирования. Различные программы могут породить совершенно разные преобразования. Речь идет здесь о программах sed
("stream editor" – потоковый редактор) и awk
, имя которой составлено из начальных букв имен ее авторов. Обе программы получаются путем обобщения команды grep
:
$ программа шаблон-действие имена_файлов...
которая сканирует последовательность файлов, ведя поиск строк, совпадающих с шаблоном, – если строка найдена, выполняется необходимое действие. Для команды grep
, как и для редактора ed
, шаблоном является регулярное выражение, а действие по умолчанию сводится к печати каждой строки, соответствующей шаблону.
В программах sed
и awk
обобщаются и шаблоны, и действия. Команда sed
, производная от ed
, выполняет "программу", состоящую из команд редактирования. Она пропускает данные из файлов через эту программу, выполняя для каждой строки команды из программы. Команда awk
не так удобна, как sed,
для манипуляций с текстом, но в ней предусмотрены арифметические операции, переменные, встроенные функции и язык программирования, схожий с Си. В данной главе не приводится полное описание обеих программ; оно есть в т. 2B справочного руководства по UNIX.
4.1 Семейство программ grep
В гл. 1 мы кратко упомянули о команде grep
, а затем использовали ее в примерах. Конструкция
$ grep шаблон имена_файлов
проводит поиск в поименованных файлах или в стандартном входном потоке и выводит на печать каждую строку, в которую входит шаблон. Команда grep
неоценима для поиска переменных в программах и слов в документах, а также для выбора части выходного потока программы:
$ grep -n variable *.[гл]
Поиск variable в тексте на Си
$ grep From $MAIL
Печать заголовков сообщений из почтовой
посылки
$ grep From $MAIL | grep -v mary
Заголовки, которые получены не от
адресата mary
$ grep -y mary $HOME/lib/phone-book
Поиск номера mary
$ who | grep mary
Выяснить, работает ли mary в системе
$ ls | grep -v temp
Имена файлов, не содержащих temp
Флаг -n
инициирует вывод номеров строк, флаг -v
меняет на противоположное значение условия, а флаг -y
допускает сопоставление строчных букв из шаблона с прописными буквами из файла (но прописные буквы все-таки могут сопоставляться только с прописными).
Во всех рассматривавшихся до сих пор примерах проводился поиск обычных строк из букв и чисел. Но команда grep
может искать и более сложные шаблоны: она интерпретирует выражения согласно простому языку для описания строк. С технической точки зрения шаблон представляет в некоторой степени ограниченную форму спецификаций строк, называемую регулярным выражением. Команда интерпретирует такие же регулярные выражения, как и редактор ed
. На самом деле, эта команда была создана (за один вечер) прямым редактированием ed
.
Регулярные выражения характеризуются тем, что ряду символов, таким, как *
и т.п., приписывается специальное значение, используемое интерпретатором. Есть еще несколько метасимволов, но, к сожалению, с различными значениями. В табл. 4.1 показаны все метасимволы регулярных выражений, и мы кратко их здесь рассмотрим.
с | Любой неспециальный символ c соответствует самому себе |
c | Указание убрать любое специальное значение символа c |
^ | Начало строки |
$ | Конец строки |
. | Любой одиночный символ |
[...] | Любой символ из ...; допустимы диапазоны типа a-z |
[^...] | Любой символ не из ... ; допустимы диапазоны |
n | Строка, соответствующая n-му выражению (...) (только для grep ) |
r* | Нуль или более вхождений r |
r+ | Одно или более вхождений r (только для egrep) |
r? | Нуль или одно вхождение r (только для egrep) |
r1r2 | За r1 следует r2 |
r1|r2 | r1 или r2 (только для egrep) |
(r) | Помеченное регулярное выражение r (только для grep ); может быть вложенным |
(r) | Регулярное выражение r (только для grep); может быть вложенным |
Никакое регулярное выражение не соответствует концу строки |
Таблица 4.1: Регулярные выражения grep
и egrep
(в порядке убывания приоритета)
Метасимволы ^
и $
привязывают шаблон к началу (^
) или концу ($
) строки. Например,
$ grep From $MAIL
ищет строки, содержащие From
в вашей почтовой посылке, но
$ grep '^From' $MAIL
выдает строки, начинающиеся с From
, которые, вероятнее всего, будут заглавными строками сообщений. Метасимволы регулярных выражений пересекаются с метасимволами интерпретатора, поэтому всегда имеет смысл заключать шаблоны команды grep
в апострофы.
Команда grep
допускает классы символов, подобные тем, что используются интерпретатором: так, [a-z]
задает любую строчную букву. Но есть и различия – если класс символов команды grep
начинается с символа слабого ударения то шаблон задает любой символ, кроме входящих в данный класс. Значит, [^0-9]
задает любой символ, кроме цифры. Как и в интерпретаторе, обратная дробная черта экранирует символы ]
и -
в классе символов, но команды grep
и ed
требуют, чтобы эти символы использовались там, где их значение недвусмысленно. Например, шаблон [][-]
задает открывающую или закрывающую квадратную скобку либо знак минус.
Точка '.'
эквивалентна '?'
в интерпретаторе: она задает любой символ. (Точка, по всей видимости, есть символ, назначение которого различно для разных программ.) Ниже приводятся два примера:
$ ls -l | grep '^d'
Список имен вложенных каталогов
$ ls -l | grep '^.......rw'
Список файлов, доступных всем для чтения и записи
Символ '^'
и семь точек задают любые семь символов в начале строки; в случае применения к выходному потоку команды ls -l
задается любая строка права доступа.
Операция "повторитель" ('*'
) применима в выражении к предваряющему ее символу или метасимволу (включая класс символов), и вместе они обозначают любое число вхождений символа или метасимвола. Например, x*
задает последовательность букв x
произвольной длины, [a-zA-Z]*
– любую строку букв, .*
– все до конца строки, а .*x
– все до последнего символа x
в строке включительно. Необходимо отметить несколько важных моментов, связанных с повторителем. Во-первых, повторитель действует только на один символ, поэтому xy*
соответствует x
, за которым идут yy...
, но не последовательности типа xyxyxy
. Во-вторых, любое число включает нуль, поэтому если вы хотите, чтобы символ присутствовал, в шаблоне его нужно повторить. Например, правильным выражением, задающим строку букв, является такое: [a-zA-Z][a-zA-Z]*
(буква, за которой следует нуль или более букв). Регулярное выражение .*
соответствует – *
, т.е. метасимволу интерпретатора, используемому для имен файлов.
Ни одно регулярное выражение команды grep
не соответствует символу перевода строки; выражения сопоставляются с каждой строкой в отдельности. Регулярные выражения делают команду grep
простым языком программирования. Вспомните, что второе поле файла паролей содержит зашифрованный пароль. Приведенная ниже команда проводит поиск пользователей, не имеющих пароля:
$ grep '^[^:]*::' /etc/passwd
Шаблон расшифровывается так: начало строки, любое число символов, отличных от двоеточия, два двоеточия.
Команда grep
– старейшая в семействе программ, к которому относятся команды fgrep
и egrep
. В основном их действие одинаково, но fgrep
может одновременно искать несколько литеральных строк, тогда как egrep
интерпретирует настоящие регулярные выражения, подобно grep
, но с использованием операций "or" и скобок для группировки выражений, что будет объяснено ниже.
Обе команды, fgrep
и egrep
, имеют флаг -f
для указания файла, из которого читается шаблон. В этом файле символы перевода строк разделяют шаблоны при параллельном поиске. Допустим, что некоторые слова вы пишете неправильно. В этом случае можно проверить документацию на наличие таких слов, поместив их в файл по одному на строке и воспользовавшись командой fgrep
:
$ fgrep -f типичные_ошибки документ
Регулярные выражения, интерпретируемые egrep
(они также приведены в табл. 4.1), – те же самые, что и в grep, но с небольшими добавлениями. Можно использовать скобки для группировки, поэтому (xy)*
задает пустую строку или любую последовательность xy
, xyxy
, xyxyxy
и т.д. Вертикальная черта |
является операцией or (или); today|tomorrow
соответствует today
или tomorrow
, как и to(day|morrow)
. Наконец, в команде egrep
есть еще две операции повторения: +
и ?
. Шаблон x+
задает один или более символов x
, а шаблон x?
– нуль или один символ x
(но не более).
Команда egrep
прекрасно подходит для игр, в которых нужно искать в словаре слова со специальными свойствами. Мы будем обращаться к словарю Вебстера (второе международное издание), хранящемуся в файле в виде списка слов по одному в строке без определений их значения. В вашей системе может быть небольшой словарь /usr/dict/words
, предназначенный для проверки правописания; просмотрите его, чтобы выяснить формат. Ниже приведен шаблон, задающий слова английского языка, содержащие все пять гласных в алфавитном порядке:
$ cat alphvowels
^[^aeiou]*a[^aeiou]*e[^aeiou]*i[^aeiou]*o[^aeiou]*u[^aeiou]*$
$ egrep -f alphvowels /usr/dict/web2 | 3
abstemious abstemiously abstentions
achelious acheirous acleistous
affectious annelidous arsenious
arterious bacterious caesious
facetious facetiously fracedinous
majestious
$
В файле alphvowels
шаблон не взят в кавычки. Если применяются кавычки для экранирования шаблона в команде egrep
, интерпретатор защищает его от интерпретации командами, но кавычки убирает, и команда egrep
никогда "не узнает" о них. Поскольку интерпретатор не заглядывает в файл, кавычки не нужны для защиты содержимого файла. Для этого примера мы могли бы использовать команду grep
, но алгоритм egrep
таков, что она осуществляет поиск намного быстрее в случае шаблонов с повторителями, особенно при просмотре больших файлов.
В другом примере требуется найти все английские слова, состоящие из шести или более букв, в которых буквы следуют в алфавитном порядке:
$ cat monotonic
^a?b?c?d?e?f?g?h?i?j?k?l?m?n?o?p?r?s?t?u?v?w?x?y?z?$
$ egrep -f monotonic /usr/dict/web2 | grep '......' | 5
abdest acfcnow adipsy agnosy almost
bedfist behint befcnow bijoux biopsy
chintz dehors dehort demos dimpsy
egilops ghosty
(Egilops – это болезнь, поражающая пшеницу.) Обратите внимание на использование команды grep
для фильтрации выходного потока egrep
.
Для чего нужны три сходные программы? Программа fgrep
не интерпретирует метасимволы, но может параллельно обрабатывать тысячи слов (после инициации время ее работы не зависит от числа слов), и поэтому она применяется прежде всего для заданий типа библиографического поиска. Размеры типичных шаблонов для программы fgrep
превосходят возможности алгоритмов, используемых в программах grep
и egrep
. Различия между двумя последними указать труднее. Программа egrep
появилась намного раньше. Она интерпретирует регулярные выражения, используемые в командах редактора ed
, в ней есть помеченные регулярные выражения и большой набор флагов. Программа egrep
интерпретирует более общие выражения (не считая помеченных), и выполняется значительно быстрее (со скоростью, не зависящей от шаблона), но ее стандартная версия требует большего времени на инициацию в случае сложного выражения. Существует новая версия, начинающая работу мгновенно, так что программы egrep
и grep
теперь можно было бы скомбинировать в одну программу поиска по шаблону.
Упражнение 4.1
Прочтите о регулярных выражениях
((
и))
в приложении 1 или справочном руководстве поed(1)
. Используйте программуgrep
для поиска палиндромов – слов, читающихся одинаково с конца и начала. Подсказка: составьте свой шаблон для слов каждой длины.
Упражнение 4.2
Алгоритм программы
grep
таков: прочесть одну строку, проверить ее на вхождение шаблона, затем продолжить цикл. Как повлияло бы на работу программы то, что регулярные выражения могли бы задавать перевод строки?