UNIX — универсальная среда программирования

Керниган Брайан Уилсон

Пайк Роб

Глава 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 таков: прочесть одну строку, проверить ее на вхождение шаблона, затем продолжить цикл. Как повлияло бы на работу программы то, что регулярные выражения могли бы задавать перевод строки?

 

4.2 Другие фильтры

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

Рассмотрим сначала программу sort, как наиболее часто используемую. В гл. I было указано ее назначение: сортировка входного потока по строкам в порядке, задаваемом множеством ASCII. Хотя это очевидный порядок для сортировки по умолчанию, существует множество других полезных способов сортировки данных, и программа sort пытается удовлетворить всех, предоставляя множество различных флагов. Например, флаг -f устраняет различие между прописными и строчными буквами, флаг -d (словарный порядок) игнорирует при сравнении все символы, кроме букв, цифр и пробелов.

Способ сравнения в алфавитном порядке является наиболее распространенным, но иногда требуется произвести сравнение в числовом порядке, флаг -n сортирует по числовому значению, а флаг -r изменяет смысл на противоположный любого условия. Итак, имеем

$ ls | sort -f     Сортировка имен файлов в алфавитном порядке

$ ls -s | sort -n  Сортировка в порядке возрастания размеров файлов

$ ls -s | sort -nr Сортировка в порядке убывания размеров файлов

Программа sort обычно сортирует целые строки, но ее можно заставить работать только с определенными полями. Обозначение +m показывает, что при сравнении пропускается m полей, а +0 обозначает начало строки, например:

$ ls -l | sort +3nr Сортировка по счетчику байтов в порядке убывания

размеров

$ who | sort +4nr   Сортировка по времени входа в систему, в порядке

возрастания размеров файлов

Еще одним полезным флагом программы является -о, задающий имя файла для выходного потока (это может быть один из входных файлов), и флаг -u, который удаляет все, за исключением одной из строк, совпадающих в сортируемых полях. Можно использовать несколько флагов, как показано в примере на странице sort(1) справочного руководства:

$ sort +0f +0 -u filenames

здесь флаг +0f сортирует строку, совмещая строчные и прописные буквы, но идентичные строки могут не быть соседними. Поэтому вводится второй флаг +0, который сортирует одинаковые строки после первой сортировки в обычном порядке ASCII. Наконец, флаг -u выбрасывает все, кроме одной из соседних повторяющихся строк. Таким образом, получив список слов по одному в строке, команда выдает неповторяющиеся слова. Указатель для этой книги был подготовлен с помощью сходной команды sort, обладающей еще большими возможностями (см. руководство по sort(1)).

Создание команды uniq явилось стимулом для введения флага -u в команде sort: флаг отбрасывает все строки, кроме одной, из группы соседних повторяющихся строк. Выведение отдельной программы для этой операции позволяет выполнять ее независимо от сортировки. Например, uniq удалит повторяющиеся пустые строки, независимо от того, сортируется входной поток или нет. Флаги предусматривают специальные способы обработки повторяющихся строк: uniq -d печатает только повторяющиеся строки, uniq -u — только уникальные, т.е. неповторяющиеся строки; uniq -c подсчитывает число вхождений каждой строки, в чем вскоре вы убедитесь на примере.

Программа comm служит для сравнения файлов. Получив два отсортированных входных файла f1 и f2, она выдает выходной поток в три столбца: строки, встречающиеся только в f1, строки, встречающиеся только в f2, и строки, встречающиеся в обоих файлах. С помощью флага можно убрать любой из этих столбцов:

$ comm -12 f1 f2

выдает только строки, содержащиеся в обоих файлах, а

$ comm -23 f1 f2

выдает строки, которые есть только в первом, но не во втором файле. Это удобно для сравнения каталогов и списка слов со словарем.

Команда tr проводит транслитерацию символов своего входного потока. Наиболее часто они используются для преобразования строчных букв в прописные и обратно:

$ tr a-z A-Z Перевести строчные буквы в прописные

$ tr A-Z a-z Перевести прописные буквы в строчные

Несколько отличается от всех рассмотренных выше команд dd. Эта команда предназначена прежде всего для обработки данных на магнитной ленте, полученных из других систем — само ее название служит напоминанием о языке управлений заданиями OS/360. Команда dd выполняет преобразование прописных букв в строчные, и наоборот (в нотации, отличной от нотации команды tr). Она осуществляет перевод из множества символов ASCII в EBCDIC, и наоборот; может читать и писать данные в формате записей фиксированного размера с дополнением пробелами, что характерно для отличных от UNIX систем. На практике команду dd часто используют для работы с исходными неотформатированными данными, откуда бы они ни были получены; она реализует набор средств для работы с двоичными данными.

Посмотрим, чего можно достичь с помощью взаимодействия фильтров на примере конвейера, который печатает 10 наиболее часто встречающихся во входном потоке слов:

cat $* |

tr -sc A-Za-z '\012' | Сжимаем все небуквы в перевод строки

sort |

uniq -с |

sort -n |

tail |

5

Команда cat собирает файлы, поскольку tr может читать только стандартный входной поток. Команда tr действует, как указано в справочном руководстве: она сжимает соседние, отличные от букв, символы в символы перевода строк, преобразуя таким образом входной поток в строки из одного слова. Затем слова сортируются и с помощью uniq -с каждая группа идентичных слов сжимается в одну строку, начинающуюся со счетчика, который используется как сортируемое поле в команде sort -n. (Эта последовательность двух команд сортировки, между которыми находится команда uniq, применяется так часто, что уже стала идиомой.) В результате получаются неповторяющиеся слова, отсортированные в порядке возрастания частоты появления в документе. Команда tail отбирает 10 наиболее часто встречающихся слов (т.е. конец отсортированного файла) и команда 5 печатает их в пять столбцов.

Заметьте, кстати, что введение символа | в конце строки — это законный способ ее продолжения.

Упражнение 4.3

Используя средства этого раздела и файл /usr/dict/words , составьте простой анализатор правильности написания текста на английском языке. Каковы его недостатки и как их исправить?

Упражнение 4.4

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

 

4.3 Потоковый редактор

sed

Вернемся теперь к редактору sed. Поскольку он происходит непосредственно от ed, вы легко изучите его и закрепите свои знания о редакторе ed. Основа редактора sed проста:

$ sed 'список команда ed' имена_файлов...

Читаются строки по одной из входных файлов; команды из списка применяются к каждой строке по одной в указанном порядке и результат редактирования записывается в стандартный выходной поток. Например, можно заменить в любом из указанных файлов UNIX на UNIX (TM) с помощью команды:

$ sed 's/\UNIX/\UNIX\ (TM)/g' имена_файлов...> выходной поток

Нужно правильно понимать действие команды. Она не изменяет содержимое своих входных файлов, а лишь пишет в стандартный выходной поток; исходные же файлы не изменяются. Вы уже достаточно разобрались в интерпретаторе, чтобы понять, что

$ sed '...' файл > файл

не лучшее решение: для замены содержимого файлов нужно задействовать временный файл или другую программу. В дальнейшем мы рассмотрим программу, реализующую задачу записи в существующий файл; обратитесь к команде overwrite в (гл. 5).

Редактор sed выдает все строки автоматически, поэтому команда p не нужна после ввода команды замены, приведенной выше; более того, если она задается, то каждая изменяемая строка печатается дважды. Однако кавычки необходимы почти всегда, поскольку многие метасимволы программы sed имеют специальные значения и для интерпретатора. Рассмотрим, например, команду du -а, порождающую список имен файлов. Обычно она выдает размер и имя файла:

$ du -a ch4*

18 ch4.1

13 ch4.2

14 ch4.3

17 ch4.4

 2 ch4.9

$

Можно использовать sed, чтобы отбросить размеры файлов, но в команде редактирования нужны кавычки для защиты символов * и табуляции от обработки интерпретатором:

$ du -a ch4.* | sed 's/.*→//'

ch4.1

ch4.2

ch4.3

ch4.4

ch4.9

$

В команде замены удаляются все символы (.*) до крайнего правого символа табуляции включительно (он показан в шаблоне как →). Аналогичным способом можно выделить из вывода команды who имена пользователей и время входа в систему:

$ who

lr  tty1 Sep 29 07:14

ron tty3 Sep 29 10:31

you tty4 Sep 29 08:36

td  tty5 Sep 29 08:47

$ who | sed 's/ .* / /'

lr  07:14

ron 10:31

you 08:36

td  08:47

$

Команда s заменяет пробел и все, что следует за ним (максимально возможно, включая дополнительные пробелы) до следующего пробела на единственный пробел. Кавычки нужны.

Почти такую же команду sed можно использовать, чтобы создать программу getname, возвращающую имя пользователя:

$ cat getname

who am i | sed 's/ .*//'

$ getname

you $

Другая команда sed применяется настолько часто, что мы поместили ее в командный файл с именем ind. Эта команда вставляет пробелы до шага табуляции; она удобна для лучшего расположения текста при печати.

Реализовать команду ind просто: достаточно установить символ табуляции в начале каждой строки:

$ sed 's/^/→/' $* Первая версия ind

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

sed '/./s/^/→/' $* Вторая версия

Шаблон /./ задает любую строку, в которой есть по крайней мере один символ, кроме символа перевода строки, поэтому команда s не выполняется для пустых строк. Вспомним, что sed выдает все строки, независимо от того, менялись они или нет, так что пустые строки по-прежнему выдаются.

Есть еще один способ определения команды ind. Можно выполнять команды только для тех строк, которые не соответствуют выбираемому шаблону, предварив команду знаком восклицания '!'. В команде

sed '/^$/!s/^/→/' $* Третья версия

шаблон /^$/ задает пустые строки (перевод строки сразу следует за ее началом), поэтому /^$/! предписывает не выполнять команду для пустых строк.

Как уже отмечалось, sed печатает каждую строку автоматически, независимо от того, какие операции над ней выполнялись (если только она не была удалена). Кроме того, можно использовать большинство команд редактора ed. Поэтому легко составить программу sed, которая напечатает, скажем, три первых строки входного потока, а затем завершится:

sed 3q

Хотя 3q не является законной командой ed, для sed она имеет смысл: копировать строки и завершить выполнение после третьей.

От вас может потребоваться другая работа с данными, например вставка пробелов. Один из способов заключается в том, чтобы выходной поток sed пропустить через команду ind, но поскольку редактор sed допускает несколько команд, можно сделать это путем одного обращения к sed (хотя и несколько необычного):

sed 'S/^/→/

3q'

Обратите внимание, где находятся кавычки и символ перевода строки: команды должны быть на отдельных строках, но редактор игнорирует пробелы и символы табуляции в начале строки.

Представляется естественным с помощью рассмотренных выше приемов составить программу head, которая будет печатать несколько строк из каждого своего файла-аргумента. Но sed 3q (или 10q) настолько просто задать, что в этом никогда не возникало потребности. Однако мы ввели команду ind, так как соответствующая последовательность для sed длиннее. (В процессе работы над книгой мы заменили существовавшую программу на языке Си в 30 строк на одну строку команды ind версии 2, приведенной выше.) Четкого критерия в отношении того, когда имеет смысл создавать отдельную программу из сложной командной строки, нет, поэтому мы предлагаем вам свое решение: поместите программу в свой каталог /bin и посмотрите, будете ли вы ее действительно применять.

Можно помещать команды редактора sed в файл и выполнять их, получая оттуда с помощью обращения:

$ sed -f командный_файл

Вы можете обращаться к строкам, используя не только их номера. Так, команда

$ sed '/шаблон/q'

выдает из входного потока все строки до первой включительно, которые соответствуют шаблону, а команда

$ sed '/шаблон/d'

удаляет каждую строку, содержащую шаблон; удаление происходит до автоматического вывода строк, поэтому удаленные строки не учитываются.

Хотя автоматический вывод обычно удобен, иногда он мешает. Его можно отключить с помощью флага -n; в этом случае в выходном потоке появятся только строки, задаваемые явной командой вывода p. Например,

$ sed -n '/шаблон/p'

эквивалентен команде grep. Условие сопоставления можно инвертировать, если завершить его символом !, поэтому

$ sed -n '/шаблон/!p'

эквивалентно команде grep -v. (Так же, как sed '/шаблон/d'.)

Для чего нужны две команды sed и grep? В конце концов, grep — всего лишь частный случай команды sed. Это в какой-то степени объясняется историческими причинами: команда grep появилась намного раньше, чем команда sed. Но она не только уцелела, но и активно применялась. В силу специфики назначения обеих команд grep значительно проще использовать, чем команду sed, так как ее использование в типичных ситуациях настолько лаконично, насколько возможно (к тому же у нее есть возможности, отсутствующие у команды sed; см., например, описание флага -b). Но все-таки программы могут "умирать". Когда-то была программа с именем gres, выполняющая простую подстановку, но она исчезла почти мгновенно, когда появилась команда sed.

Используя запись, такую же, как в редакторе ed, можно вставлять символы перевода строк с помощью команды sed:

$ sed '/$/\

> /'

Здесь добавляется символ перевода строки к каждой строке, и таким образом пустые строки вставляются во входной поток, а команда

$ sed 's/[→][→]*/\

>/g'

заменяет каждую последовательность пробелов или символов табуляции на символ перевода строки, т. е. разбивает входной поток на строки из одного слова. (Регулярное выражение '[→]' задает пробел или символ табуляции, '[→]*' задает нуль или более таких символов, а весь шаблон — один или более пробелов и/или символов табуляции.)

Можно также использовать пару регулярных выражений или номеров строк для задания диапазона строк, к которому будет применяться произвольная команда.

$ sed -n '20,30p'       Печать только строк с 20-й по 30-ю

$ sed '1,10d'           Удаление строк с 1-й до 10-й (=tail +11)

$ sed '1,/^$/cd'        Удаление всех строк до первой пустой включительно

$ sed -n '/^$/,/^end/p' Печать всех групп строк, начиная от пустой строки до

строки, начинающейся с end

$ sed '$d'              Удаление последней строки

Строки нумеруются с начала входного потока; обнуление не происходит с началом нового файла.

У команды sed есть существенное ограничение, которое, однако, отсутствует в редакторе ed: в ней поддерживается относительная нумерация строк. В частности, операции + и - не действуют в выражениях, задающих номера строк, поэтому невозможно двигаться назад во входном потоке:

$ sed '$-1d' Недопустима обратная адресация

Unrecognized command: $-1d

$

Если строка считана, предыдущая исчезла навсегда: нет способа специфицировать предыдущую строку, а именно это требуется в команде. В принципе такой способ есть в команде sed, но он слишком изощренный. (См. команду hold в справочном руководстве по UNIX.) Невозможна и относительная прямая адресация:

$ sed '/что-то/+1d' Недопустима прямая адресация

Редактор sed имеет возможность записывать в несколько выходных файлов. Например, команда

$ sed -n '/шабл/w файл1

> /шабл/!w файл2' имена_файлов...

$

записывает строки, соответствующие "шабл", в файл1, а не соответствующие — в файл2, или, если вернуться к нашему первому примеру:

$ sed 's/\UNIX(TM)/gw u.out' имена_файлов...> выход

то здесь, как и ранее, весь выходной поток записывается в файл "выход", но к тому же измененные строки записываются в файл u.out.

Иногда нужна помощь со стороны интерпретатора, чтобы в команду редактора включить аргументы командного файла. Одним из примеров служит программа newer, которая выдает все более новые, чем заданный, файлы каталога:

$ cat newer

# newer f: список файлов, созданных после f

ls -t | sed '/'$1'$/q'

$

Кавычки защищают различные специальные символы, предназначенные для редактора, оставляя $1 открытым для интерпретатора, чтобы он заменил его на имя файла. Существует альтернативный способ записи аргумента:

"/^$1\$/q"

так как $1 заменяется на аргумент, тогда как \$ становится просто $.

Аналогично можно составить программу older, которая выдает в качестве параметра все файлы, более старые, чем заданный:

$ cat older

# older f: список файлов, созданных ранее f

ls -tr | sed '/'$1'$/q'

$

Единственное различие состоит в применении флага -r в команде ls для изменения порядка выдачи файлов.

Хотя редактор sed способен на гораздо большее, чем мы вам продемонстрировали, включая проверку условий, циклы и ветвления, запоминание предыдущих строк, и, конечно, в нем допустимы многие команды редактора ed, описанные в приложении 1. Тем не менее в основном sed используется так, как было показано; одна или две простые команды редактирования, а не длинные и сложные последовательности. В табл. 4.2 собраны некоторые команды sed, хотя и не приведены операции над несколькими строками.

a\ Добавлять строки к выходному потоку, пока одна из них не закончится на \
b label Перейти на команду: label
c\ Заменить строки на последующий текст, как в команде a
d Удалить строку; прочесть следующую входную строку
i\ Вставить последующий текст перед следующим выходным потоком
l Выдать строку, напечатав все невидимые символы
p Выдать строку
q Выйти
r file Читать file , содержимое его переслать в выходной поток
s/old/new/f Заменить old на new . Если f = g , заменить все вхождения; f = p , вывод; f = w файл, запись в файл
t label Проверка: переход на метку, если была замена в текущей строке
w file Записать строку в файл
y/str1/str2/ Заменить каждый символ строки str1 на соответствующий символ строки str2 (диапазоны недопустимы)
= Выдать текущую нумерацию входной строки
!cmd Выполнить команду sed cmd , только если строка не выбрана
: label Установить метку для команд b и t
{ Команды до соответствующей скобки } рассматривать как группу

Таблица 4.2: Сводка команд sed

Редактор sed удобен потому, что позволяет работать с произвольно длинными входными строками. Это "быстрый" редактор, который сходен с редактором ed в интерпретации регулярных выражений и в обработке отдельных строк. Однако, с другой стороны, его возможности запоминания ограничены (трудно запомнить текст от одной строки до другой) — делается только один проход по данным, нельзя двигаться назад, нет способов прямой адресации типа /.../+1: и нет средств для работы с числами, т.е. он является чисто текстовым редактором.

Упражнение 4.5

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

Упражнение 4.6

С помощью редактора sed сделайте программу bundle совершенно надежной. Подсказка: в конструкции "документ здесь" слово, отмечающее конец данных, распознается только в том случае, когда оно совпадает со строкой полностью.

 

4.4 Язык

awk

поиска и обработки шаблонов

Некоторые ограничения sed преодолены в программе awk. Принцип работы этой программы сходен с принципом работы программы sed, но синтаксически она ближе к языку программирования Си, чем к текстовому редактору. Способ задания команды такой же, как и для sed:

$ awk 'программа' имена_файлов...

но программа другая:

шаблон {действие}

шаблон {действие}

...

Программа awk читает входной поток по одной строке из указанных файлов. Строки сопоставляются с шаблонами по порядку; для каждого шаблона, соответствующего строке, выполняется необходимое действие. Как и в редакторе sed, входные файлы здесь не изменяются.

Шаблоны могут быть регулярными выражениями в sed или более сложными условиями, напоминающими язык Си. Приведем простой пример (такого же результата можно добиться с помощью команды egrep):

$ awk '/регулярное_выражение/ {print}' имена_файлов...

Печатается каждая строка, соответствующая регулярному выражению.

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

$ awk '/регулярное_выражение/' имена_файлов...

эквивалентна предыдущей. Наоборот, если отсутствует шаблон, то действие выполняется для каждой входной строки. Следовательно, команда

$ awk '{print}' имена_файлов...

дает те же результаты, что и команда cat, хотя действует медленнее.

Теперь перейдем к более интересным примерам, но прежде сделаем одно замечание. Как и в случае sed, программу команды awk можно получать из файла:

$ awk -f кмд файл имена_файлов...

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

$ who

you tty2 sep 29 11:53

jim tty4 sep 29 11:27

$

Поля обозначаются как $1, $2, …, $NF, где NF — переменная, значение которой установлено равным числу полей. В нашем случае NF=5 для обеих строк. (Учтите разницу между NF, числом полей и $NF — последним полем строки. В отличие от интерпретатора в программе awk только номера полей начинаются с $; переменные не имеют такого префикса.) Например, следующая команда выдаст поле "размер файла" из результата выполнения команды du -а

$ du -a | awk '{print $2}'

а для печати имен пользователей, работающих в системе, и времени входа нужно задать:

$ who awk '{print $1, $5}'

you 11:53

jim 11:27 $

Для печати имени и времени входа в систему, упорядоченных по времени, зададим:

$ who awk '{print $5, $1}' | sort

11:27 jim

11:53 you

$

Это альтернативные решения примеров, приведенных выше в данной главе, в которых использовалась команда sed. Хотя с программой awk проще работать в подобных случаях, она обычно выполняется медленнее как в начальной фазе, так и при большом входном потоке.

Обычно предполагается, что поля разделяются произвольным числом пробелов и символов табуляций, но можно определить в качестве разделителя любой одиночный символ. Один из способов состоит в задании в командной строке флага -F (здесь прописная буква). Например, поля в файле паролей /etc/passwd разделяются двоеточиями:

$ sed 3q /etc/passwd

root:3D.fHR5KoB.3s:0:1:S.User:/:

ken:y.68wdl.ijayz:6:1:K.Thompson:/usr/ken:

dmr:z4u3dJWbg7wCk:7:1:D.M.Ritchie:/usr/dmr:

$

Для печати имен пользователей, образующих первое поле, можно задать:

$ sed 3q /etc/passwd | awk -F : '{print $1}'

root

ken

dmr

Обработка пробелов и символов табуляции здесь особая. По умолчанию и пробелы, и символы табуляции служат разделителями, а разделители в начале строки отбрасываются. Однако если в качестве разделителя определен не пробел, то разделители в начале строки учитываются при определении полей. В частности, если используется символ табуляции, то пробелы не являются символами разделителями, пробелы в начале строки вводят в поле, и каждый символ табуляции определяет поле.

Печать

В программе awk, помимо числа входных полей, доступна и другая интересная информация. Встроенная переменная NR хранит номер текущей входной "записи", т.е. строки. Поэтому для вставки номера строки перед строкой входного потока достаточно задать:

$ awk '{print NR, $0}'

Поле $0 обозначает всю входную строку без изменений. В операторе print фрагменты, отделяемые запятой, печатаются через символы разделения полей выходного потока, которые по умолчанию служат пробелами.

Формат печати оператора print обычно является приемлемым. При несоответствующем формате используйте оператор printf, обеспечивающий полный контроль над выходным потоком. Например, для печати номеров строк в поле размером в четыре цифры можно задать такую команду:

$ awk '{printf "%4d %s\n", NR, $0}'

Выражение %4 задает десятичное целое число (NR) в поле размером в четыре цифры, %S — строка символов ($0), \n — символ перевода строки, который нужен потому, что оператор printf не выдает автоматически пробелы или символы перевода строк. Оператор printf сходен с аналогичной Си функцией (см. справочное руководство по printf(3)).

Мы могли бы определить программу ind (рассматривавшуюся в начале главы) следующим образом:

$ awk '{printf "\t%s\n", $0}' $*

Здесь выдается символ табуляции (\t) и входная строка.

Шаблоны

Предположим, что вы хотите найти в файле /etc/passwd пользователей, не имеющих пароля. Зашифрованный пароль находится во втором поле, поэтому программа состоит из одного шаблона:

$ awk -F: '$2 == ""' /etc/passwd

Шаблон проверяет, является ли второе поле пустой строкой (операция == — это проверка на равенство).

Такой шаблон можно задать различными способами:

$2==""          Второе поле пусто

$2~/^$/         Второе поле соответствует пустой строке

$2!~/./         Второе поле не содержит ни одного символа

length($2) == 0 Длина второго поля равна нулю

Символ ~ обозначает соответствие регулярному выражению, а символ ! — отсутствие соответствия. Само регулярное выражение заключено в символы дробной черты.

Встроенная функция length программы awk вычисляет длину строки символов. Шаблону может предшествовать символ ! для отрицания его, например,

!($2=="")

Операция ! подобна такой же операции в языке Си, но в редакторе sed эта операция следует за шаблоном.

Наиболее типичное использование шаблонов в программе awk сводится к задачам простой проверки данных. Большинство из них немногим сложнее, чем поиск строк, не удовлетворяющих какому-то критерию; если нет выходного потока, то считается, что данные удовлетворяют соответствующему критерию (по принципу "отсутствие новостей — хорошая новость"). Например, в следующем шаблоне проверяется с помощью операции %, вычисляющей остаток от деления, четно или не четно число полей в каждой входной строке:

$ NF % 2 != 0 # напечатать, если нечетное число полей

Другой шаблон выдает исключительно длинные строки, используя встроенную функцию length:

length ($0) >72 # напечатать, если слишком длинная строка

В программе awk используется то же соглашение о комментарии, что и в интерпретаторе: символ # отмечает начало комментария.

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

length($0) > 72 {print "Строка", NR, "длинная" : substr($0, 1, 60)}

Функция substr(s, m, n) выделяет подстроку из строки s, начинающуюся с символа с номером m и длиной в n символов. (Символы в строке нумеруются с 1.) Если n отсутствует, то берется подстрока от m до конца строки. Эту функцию можно использовать для выделения полей с фиксированным положением, например выделить время в часах и минутах из результата выполнения команды date:

$ date

Thu Sep 29 12:17:01 EDT 1983

$ date | awk '{print substr($4, 1, 5) }'

12:17

$

Упражнение 4.7

Сколько различных программ awk вы можете составить для переписи входного потока в выходной, как это делает команда cat ? Какая из них самая короткая?

Шаблоны BEGIN и END

Программа awk имеет два специальных шаблона BEGIN и END. Действия, соответствующие BEGIN, выполняются прежде, чем читается первая входная строка; можно использовать этот шаблон для инициации переменных, печати заголовков или для установки символа разделителя полей, присваивая его переменной FS.

$ awk 'BEGIN { FS = ":" }

> $2 == "" ' /etc/paswd

$ Результата нет: все работают с паролями

Действия, указанные в шаблоне END, выполняются после обработки последней входной строки:

$ awk 'END {print NR}'...

Здесь печатается число строк входного потока.

Арифметика и переменные

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

{s=s+$1}

END {print s}

Поскольку число значений доступно с помощью переменной NR, изменив последнюю строку на

END {print s, s/NR}

мы получим и сумму, и среднее значение.

Этот пример показывает, как используются переменные в awk. Переменная s не является встроенной, она определяется самим фактом использования. По умолчанию переменные инициируются нулем, так что, как правило, не нужно беспокоиться об их инициации.

В программе awk есть такие же сокращенные формы арифметических операторов, как и в языке Си, поэтому естественная запись примера имела бы вид:

{s+=$1}

END {print}

Запись s+=$1 равноценна записи s=s+$1, но более компактна. Можно обобщить пример по подсчету входных строк:

    { nc+=length($0) +1 # число символов, +1 для \n

     nw += NF           # число слов

    }

END {print NR, nw, nc }

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

В качестве другого примера выполнения арифметических операций рассмотрим программу, подсчитывающую число страниц по 66 строк в каждой. Страницы получаются при прогоне несколько файлов через команду pr. Это можно оформить в виде команды prpages:

$ cat prpages

# prpages: подсчет числа страниц, выдаваемых pr

wc $* |

awk '!/total$/ { n += int(($1+55)/56) }

     END       { print n }'

$

Команда pr помещает на каждую страницу 56 строк текста (это число определяется эмпирически). Для каждой строки вывода команды wc, которая не содержит слово total в конце строки, число страниц округляется, а затем выделяется целая часть с помощью встроенной функции int.

$ wc ch4.*

 753  3090 18129 ch4.1

 612  2421 13242 ch4.2

 637  2462 13455 ch4.3

 802  2986 16904 ch4.4

  50   213  1117 ch4.9

2854 11172 62847 total

$ prpages ch4.*

53

$

Для проверки этого результата запустим команды pr и awk одновременно:

$ pr ch4.* | awk 'END {print NR/66}'

53

$

Переменные программы awk могут также хранить строки символов. Рассматривать ли переменную как число или как строку символов — зависит от контекста. Грубо говоря, в арифметических выражениях типа s+=$1 используется числовое значение в контексте операций со строками типа x=="abc" — строковое значение в неясных случаях, например x>y, — строковое значение, если только операнды не являются явно числовыми. (Правила четко сформулированы в справочном руководстве по применению команды awk.) Строковые переменные инициируются пустой строкой. В последующих разделах строки будут активно использоваться.

В программе awk есть несколько своих встроенных переменных обоих типов, таких, как NR и FS. Их полный список приведен в табл. 4.3, а в табл. 4.4 перечислены операции, выполняемые командой.

FILENAME Имя текущего входного файла
FS Символ разделения полей (по умолчанию приняты пробел и символ табуляции)
NF Число полей входной строки
NR Число входных строк
OFMT Формат вывода чисел (по умолчанию принят %g ; обратитесь к руководству по printf(3y) )
OFS Строка разделитель полей в выходном потоке (пробел по умолчанию)
ORS Строка-разделитель строк в выходном потоке (символ перевода строки по умолчанию)
RS Символ разделения входных строк (символ перевода строки по умолчанию)

Таблица 4.3: Встроенные переменные awk

= += -= /= %= Присваивание; v ор=expr есть v=v op (expr)
|| ИЛИ: expr1 || expr2 истина, если одно или оба истинны; expr2 не вычисляется, если expr1 истинна
&& И: expr1 && expr2 истина, если оба истинны; expr2 не вычисляется, если expr1 ложь
! Отрицание значения выражения
>>= <<= == != ~ !~ Операция отношения; ! и !~ это соответствие и несоответствие
пусто Конкатенация строк
+ - Сложение, вычитание
* / % Умножение, деление, вычисление остатка
++ -- Увеличение, уменьшение (префиксное или постпрефиксное)

Таблица 4.4: Операции, выполняемые awk (в порядке возрастания приоритета)

Упражнение 4.8

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

Управление

При редактировании большого файла очень легко (судя по опыту) случайно создать копию соседнего слова, что практически никогда не происходит преднамеренно. Для устранения таких ошибок в семействе программ Writers Workbench (рабочий набор редактора) существует программа double, отыскивающая пары идентичных соседних слов. Ниже показана реализация этой программы с помощью awk:

$ cat double

awk '

FILENAME != prevfile { # new file

 NR = 1                # reset line number

 prevfile = FILENAME

}

NF > 0 {

 if ($1 == lastword)

  printf "double %s, file %s, line %d\n" ,$1, FILENAME, NR

 for (i = 2; i <= NF; i++)

  if ($i == $(i-1))

   printf "double %s, file %s, line %d\n", $i, FILENAME, NR

 if (NF > 0)

  lastword = $NF

}' $*

*

$

Операция ++ означает автоувеличение операнда, а операция -- — его автоуменьшение.

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

Оператор if — такой же, как в языке Си:

if (условие)

 оператор1

else

 оператор2

Если условие верно, то выполняется оператор1; если оно ложно и если альтернативная часть присутствует, то выполняется оператор2. Альтернативная часть не обязательна.

Цикл for аналогичен таковому в языке Си, но отличается от цикла в языке shell:

for (выражение1; условие; выражение2)

 оператор

Цикл for идентичен приведенному ниже оператору, который также допустим в программе awk:

Выражение1 while (условие) {

 оператор

 выражение2

}

Например, конструкция

for (i=2; i <= NF; i++)

является циклом с i, принимающим значения 2, 3 и т.д., включая число полей NF.

Оператор break вызывает немедленный выход из цикла while или for; оператор continue инициирует переход к следующему шагу цикла (к условию в операторе while или к выражению2 в операторе for). Оператор next вызывает чтение следующей входной строки и сопоставление ее с шаблонами с начала программы awk, а оператор exit — немедленный переход на действия, определенные в шаблоне END.

Массивы

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

$ cat backwards

# backwards: print input in backward line order

awk ' { line[NR] = $0 }

END   { for (i = NR; i > 0; i--) print line[i] } ' $*

$

Заметьте, что подобно переменным, массивы не нужно описывать; размер массива ограничен только объемом памяти, доступным на вашей машине. Конечно, если очень большой файл заносится в массив, в конце концов, это может привести к исчерпанию ресурсов памяти. Для печати конца большого файла в обратном порядке следует обратиться за помощью к команде tail:

$ tail -5 /usr/dict/web2 | backwards

zymurgy

zymotically

zymotic

zymosthenic

zymosis

$

Команда tail использует возможности файловой системы — операцию "поиск" (seeking), позволяющую перейти к концу файла без чтения всей предшествующей информации. Подробнее эта операция будет рассмотрена при обсуждении функции lseek в гл. 7. (В нашей команде tail есть флаг -r, который определяет печать строк в обратном порядке, заменяя команду backwards).

При обычной обработке входная строка разбивается на поля. Эту операцию можно выполнить с помощью встроенной функции split над любой строкой:

n = split(s, arr, sep)

Строка s разбивается на поля, записываемые в элементы массива arr от 1 до n. Используется символ разделения полей sep, если он задан; в противном случае применяется текущее значение переменной FS. Например, обращение split($0, а, ":") разбивает входную строку на столбцы, что подходит для обработки файла /etc/passwd, поэтому обращение split("9/29/83", date, "/") разбивает дату по символам дробной черты.

$ sed 1q /etc/passwd | awk '{split($0, a, ":"); print a[1]}'

root

$ echo 9/29/83 | awk '{split($0, date, "/"); print date[3]}'

83

$

В табл. 4.5 перечислены встроенные функции awk.

cos(expr) Косинус expr
exp(expr) Возведение в степень expr
getline() Чтение следующей входной строки; возвращает 0 в случае конца файла, в противном случае 1
index(s1, s2) Положение строки s2 в s1 ; возвращает 0, если строка не входит
int(expr) Целая часть expr ; округляет по минимуму
length(s) Длина строки s
log(expr) Натуральный логарифм expr
sin(expr) Синус expr
split(s, a, c) Разбиение s на а[1] ... a[n] по символу c ; возвращает n
sprintf(fmt, ...) Форматирование в соответствии со спецификацией fmt
substr(s,m,n) Подстрока в n символов строки s , начинающаяся с индекса m

Таблица 4.5: Встроенные функции awk

Ассоциативные массивы

Стандартной задачей обработки данных является получение суммарных значений для множества пар имя значение. Иными словами, по входному потоку типа

Susie 400

John  100

Mary  200

Mary  300

John  100

Susie 100

Mary  100

мы хотим получить суммарные значения для каждого имени:

John  200

Mary  600

Susie 500

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

    {sum[$1] += $2}

END {for (name in sum) print name sum [name]}

задает всю программу подсчета n печати сумм для пар имя значение независимо от порядка следования этих пар. Каждое имя ($1) служит индексом в массиве sum; в конце применена специальная форма цикла for для перебора всех элементов sum и их печати. Синтаксис этого варианта цикла for таков:

for (перем in массив)

 оператор

Хотя он может показаться вам искусственным, как цикл for языка shell, они никак не связаны. Цикл охватывает индексы массива, а не его элементы, устанавливая значение "перем" равным каждому индексу поочередно. Однако порядок появления индексов непредсказуем, поэтому может возникнуть необходимость в их сортировке. В приведенном примере выходной поток можно по конвейеру передать команде sort, чтобы имена шли в порядке убывания значений:

$ awk '...' | sort +1nr

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

Использование ассоциативных массивов эффективно для вычислительных задач, таких, как подсчет частоты появления слов во входном потоке:

$ cat wordfreq

awk ' { for (i = 1; i <= NF; i++) num[$i]++ }

END   {for (word in num) print word, num[word] }

' $*

$ wordfreq ch4.* | sort +1 -nr | sed 20q | 4

the 372 .CW 345 of  220 is   185

to  175 a   167 in  109 and  100

.PI  94 .P2  94 .PP  90 $     87

awk  87 sed  83 that 76 for   75

The  63 are  61 line 55 print 52

$

В первом цикле for выбирается каждое слово из входной строки и заполняется массив num, индексируемый словами. (Не путайте $i, обозначающее в awk i-е поле входной строки, с переменными языка shell.) После того как файл будет прочитан, во втором цикле for печатаются в произвольном порядке слова и частота их появления.

Упражнение 4.9

В результат действия команды wordfreq попали команды форматирования типа .CW , которые применяются для печати слов определенным шрифтом. Как избавиться от таких ненастоящих слов? Как бы вы использовали команду tr , чтобы программа wordfreq работала правильно, независимо от того, прописные или строчные буквы задействованы во входном потоке? Сравните реализацию и скорость выполнения программы wordfreq , конвейера из разд. 4.2 и предлагаемого ниже решения.

sed 's/[→][→]*/\

/q' $* | sort | uniq -c | sort -nr

Строки

Хотя обе команды, и sed и awk, предназначены для решения небольших задач типа выбора определенного поля, только awk используется в той степени, в какой предполагает настоящее программирование. Примером может служить программа, которая разбивает длинные строки, чтобы они занимали не более 80 позиций. Каждая строка, превышающая 80 символов, завершается после 80-го символа; в качестве предупреждения добавляется \ и обрабатывается остаток строки. Хвост разбиваемой строки сдвигается к ее правому концу, а не к левому, что более удобно для программ печати, и именно поэтому мы обратимся к программе fold. Рассмотрим, в частности, строки из 20, а не из 80 позиций:

$ cat тест

Короткая строка

Строка немного длиннее

Эта строка еще длиннее, чем предыдущая строка

$ fold тест

Короткая строка

Строка немного длиннее

Эта строка еще длиннее,

 чем предыдущая строка

$

Вам может показаться странным, что в седьмой версии системы нет программы для добавления или удаления символов табуляции, хотя команда pr в System V выполняет и то и другое. Наша реализация программы fold использует редактор sed, чтобы перевести символы табуляции в пробелы и чтобы счетчик числа символов в awk принял правильное значение. Это хороший способ при табуляции в начале строки (что типично для языковых программ), но номер позиции сбивается, если символ табуляции оказывается в середине строки:

# fold: fold long lines

sed 's/\(->/ /g' $* |      # convert tabs to spaces

awk '

 BEGIN {

  N = 80                   # folds at column 80

  for (i = 1; i <= N; i++) # make a string of blanks

   blanks = blanks " "

 }

 {

  if ((n = length($0)) <= N)

   print

  else {

   for (i = 1; n > N; n -= N) {

    printf "%s\\\n", substr($0,i,N)

    i += N;

   }

   printf "%s%s\n" , substr(blanks, 1, N-n), substr($0, I)

  }

 } '

На языке awk нет явной операции конкатенации строк; строки соединяются, если они следуют подряд. Вначале blanks является пустой строкой. Цикл в части BEGIN создает длинную строку пробелов конкатенацией: каждый шаг цикла прибавляет еще один пробел к концу строки blanks. Во втором цикле входная строка разбивается на части, пока оставшаяся часть не станет достаточно короткой. Как и в языке Си, операцию присваивания можно использовать в качестве выражения, поэтому в конструкции

if ((n=length($0)) <= N)...

длина входной строки присваивается n до проверки значения. Обратите внимание на скобки.

Упражнение 4.10

Измените программу fold так, чтобы разрыв строки происходил на пробеле или символе табуляции, а не посреди слова. Сделайте эту программу пригодной и для длинных слов.

Взаимодействие с интерпретатором

Допустим, что вы намереваетесь написать программу field n. Эта программа будет печатать n-е поле каждой входной строки так, чтобы можно было, например, задать:

$ who | field 1

для печати только имен, под которыми пользователи входят в систему. Язык awk явно предоставляет возможность выбора полей. Наша основная задача — передать номер n программе awk. Ниже приведено одно из возможных решений:

$ awk '{print $'$1'}'

Здесь $1 открыто (не внутри каких либо кавычек), и поэтому становится номером поля, доступным в программе awk. При ином решении используются кавычки:

awk "{print \$$1}"

Аргумент обрабатывается интерпретатором, поэтому \$ становится $, а $1 заменяется на значение n. Мы предпочитаем решение с апострофами (одиночными кавычками), поскольку при использовании кавычек в типичной программе awk появится слишком много символов \.

Другим примером может служить программа addup n, суммирующая значения n-го поля:

awk '{s += $'$1'}

END {print s}'

В третьем примере вычисляются отдельные суммы значений каждого n-го поля и полная сумма:

awk '

BEGIN { n = '$1' }

{ for (i=1; i <= n; i++)

   sum[i] += $1

}

END { for(i = 1; i <= n; i++)

      {

       printf "%6g ", sum[i]

       total += sum[i]

      }

      printf "; total = %6g ", total

    }'

Нам удобнее было использовать часть BEGIN для засылки значения в переменную n, чем засорять конец программы кавычками.

Основная трудность во всех приведенных выше примерах состоит не в том, чтобы следить за кавычками (хотя и это хлопотно), а в том, что программы, составленные показанным способом, могут читать только свой стандартный входной поток. Нет никакой возможности передать им сразу и параметр n, и произвольно длинный список имен файлов. Для этого требуется определенная техника программирования на языке shell; которую мы рассмотрим в следующей главе.

Служебная программа-календарь на языке awk

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

Задача состоит в создании системы, посылающей вам каждое утро почту с напоминанием об ожидаемых событиях. (Возможно, такая календарная система уже есть; см. руководство по calendar(1).) В этом разделе применяется иной подход. Вам будут перечислены события, происходящие сегодня и, кроме того, предстоящие сегодняшние и завтрашние события. Правильный учет праздников и выходных оставлен вам в качестве упражнения.

Прежде всего нужно предусмотреть место, где будет храниться календарь. Имеет смысл разместить его в файле с именем calendar в каталоге /usr/you:

$ cat calendar

Sep 30 день рождения мамы

Oct  1 обед с Джо, полдень

Oct  1 встреча в 16:00

$

Далее, необходимо уметь просматривать календарь, отыскивая определенную дату. Существует масса вариантов; мы остановимся на языке awk, поскольку с его помощью легче выполнять арифметические операции по переходу от одной даты к другой, однако для этой цели подходят и другие программы, например sed и egrep. Конечно, строки, выбранные из файла calendar, посылаются командой mail.

Наконец, вам придется научиться автоматически и безотказно просматривать календарь каждый день, скажем, рано утром. Это можно сделать с помощью команды at, о которой упоминалось в гл. 1.

Если ограничить календарь таким форматом, в котором каждая строка начинается с названия месяца и числа (как это делает команда date), то составить первый вариант программы календаря нетрудно:

$ date

Thu Sep 29 15:23:12 EDT 1983

$ cat bin/calendar

# calendar: version 1 - today only

awk <$HOME/calendar '

 BEGIN { split ("'"`date`"'", date) }

 $1 == date[2] && $2 == date[3]

' | mail $NAME

$

Функция в части BEGIN разбивает дату, выдаваемую командой date, и заносит ее в массив; второй и третий элементы массива — месяц и число. Мы предполагаем, что в переменной интерпретатора NAME находится имя, под которым вы вошли в систему. Вы заметили, какая нужна сложная последовательность кавычек, чтобы "поймать" результат действия команды date в середине строки программы awk. Более простым решением является передача даты в первой строке входного потока:

$ cat /bin/calendar

# calendar: version 2 - today only, no quotes

(date; cat $HOME/calendar) |

awk '

 NR == 1 { mon = $2; day = $3 }   # set the date

 NR > 1 && $1 == mon && $2 == day # print calendar lines

' | mail $NAME

$

На следующем шаге требуется так изменить программу, чтобы искать сообщение с завтрашней датой так же, как и с сегодняшней. Наибольшие усилия затрачиваются на прибавление единицы к сегодняшнему числу. Но в конце месяца нужно перейти к следующему месяцу, а число приравнять единице. Конечно, число дней в разных месяцах различно. Именно здесь на помощь приходит ассоциативный массив. Два массива days и nextmon, индексами которых служат названия месяцев, содержат число дней месяца и название следующего месяца. Например, days["Jan"] равно 31, a nextmon["Jan"] есть Feb. Вместо того чтобы написать множество операторов типа

days["Jan"] = 31; nextmon["Jan"] = "Feb"

days["Feb"] = 28; nextmon["Feb"] = "Mar"

...

мы воспользуемся функцией split для преобразования удобно записываемой структуры данных в то, что требуется:

$ cat calendar

# calendar: version 3 -- today and tomorrow

awk <$HOME/calendar '

BEGIN {

 x = "Jan 31 Feb 28 Mar 31 Apr 30 May 31 Jun 30 " \

     "Jul 31 Aug 31 Sep 30 Oct 31 Nov 30 Dec 31 Jan 31"

 split(x, data)

 for (i = 1; i < 24; i += 2) {

  days[data[i]] = data[i+1]

  nextmon[data[i]] = data[i+2]

 }

 split("'"`date`", date)

 mon1 = date[2]; day1 = date[3]

 mon2 = mon1; day2 = day1 + 1

 if (day1 >= days[mon1]) {

  day2 = 1

  mon2 = nextmon[mon1]

 }

}

$1 == mon1 && $2 == day1 || $1 == mon2 && $2 == day2

' | mail $NAME

$

Обратите внимание на то, что Jan появляется дважды в структуре данных; такое "сторожевое" значение упрощает обработку для декабря.

На последнем шаге нужно обеспечить запуск программы календаря на каждый день. Можно делать это и самому, не забывая задавать команду (каждый день!)

$ at 5 am

calendar

ctl-d

$

Однако такое решение нельзя считать автоматическим или надежным. Хитрость заключается в том, чтобы не только запустить программу calendar, но и обеспечить следующий ее запуск.

$ cat early.morning

calendar

echo early morning | at 5am

$

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

Упражнение 4.11

Измените программу calendar так, чтобы она учитывала выходные дни: для пятницы "завтра" должно означать субботу, воскресенье или понедельник. Далее измените ее так, чтобы можно было учесть високосные годы. Следует ли учитывать праздники? Как бы вы это сделали?

Упражнение 4.12

Должна ли программа календарь учитывать даты, находящиеся в середине строки, а не только в ее начале? Как быть с датой, заданной в другом формате, например 10/1/83?

Упражнение 4.13

Почему в программе calendar используется $NAME , а не обращение к getname ?

Упражнение 4.14

Напишите вашу версию команды rm, которая не удаляет файлы, а пересылает их во временный каталог, используя команду at для очистки каталога в то время, пока вы не работаете.

Дополнительная информация

Язык awk довольно громоздкий, и в рамках одной главы трудно показать все его возможности. Поэтому мы перечислим здесь еще ряд моментов, на которые необходимо обратить внимание в справочном руководстве:

• Переключение выходного потока оператора print в файлы и программные каналы: за каждым оператором print или printf может следовать символ > и имя файла (в виде строки в кавычках или переменной); выходной поток будет направлен в этот файл. Как и для интерпретатора, >> означает добавление, а не запись. Для вывода в программный канал используется символ |, а не >.

• Запись в несколько строк: если разделитель записей RS установлен равным концу строки, то входные записи разделяются пустой строкой. В таком случае несколько входных строк могут рассматриваться как одна запись.

• "Шаблон, шаблон" в качестве селектора: как и в случае команд sed и ed, с помощью пары шаблонов можно указать диапазон строк. Так выбираются строки, начиная с соответствующей первому шаблону, до строки, соответствующей второму шаблону. Приведем простой пример:

NR == 10, NR == 20

Здесь задаются строки от 10-й по 20-ю включительно.

 

4.5 Хорошие файлы и хорошие фильтры

Несмотря на то что в качестве примеров использования языка awk приводились независимые программы, в большинстве случаев его применяют для написания простых программ в одну или две строки, являющихся фильтрами в больших конвейерах. Это справедливо для большинства фильтров: редко поставленная задача может быть решена с помощью одного фильтра, чаще она разбивается на подзадачи, где фигурируют несколько фильтров, объединенных в конвейер. Такую реализацию программных компонентов называют основным принципом организации программного мира UNIX. Фильтры буквально "пронизывают" всю систему, и очень важно понимать причины этого.

Программы UNIX выдают выходной поток в таком формате, что его можно использовать в качестве входного потока для других программ. Файлы, пропускаемые через фильтр, состоят из строк текста, свободных от декоративных заголовков, завершителей или пустых строк. Каждая строка представляет интерес — это имя файла, слово, описатель выполняемого процесса, поэтому программы типа wc или grep могут рассчитывать определенные характеристики объектов или искать их по именам. Если о каждом объекте имеется большая информация, файл все равно состоит из строк, разбиваемых на поля пробелами или символами табуляции, как в выводе команды ls -l. Располагая данными, разбитыми на такие поля, программы типа awk могут легко выбрать, обработать или переупорядочить информацию.

Фильтры построены по общей схеме. Каждый из них пишет в стандартный выходной поток результат обработки файлов-аргументов или стандартного выходного потока, если аргументов нет. Аргументы задают входной поток и никогда не задают выходной , поэтому выходной поток команда всегда может передать в конвейер. Необязательные аргументы (или аргументы, не являющиеся файлами, такие, как шаблон в команде grep) задаются перед именем файлов. Наконец, сообщения об ошибках пишутся в стандартный поток диагностики, поэтому они не могут исчезнуть в конвейере.

Эти соглашения не оказывают большого влияния на программы пользователя, но единообразное применение их ко всем программам обеспечивает простоту взаимодействия, что подтверждается многочисленными примерами на протяжении всей книги и наиболее наглядно продемонстрировано программой подсчета слов в конце разд. 4.2. Если каким-либо программам потребуется входной или выходной файл с конкретным именем, определенное обращение для спецификации параметров или создание заголовков и завершений, то схема конвейера работать не будет. И конечно, если бы система UNIX не предоставляла программные каналы, кому-то пришлось создать подобное стандартное средство. Однако программные каналы есть, и конвейеры работают. Их даже можно запрограммировать, но для этого вы должны знать возможности системы.

Упражнение 4.15

Команда ps выдает поясняющий заголовок, а команда ls -l сообщает общее число блоков файла. Прокомментируйте действие команд.

Историческая и библиографическая справка

Хороший обзор алгоритмов сопоставления шаблонов дается в статье Э. Ахо, создателя команды egrep , "Pattern matching in strings" (Proceedings of the Symposium on Formal Language Theory, Santa Barbara, 1979). Редактор sed разработан и реализован на базе редактора ed Л. Мак-Махоном. Язык awk был разработан и реализован Э. Ахо, П. Вайнбергером и Б. Керниганом, но это решение не очень элегантно. К тому же выбор названия языка по первым буквам имен создателей представляется не вполне удачным. Проект обсуждался в статье авторов "AWK — а pattern scanning and processing language" (Software-Practice and Experience, July, 1978). Язык awk имеет несколько источников, но, безусловно, некоторые идеи заимствованы из языка Снобол4, редактора sed , языка проверки условий, разработанного М. Рочкиндом, языковых средств yacc и lex и, конечно, языка Си. В действительности сходство между awk и Си порождает ряд проблем. Язык подобен Си, но они не совпадают: одни конструкции в awk отсутствуют, другие отличаются от соответствующих конструкций Си неочевидным образом.

В статье Д. Комера "The flat file system FFG: a database system consisting of primitives". (Software — Practice and Experience, Nov., 1982) обсуждается использование интерпретатора и awk для создания системной базы данных.