sed & awk

Дейл Догерти & Арнольд Роббинс

1997

перевод В.Айсин

Мириам, за твои любовь и терпение

--Арнольд Роббинс

Предисловие

Эта книга посвящена набору утилит UNIX со странными названиями, sed и awk. У этих утилит много общего, включая использование регулярных выражений для сопоставления с образцом. Так как сопоставление паттернов является важной частью их использования, эта книга объясняет синтаксис регулярных выражений UNIX очень подробно. Поскольку обучение от grep до sed и awk происходит естественным образом, мы рассмотрим все три программы, хотя основное внимание уделим sed и awk.

Sed и awk - это инструменты, используемые пользователями, программистами и системными администраторами - всеми, кто работает с текстовыми файлами. Sed, названный так потому, что это потоковый редактор, и он идеально подходит для применения серии правок к нескольким файлам. Awk, названный в честь разработчиков Ахо, Вайнбергера и Кернигана, является языком программирования, который позволяет легко манипулировать структурированными данными и создавать форматированные отчеты. Эта книга подчеркивает определение awk в POSIX. Кроме того, в книге кратко описывается оригинальная версия awk, прежде чем обсуждать три свободно доступные версии awk и две коммерческие, все они реализуют POSIX awk.

Основное внимание в этой книге уделяется написанию сценариев для sed и awk, которые быстро решают ряд проблем пользователя. Многие из этих сценариев можно назвать «быстрыми исправлениями». Кроме того, мы рассмотрим сценарии, которые решают более крупные проблемы, требующие более тщательного проектирования и разработки.

Сфера применения данного справочника

Глава 1, Инструменты для редактирования, представляет собой обзор функций и возможностей sed и awk.

Глава 2, Понимание основных операций, демонстрирует основные операции sed и awk, показывая прогресс в функциональности от sed до awk. Оба имеют схожий синтаксис командной строки, принимая инструкции пользователя в виде скрипта.

Глава 3, Понимание синтаксиса регулярных выражений, подробно описывается синтаксис регулярных выражений UNIX. Новых пользователей часто пугают эти странные выражения, используемые для сопоставления шаблонов. Важно овладеть синтаксисом регулярных выражений, чтобы получить максимальную отдачу от sed и awk. Примеры сопоставления шаблонов в этой главе по большей части основаны на grep и egrep.

Глава 4, Написание сценариев sed, начинает раздел из трех глав, посвященный sed. В этой главе рассматриваются основные элементы написания сценария sed с использованием всего нескольких команд sed. Также представлен сценарий оболочки, который упрощает вызов сценариев sed.

Глава 5, Основные команды sed и Глава 6. Расширенные команды sed, разделяют набор команд sed на базовые и расширенные команды. Основные команды - это команды, которые параллельно редактируют вручную, а расширенные команды предоставляют простые возможности программирования. Среди расширенных команд есть те, которые управляют буфером хранения (hold space) - выделенным временным буфером.

Глава 7, Написание скриптов для awk, начинает раздел, состоящий из пяти глав, посвященных awk. В этой главе представлены основные особенности этого языка сценариев. Объясняется ряд сценариев, в том числе тот, который изменяет вывод команды ls.

Глава 8, Условные выражения, циклы и массивы, описывает, как использовать общие конструкции программирования, такие, как условные выражения, циклы и массивы.

Глава 9, Функции, описывает, как использовать встроенные функции awk, а также как писать пользовательские функции.

Глава 10, Нижний ящик, охватывает множество различных тем, связанных с awk. Описывает, как выполнить команды UNIX из сценария awk и способы прямого вывода в файлы и каналы. Также предлагает некоторые (скудные) советы по отладке сценариев awk.

Глава 11, Семейство awk, описывает исходную версию awk V7, текущую версию Bell Labs awk, GNU awk (gawk) от Фонда свободного программного обеспечения и mawk Майкла Бреннана. Последние три распространяются с открытым исходным кодом. В этой главе также описаны две коммерческие реализации, MKS awk и Thomson Automation awk (tawk), а также VSAwk, который привносит awk-подобные возможности в среду Visual Basic.

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

Глава 13, Сборник сценариев, представлен ряд сценариев, добавленных пользователями, которые показывают разные стили и приемы написания скриптов для sed и awk.

Приложение A, Краткий справочник по sed, представляет собой краткий справочник, описывающий команды sed и параметры командной строки.

Приложение B, Краткий справочник по awk, представляет собой краткое руководство по параметрам командной строки awk и полное описание его скриптового языка.

Приложение C, Дополнение к Главе 12, представлены полные листинги скриптов spellcheck.awk и masterindex, описанные в главе 12.

Доступность sed и awk

Sed и awk были частью UNIX версии 7 (также известной как «V7» и «Seventh Edition») и с тех пор являются частью стандартного дистрибутива. Sed остается неизменным с тех пор, как был представлен.

Версия sed проекта Free Software Foundation GNU находится в свободном доступе, хотя технически и не является общественным достоянием. Исходный код GNU sed доступен через анонимный FTP* ftp.gnu.org/pub/gnu/sed/. Это tar-файл, сжатый с помощью программы gzip, исходный код которой доступен в том же каталоге. Есть много сайтов по всему миру, которые «зеркалируют» файлы с основного сайта дистрибутива GNU; если вы знаете один из них рядом с вами, вы должны получить файлы оттуда. Обязательно используйте режим «binary» или «image» для передачи файла(ов).

В 1985 году авторы awk расширили язык, добавив множество полезных функций. К сожалению, эта новая версия оставалась внутри AT&T в течение нескольких лет. Она стала частью UNIX System V начиная с версии 3.1. Ее можно найти под именем nawk, для нового awk; более старая версия все еще существует под своим первоначальным именем. Это все еще имеет место в системах System V Release 4.

В коммерческих системах UNIX, таких как Hewlett-Packard, Sun, IBM, Digital и других, ситуация именования более сложная. Все эти системы имеют некоторую версию как старого, так и нового awk, но то, как каждый поставщик называет каждую программу, варьируется. Некоторые называют oawk и awk, некоторые awk и nawk. Лучший совет, который мы можем дать, - это проверить вашу локальную документацию**. В этой книге мы используем термин awk для описания POSIX awk. Конкретные реализации будут называться по имени, например «gawk» или «The Bell Labs awk». В Главе 11 обсуждаются три свободно доступных awk (включая то, где их взять), а также несколько коммерческих.

NOTE: Начиная с первого издания этой книги, язык awk был стандартизирован как часть командного языка и служебных программ POSIX (P1003.2). Все современные реализации awk направлены на то, чтобы быть полностью совместимыми со стандартом POSIX.

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

* Если у вас нет доступа в Интернет и вы хотите получить копию GNU sed, свяжитесь с FreeSoftware Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, США. Номер телефона: 1-617-5425942, номер факса: 1-617-542-2652.

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

Версии для DOS

Gawk, mawk и GNU sed были перенесены в DOS. На основном сайте дистрибутива GNU есть файлы с указателями на версии этих программ для DOS. Кроме того, gawk был перенесен на OS/2, VMS и микрокомпьютеры Atari и Amiga с переносом на другие системы (Macintosh, Windows).

egrep, sed и awk доступны для компьютеров под управлением MS-DOS как часть MKS Toolkit (MorticeKern Systems, Inc., Онтарио, Канада). Их реализация awk поддерживает функции POSIX awk.

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

Thompson Automation Software* имеет компилятор awk для UNIX, DOS и Microsoft Windows. Эта версия интересна тем, что она имеет ряд расширений языка и включает отладчик awk, написанный на awk!

Мы иногда использовали ПК, потому что Ventura Publisher - это потрясающий пакет форматирования. Одна из причин, по которой он нам нравится, заключается в том, что мы можем продолжать использовать vi для создания и редактирования текстовых файлов и использовать sed для написания сценариев редактирования. Мы использовали sed для написания программ преобразования, которые переводят макросы troff в теги таблиц стилей Ventura. Мы также использовали его для вставки тегов в пакетном режиме. Это может избавить вас от необходимости вручную помечать повторяющиеся элементы в файле.

Sed и awk также полезны для написания программ преобразования, которые работают с различными форматами файлов.

* 5616 SW Jefferson, Portland, OR 97221 USA, 1-800-944-0139 в США, 1-503-224-1639 в другом месте.

Другие источники информации о sed и awk

Долгое время основным источником информации об этих утилитах были две статьи, содержащиеся в томе 2 UNIX Programmer's Guide. Статья awk - A Pattern Scanning and Processing Language (1 сентября 1978 г.) была написана тремя авторами языка. На 10 страницах предлагается краткое руководство и обсуждение несколько вопросов проектирования и реализации. Статья SED - A Non-Interactive Text Editor (15 августа 1978 г.) была написана Ли Э. МакМахоном. Это справочник, дающий полное описание каждой функции и включает несколько полезных примеров (с использованием Coleridge Xanadu в качестве входных данных).

В отраслевых книгах наиболее значимая трактовка sed и awk содержится в The UNIX Programming Environment Брайана В. Кернигана и Роба Пайка (Прентис-Холл, 1984). Глава «Фильтры» не только объясняет, как работают эти программы, но и показывает, как они могут работать вместе для создания полезных приложений.

Авторы awk совместно работали над книгой, описывающей расширенную версию: The AWK Programming Language (Аддисон-Уэсли, 1988). Она содержит множество полных примеров и демонстрирует широкий спектр областей, в которых можно применить awk. Она следует стилю UNIX Programming Environment, что иногда делает ее слишком плотной для некоторых читателей, которые являются новыми пользователями. Исходный код примеров программ в книге можно найти в по адресу netlib.belllabs.com/netlib/research/awkbookcode.

IEEE Standard for Information and Technology Portable Operating System Interface (POSIX) Part 2: Shell and Utilities (Standard 1003.2-1992)* описывает как sed, так и awk**. Это «официальное» слово о функциях, доступных для переносимых программ оболочки, использующих sed и awk. Поскольку awk сам по себе является языком программирования, POSIX также является официальным словом для портативных программ awk.

В 1996 году Фонд свободного программного обеспечения опубликовал The GNU Awk User's Guide Арнольда Роббинса. Это документация для gawk, написанная в более учебном стиле, чем книга Aho, Kernighan и Weinberger. Она состоит из двух полных глав с примерами и охватывает POSIX awk. Эта книга также издается SSC под заголовком Effective AWK Programming, а исходный код Texinfo для книги поставляется с дистрибутивом gawk.

Одним из текущих недостатков GNU sed является отсутствие собственной документации, даже страницы руководства.

Большинство общих введений в UNIX представляют sed и awk в длинном параде утилит. Из подобных книг книга Генри МакГилтона и Рэйчел Морган Introducing the UNIX System предлагает наилучшее рассмотрение основных навыков редактирования, включая использование всех текстовых редакторов UNIX.

UNIX Text Processing (Hayden Books, 1987), написанная автором этого справочника и Тимом О'Рейли, полностью охватывает sed и awk, хотя мы не включили новую версию awk. Читатели этой книги обнаружат, что некоторые части дублируются в этой книге, но в целом здесь был использован другой подход. В то время как в учебнике мы рассматриваем sed и awk отдельно, ожидая, что только продвинутые пользователи будут заниматься awk, здесь мы пытаемся представить обе программы по отношению друг к другу. Это разные инструменты, которые можно использовать индивидуально или вместе, чтобы получить интересные возможности для обработки текста.

Наконец, в 1995 году появилась группа новостей Usenet comp.lang.awk. Если вы не можете найти то, что вам нужно, чтобы узнать в одной из вышеперечисленных книг, вы можете задать вопрос в группе новостей, с большой вероятностью кто-нибудь сможет вам помочь.

В группе новостей также есть статья «Часто задаваемые вопросы» (FAQ), которая публикуется регулярно. Кроме ответов на вопросы об awk, в FAQ перечислено множество сайтов, где вы можете получить бинарные файлы различных версии awk для разных систем. Вы можете получить FAQ через FTP в файле rtfm.mit.edu/pub/usenetby-hierarchy/.

* Уф! Скажи это в три раза быстрее!

** Стандарт не доступен в интернете. Его можно заказать в IEEE по телефону 1-800-678-IEEE (4333) в США и Канаде, 1-908-981-0060 в других странах. Или, см. http://www.ieee.org/ из веб-браузера. Стоимость составляет 228 долларов США, включая стандарт 1003.2 d-1994-поправка 1 для пакетных сред. Члены и/или сообщества IEEE получают скидку.

Примеры программ

Примеры программ в этой книге изначально были написаны и протестированы на Mac и выполнены на A/UX 2.0 (UNIX System V Release 2) и SparcStation под управлением SunOS 4.0. Программы, требующие POSIX awk, были повторно протестированы с использованием gawk 3.0.0, а также версии Bell Labs awk от августа 1994 года с FTP-сайта Bell Labs (подробности FTP см. в главе 11). Программы Sed были повторно протестированы с SunOS 4.1.3.sed и GNU sed 2.05.

Получение примеров исходного кода

NOTE: Перечисленные ниже способы получения исходного кода книги слегка устарели. Проще и удобней получить исходный код на странице книги на сайте издательства: https://www.oreilly.com/library/view/sed-awk/1565922255/, или здесь: progs.tar.gz. [Прим. пер.]

Исходный код программ, представленных в этой книге, вы можете получить у компании O'Reilly & Associates через их интернет-сервер. Примеры программ в этой книге доступны в электронном виде несколькими способами: по FTP, Ftpmail, BITFTP и UUCP. Самые дешевые, быстрые и простые способы перечислены в первую очередь. Если Вы читаете сверху вниз, то первое, что работает для вас, вероятно, лучше всего. Используйте FTP, если вы находитесь непосредственно в Интернете. Используйте Ftpmail, если вы не находитесь в интернете, но можете отправлять и получать электронную почту на интернет-сайты (это включает пользователей CompuServe). Используйте BITFTP, если вы можете отправлять электронную почту через BITNET. Используйте UUCP, если ничего из вышеперечисленного не работает.

FTP

Для использования FTP вам понадобится машина с прямым доступом в Интернет. Показан образец сеанса, то что следует набрать, показано жирным шрифтом.

$ ftp ftp.oreilly.com
Connected to ftp.oreilly.com.
220 FTP server (Version 6.21 Tue Mar 10 22:09:55 EST 1992) ready.
Name (ftp.oreilly.com:yourname): anonymous
331 Guest login ok, send domain style e-mail address as password.
Password: yourname@domain.name
(Use your user name and host here)
230 Guest login ok, access restrictions apply.
ftp> cd /published/oreilly/nutshell/sedawk_2
250 CWD command successful.
ftp> binary (Очень важно! Вы должны указать двоичную передачу для сжатых файлов.)
200 Type set to I.
ftp> get progs.tar.gz
200 PORT command successful.
150 Opening BINARY mode data connection for progs.tar.gz.
226 Transfer complete.
ftp> quit
221 Goodbye.

Файл представляет собой сжатый gzip архив tar; извлеките файлы из архива, набрав:

$ gzcat progs.tar.gz | tar xvf -

Для систем System V требуется следующая команда tar:

$ gzcat progs.tar.gz | tar xof -

Если gzcat недоступен в вашей системе, используйте отдельные команды gunzip и tar.

$ gunzip progs.tar.gz
$ tar xvf progs.tar

Ftpmail

Ftpmail - это почтовый сервер, доступный любому, кто может отправлять электронную почту и получать ее с интернет-сайтов. Это относится к любой компании или поставщику услуг, которые разрешают подключение электронной почты к интернету. Вот как вы это делаете. Вы отправляете почту по адресу ftpmail@online.oreilly.com в теле сообщения укажите команды FTP, которые вы хотите запустить. Сервер запустит для вас анонимный FTP и отправит файлы вам. Чтобы получить полный файл справки, отправьте сообщение без темы и с одним словом «help» в теле. Ниже приведен образец почтового сеанса, который должен предоставить вам примеры. Эта команда отправляет вам список файлов в выбранном каталоге и запрошенные примеры файлов. Этот список полезен, если есть более поздняя версия примеров, которые вас интересуют.

$ mail ftpmail@online.oreilly.com
Subject:
reply-to yourname@domain.name (Куда вы хотите отправить файлы по почте.)
open
cd /published/oreilly/nutshell/sedawk_2
dir
mode binary
uuencode
get progs.tar.gz
quit
.

Подпись в конце сообщения допустима, если она стоит после «quit».

BITFTP

BITFTP - это почтовый сервер для пользователей BITNET. Вы отправляете ему сообщения электронной почты с запросом файлов, и он отправляет вам файлы по электронной почте. BITFTP в настоящее время обслуживает только пользователей, которые отправляют ему почту c узлов, которые находятся непосредственно в BITNET, EARN или NetNorth. Чтобы использовать BITFTP, отправьте письмо с вашими командами ftp для BITFTP@PUCC. Чтобы получить полный файл справки, отправьте HELP в теле сообщения. Ниже приведено тело сообщения, отправляемого в BITFTP:

FTP ftp.oreilly.com NETDATA
USER anonymous
PASS yourname@yourhost.edu (Укажите здесь свой адрес электронной почты в Интернете (не свой адрес BITNET))
CD /published/oreilly/nutshell/sedawk_2
DIR
BINARY
GET progs.tar.gz
QUIT

Как только вы получите нужный файл, следуйте инструкциям в разделе FTP, чтобы извлечь файлы из архива. Поскольку вы, вероятно, не находитесь в системе UNIX, вам может потребоваться получить версии uudecode, gunzip, atob и tar для вашей системы. Доступны версии VMS, DOS и Mac.

UUCP

UUCP является стандартным практически для всех UNIX-систем и доступен для пользователей IBM-совместимых ПК и Apple Macintosh. Примеры доступны UUCP через модем от UUNET; за время соединения UUNET взимается плата. Если у вас или вашей компании есть учетная запись в UUNET, у вас есть система где-то с прямым соединением UUCP с UUNET, найдите эту систему и введите:

uucp uunet\!~/published/oreilly/nutshell/sedawk_2/progs.tar.gz yourhost\!~/yourname/

Обратные косые черты могут быть опущены, если вы используете оболочку в стиле Борна (sh, ksh, bash, zsh, pdksh) вместо csh. Файл должен появиться некоторое время спустя (до суток и более) в каталоге /usr/spool/uucppublic/yourname. Если у вас нет учетной записи, но вы хотели бы получить ее по электронной почте, свяжитесь с UUNET по телефону 703-206-5400. Это хорошая идея - получить файл /published/oreilly/ls-lR.Z как короткий тестовый файл, содержащий имена и размеры всех доступных файлов. Как только вы получите желаемый файл, следуйте инструкциям в разделе FTP, чтобы извлечь файлы из архива.

Условные обозначения, используемые в этом справочнике

В этой книге используются следующие условные обозначения:

Bold
используется для операторов и функций, идентификаторов и имен программ.
Italic
используется для имен файлов и каталогов, когда они появляются в теле абзаца, а также для типов данных и подчеркивания новых терминов и концепций при их введении.
Constant Width
используется в примерах для отображения содержимого файлов или вывода команд.
Constant Bold
используется в примерах для отображения командных строк и параметров, которые пользователь должен вводить буквально. (Например, rm foo означает вводить «rm foo» в точности так, как оно отображается в тексте или примере.)
""
используются для обозначения фрагмента кода в пояснительном тексте. Системные сообщения и символы также оборачиваются в кавычки.
$
это приглашение оболочки Bourne или оболочки Korn UNIX.
[]
окружают необязательные элементы в описании синтаксиса программы. (Сами скобки набирать не нужно, если не указано иное.)
...
обозначает текст (обычно компьютерный вывод), опущенный для ясности или для экономии места.
обозначает литеральный пробел. Этот символ используется, чтобы сделать пробелы видимыми в примерах, а также в тексте.
обозначает литеральный символ TAB. Этот символ используется для отображения табуляции в примерах, а также в тексте.

Обозначение CTRL-X или ^X указывает на использование управляющих символов. Это значит зажать клавишу «control» при вводе символа «x». Аналогичным образом мы обозначаем другие клавиши (например, RETURN указывает возврат каретки). За всеми примерами командных строк следует RETURN, если не указано иное.

О втором издании

С тех пор как эта книга была впервые опубликована в 1990 году, она стала одной из самых фундаментальных книг O'Reilly & Associates Nutshell Handbooks. После написания произошли три важных события. Первым была публикация стандарта POSIX для sed и, что более важно, для awk. Вторым (возможно, из-за первого) было повсеместное распространение той или иной версии нового awk на всех современных UNIX системах, как коммерческих, так и свободно доступных UNIX-подобных системах, таких как NetBSD, FreeBSD и Linux. Третьим - доступность исходного кода GNU sed и трех версий awk вместо только gawk.

По этим и другим причинам компания O'Reilly & Associates решила, что это руководство необходимо обновить. Цели исправления заключались в том, чтобы сохранить неповторимый вкус книги («если не сломано - не чините»), переориентировать awk-часть книги на awk POSIX, исправить ошибки и обновить книгу.

Я хотел бы поблагодарить Джиджи Эстабрук, Криса Рейли и Ленни Мюллнера из O'Reilly & Associates за их помощь, Марка Воклера, французского переводчика первого издания, за многие полезные комментарии и Джона Дзуберу за его комментарии к первому изданию. Майкл Бреннан, Генри Спенсер и Озан Йигит выступили в качестве технических рецензентов этого издания, и я хотел бы их поблагодарить за их вклад. Озан Йигит, в частности, заслуживает дополнительной благодарности за то, что заставил меня быть очень строгим в моем тестировании. Пэт Томпсон из Thompson Automation Software любезно предоставила ознакомительную копию tawk для просмотра в этой книге. Ричард Монтгомери из Videosoft предоставил мне информацию о VSAwk.

Следующие люди предоставили скрипты к Главе 13: Джон Л. Бентли, Том Кристиансен, Джефф Клэр, Роджер А. Корнелиус, Рахул Деси, Ник Холлоуэй, Норман Джозеф, Уэс Морган, Том Ван Раалте и Мартин Вайцель. Мы с благодарностью отмечаем их вклад.

Спасибо также сотрудникам O'Reilly & Associates. Николь Гипсон Ариго была редактором и менеджером проекта. Дэвид Сьюэлл был редактором, а Клэрмари Фишер О'Лири вычитала книгу. Джейн Эллин и Шерил Авруч выполнили проверки качества. Сет Мэйслин написал указатель. Эрик Рэй, Эллен Сивер и Ленни Мюлльнер работали с инструментами для создания книги. Крис Рейли настроил цифры. Нэнси Прист и Мэри Джейн Уолш разработали внутреннюю компоновку книги, а Эди Фридман разработала переднюю обложку.

Мои родственники, Маршалл и Элейн Хартхольц из Сиэтла, заслуживают особой благодарности за то, что взяли наших детей в поход на неделю, позволив мне добиться значительного прогресса на важном этапе обновления. ☺

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

Арнольд Роббинс

Благодарности из первого издания

Не будет преувеличением сказать, что эту книгу давно ждали. Я опубликовал три статьи по awk в UNIX/World весной и летом 1987 г., ошибочно утверждая, что эти статьи из готовящегося к выпуску Nutshell Handbook, Sed & Awk. Я предложил Тиму О'Рейли адаптировать статьи и создать книгу как проект, над которым я мог бы работать дома вскоре после рождения моего сына Бенджамина. Я думал доделаю за несколько месяцев. Что ж, моему сыну исполнилось три года, когда я заканчивал первый черновик. Кэти Бреннан и представители службы поддержки клиентов терпеливо обрабатывали запросы на книгу с тех пор, как появились статьи UNIX/World. Кэти сказала, что ей даже звонили и заказывали книгу, клянясь, что она доступна, потому что они знали людей, которые ее читали. Я в долгу перед ней и ее сотрудниками, а также перед читателями, которых я ждал.

Я благодарю Тима О'Рейли за создание отличной компании, в которой можно легко отвлечься на ряд интересных проектов. Как редактор он подтолкнул меня к завершению книги, но не позволил, чтобы она была завершена без ее написания целиком. Как обычно, его предложения заставили меня поработать над улучшением книги. Спасибо всем авторам и производственным редакторам o'Reilly & Associates, которые представили интересные проблемы для решения с помощью sed и awk. Спасибо Элли Катлер, которая была редактором по производству книги и также написала индекс. Спасибо Ленни Мюллнеру за то, что он позволил мне цитировать его на протяжении всей книги. Спасибо также Сью Уиллинг и Донне Вунтейлер за их усилия по выпуску книги в печать. Спасибо Крису Рейли, который сделал иллюстрации. Спасибо отдельным авторам скриптов sed и awk в Главе 13. Спасибо также Кевину К. Кастнеру, Тиму Ирвину, Марку Шальцу, Алексу Хьюмезу, Гленну Сайто, Джеффу Хейгелу, Тони Херсону, Джерри Пику, Майку Тиллеру и Ленни Мюллнеру, которые присылали мне письма с указанием опечаток и ошибок.

Наконец, огромное спасибо Нэнси и Кэти, Бену и Гленде.

Дейл Догерти

Комментарии и вопросы

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

O'Reilly & Associates, Inc.
101 Morris Street
Sebastopol, CA 95472
1-800-998-9938 (in the U.S. or Canada)
1-707-829-0515 (international or local)
1-707-829-0104 (FAX)

Вы можете отправлять нам сообщения в электронном виде. Чтобы попасть в список рассылки или запросить каталог, отправьте электронное письмо по адресу:

info@oreilly.com

Чтобы задать технические вопросы или прокомментировать книгу, отправьте электронное письмо по адресу:

bookquestions@oreilly.com

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

http://www.oreilly.com/catalog/sed2/

Для получения дополнительной информации об этой и других книгах посетите вебсайт O'Reilly:

http://www.oreilly.com

Глава 1.
Инструменты для редактирования

Моя жена не позволяет мне купить электропилу. Она боится несчастного случая, если я ею воспользуюсь. Поэтому я полагаюсь на ручную пилу для различных проектов выходного дня, таких как сооружения полок. Однако если бы я зарабатывал себе на жизнь плотничеством, мне пришлось бы пользоваться электропилой. Скорость и эффективность, обеспечиваемые электроинструментами, были бы необходимы для того, чтобы быть продуктивным. [Д.Д.]

Для людей, которые создают и изменяют текстовые файлы, sed и awk - это мощные инструменты для редактирования. Большинство вещей, которые вы можете сделать с помощью этих программ, можно сделать в интерактивном режиме с помощью текстового редактора. Однако использование sed и awk может сэкономить много часов повторяющейся работы для достижения одного и того же результата.

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

1.1 Вы сможете решать интересные проблемы

Основной мотивацией для изучения sed и awk является то, что они полезны для разработки общих решений при редактировании текста*. Для некоторых людей, включая меня, удовлетворение от решения проблемы - это разница между работой и тяжелой работой. Учитывая выбор использования vi или sed для выполнения серии повторных правок в нескольких файлах, я выберу sed просто потому, что это делает проблему более интересной для меня. Я уточняю решение вместо того, чтобы повторять серию нажатий клавиш. Кроме того, как только я выполню свою задачу, я поздравляю себя с тем, что я умный. Я чувствую себя так, словно немного поколдовал и избавил себя от скучного труда.

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

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

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

\*[CHerrorhand]

«CHerrorhand» - это имя, дающее ссылку, а «\*[» и «]» - вызывающие последовательности, которые разграничивают ссылки от другого текста. В центральном файле имена, используемые для перекрестных ссылок в документе, определяются как строки sqtroff. Например, «CHerrorhand» определяется как «Глава 16, Ошибка обращения». (Преимущество использования символической схемы перекрестных ссылок, подобной этой, вместо явных ссылок заключается в том, что если главы добавляются, удаляются или переупорядочиваются, то редактировать нужно только центральный файл, чтобы отразить новую организацию.) Когда программное обеспечение форматирования обрабатывает документ, ссылки правильно разрешаются и расширяются.

Проблема, с которой мы столкнулись, заключалась в том, что нам пришлось использовать одни и те же файлы для создания онлайн-версии книги. Поскольку наше программное обеспечение форматирования sqtroff не будет использоваться, нам нужен был какой-то способ расширить перекрестные ссылки в файлах. Другими словами, мы не хотели, чтобы файлы содержали «\*[CHerrorhand]»; вместо этого нам нужно было получить то, что подразумевалось под «CHerrorhand».

Было три возможных способа решить эту проблему:

Очевидно, что первый способ трудоемкий (и не очень интересный!). Второй способ, использующий sed, имеет то преимущество, что создает инструмент для выполнения этой работы. Довольно просто написать сценарий sed, который ищет «\*[CHerrorhand]» и заменяет его, например, на «Глава 16, Обработка ошибок». Такой же скрипт можно использовать для изменения каждого из файлов документа. Недостаток в том, что замены жестко запрограммированы; то есть для каждой перекрестной ссылки вам нужно написать команду, которая делает замену. Третий метод, использующий awk, создает инструмент, который работает с любой перекрестной ссылкой, имеющей такой же синтаксис. Этот скрипт также можно использовать для расширения перекрестных ссылок в других книгах. Это спасает вас от необходимости составлять список конкретных замен. Это наиболее общее решение из трех и разработано для максимально возможного повторного использования в качестве инструмента.

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

* Полагаю, что название этого раздела представляет собой комбинацию древнего китайского проклятия «чтоб ты жил в эпоху перемен» и того, что Тим О'Рейли однажды сказал мне, что кто-то решит проблему, если найдет ее интересной. [Д.Д.]

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

Sed - «неинтерактивный» потоковый редактор. Он ориентирован на потоки, потому что, как и многие UNIX-программы, ввод проходит через программу и направляется на стандартный вывод. (vi, например, не ориентирован на поток. Как и большинство приложений DOS.) Ввод обычно поступает из файла, но может быть направлен с клавиатуры*. По умолчанию вывод выводится на экран терминала, но вместо этого может быть записан в файл. Sed работает, интерпретируя сценарий, определяющий действия, которые необходимо выполнить.

Sed предлагает возможности, которые кажутся естественным расширением интерактивного редактирования текста. Например, он предлагает функцию поиска и замены, которая может быть применена глобально к одному файлу или группе файлов. Хотя вы обычно не используете sed для изменения термина, который появляется один раз в конкретном файле, вам будет очень полезно внести ряд изменений в несколько файлов. Подумайте о том, чтобы сделать 20 различных правок в более чем 100 файлах за считанные минуты, и вы получите представление о том, насколько мощным может быть sed.

Использование sed аналогично написанию простых сценариев оболочки (или пакетных файлов в DOS). Вы задаете последовательность действий, которые должны выполняться последовательно. Большинство этих действий можно было бы выполнить вручную из vi: Замена текста, удаление строк, вставка нового текста и т.д. Преимущество sed заключается в том, что вы можете указать все инструкции по редактированию в одном месте, а затем выполнить их за один проход через файл. Вам не нужно заходить в каждый файл, чтобы внести каждое изменение. Sed также можно эффективно использовать для редактирования очень больших файлов, которые будут медленно редактироваться в интерактивном режиме.

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

Sed можно использовать для достижения согласованности во всем документе. Вы можете искать все различные способы использования определенного термина и сделать их все одинаковыми. Вы можете использовать sed для вставки специальных наборных кодов или символов перед форматированием troff. Например, он может быть использован для замены кавычек кодами символов ASCII для прямых и обратных двойных кавычек («фигурные кавычки» вместо «квадратных»).

Sed также может использоваться в качестве фильтра редактирования. Другими словами, вы можете обработать входной файл и отправить выходные данные в другую программу. Например, вы можете использовать sed для анализа обычного текстового файла и вставки макросов troff, прежде чем направлять выходные данные в troff для форматирования. Это позволяет вам вносить изменения на лету, возможно, те, которые являются временными.

Автор или издатель может использовать sed для написания многочисленных программ преобразования - например, для преобразования кодов форматирования в файлах Scribe или TeX в troff или для преобразования файлов текстовых редакторов ПК, таких как WordStar. Позже мы рассмотрим сценарий sed, который преобразует макрос troff в теги таблицы стилей для использования в Ventura Publisher. (Возможно, sed можно было бы использовать для перевода программы, написанной в синтаксисе одного языка, в синтаксис другого языка.) Когда Sun Microsystems впервые выпустила Xview, они выпустили программу преобразования для преобразования программ SunView в XView, и программа в основном состояла из sed-скриптов, конвертирующих имена различных функций.

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

Все сценарии sed, кроме простейших, обычно вызываются из «оболочки» (shell wrapper), сценария оболочки, который вызывает sed и также содержит команды, которые выполняет sed. Оболочка - это простой способ назвать и выполнить команду из одного слова. Пользователям команды даже не нужно знать, что используется sed. Одним из примеров такой оболочки является скрипт phrase, который мы рассмотрим позже в этой книге. Он позволяет сопоставить шаблон, который может занимать две строки, устраняя конкретное ограничение grep.

Таким образом, используйте sed:

* Однако это не особенно полезно.

1.3 Язык программирования с сопоставлением с образцом

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

Sed можно рассматривать как оборотную сторону интерактивного редактирования. Процедура sed достаточно близко соответствует тому, как применить команды редактирования вручную. Sed ограничивает вас методами, которые вы используете в текстовом редакторе. Awk предлагает более общую вычислительную модель для обработки файла.

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

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

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

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

Вот некоторые из вещей, которые позволяет вам делать awk:

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

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

1.4 Четыре препятствия на пути к освоению sed и awk

Существует ряд вводных книг по UNIX, которые познакомят вас с sed и awk. Цель этой книги продвинуть вас намного дальше - помочь вам освоить sed и awk и сократить время и усилия, необходимые для достижения этой цели.

На пути к освоению sed и awk есть четыре препятствия. Вы должны научиться:

  1. Использовать sed и awk. Это относительно небольшое препятствие, которое нужно преодолеть, потому что, к счастью, sed и awk работают очень похожим образом, основываясь на строчном редакторе ed. Глава 2, Понимание основных операций, описывает механику использования sed и awk.
  2. Применять синтаксис регулярных выражений UNIX. Использование синтаксиса регулярных выражений UNIX для шаблона сопоставления характерно как для sed, так и для awk, а также для многих других программ UNIX. Это может быть трудное препятствие по двум причинам: синтаксис непонятен, и хотя у многих есть некоторый опыт использования регулярных выражений, немногие усердно овладели синтаксисом. Чем проще вам использовать этот синтаксис, тем проще использовать sed и awk. Вот почему много времени посвящено регулярным выражениям в Главе 3, Понимание синтаксиса регулярных выражений.
  3. Взаимодействовать с оболочкой. Хотя это напрямую не связано с самими sed и awk, управление взаимодействием с командной оболочкой часто является неприятной проблемой, поскольку оболочка использует ряд специальных символов для обеих программ. По возможности избегайте проблемы, поместив свой скрипт в отдельный файл. Если нет, используйте для своих сценариев оболочку, совместимую с Bourne (правила цитирования более просты), и используйте одинарные кавычки, чтобы содержать свой сценарий. Если вы используете csh в качестве интерактивной оболочки, не забудьте избегать восклицательных знаков обратной косой чертой («\!»). Нет другого способа заставить csh оставить восклицательный знак в покое*.
  4. Уметь писать скрипты. Это самое трудное, скорее как ряд высоких препятствий. Из-за этого основная часть книги посвящена написанию сценариев. С помощью sed вы должны изучить набор однобуквенных команд. С awk вы должны изучить операторы языка программирования. Однако, чтобы получить навык написания сценариев, вы просто должны изучить множество примеров и, конечно же, попробовать свои силы в написании сценариев самостоятельно.

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

* Ну, еще вы можете установить переменную histchars. См. Справочную страницу csh.

Глава 2.
Понимание основных операций

Если вы только начинаете изучать sed и awk, вам будет полезно посмотреть, сколько у них общего.

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

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

В этой главе рассматривается синтаксис командной строки sed и awk и базовая структура скриптов. Также в ней предлагается учебное пособие с использованием списка рассылки, которое даст вам представление о написании сценария. Ценно увидеть sed и awk-скрипты бок о бок, прежде чем вы сосредоточитесь на одном из них.

2.1 Awk от Sed и Grep из Ed

Происхождение awk можно ретроспективно проследить до sed и grep, а через эти две программы - до ed, исходного строкового редактора UNIX.

Вы когда-нибудь использовали строковый редактор? Если это так, то вам будет гораздо легче понять строчную ориентацию sed и awk. Если вы использовали vi, полноэкранный редактор, то вы знакомы с рядом команд, производных от его базового строкового редактора ex (который, в свою очередь, является надмножеством функций в ed).

Давайте рассмотрим некоторые основные операции с помощью строкового редактора ed. Не волнуйтесь - это упражнение предназначено, чтобы помочь вам узнать sed и awk, а не попытаться убедить вас в чудесах строковых редакторов. Команды ed, показанные в этом упражнении, идентичны командам sed, которые вы узнаете позже. Не стесняйтесь экспериментировать с ed самостоятельно, чтобы получить представление о том, как это работает. (Если вы уже знакомы с ed, не стесняйтесь, переходите к следующему разделу.)

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

$ ed test
339

Других подсказок нет. Если вы вводите команду, которую ed не понимает, он выводит знак вопроса как сообщение об ошибке. Вы можете ввести команду печати, p, чтобы отобразить текущую строку.

$ p
label on the first box.

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

1
You might think of a regular expression
d

Ввод «1» делает первую строку текущей строкой, отображая ее на экране. Команда delete в ed - это d, и здесь она удаляет текущую строку. Вместо того чтобы переходить к строке и затем редактировать ее, вы можете приставить к команде редактирования адрес, указывающий, какая строка или диапазон строк является объектом команды. Если вы введете «1d», то первая строка будет удалена.

Вы также можете указать регулярное выражение в качестве адреса. Чтобы удалить строку, содержащую слово «regular», вы можете выполнить эту команду:

/regular/d

где косые черты разделяют регулярное выражение, а «regular» - это строка, которую вы хотите сопоставить. Эта команда удаляет первую строку, содержащую «regular», и делает следующую за ней строку текущей строкой.

NOTE: Убедитесь, что вы понимаете, что команда delete удаляет всю строку. Она не просто удаляет слово «regular» в строке.

Чтобы удалить все строки, содержащие регулярное выражение, нужно в начале выражения ввести букву g, означающую global.

g/regular/d

Глобальная команда делает все строки, соответствующие регулярному выражению, объектами указанной команды.

Удаление текста может увести вас далеко. Подстановка текста (замена одного фрагмента текста другим) намного интереснее. Команда подстановки в ed - это s:

[address]s/pattern/replacement/flag

pattern - это регулярное выражение, которое соответствует фрагменту в текущей строке, который нужно заменить на replacement. Например, следующая команда заменяет первое вхождение слова «regular» на «complex» в текущей строке.

s/regular/complex/

Адрес не указан, поэтому он влияет только на первое вхождение в текущей строке. Если в текущей строке не найден «regular», это ошибка. Чтобы найти несколько вхождений в одной строке, вы должны указать флаг g:

s/regular/complex/g

Эта команда изменяет все вхождения в текущей строке. Адрес должен быть указан, чтобы эта команда действовала не только на текущую строку. Следующая команда подстановки указывает адрес:

/regular/s/regular/complex/g

Эта команда влияет на первую строку, которая соответствует адресу в файле. Помните, что первый «regular» - это адрес, а второй - шаблон для подстановки команды. Чтобы применить его ко всем строкам, используйте глобальную команду, поставив g перед адресом.

g/regular/s/regular/complex/g

Теперь замена производится везде - все вхождения во всех строках.

NOTE: Обратите внимание на разные значения «g». Буква «g» в начале - это глобальная команда, которая означает внесение изменений во все строки, соответствующие адресу. Буква «g» в конце - это флаг, который означает изменение каждого вхождения в строке, а не только первого.

Адрес и образец не обязательно должны совпадать.

g/regular expression/s/regular/complex/g

В любой строке, содержащей строку «regular expression», замените «regular» на «complex». Если адрес и шаблон совпадают, вы можете сказать об этом редактору ed, указав два последовательных разделителя (//).

g/regular/s//complex/g

В этом примере «regular» указан как адрес, а шаблон, который должен быть сопоставлен для замены, - им же. Если кажется, что мы слишком поверхностно рассмотрели эти команды, не беспокойтесь. Позже мы рассмотрим их еще раз.

Знакомая утилита grep для UNIX является производной от следующей глобальной команды в ed:

g/re/p

что означает «печать глобального регулярного выражения». Grep - это команда редактирования строки, извлеченная из ed и ставшая доступной как внешняя программа. Она запрограммирована на выполнение одной команды редактирования. Она принимает регулярное выражение как аргумент в командной строке и использует его как адрес строк для печати. Вот пример поиска строк, соответствующих «box»:

$ grep 'box' test
You are given a series of boxes, the first one labeled "A",
label on the first box.

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

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

ed test < ed-script

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

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

ed не ориентирован на поток, и изменения вносятся в сам файл. Скрипт ed должен содержать команды для сохранения файла и выхода из редактора. Он не выводит на экран никаких результатов, кроме тех, которые может сгенерировать конкретная команда.

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

Посмотрите на следующую команду подстановки:

s/regular/complex/

Если вы введете эту команду в интерактивном режиме в ed, вы замените первое вхождение «regular» на «complex» в текущей строке. В сценарии ed, если это первая команда в сценарии, это будет применяется только к последней строке файла (текущая строка ed по умолчанию). Однако в сценарии sed та же самая команда применяется ко всем строкам. То есть команды sed неявно глобальны. В sed предыдущий пример имеет тот же результат, что и следующая глобальная команда в ed:

g/regular/s//complex/

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

Sed также был разработан с рядом дополнительных команд, поддерживающих написание сценариев. Мы посмотрим на многие из этих команд в Главе 6. Расширенные команды sed.

Awk был разработан как программируемый редактор, который, как и sed, ориентирован на поток и интерпретирует сценарий команды редактирования. Где awk отличается от sed, так это в отказе от набора команд строчного редактора. Он предлагает на это место язык программирования, созданный по образцу языка C. Оператор print заменяет команду p, например. Концепция адресации сохраняется, так что:

/regular/ { print }

печатает те строки, которые соответствуют «regular». Фигурные скобки ({}) оборачивают серию из одного или нескольких операторов, которые применяются к одному и тому же адресу.

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

Одной из наиболее отличительных особенностей awk является то, что он парсит или разбивает каждую строку ввода и делает отдельные слова, доступными для обработки скриптом. (Такой редактор, как vi, также распознает слова, позволяя вам перемещать слово за словом или делать слово объектом действия, но эти функции могут использоваться только в интерактивном режиме.)

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

2.2 Синтаксис командной строки

Вы вызываете sed и awk примерно так:

command [options] script filename

Как и почти все программы UNIX, sed и awk могут принимать входные данные из стандартного ввода и отправлять выходные данные на стандартный вывод. Если filename указан, то входные данные берутся из этого файла. Выходные данные содержат обработанную информацию. Стандартный вывод - это экран дисплея, и обычно вывод из этих программ направляется туда. Он также может быть отправлен в файл с использованием перенаправления ввода-вывода в оболочке, но он не должен идти в тот же файл, который предоставляет входные данные для программы.

options для каждой команды различны. Мы продемонстрируем многие из этих вариантов в следующих разделах. (Полный список опций командной строки для sed можно найти в Приложении A. Краткий справочник по sed; полный список опций для awk находится в Приложении B. Краткий справочник по awk.)

script определяет, какие инструкции выполнять. Если он указан в командной строке, то сценарий должен быть заключен в одинарные кавычки - в том случае, если он содержит пробел или любые символы, которые могут быть интерпретированы оболочкой (например, $ и *).

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

sed -f scriptfile inputfile

На Рис. 2.1 показаны основные операции sed и awk. Каждая программа считывает одну входную строку за раз из входного файла, создает копию входной строки и выполняет инструкции, указанные в скрипте на этой копии. Таким образом, изменения, внесенные во входную строку, не влияют на фактический входной файл.

Рис. 2.1: Как работают sed и awk

2.2.1 Создание сценариев

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

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

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

Когда все применимые инструкции будут интерпретированы и применены для одной строки, sed выводит строку и повторяет цикл для каждой входной строки. Awk, с другой стороны, автоматически не выводит строки; инструкции в вашем сценарии управляют тем, что в конечном итоге делается с ними.

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

В awk процедура состоит из программных операторов и функций. Процедура должна быть в скобках.

В следующих разделах мы рассмотрим несколько сценариев, которые обрабатывают пример списка рассылки.

2.2.2 Пример списка рассылки

В следующих разделах в примерах используется образец файла с именем list. Он содержит список имен и адреса, как показано ниже.

$ cat list
John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

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

2.3 Использование sed

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

2.3.1 Указание простых инструкций

Вы можете указать простые команды редактирования в командной строке.

sed [-e] 'instruction' file

Параметр -e необходим только в том случае, если вы вводите более одной инструкции в командной строке. Он указывает sed, что нужно интерпретировать следующий аргумент как инструкцию. Когда есть одна инструкция, sed может сделать это определение самостоятельно. Давайте посмотрим на несколько примеров.

Используем входной файл list, в следующем примере используется команда s для замены подстроки «MA» на «Massachusetts».

$ sed 's/MA/Massachusetts/' list
John Daggett, 341 King Road, Plymouth Massachusetts
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury Massachusetts
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston Massachusetts

Инструкция влияет на три строки, но отображаются все строки.

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

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

$ sed 's/ MA/, Massachusetts/' list
John Daggett, 341 King Road, Plymouth, Massachusetts
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury, Massachusetts
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston, Massachusetts

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

Есть три способа указать несколько инструкций в командной строке:

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

* В наши дни существует множество оболочек, совместимых с оболочкой Борна и работающих так, как описано здесь: ksh, bash, pdksh и zsh, и это лишь некоторые из них.

2.3.1.1 Искажение команды

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

$ sed -e 's/MA/Massachusetts' list
sed: command garbled: s/MA/Massachusetts

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

$ gsed -e 's/MA/Massachusetts' list
gsed: Unterminated ‘s' command

* Некоторые поставщики, кажется, улучшили ситуацию. Например, в SunOS 4.1.x sed сообщает «sed: Ending delimiter missing on substitution: s/MA/Massachusetts»

2.3.2 Файлы сценариев

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

sed -f scriptfile file

Все команды редактирования, которые мы хотим выполнить, помещаются в файл. Мы придерживаемся соглашения о создании временных файлов сценариев с именем sedscr.

$ cat sedscr
s/ MA/, Massachusetts/
s/ PA/, Pennsylvania/
s/ CA/, California/
s/ VA/, Virginia/
s/ OK/, Oklahoma/

Следующая команда считывает все команды подстановки в sedscr и применяет их к каждой строке во входном файле list:

$ sed -f sedscr list
John Daggett, 341 King Road, Plymouth, Massachusetts
Alice Ford, 22 East Broadway, Richmond, Virginia
Orville Thomas, 11345 Oak Bridge Road, Tulsa, Oklahoma
Terry Kalkas, 402 Lans Road, Beaver Falls, Pennsylvania
Eric Adams, 20 Post Road, Sudbury, Massachusetts
Hubert Sims, 328A Brook Road, Roanoke, Virginia
Amy Wilde, 334 Bayshore Pkwy, Mountain View, California
Sal Carpenter, 73 6th Street, Boston, Massachusetts

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

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

2.3.2.1 Сохранение вывода

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

$ sed -f sedscr list > newlist

Не перенаправляйте вывод в файл, который вы редактируете, иначе вы его испортите. (Оператор перенаправления «>» усекает файл до того, как оболочка сделает что-либо еще.) Если вы хотите, чтобы выходной файл заменял входной файл, вы можете сделать это как отдельный шаг, используя команду mv. Но сначала убедитесь, что ваш скрипт редактирования работает правильно!

В Главе 4, Написание сценариев sed мы рассмотрим сценарий оболочки с именем runsed, который автоматизирует процесс создания временного файла и использования mv для перезаписи исходного файла.

2.3.2.2 Подавление автоматического отображения строк ввода

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

$ sed -n -e 's/MA/Massachusetts/p' list
John Daggett, 341 King Road, Plymouth Massachusetts
Eric Adams, 20 Post Road, Sudbury Massachusetts
Sal Carpenter, 73 6th Street, Boston Massachusetts

Сравните этот вывод с первым примером в этом разделе. Здесь только строки, которые были затронуты командой sed.

2.3.2.3 Параметры смешивания (POSIX)

Вы можете создать сценарий, объединив в командной строке параметры -e и -f. Сценарий - это комбинация всех команд в указанном порядке. Похоже, это поддерживается в версиях UNIX sed, но эта функция четко не задокументирована на странице руководства. Стандарт POSIX явно требует это поведение.

2.3.2.4 Список параметров

В Таблице 2.1 приведены параметры командной строки sed.

Таблица 2.1: Параметры командной строки sed

Опция Описание
-e Далее следует инструкция по редактированию.
-f Далее следует имя файла сценария.
-n Подавить автоматический вывод входных строк.

2.4 Использование awk

Как и sed, awk выполняет набор инструкций для каждой строки ввода. Вы можете указать инструкции в командной строке или создать файл сценария.

2.4.1 Запуск awk

Для запуска в командной строке синтаксис таков:

awk 'instructions' files

Входные данные считываются по строке за раз из одного или нескольких files или из стандартного ввода. Инструкции должны быть заключены в одинарные кавычки, чтобы защитить их от оболочки. (Инструкции почти всегда содержат фигурные скобки и/или знаки доллара, которые интерпретируются оболочкой как специальные символы.) Несколько командных строк можно вводить таким же образом, как показано для sed: разделяя команды точками с запятой или используя возможность многострочного ввода оболочки Bourne.

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

awk -f script files

Опция -f работает так же, как и в sed.

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

Awk, в обычном случае, интерпретирует каждую входную строку как запись, а каждое слово в этой строке, разделенное пробелами или табуляциями, как поле. (Эти значения по умолчанию можно изменить.) Один или несколько последовательных пробелов или табуляций считаются одним разделителем. Awk позволяет ссылаться на эти поля либо в шаблонах, либо в процедурах. $0 представляет собой вся входная строка. $1, $2, ... обращение к отдельным полям в строке ввода. Awk разбивает входную запись перед применением скрипта. Давайте рассмотрим несколько примеров, используя пример списка входных файлов list.

Первый пример содержит одну инструкцию, которая печатает первое поле каждой строки входного файла.

$ awk '{ print $1 }' list
John
Alice
Orville
Terry
Eric
Hubert
Amy
Sal

«$1» относится к значению первого поля в каждой строке ввода. Поскольку шаблон не задан, оператор print применяется ко всем строкам. В следующем примере задается шаблон «/MA/», но нет никакой процедуры. По умолчанию выполняется печать каждой строки, соответствующей шаблону.

$ awk '/MA/' list
John Daggett, 341 King Road, Plymouth MA
Eric Adams, 20 Post Road, Sudbury MA
Sal Carpenter, 73 6th Street, Boston MA

Печатаются три строки. Как уже упоминалось в первой главе, программа awk может использоваться скорее как язык запросов, извлекая полезную информацию из файла. Можно сказать, что паттерн поставил условие выбора записей для включения в отчет, а именно, что они должны содержать строку «MA». Теперь мы также можем указать, какую часть записи включить в отчет. В следующем примере оператор print используется для ограничения вывода первым полем каждой записи.

$ awk '/MA/ { print $1 }' list
John
Eric
Sal

Если мы попробуем прочитать вышеприведенную инструкцию вслух, это поможет ее понять: Печатать первое слово каждой строки, содержащей строку «MA». Мы можем сказать «слово», потому что по умолчанию awk разделяет входные данные на поля, используя либо пробелы, либо табуляции в качестве разделителя полей.

В следующем примере мы используем опцию -F, чтобы изменить разделитель полей на запятую. Это позволяет нам получить любое из трех полей: полное имя, адрес улицы или город и штат.

$ awk -F, '/MA/ { print $1 }' list
John Daggett
Eric Adams
Sal Carpenter

Не путайте опцию -F для изменения разделителя полей с опцией -f для указания имени файла скрипта.

В следующем примере мы печатаем каждое поле в отдельной строке. Несколько команд разделяются точками с запятой.

$ awk -F, '{ print $1; print $2; print $3 }' list
John Daggett
 341 King Road
 Plymouth MA
Alice Ford
 22 East Broadway
 Richmond VA
Orville Thomas
 11345 Oak Bridge Road
 Tulsa OK
Terry Kalkas
 402 Lans Road
 Beaver Falls PA
Eric Adams
 20 Post Road
 Sudbury MA
Hubert Sims
 328A Brook Road
 Roanoke VA
Amy Wilde
 334 Bayshore Pkwy
 Mountain View CA
Sal Carpenter
 73 6th Street
 Boston MA

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

2.4.2 Сообщения об ошибках

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

2.4.3 Список параметров

В Таблице 2.2 приведены параметры командной строки awk.

Таблица 2.2: Параметры командной строки awk

Параметр Описание
-f Далее следует имя файла сценария.
-F Изменить разделитель полей.
-v Далее следует var=value.

Параметр -v для инструкций в строке обсуждается в Главе 7. Написание скриптов для awk.

2.5 Совместное использование sed и awk

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

$ cat nameState
s/ CA/, California/
s/ MA/, Massachusetts/
s/ OK/, Oklahoma/
s/ PA/, Pennsylvania/
s/ VA/, Virginia/

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

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

$ sed -f nameState list | awk -F, '{ print $4 }'
 Massachusetts
 Virginia
 Oklahoma
 Pennsylvania
 Massachusetts
 Virginia
 California
 Massachusetts

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

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

Хотя результат этой программы не очень полезен, его можно было бы передать в sort | uniq -c, который сортировал бы штаты в алфавитный список с подсчетом количества вхождений каждого штата.

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

#! /bin/sh
awk -F, '{
    print $4 ", " $0
    }' $* |
sort |
awk -F, '
$1 == LastState {
    print "\t" $2
}
$1 != LastState {
    LastState = $1
    print $1
    print "\t" $2
}'

Этот сценарий оболочки состоит из трех частей. Программа вызывает awk для получения входных данных для программы sort, а затем снова вызывает awk для проверки отсортированных входных данных и определения того, совпадает ли имя штата в текущей записи с именем предыдущей записи. Давайте посмотрим сценарий в действии:

$ sed -f nameState list | byState
California
    Amy Wilde
Massachusetts
    Eric Adams
    John Daggett
    Sal Carpenter
Oklahoma
    Orville Thomas
Pennsylvania
    Terry Kalkas
Virginia
    Alice Ford
    Hubert Sims

Имена сортируются по штатам. Это типичный пример использования awk для создания отчета из структурированных данных.

Чтобы изучить, как работает программа byState, давайте рассмотрим каждую часть отдельно. Она предназначена для считывания входных данных из программы nameState и ожидает, что «$4» будет именем штата. Посмотрите на результат, полученный первой строкой программы:

$ sed -f nameState list | awk -F, '{ print $4 ", " $0 }'
 Massachusetts, John Daggett, 341 King Road, Plymouth, Massachusetts
 Virginia, Alice Ford, 22 East Broadway, Richmond, Virginia
 Oklahoma, Orville Thomas, 11345 Oak Bridge Road, Tulsa, Oklahoma
 Pennsylvania, Terry Kalkas, 402 Lans Road, Beaver Falls, Pennsylvania
 Massachusetts, Eric Adams, 20 Post Road, Sudbury, Massachusetts
 Virginia, Hubert Sims, 328A Brook Road, Roanoke, Virginia
 California, Amy Wilde, 334 Bayshore Pkwy, Mountain View, California
 Massachusetts, Sal Carpenter, 73 6th Street, Boston, Massachusetts

Программа sort по умолчанию сортирует строки в алфавитном порядке, рассматривая символы слева направо. Чтобы отсортировать записи по штатам, а не по именам, мы вставляем штат в качестве ключа сортировки в начале записи. Теперь программа sort может сделать свою работу за нас. (Обратите внимание, что использование утилиты sort избавляет нас от необходимости писать процедуры сортировки внутри awk.)

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

$1 == LastState {
    print "\t" $2
}
$1 != LastState {
    LastState = $1
    print $1
    print "\t" $2
}'

Здесь есть несколько важных вещей, в том числе назначение переменной, проверка первого поля каждой входной строки, чтобы увидеть, содержит ли она переменную строку, и печать табуляции для выравнивания выходных данных. Обратите внимание, что нам не нужно выполнять присвоение переменной перед ее использованием (поскольку переменные awk инициализируются пустой строкой). Это небольшой сценарий, но вы увидите тот же тип процедуры, который используется для сравнения записей индекса в гораздо более крупной программе индексирования в Главе 12. Полнофункциональные приложения. Однако пока не стоит слишком беспокоиться о понимании того, что делает каждое утверждение. Наша цель - дать вам обзор возможностей sed и awk.

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

Глава 3.
Понимание синтаксиса регулярных выражений

Когда маленький ребенок пытается понять значение идиоматического выражения, такого как «Шила в мешке не утаишь», вы можете помочь, объяснив, что это выражение означает не то, о чем буквально говорит.

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

Хотя регулярные выражения являются базовой частью UNIX, не все имеют полное представление об их синтаксисе. На самом деле, может быть довольно сложно проверять такие выражения, как:

^□□*.*

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

Если вы обычно используете любой текстовый редактор UNIX, вы, вероятно, в той или иной мере знакомы с синтаксисом регулярных выражений. grep, sed и awk используют регулярные выражения. Однако не все метасимволы, используемые в синтаксисе регулярных выражений, доступны для всех трех программ. Базовый набор метасимволов был представлен в строчном редакторе ed и доступен в grep. Sed использует тот же набор метасимволов. Позже появилась программа egrep, которая предлагает расширенный набор метасимволов. Awk использует по существу тот же набор метасимволов, что и egrep.

Чтобы понять синтаксис регулярных выражений, вы должны изучить функции, выполняемые различными метасимволами. Но вы также должны увидеть много примеров их работы в различных комбинациях. Именно таков наш подход в этой главе - представить каждый метасимвол и привести множество примеров, которые по большей части используют grep и его двоюродный брат egrep для демонстрации практических применений.

Если вы уже разбираетесь в синтаксисе регулярных выражений, не стесняйтесь пропустить эту главу. Полный список метасимволов регулярных выражений можно найти в Таблице 3.1, а также в Приложении A. Краткий справочник по sed, и Приложении B. Краткий справочник по awk. Для тех, кто интересуется, книга Джеффри Э. Ф. Фридла «Регулярные выражения» издательства О'Рейли дает исчерпывающее описание построения и использования регулярных выражений.

3.1 Это выражение

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

2 + 4

«Два плюс четыре» состоит из нескольких констант или литеральных значений и оператора. Программа-калькулятор должна распознать, например, что «2» является числовой константой и что знак плюс представляет собой оператор, а не интерпретируется как символ «+».

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

Выражение может быть более сложным, чем «2 + 4»; фактически, оно может состоять из нескольких простых выражений, таких как:

2 + 3 * 4

Калькулятор обычно вычисляет выражение слева направо. Однако некоторые операторы имеют преимущество перед другими: то есть они будут работать в первую очередь. Таким образом, приведенное выше выражение будет иметь значение 14, а не 20, потому что умножение имеет более высокий приоритет, чем сложение. Приоритет можно переопределить, поместив простое выражение в круглые скобки. Таким образом, «(2 + 3) * 4» или «сумма два плюс три умножить на четыре» будет равняться 20. Круглые скобки - это символы, указывающие калькулятору на изменение порядка вычисления выражения.

Регулярное выражение, напротив, описывает шаблон или последовательность символов. Конкатенация - это основная операция, подразумеваемая в каждом регулярном выражении. То есть шаблон соответствует соседним символам. Посмотрите на следующее регулярное выражение:

ABE

Каждый буквальный символ - это регулярное выражение, которое соответствует только этому единственному символу. Это выражение описывает «символ А, за которым следует В, за которым следует Е» или просто «строка ABE». Термин «строка» означает каждый символ, связанный с предшествующим ему. Важно подчеркнуть, что регулярное выражение описывает последовательность символов. (Начинающие пользователи склонны думать в единицах более высокого уровня, таких как слова, а не отдельные символы.) Регулярные выражения чувствительны к регистру; «А» не соответствует «а»*.

Такие программы, как grep, которые принимают регулярные выражения, должны сначала оценить синтаксис регулярного выражения, чтобы создать шаблон. Затем они считывают входные данные строка за строкой, пытаясь найти соответствие шаблону. Входная строка - это строка, и чтобы проверить, соответствует ли строка шаблону, программа сравнивает первый символ в строке с первым символом шаблона. Если есть совпадение, он сравнивает второй символ в строке со вторым символом шаблона. Всякий раз, когда ему не удается найти совпадение, он возвращается назад и пытается снова, начиная со следующего символа в строке. Рис. 3.1 иллюстрирует этот процесс, пытаясь найти совпадение с шаблоном «abe» во входной строке.

Рис. 3.1: Интерпретация регулярного выражения

Регулярное выражение не ограничивается литеральными символами. Есть, например, метасимвол точка (.) - который может быть использован в качестве «подстановочного знака» (wildcard) для соответствия любому отдельному символу. Вы можете думать об этом подстановочном знаке как об аналоге пустой плитки в Scrabble, где это означает любую букву. Таким образом, мы можем указать регулярное выражение «A.E», и оно будет соответствовать «ACE», «ABE» и «ALE». Оно будет соответствовать любому символу в позиции, следующей за «A».

Метасимвол *, звездочка, используется для сопоставления нуля или более вхождений предыдущего регулярного выражения, которое обычно представляет собой один символ. Возможно, вы знакомы с символом * как метасимволом оболочки, где он означает «ноль или более символов». Но это значение очень отличается от * в регулярном выражении. Сам по себе метасимвол звездочки не соответствует ничему; он изменяет то, что идет перед ним. Регулярное выражение .* соответствует любому количеству символов, тогда как в оболочке * имеет такое же значение. (Например, в командной строке ls * перечислит все файлы в текущем каталоге.) Регулярное выражение «A.*E» соответствует любой строке, которая соответствует «A.E», но оно также будет соответствовать любому количеству символов между «A» и «E»: «AIRPLANE», «A FINE», «AFFABLE», или «A LONG WAY HOME», например. Обратите внимание, что «любое количество символов» может быть даже равно нулю!

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

Также должно быть очевидно, что с помощью метасимволов вы можете расширить или ограничить возможные совпадения. У вас больше контроля над тем, что совпадает, а что нет.

* Некоторые другие программы, использующие регулярные выражения, предлагают возможность сделать их нечувствительными к регистру, но sed и awk этого не делают.

3.2 Линейка символов

Мы видели два основных элемента в выражении:

  1. Значение, выраженное в виде литерала или переменной.
  2. Оператор.

Регулярное выражение состоит из этих же элементов. Любой символ, за исключением метасимволов в Таблице 3.1 интерпретируется как литерал, соответствующий только самому себе.

Таблица 3.1: Список метасимволов

Спецсимволы Использование
. Соответствует любому одиночному символу, кроме новой строки. В awk точка также может соответствовать новой строке.
* Соответствует любому числу (включая ноль) одного символа (включая символ, заданный регулярным выражением), которое непосредственно ему предшествует.
[...] Соответствует одному любому символу из класса, заключенных между скобками. Крышка (^) в качестве первого символа внутри скобок изменяет соответствие на все символы, кроме новой строки и тех, которые перечислены в классе. В awk новая строка также будет соответствовать. Дефис (-) используется для обозначения диапазона символов. Закрывающая скобка (]) в качестве первого символа в классе является членом класса. Все остальные метасимволы теряют свое значение, если они указаны как члены класса.
^ Первый символ регулярного выражения, соответствует началу строки. Соответствует началу строки в awk, даже если строка содержит встроенные символы новой строки.
$ Как последний символ регулярного выражения, соответствует концу строки. Соответствует концу строки в awk, даже если строка содержит встроенные символы новой строки.
\{n,m\} Соответствует диапазону вхождений одного символа (включая символ, заданный регулярным выражением), который непосредственно ему предшествует. \{n\} - будет точно соответствовать n вхождениям, \{n,\} - будет соответствовать по крайней мере n вхождениям, а \{n,m\} - будет соответствовать любому числу вхождений в диапазоне между n и m. (В некоторых очень старых версиях sed и grep метасимвол может отсутствовать).
\ Экранирует специальный символ, который следует за ним.
Расширенные метасимволы (egrep и awk)
+ Соответствует одному или нескольким вхождениям предыдущего регулярного выражения.
? Соответствует нулю или одному вхождению предыдущего регулярного выражения.
| Указывает, что и предыдущее и следующее регулярное выражение могут соответствовать (чередование).
() Группы регулярных выражений.
{n,m} Соответствует диапазону вхождений одного символа (включая символ, заданный регулярным выражением), который непосредственно ему предшествует. {n} будет соответствовать ровно n вхождений, {n,} будет соответствовать, по крайней мере, n раз, и {n,m} будет соответствовать любому числу вхождений в диапазоне от n до m. (Для POSIX egrep и POSIX awk, но не для традиционных egrep или awk.)a

a Большинство реализаций awk еще не поддерживают эту нотацию.

Метасимволы имеют особое значение в регулярных выражениях, почти так же, как + и * имеют особое значение в арифметических выражениях. Несколько метасимволов (+ ? ( ) |) доступны только как часть расширенного набора, используемого такими программами, как egrep и awk. Мы рассмотрим, что делает каждый метасимвол в следующих разделах, начиная с обратной косой черты.

3.2.1 Вездесущий обратный слеш

Метасимвол обратной косой черты (\) преобразует метасимволы в обычные символы (а обычные символы - в метасимволы). Он заставляет буквально интерпретировать любой метасимвол так, чтобы он соответствовал самому себе. Например, точка (.) - это метасимвол, который должен быть экранирован обратной косой чертой, если вы хотите, чтобы он соответствовал точке. Это регулярное выражение соответствует точке, за которой следуют три пробела.

\.□□□

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

\.nf

Вы также можете использовать обратную косую черту, чтобы экранировать метасимвол обратной косой черты. Например, запрос на изменение шрифта в troff - это «\f». Для поиска строк, содержащих этот запрос, можно использовать следующее регулярное выражение:

\\f

Кроме того, обратная косая черта используется, чтобы заставить группу обычных символов интерпретироваться как метасимволы, как показано на Рис. 3.2.

\(  \)  \{  \}  \n

Рис. 3.2: Экранирование метасимволов в sed

Символ n в конструкции «\n» представляет собой цифру от 1 до 9; его использование будет объяснено в Главе 5, .

3.2.2 Подстановочный символ (wildcard)

Подстановочный метасимвол или точку (.), можно считать эквивалентным переменной. Переменная представляет любое значение в арифметическом выражении. В регулярном выражении точка (.) - это подстановочный знак, представляющий любой символ, кроме новой строки. (В awk точка может даже соответствовать встроенному символу новой строки.)

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

80.86

будет соответствовать строкам, содержащим «80286», «80386» или «80486»*. Чтобы искать собственно точку, вы должны экранировать точку обратной косой чертой.

Редко бывает полезно сопоставлять только какой-либо символ в начале или конце шаблона. Кроме того, подстановочному знаку обычно предшествует и следует литеральный символ или другой метасимвол. Например, для поиска ссылок на Главы может быть написано следующее регулярное выражение:

Chapter.

Он ищет «строку 'Chapter', за которой следует любой символ». При поиске этого выражения оно оказалось бы практически таким же совпадением, как и фиксированный строковый шаблон «Chapter». Посмотрите на следующий пример:

$ grep 'Chapter.' sample
you will find several examples in Chapter 9.
"Quote me 'Chapter and Verse'," she said.
Chapter Ten

При поиске по слову «Chapter», а не «Chapter.» совпадали бы все те же строки. Однако есть один отличающийся случай - если бы «Chapter» появился в конце строки. Подстановочный знак не соответствует новой строке, поэтому «Chapter.» не будет соответствовать этой строке, в то время как шаблон фиксированной строки будет соответствовать.

* Семейство микропроцессоров Pentium нарушает наш простой эксперимент по подбору шаблонов, портя удовольствие. Не говоря уже об оригинальном 8086.

3.2.3 Написание регулярных выражений

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

Другими словами, место, где нужно искать, чтобы исправить проблему, - это выражение, в котором вы описали желаемый результат. Либо это выражение неполное, либо оно неправильно сформулировано. Например, если программа вычисляет это выражение:

PAY = WEEKLY_SALARY * 52

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

PAY = WEEKLY_SALARY * 52 + COMMISSION

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

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

Этот процесс является практически тем же самым процессом, которому следует программист для разработки программы. Шаг 1 можно рассматривать как спецификацию, которая должна отражать понимание проблемы, подлежащей решению, а также того, как ее решить. Шаг 2 аналогичен фактическому кодированию программы, и Шаг 3 включает в себя запуск программы и тестирование ее в соответствии со спецификацией. Шаги 2 и 3 образуют цикл, который повторяется до тех пор, пока программа не будет работать удовлетворительно.

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

Совпадения
Строки, которым я хотел соответствовать.
Пропуски
Строки, которым я не хотел соответствовать.
Упущения
Строки, которым я не соответствовал, но хотел соответствовать.
Ложные срабатывания
Строки, которым я соответствовал, но не хотел соответствовать.

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

Трудность особенно очевидна, когда вы должны описывать паттерны, используя фиксированные строки. Каждый символ, который вы удаляете из шаблона фиксированной строки, увеличивает число возможных совпадений. Например, при поиске строки «what» вы определяете, что вы также хотели бы соответствовать «What». Единственный шаблон фиксированной строки, который будет соответствовать «What» и «what» - это «hat», самая длинная строка, общая для обоих. Очевидно, однако, что поиск «hat» приведет к нежелательным совпадениям. Каждый символ, добавляемый в шаблон с фиксированной строкой, уменьшает число возможных совпадений. Строка «them» обычно дает меньше совпадений, чем строка «the».

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

3.2.4 Классы символов

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

Классы символов полезны, например, для работы с прописными и строчными буквами. Если «what» может появиться либо с начальной заглавной буквы, либо со строчной буквы, вы можете указать:

[Ww]hat

Это регулярное выражение может соответствовать «what» или «What». Оно будет соответствовать любой строке, содержащей эту четырехсимвольную строку, первый символ которой является либо «W», либо «w». Кроме того, оно может соответствовать «Whatever» или «somewhat».

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

\.H[12345]

Этот шаблон соответствует трехсимвольной строке, где последним символом является любое число от 1 до 5.

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

$ grep '\.H[123]' ch0[12]
ch01:.H1 "Contents of Distribution Tape"
ch01:.H1 "Installing the Software"
ch01:.H1 "Configuring the System"
ch01:.H2 "Specifying Input Devices"
ch01:.H3 "Using the Touch Screen"
ch01:.H3 "Using the Mouse"
ch01:.H2 "Specifying Printers"
ch02:.H1 "Getting Started"
ch02:.H2 "A Quick Tour"
.
.
.

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

.[!?;:,".]□□.

Этот шаблон соответствует «любому символу, за которым следует восклицательный или вопросительный знак, точка с запятой, двоеточие, запятая, кавычка или точка, а затем два пробела и любой символ. Он может быть использован для поиска мест, где между концом предложения и началом следующего предложения оставлены два пробела, когда это происходит в одной строке. Обратите внимание, что в этом выражении есть три точки. Первая и последняя точки являются подстановочными метасимволами, но вторая точка интерпретируется буквально. Внутри квадратных скобок стандартные метасимволы теряют свое значение. Таким образом, точка внутри квадратных скобок указывает на точку. В Таблице 3.2 перечислены символы, имеющие особое значение, если они заключены в квадратные скобки.

Таблица 3.2: Спецсимволы в классах символов

Символ Функция
\ Экранирует любой специальный символ (только awk)
- Указывает диапазон, если не находится в первой или последней позиции.
^ Указывает на обратное совпадение, если стоит в первой позиции.

Обратная косая черта является спецсимволом только в awk, что позволяет писать «[a\]1]» для класса символов, который будет соответствовать a, правой скобке или 1.

3.2.4.1 Диапазон символов

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

[A-Z]

Диапазон однозначных чисел может быть задан следующим образом:

[0-9]

Этот класс символов помогает решить упомянутую чуть выше проблему сопоставления ссылок на главы. Посмотрите на следующее регулярное выражение:

[cC]hapter [1-9]

Оно соответствует строке «chapter» или «Chapter», за которой следует пробел, а затем следует любое однозначное число от 1 до 9. Каждая из следующих строк соответствует шаблону:

you will find the information in chapter 9
and chapter 12.
Chapter 4 contains a summary at the end.

В зависимости от задачи, вторая строка в этом примере может рассматриваться как ложное срабатывание. Вы можете добавить пробел после «[1-9]», чтобы избежать совпадения двухзначных чисел. Вы также можете указать класс символов, которые не будут совпадать в этой позиции, как мы увидим в следующем разделе. Можно указать несколько диапазонов, а также смешать их с буквенными символами:

[0-9a-z?,.;:'"]

Это выражение будет соответствовать «любому одиночному символу, который является числовым, строчным буквенным или вопросительным знаком, запятой, точкой, точкой с запятой, двоеточием, одинарной кавычкой или кавычкой». Помните, что каждый класс символов соответствует одному символу. Если вы указываете несколько классов, вы описываете несколько последовательных символов, таких как:

[a-zA-Z][.?!]

Это выражение будет соответствовать «любой строчной или прописной букве, за которой следует точка, вопросительный или восклицательный знак».

Закрывающая скобка (]) интерпретируется как член класса, если она встречается как первый символ в классе (или как первый символ после символа крышка (карет) (^); см. следующий раздел). Дефис теряет свое особое значение в классе, если он является первым или последним символом. Поэтому, чтобы соответствовать арифметическим операторам, мы ставим дефис (-) первым в следующем примере:

[-+*/]

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

Попытка сопоставить даты с регулярным выражением - интересная задача. Существует два возможных формата:

MM-DD-YY
MM/DD/YY

Следующее регулярное выражение указывает возможный диапазон значений для каждой позиции символа:

[0-1][0-9][-/][0-3][0-9][-/][0-9][0-9]

Разделителем может быть либо «-», либо «/». Установка дефиса в первую позицию гарантирует, что он будет интерпретироваться в символьном классе буквально, как дефис, а не как указание диапазона**.

* На самом деле это может быть очень сложно при работе с наборами символов, отличными от ASCII, и/или на языках, отличных от английского. Стандарт POSIX решает эту проблему; новые функции POSIX представлены ниже.

** Обратите внимание, что выражение соответствует датам, которые смешивают свои разделители, а также невозможным датам, таким как «15/32/78».

3.2.4.2 Исключение класса символов

Обычно класс символов включает в себя все символы, которые вы хотите сопоставить в этой позиции. Крышка (^) как первый символ в классе исключает все символы в классе из сопоставления. Вместо этого будет сопоставлен любой символ, кроме новой строки*, который не указан в квадратных скобках. Следующий шаблон будет соответствовать любому нечисловому символу:

[^0-9]

Он соответствует всем прописным и строчным буквам алфавита и всем специальным символам, таким как знаки препинания.

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

[^aeiou]

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

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

\.DS "[^1]"

Это выражение соответствует строке «.DS», за которой следует пробел, кавычка, за которой следует любой символ, кроме цифры «1», за которой следует кавычка**. Он предназначен для того, чтобы избежать соответствия следующей строке:

.DS "1"

при совпадающих строках например:

.DS "I"
.DS "2"

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

* В awk новая строка также может быть сопоставлена.

** При вводе этого шаблона в командной строке обязательно заключите его в одинарные кавычки. ^ является спецсимволом в оболочке Борна.

3.2.4.3 Дополнение к символьному классу POSIX

Стандарт POSIX формализует значение символов и операторов регулярных выражений. Стандарт определяет два класса регулярных выражений: Basic Regular Expressions (BREs) (базовый набор), которые используются grep и sed, и Extended Regular Expressions (расширенный набор (ERE)), которые используются egrep и awk.

Чтобы приспособить неанглийские среды, стандарт POSIX расширил способность классов символов соответствовать символам, не входящим в английский алфавит. Например, французский è - это алфавитный символ, но типичный класс символов [a-z] не будет соответствовать ему. Кроме того, стандарт предусматривает последовательности символов, которые должны рассматриваться как единое целое при сравнении и сопоставлении (сортировке) строковых данных.

POSIX также изменил то, что было общепринятой терминологией. То, что мы называем «символьным классом», в стандарте POSIX называется «скобочным выражением». Внутри скобочных выражений, рядом с литеральными символами, такими как a, !, и так далее, вы можете иметь дополнительные компоненты. Это:

Все три эти конструкции должны быть заключены в квадратные скобки скобочного выражения. Например [[:alpha:]!] соответствует любому отдельному буквенному символу или восклицательному знаку, [[.ch.]] соответствует элементу сортировки ch, но не соответствует только букве c или букве h. Во французском языке [[=e=]] может соответствовать любому из символов e, è или é. Классы и соответствующие им символы приведены в Таблице 3.3.

Таблица 3.3: Классы символов POSIX

Класс Группы символов
[:alnum:] Печатные символы (включая пробелы)
[:alpha:] Буквенные символы
[:blank:] Пробел и символы табуляции
[:cntrl:] Управляющие символы
[:digit:] Числовые символы
[:graph:] Печатаемые и видимые (без пробела) символы
[:lower:] Строчные символы
[:print:] Печатаемые символы (включая пробелы)
[:punct:] Знаки препинания
[:space:] Символы пробела
[:upper:] Символы верхнего регистра
[:xdigit:] Шестнадцатеричные цифры

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

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

3.2.5 Повторяющиеся вхождения символа

Метасимвол звездочка (*) указывает, что предыдущее регулярное выражение может встречаться ноль или более раз. То есть, если он следует после символа, то этот символ может отсутствовать в этом месте, или может быть в количестве от одного раза и более. Вы можете использовать метасимвол звездочки, чтобы сопоставить слово, которое может появиться в кавычках или без них.

□"*hypertext"*□

Слово «hypertext» будет соответствовать независимо от того, появляется ли оно в кавычках или без них.

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

1
5
10
50
100
500
1000
5000

Регулярное выражение

[15]0*

будет соответствовать всем строкам, тогда как регулярное выражение

[15]00*

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

□□*

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

".*"

Это будет соответствовать всем символам между первой и последней кавычками в строке плюс сами кавычки. Совпавший с шаблоном «.*» участок всегда самый длинный из возможных. Сейчас это может показаться не важным, но вы поймете, что это важно, как только узнаете о замене строки, которая была сопоставлена.

В качестве другого примера можно привести пару угловых скобок, которые обычно используются для обозначения инструкций форматирования, используемых в языках разметки, таких как SGML, HTML и Ventura Publisher.

Вы можете распечатать все строки с этими метками, указав:

$ grep '<.*>' sample

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

I can do it
I cannot do it
I can not do it
I can't do it
I cant do it

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

can[□no']*t

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

$ grep "can[ no']*t" sample
I cannot do it
I can not do it
I can't do it
I cant do it

Есть четыре совпадения и один пропуск, положительное утверждение. Обратите внимание, что если бы регулярное выражение пыталось сопоставить любое количество символов между строкой «can» и «t», как в следующем примере:

can.*t

это соответствовало бы всем строкам.

Способность сопоставить «ноль или более» чего-либо известна под техническим термином «closure» (замыкание). Расширенный набор метасимволов, используемых egrep и awk, предоставляет несколько вариантов замыкания, которые могут быть весьма полезны. Знак плюс (+) соответствует одному или нескольким вхождениям в предыдущем регулярном выражении. Наш предыдущий пример сопоставления одного или нескольких пробелов можно упростить до такого:

□+

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

Вопросительный знак (?) соответствует нулю или одному вхождению. Например, в предыдущем примере мы использовали регулярное выражение для сопоставления «80286», «80386» и «80486». Если бы мы хотели также соответствовать строке «8086», мы могли бы написать обычное регулярное выражение, которое можно было бы использовать с egrep или awk:

4980[234]?86

Оно совпадает со строкой «80», за которой следует «2», «3», «4» или нет символа, за которым следует строка «86». Не путай символ ? в регулярном выражении с символом ? подстановочным знаком в оболочке. В оболочке ? представляет собой один символ, эквивалентный точке (.) в регулярном выражении.

3.2.6 Что такое слово? Часть I

Как вы, вероятно, уже поняли, иногда бывает трудно подобрать полное слово. Например, если бы мы хотели соответствовать шаблону «book», наш поиск попадет в строки, содержащие слова «book» и «books», но также и слова «bookish», «handbook» и «booky». Очевидная вещь, которую нужно сделать, чтобы ограничить соответствие, - это окружить «book» пробелами.

□book□

Однако это выражение будет соответствовать только слову «book«; оно пропустит множественное число «books». Чтобы сопоставить слово в единственном или множественном числе, можно использовать метасимвол звездочки:

□books*□

Это будет соответствовать «book» или «books». Однако он не будет соответствовать «book», если за ним следует точка, запятая, вопросительный знак или кавычка. При объединении звездочки с метасимволом подстановочного знака (.) вы можете сопоставить ноль или более вхождений любого символа. В предыдущем примере мы могли бы написать более полное регулярное выражение следующим образом:

□book.*□

Это выражение соответствует строке «book», за которой следует «любое количество символов или ни одного, за которым следует пробел». Вот несколько строк, которые могли бы подойти:

Here are the books that you requested
Yes, it is a good book for children
It is amazing to think that it was called a "harmful book" when
once you get to the end of the book, you can't believe

(Обратите внимание, что только вторая строка будет соответствовать фиксированной строке «□book□».) Выражение «□book.*□» соответствует строкам, содержащим такие слова, как «booky», «bookworm» и «bookish». Мы могли бы исключить два из этих совпадений, используя другой модификатор. Вопросительный знак (?), который является частью расширенного набора метасимволов, соответствует 0 или 1 вхождению предыдущего символа. Таким образом, выражение:

□book.?□

соответствовало бы «book», «books» и «booky», но не «bookish» и «bookworm». Чтобы исключить такое слово, как «booky», мы должны были бы использовать классы символов, чтобы указать все символы в этой позиции, которым мы хотим соответствовать. Кроме того, поскольку метасимвол вопросительного знака недоступен в sed, нам все равно придется прибегнуть к классам символов, как вы увидите позже.

Попытка быть всеохватывающим не всегда практична с регулярным выражением, особенно при использовании grep. Иногда лучше всего сохранить выражение простым и учитывать исключения. Однако, поскольку вы используете регулярные выражения в sed для выполнения замен, вам нужно будет быть более уверенным в том, что ваше регулярное выражение является полным. Мы рассмотрим более понятное регулярное выражение для поиска слов в Части II раздела «Что такое слово?» позже в этой главе.

3.2.7 Позиционные метасимволы

Это два метасимвола, которые позволяют указать контекст, в котором появляется строка, либо в начале строки, либо в конце строки. Метасимвол крышка (или карет) (^) - это односимвольное регулярное выражение, указывающее начало строки. Метасимвол знака доллара ($) - это односимвольное регулярное выражение, указывающее на конец строки. Их часто называют «якорями», поскольку они закрепляют или ограничивают соответствие определенной позиции. Вы можете печатать строки, которые начинаются с табуляции:

^•

(Символ • представляет собой литеральный символ табуляции, который обычно невидим.) Без метасимвола ^ это выражение будет печатать любую строку, содержащую табуляцию.

Кроме того, используя vi для ввода текста, подлежащего обработке troff, вы не хотите, чтобы пробелы появлялись в конце строк. Если вы хотите найти (и удалить) их, это регулярное выражение будет соответствовать строкам с одним или несколькими пробелами в конце строки:

□□*$

Запросы troff и макросы должны быть введены в начале строки. Это двухсимвольные строки, перед которыми стоит точка. Если запрос или макрос имеет аргумент, за ним обычно следует пробел. Регулярное выражение, используемое для поиска таких запросов, является:

^\...□

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

Вы можете использовать оба позиционных метасимвола вместе, чтобы соответствовать пустым строкам:

^$

Этот шаблон можно использовать для подсчета количества пустых строк в файле с помощью опции count, -c, в grep:

$ grep -c '^$' ch04
5

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

^□*$

Точно так же вы можете сопоставить всю строку:

^.*$

а это то, что вы, возможно, захотите сделать в sed.

В sed (и grep) «^» и «$» являются особыми только тогда, когда они встречаются в начале или конце регулярного выражения соответственно. Таким образом, «^abc» означает «совпадение букв a, b и c только в начале строки», в то время как «ab^c» означает «совпадение букв a, b, символа ^, а затем c, в любом месте строки». То же самое верно и для «$».

В awk все по-другому; «^» и «$» всегда спецсимволы, хотя в этом случае становится возможным писать регулярные выражения, которые ни с чем не совпадают. Достаточно сказать, что в awk, когда вы хотите сопоставить литерал «^» или «$», вы всегда должны экранировать его обратной косой чертой, независимо от его положения в регулярном выражении.

3.2.7.1 Фразы

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

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

Almond Joy
Almond$
^Joy

Это не идеально, так как второе регулярное выражение будет соответствовать «Almond» в конце строки, независимо от того, начинается ли следующая строка с «Joy». Аналогичная проблема существует и с третьим регулярным выражением.

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

3.2.8 Набор символов

Метасимволы, позволяющие указать повторяющиеся вхождения символа (*+?) обозначают отрезок неопределенной длины. Рассмотрим следующее выражение:

11*0

Он будет соответствовать каждой из следующих строк:

10
110
111110
1111111111111111111111111110

Эти метасимволы придают эластичность регулярному выражению.

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

\{ и \} доступны в grep и sed*. POSIX egrep и POSIX awk используют { и }. В любом случае фигурные скобки заключают в себя один или два аргумента.

\{n,m\}

n и m - целые числа от 0 до 255. Если вы зададите \{n\} само по себе, то это будет соответствовать ровно n вхождений предыдущего символа или регулярного выражения. Если вы укажете \{n,\}, то это будет сопоставлено по крайней мере n вхождений. Если вы укажете \{n,m\}, то будет сопоставлено любое число вхождений между n и m**.

Например, следующее выражение будет соответствовать «1001», «10001» и «100001», но не «101» или «1000001»:

10\{2,4\}1

Эта пара метасимволов может быть полезна для сопоставления данных в полях фиксированной длины, которые, возможно, были извлечены из базы данных. Она также может быть использована для сопоставления форматированных данных, таких как телефонные номера, номера социального страхования США, идентификаторы инвентарных частей и т. д. Например, формат номера социального страхования состоит из трех цифр, дефиса, за которыми следуют две цифры, дефис и затем еще четыре цифры. Эту закономерность можно описать следующим образом:

[0-9]\{3\}-[0-9]\{2\}-[0-9]\{4\}

Аналогично, местный телефонный номер США может быть описан следующим регулярным выражением:

[0-9]\{3\}-[0-9]\{4\}

Если вы используете pre-POSIX awk, где у вас нет доступных фигурных скобок, вы можете просто повторить классы символов соответствующее количество раз:

[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]

* Очень старые версии могут не иметь их; Caveat emptor.

** Обратите внимание, что «?» эквивалентно «\{0,1\}», «*» эквивалентно «\{0,\}», «+» эквивалентно «\{1,\}», и никакой модификатор не эквивалентен «\{1\}».

3.2.9 Альтернативные операции

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

UNIX|LINUX

будут соответствовать строки, содержащие «UNIX» или «LINUX». Можно указать более одной альтернативы:

UNIX|LINUX|NETBSD

Строка, соответствующая любому из этих трех шаблонов, будет напечатана egrep.

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

3.2.10 Групповые операции

Круглые скобки () используются для группировки регулярных выражений и установления приоритетов. Они являются частью расширенного набора метасимволов. Предположим, что название компании в текстовом файле называется «BigOne» или «BigOne Computer»:

BigOne(□Computer)?

Это выражение будет соответствовать строке «BigOne» само по себе или сопровождаться одним вхождением строки «□Computer». Аналогично, если термин иногда прописывается, а в других случаях сокращается:

$ egrep "Lab(oratorie)?s" mail.list
Bell Laboratories, Lucent Technologies
Bell Labs

Вы можете использовать круглые скобки с вертикальной чертой для группировки альтернативных операций. В следующем примере мы используем его для указания соответствия единственного или множественного числа слова «компания».

compan(y|ies)

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

3.2.11 Что такое слово? Часть II

Давайте заново оценим регулярное выражение для поиска одного слова в свете новых метасимволов, которые мы обсуждали. Наша первая попытка написать регулярное выражение для grep для поиска слова завершилась следующим выражением:

□book.*□

Это выражение довольно простое, соответствующее пробелу, за которым следует строка «book», за которой следует любое количество символов, за которыми следует пробел. Однако это не соответствует всем возможным вхождениям и совпадает с несколькими ненужными нам словами.

Следующий тестовый файл содержит многочисленные вхождения «book». Мы добавили нотацию, которая не является частью файла, чтобы указать, должна ли входная строка быть «совпадением» (>) и включена в вывод или должна быть «несовпадением» (<). Мы постарались включить как можно больше различных примеров.

$ cat bookwords
> This file tests for book in various places, such as
> book at the beginning of a line or
> at the end of a line book
> as well as the plural books and
< handbooks. Here are some
< phrases that use the word in different ways:
> "book of the year award"
> to look for a line with the word "book"
> A GREAT book!
> A great book? No.
> told them about (the books) until it
> Here are the books that you requested
> Yes, it is a good book for children
> amazing that it was called a "harmful book" when
> once you get to the end of the book, you can't believe
< A well-written regular expression should
< avoid matching unrelated words,
< such as booky (is that a word?)
< and bookish and
< bookworm and so on.

Когда мы ищем вхождения слова «book», есть 13 строк, которые должны быть сопоставлены, и 7 строк, которые не должны быть сопоставлены. Во-первых, давайте запустим предыдущее регулярное выражение в файле примера и проверим результаты.

$ grep ' book.* ' bookwords
This file tests for book in various places, such as
as well as the plural books and
A great book? No.
told them about (the books) until it
Here are the books that you requested
Yes, it is a good book for children
amazing that it was called a "harmful book" when
once you get to the end of the book, you can't believe
such as booky (is that a word?)
and bookish and

grep печатает только 8 из 13 строк, которые мы хотим сопоставить, и он печатает 2 строки, которые мы не хотим сопоставлять. Выражение соответствует строкам, содержащим слова «booky» и «bookish». При этом игнорирует «book» в начале и в конце строки. Он игнорирует «book», когда речь идет о некоторых знаках препинания. Чтобы еще больше ограничить поиск, мы должны использовать классы символов. Как правило, список символов, которые могут заканчивать слово, - это знаки препинания, такие как:

? . , ! ; : '

Кроме того, кавычки и скобки - круглые, фигурные и квадратные - могут окружать слово полностью, либо открывать или закрывать слово:

" () {} []

Вы также должны были бы приспособить множественное число или притяжательные формы слова.

Таким образом, у вас будет два разных класса символов: до и после слова. Помните, что все, что нам нужно сделать, это перечислить члены класса в квадратных скобках. Перед словом мы теперь имеем:

["[{(]

и после слова:

[]})"?!.,;:'s ]

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

□["[{(]*book[]})"?!.,;:'s]*□

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

$ grep " [\"[{(]*book[]})\"?!.,;:'s]* " bookwords
This file tests for book in various places, such as
as well as the plural books and
A great book? No.
told them about (the books) until it
Here are the books that you requested
Yes, it is a good book for children
amazing that it was called a "harmful book" when
once you get to the end of the book, you can't believe

Мы устранили строки, которые нам не нужны, но есть четыре строки, которые нам нужны, но мы их не получаем. Давайте их рассмотрим:

book at the beginning of a line or
at the end of a line book
"book of the year award"
A GREAT book!

Все эти проблемы вызваны тем, что строка появляется в начале или в конце строки. Поскольку в начале или в конце строки нет пробела, шаблон не соответствует. Мы можем использовать позиционные метасимволы ^ и $. Поскольку мы хотим сопоставить либо пробел, либо начало или конец строки, мы можем использовать egrep и указать метасимвол «или» вместе с круглыми скобками для группировки. Например, чтобы соответствовать началу строки или пробелу, можно написать выражение:

(^|□)

(Поскольку | и () являются частью расширенного набора метасимволов, если бы вы использовали sed, вам пришлось бы писать различные выражения для обработки каждого случая.)

Вот исправленное регулярное выражение:

(^|□)["[{(]*book[]})"?\!.,;:'s]*(□|$)

Теперь давайте посмотрим, как это работает:

$ egrep "(^| )[\"[{(]*book[]})\"?\!.,;:'s]*( |$)" bookwords
This file tests for book in various places, such as
book at the beginning of a line or
at the end of a line book
as well as the plural books and
"book of the year award"
to look for a line with the word "book"
A GREAT book!
A great book? No.
told them about (the books) until it
Here are the books that you requested
Yes, it is a good book for children
amazing that it was called a "harmful book" when
once you get to the end of the book, you can't believe

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

Вы также можете создать простой сценарий оболочки, чтобы заменить «book» аргументом командной строки. Единственная проблема может заключаться в том, что множественное число некоторых слов - это не просто «s». Ловкостью рук вы могли бы справиться с множественным числом «es», добавив «e» к классу символов, следующему за словом; это сработало бы во многих случаях.

Кроме того, текстовые редакторы ex и vi имеют специальный метасимвол для сопоставления строки в начале слова, \<, и один для сопоставления строки в конце слова, \>. Используемые как пара, они могут соответствовать строке только тогда, когда это полное слово. (Для этих операторов слово - это строка непробельных символов с пробелами с обеих сторон или в начале или в конце строки.) Сопоставление слова является настолько распространенным случаем, что эти метасимволы были бы широко использованы, если бы они были доступны для всех регулярных выражений*.

* Программы GNU, такие как GNU-версии awk, sed и grep, также поддерживают \< и \>.

3.2.12 Ваша замена здесь

При использовании grep редко имеет значение, как вы соответствуете строке, если вы соответствуете ей. Однако, когда вы хотите произвести замену, вы должны учитывать степень соответствия. Итак, с какими символами в строке вы на самом деле совпали?

В этом разделе мы рассмотрим несколько примеров, демонстрирующих степень соответствия. Затем мы воспользуемся программой, которая работает как grep, но также позволяет указать строку замены. Наконец, мы рассмотрим несколько метасимволов, используемых для описания строки замены.

3.2.12.1 Степень совпадения

Давайте рассмотрим следующее регулярное выражение:

A*Z

Шаблон соответствует «нулю или более вхождениям буквы A, за которой следует Z». Результат будет тот же, что и при простом указании «Z». Буква «A» может присутствовать или нет; фактически, буква «Z» - единственный совпадающий символ. Вот пример двухстрочного файла:

All of us, including Zippy, our dog
Some of us, including Zippy, our dog

Если мы попытаемся сопоставить предыдущее регулярное выражение, то выведем обе строки. Интересно, что фактическое совпадение в обоих случаях производится на «Z» и только на «Z». Мы можем использовать команду gres (см.ниже врезку «Программа для выполнения одиночных замен»), чтобы продемонстрировать степень соответствия.

$ gres "A*Z" "00" test
All of us, including 00ippy, our dog
Some of us, including 00ippy, our dog

Программа для выполнения одиночных замен

The MKS Toolkit, набор утилит UNIX для DOS от Mortice Kern Systems, Inc., содержит очень полезную программу под названием gres (global regular expression substitution). Точно так же, как grep, она ищет шаблон в файле; однако она позволяет указать замену для строки, которой вы соответствуете. Эта программа на самом деле является упрощенной версией sed, и, как и sed, она печатает все строки независимо от того, была ли произведена замена или нет. Она не делает замену в самом файле. Вы должны перенаправить вывод из программы в файл, если хотите сохранить изменения.

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

#!/bin/sh
$ cat gres
if [ $# -lt 3 ]
then
    echo Usage: gres pattern replacement file >&2
    exit 1
fi
pattern=$1
replacement=$2
if [ -f $3 ]
then
    file=$3
else
    echo $3 is not a file. >&2
    exit 1
fi
A="‘echo | tr '\012' '\001' ‘" # См. сноску*

sed -e "s$A$pattern$A$replacement$A" $file

В остальной части главы мы будем использовать gres, чтобы продемонстрировать использование замещающих метасимволов. Помните, что все, что относится к gres, относится и к sed. Здесь мы заменяем строку, соответствующую регулярному выражению «A.*Z», двойным нулем (00).

$ gres "A.*Z" "00" sample
00ippy, our dog
00iggy
00elda

* Строка echo | tr ... - это сложный, но переносимый способ генерирования символа Control-A, который будет использоваться в качестве разделителя для команды замены sed. Это значительно снижает вероятность появления символа разделителя в шаблоне или текстах замены.

Мы ожидали бы, что степень соответствия на первой строке будет от «A» до «Z», но на самом деле соответствует только «Z». Этот результат может быть более очевидным, если мы немного изменим регулярное выражение:

A.*Z

«.*» может быть истолковано как «ноль или более вхождений любого символа», что означает, что «любое количество символов» может быть найдено, включая ни одного вообще. Все выражение может быть оценено как «A, за которым следует любое количество символов, за которыми следует Z». «A» - это начальный символ в шаблоне, а «Z» - последний символ; между ними может произойти что угодно или ничего. Запуск grep в одном и том же двухстрочном файле приводит к получению одной строки вывода. Мы добавили строку каре (^) внизу, чтобы отметить, что было сопоставлено.

All of us, including Zippy, our dog
^^^^^^^^^^^^^^^^^^^^^^

Степень совпадения - от «A» до «Z». То же самое регулярное выражение будет также соответствовать следующей строке:

I heard it on radio station WVAZ 1060.
                              ^^

Строка «A.*Z» соответствует «A, за которым следует любое количество символов (включая ноль), за которым следует Z». Теперь давайте посмотрим на аналогичный набор образцов строк, которые содержат несколько вхождений «A» и «Z».

All of us, including Zippy, our dog
All of us, including Zippy and Ziggy
All of us, including Zippy and Ziggy and Zelda

Регулярное выражение «A.*Z» будет соответствовать максимально возможному совпадению в каждом конкретном случае.

All of us, including Zippy, our dog
^^^^^^^^^^^^^^^^^^^^^^
All of us, including Zippy and Ziggy
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
All of us, including Zippy and Ziggy and Zelda
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Это может вызвать проблемы, если вы хотите, чтобы совпадение было как можно более коротким.

3.2.13 Ограничение протяжения (ограничение жадности)

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

".*"

Давайте рассмотрим макрос troff, который имеет два приведенных аргумента, как показано ниже:

.Se "Appendix" "Full Program Listings"

Чтобы соответствовать первому аргументу, мы могли бы описать шаблон с помощью следующего регулярного выражения:

\.Se ".*"

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

\.Se ".*" ".*"

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

"[^"]*"

Оно соответствует «кавычке, за которой следует любое количество символов, которые не соответствуют кавычке, за которыми следует кавычка»:

$ gres '"[^"]*"' '00' sampleLine
.Se 00 "Full Program Listings"

Теперь давайте рассмотрим несколько строк с символом точки (.), используемым в качестве заполнителя между двумя столбцами чисел:

1........5
5........10
10.......20
100......200

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

[0-9][0-9]*\.\.*[0-9][0-9]*

Это выражение может неожиданно соответствовать строке:

see Section 2.3

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

[0-9][0-9]*\.\{5,\}[0-9][0-9]*

Это выражение использует фигурные скобки, доступные в sed, чтобы соответствовать «одному числу, за которым следует по крайней мере пять точек, а затем следует одно число». Чтобы увидеть это в действии, мы покажем команду sed, которая заменяет ведущие точки дефисом. Однако мы не рассмотрели синтаксис замещающих метасимволов sed - \( и \) для сохранения части регулярного выражения и \1 и \2 для вызова сохраненной части. Эта команда, следовательно, может выглядеть довольно сложной (это так!) но она выполняет свою работу.

$ sed 's/\([0-9][0-9]*\)\.\{5,\}\([0-9][0-9]*\)/\1-\2/' sample
1-5
5-10
10-20
100-200

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

3.3 Я никогда не использовал метасимволы, которые мне не нравились

В Таблице 3.4 приведены интересные примеры регулярных выражений, многие из которых были описаны в этой главе.

Таблица 3.4: Полезные регулярные выражения

Применение Регулярное выражение
Почтовая аббревиатура штата □[A-Z][A-Z]□
Город, штат ^.*,□[A-Z][A-Z]
Город, штат, индекс (POSIX egrep) ^.*,□[A-Z][A-Z]□[0-9]{5}(-[0-9]{4})?
Месяц, день, год [A-Z][a-z]\{3,9\}□[0-9]\{1,2\},□[0-9]\{4\}
Номер социального страхования [0-9]\{3\}-[0-9]\{2\}-[0-9]\{4\}
Местный телефон (США) [0-9]\{3\}-[0-9]\{4\}
Форматированная сумма ($) \$[□0-9]*\.[0-9][0-9]
Запросы встроенного шрифта troff \\f[(BIRP]C*[BW]*
Запросы troff ^\.[a-z]\{2\}
Макросы troff ^\.[A-Z12].
Макросы troff с аргументами ^\.[A-Z12].□".*"
HTML тег <[^>]*>
Стиль в Ventura Publisher ^@.*□=□.*
Пустая строка ^$
Вся строка ^.*$
Один или более пробелов □□*

Глава 4. Написание сценариев sed

Чтобы использовать sed, вы пишете сценарий, который содержит серию действий редактирования, а затем запускаете сценарий для входного файла. Sed позволяет вам взять то, что было бы практической процедурой (hands-on) в редакторе, таком как vi, и преобразовать ее в автоматическую процедуру (look-no-hands), которая выполняется из сценария.

При редактировании вручную вы начинаете доверять причинно-следственной связи, заключающейся в том, что вы вводите команду редактирования и видите немедленный результат. Обычно существует команда «undo», которая позволяет отменить действие команды и вернуть текстовый файл в его предыдущее состояние. Изучив интерактивный текстовый редактор, вы почувствуете, что вносите изменения безопасным и контролируемым образом, шаг за шагом.

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

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

  1. Хорошо подумайте, что вы хотите сделать, прежде чем делать это.
  2. Однозначно опишите, как это сделать.
  3. Протестируйте процедуру несколько раз, прежде чем вносить какие-либо окончательные изменения.

Эти шаги - просто повторение того же процесса, который мы описали для написания регулярных выражений в Главе 3, Понимание синтаксиса регулярных выражений. Они описывают методологию написания программ любого типа. Лучший способ проверить, работает ли ваш скрипт, - это запустить тесты на различных входных выборках и наблюдать за результатами.

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

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

После рассмотрения этих основных принципов мы рассмотрим четыре типа сценариев, демонстрирующих различные приложения sed. Эти сценарии предоставляют базовые модели для сценариев, которые вы будете писать. Хотя в sed доступно несколько команд, сценарии в этой главе намеренно используют только несколько команд. Тем не менее, вы можете быть удивлены, как много вы можете сделать с таким малым количеством. (Глава 5, Основные команды sed и Глава 6, Расширенные команды sed представляют основные и расширенные команды sed соответственно.) Идея состоит в том, чтобы с самого начала сосредоточиться на понимании того, как работает скрипт и как его использовать, прежде чем исследовать все команды, которые могут быть использованы в скриптах.

4.1 Применение команд в скрипте

Объединение серий изменений в сценарии может привести к неожиданным результатам. Вы можете не думать о последствиях одной правки для другой. Новые пользователи обычно думают, что sed применяет отдельную команду редактирования ко всем строкам ввода перед применением следующей команды редактирования. Но все наоборот. Sed применяет весь сценарий к первой строке ввода перед чтением второй строки ввода и применением к ней сценария редактирования. Поскольку sed всегда работает с последней версией исходной строки, любое внесенное изменение изменяет строку для последующих команд. Sed не сохраняет оригинал. Это означает, что шаблон, который мог соответствовать исходной строке ввода, может больше не соответствовать строке после того, как было сделано редактирование.

Давайте рассмотрим пример, в котором используется команда замены. Предположим, кто-то быстро написал следующий сценарий, чтобы заменить «pig» на «cow» и «cow» на «horse»:

s/pig/cow/g
s/cow/horse/g

Как вы думаете, что произошло? Попробуйте сделать это на примере файла. Мы обсудим то, что произошло позже, после того, как посмотрим, как работает sed.

4.1.1 Буфер шаблона (pattern space)

Sed поддерживает буфер шаблона (pattern space), рабочее пространство или временный буфер, в котором удерживается одна строка ввода, пока применяются команды редактирования*. Преобразование буфера шаблона двухстрочным скриптом показано на Рис. 4.1. Он изменяет «The Unix System» на «The UNIX Operating System».

Изначально буфер шаблона содержит копию одной входной строки. На Рис. 4.1 эта строка содержит «The Unix System». Обычно сценарий выполняет каждую команду для этой строки до тех пор, пока не будет достигнут конец сценария. К этой строке применяется первая команда сценария, изменяющая «Unix» на «UNIX». Затем применяется вторая команда, изменяющая «UNIX System» на «Unix Operating System»**. Обратите внимание, что шаблон для второй команды замены не соответствует исходной входной строке; он соответствует текущей строке, поскольку она изменилась в буфере шаблона.

Рис. 4.1: Команды в скрипте изменяют содержимое буфера шаблона

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

Как следствие, любая команда sed может изменить содержимое буфера шаблона для следующей команды. Содержимое буфера шаблона является динамическим и не всегда соответствует исходной строке ввода. В этом и заключалась проблема с образцом сценария в начале этой главы. Первая команда изменит «pig» на «cow», как и ожидалось. Однако, когда вторая команда изменила «cow» на «horse» в той же строке, она также изменила «cow», которая была «pig». Итак, если входной файл содержал pig и cow, то выходной - только horse!

Эта ошибка - просто проблема порядка команд в скрипте. Изменение порядка команд на противоположный - «cow» на «horse» перед изменением «pig» на «cow» - позволяет добиться нужной цели.

s/cow/horse/g
s/pig/cow/g

Некоторые команды sed изменяют поток выполнения скрипта, как мы увидим в следующих главах. Например, команда N считывает следующую строку в буфер шаблона, не удаляя текущую, поэтому вы можете проверить шаблоны в нескольких строках. Другие команды говорят sed выйти, не дойдя до конца скрипта, или перейти к помеченной команде. Sed также поддерживает второй временный буфер, называемый буфер хранения (hold space). Вы можете скопировать содержимое области шаблона в область удержания и получить его позже. Команды, использующие буфер хранения, обсуждаются в Главе 6.

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

** Да, мы могли бы изменить «Unix System» на «Unix Operating System» за один шаг. Однако входной файл может содержать экземпляры «UNIX System», а также «Unix System». Таким образом, изменяя «Unix» на «UNIX», мы делаем оба экземпляра согласованными, прежде чем изменить их на «UNIX Operating System».

4.2 Глобальная перспектива адресации

Первое, что вы заметите в sed, - это то, что sed свои команды применяет к каждой строке ввода. Sed неявно глобален, в отличие от ed, ex или vi. Следующая команда изменит каждый «CA» на «California».

s/CA/California/g

Если эта же команда будет введена из командной строки ex в vi, она произведет замену для всех вхождений только в текущей строке. В sed это похоже на то, как если бы каждая строка стала текущей строкой, и поэтому команда применяется к каждой строке. Адреса строк используются для предоставления контекста или ограничения операции. (Короче говоря: в vi ничего не будет сделано, если вы не укажете, над какими строками работать, а sed будет работать с каждой строкой, если вы не укажете ему этого не делать.) Например, указав адрес «Sebastopol» в предыдущей команде замены, мы можем ограничить замену «CA» на «California» только строками, содержащими «Sebastopol».

/Sebastopol/s/CA/California/g

Входная строка, состоящая из «Sebastopol, CA», будет соответствовать адресу, и будет сделана замена ее на «Sebastopol, California». Строка, состоящая из «San Francisco, CA», не совпадет с адресом, и замена не будет сделана.

Команде sed можно указать ноль, один или два адреса. Адрес может быть регулярным выражением, описывающим шаблон, номер строки или символ адресации строки.

Чтобы проиллюстрировать, как работает адресация, давайте рассмотрим примеры использования команды delete, d. Скрипт, состоящий просто из команды d и никакого адреса, не производит никаких выходных данных, так как он удаляет все строки:

d

Если в качестве адреса указан номер строки, команда влияет только на эту строку. Например, в следующем примере удаляется только первая строка:

1d

Номер строки относится к внутреннему счетчику строк, поддерживаемому sed. Этот счетчик не сбрасывается для нескольких входных файлов. Таким образом, независимо от того, сколько файлов было указано в качестве входных, во входном потоке есть только одна строка 1.

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

$d

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

Если в качестве адреса указано регулярное выражение, команда влияет только на строки, соответствующие этому шаблону. Регулярное выражение должно быть заключено в косую черту (/). Следующая команда удаления

/^$/d

удаляет только пустые строки. Все остальные строки остаются нетронутыми.

Если вы указываете два адреса, то это соответствует диапазону строк, для которого выполняется команда. В следующем примере показано, как удалить все строки, ограниченные парой макросов, в данном случае .TS и .TE, которые отмечают tbl ввод.

/^\.TS/,/^\.TE/d

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

50,$d

Вы можете смешивать адрес строки и адрес, заданный регулярным выражением:

1,/^$/d

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

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

Восклицательный знак (!) после адреса меняет смысл совпадения. Например, следующий скрипт удаляет все строки, кроме тех, что находятся внутри tbl ввода:

/^\.TS/,/^\.TE/!d

Этот сценарий, по сути, извлекает входные данные tbl из исходного файла.

4.2.1 Группировка команд

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

/^\.TS/,/^\.TE/{
    /^$/d
}

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

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

/^\.TS/,/^\.TE/{
    /^$/d
    s/^\.ps 10/.ps 8/
    s/^\.vs 12/.vs 10/
}

Этот пример не только удаляет пустые строки в tbl вводе, но и использует команду замены, s, для изменения нескольких запросов troff. Эти команды применяются только к строкам внутри блока .TS/.TE.

4.3 Тестирование и сохранение выходных данных

В нашем предыдущем обсуждении буфера шаблона вы видели, что sed:

  1. Создает копию входной строки.
  2. Изменяет эту копию в буфере шаблона.
  3. Выводит копию на стандартный вывод.

Это означает, что sed имеет встроенную защиту от изменений исходного файла. Таким образом, следующая командная строка:

$ sed -f sedscr testfile

не вносит изменений в testfile. Она отправляет все строки в стандартный вывод (обычно экран) - строки, которые были изменены, а также строки, которые остались неизменными. Вы должны записать этот вывод в новый файл, если хотите сохранить его.

$ sed -f sedscr testfile > newfile

Символ перенаправления «>» направляет выходные данные из sed в файл newfile. Не перенаправляйте вывод команды обратно во входной файл, иначе вы перепишете входной файл. Это произойдет еще до того, как sed получит возможность обработать файл, эффективно деструктируя ваши данные.

Одна из важных причин для повторного копирования выходных данных в файл - это проверка ваших результатов. Вы можете изучить содержимое newfile и сравнить его с testfile. Если вы хотите быть очень методичным в проверке ваших результатов (а вы должны быть таковым), используйте программу diff, чтобы указать на различия между этими двумя файлами.

$ diff testfile newfile

Эта команда отобразит строки, уникальные для testfile, символом «<» в начале этих строк, а уникальные для newfile, символом «>» в начале строк. После проверки результатов сделайте резервную копию исходного входного файла, а затем используйте команду mv, чтобы перезаписать оригинал новой версией. Перед тем, как отказаться от исходной версии, убедитесь, что скрипт редактирования работает правильно.

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

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

4.3.1 testsed

Сценарий оболочки testsed автоматизирует процесс сохранения вывода sed во временный файл. Он ожидает найти файл сценария sedscr в текущем каталоге и применяет эти инструкции к входному файлу, указанному в командной строке. Вывод помещаются во временный файл.

for x
do
    sed -f sedscr $x > tmp.$x
done

Имя файла необходимо указать в командной строке. В результате этот сценарий оболочки сохраняет вывод во временном файле с префиксом «tmp.». Вы можете проверить временный файл, чтобы определить, правильно ли были внесены изменения. Если вы одобряете результаты, вы можете использовать mv для перезаписи исходного файла временным файлом.

Вы также можете включить команду diff в сценарий оболочки. (Добавить diff $x tmp.$x после команды sed.)

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

4.3.2 runsed

Скрипт runsed был разработан для постоянного внесения изменений во входной файл. Другими словами, он используется в тех случаях, когда вы хотите, чтобы входной и выходной файл были одинаковыми. Как и в testsed, он создает временный файл, но затем делает следующий шаг: копирует файл поверх оригинала.

#! /bin/sh

for x
do
    echo "editing $x: \c"
    if test "$x" = sedscr; then
        echo "not editing sedscript!"
    elif test -s $x; then
        sed -f sedscr $x > /tmp/$x$$
        if test -s /tmp/$x$$
        then
            if cmp -s $x /tmp/$x$$
            then
                echo "file not changed: \c"
            else
                mv $x $x.bak # save original, just in case
                cp /tmp/$x$$ $x
            fi
            echo "done"
        else
            echo "Sed produced an empty file\c"
            echo " - check your sedscript."
        fi
        rm -f /tmp/$x$$
    else
        echo "original file is empty."
    fi
done
echo "all done"

Чтобы использовать runsed, создайте сценарий sed с именем sedscr в каталоге, где вы хотите внести изменения. Укажите имя или имена файлов для редактирования в командной строке. Метасимволы оболочки можно использовать для указания набора файлов.

$ runsed ch0?

runsed просто вызывает sed -f sedscr для именованных файлов, по одному за раз, и перенаправляет выходные данные во временный файл. Затем runsed проверяет этот временный файл, чтобы убедиться, что выходные данные были получены перед копированием его поверх оригинала.

Сила этого сценария оболочки (строка 9) по существу такая же, как и testsed. Дополнительные строки предназначены для тестирования на неудачные запуски - например, когда выход не производится. Он сравнивает два файла, чтобы увидеть, действительно ли были внесены изменения или был создан пустой выходной файл перед перезаписью оригинала.

Однако runsed не защищает вас от несовершенных сценариев редактирования. Сначала вы должны использовать testsed для проверки ваших изменений, прежде чем фактически сделать их постоянными с использованием runsed.

4.4 Четыре типа сценариев sed

В этом разделе мы рассмотрим четыре типа сценариев, каждый из которых иллюстрирует типичное приложение sed.

4.4.1 Несколько правок в одном файле

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

Один из авторов однажды написал проект для компьютерной компании, именуемой здесь BigOne Computer. Документ должен был включать бюллетень продукта для «Horsefeathers Software». Компания пообещала, что бюллетень по продукту будет в сети и что они его отправят. К сожалению, когда файл прибыл, он содержал форматированный вывод для строчного принтера, единственный способ, которым они могли его предоставить. Далее следует часть этого файла (сохраненная для тестирования в файле с именем horsefeathers).

                 HORSEFEATHERS SOFTWARE PRODUCT BULLETIN

  DESCRIPTION
+   _____________

  BigOne Computer  offers three  software packages from the  suite
  of Horsefeathers  software products  --  Horsefeathers  Business
  BASIC, BASIC  Librarian,  and LIDO. These software products  can
  fill   your    requirements    for   powerful,    sophisticated,
  general-purpose business  software providing you with a base for
  software customization or development.

  Horsefeathers  BASIC is  BASIC optimized for use on  the  BigOne
  machine with UNIX  or MS-DOS operating systems.  BASIC Librarian
  is a full screen program editor, which also provides the ability

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

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

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

Вот список очевидных изменений, которые необходимо внести в бюллетень Horsefeathers Software:

  1. Заменить все пустые строки макросом абзаца (.LP).
  2. Удалить все начальные пробелы из каждой строки.
  3. Удалить строку подчеркивания принтера, которая начинается со знака «+».
  4. Удалить несколько пробелов, добавленных между словами.

Первое редактирование требует сопоставления пустых строк. Однако при просмотре входного файла не было очевидно, есть ли в пустых строках начальные пробелы или нет. Оказалось, что это не так, поэтому пустые строки можно сопоставить с помощью шаблона «^$». (Если в строке есть пробелы, то паттерн можно записать как «^□*$».) Таким образом, первое редактирование довольно просто выполнить:

s/^$/.LP/

Команда заменяет каждую пустую строку на «.LP». Обратите внимание, что вы не экранируете точку в команде замены. Мы можем поместить эту команду в файл с именем sedscr и протестировать ее следующим образом:

$ sed -f sedscr horsefeathers
                  HORSEFEATHERS SOFTWARE PRODUCT BULLETIN
.LP
  DESCRIPTION
+   _____________
.LP
  BigOne Computer  offers three  software packages from the  suite
  of Horsefeathers  software products  --  Horsefeathers  Business
  BASIC, BASIC  Librarian,  and LIDO. These software products  can
  fill   your    requirements    for   powerful,    sophisticated,
  general-purpose business  software providing you with a base for
  software customization or development.
.LP
  Horsefeathers  BASIC is  BASIC optimized for use on  the  BigOne
  machine with UNIX  or MS-DOS operating systems.  BASIC Librarian
  is a full screen program editor, which also provides the ability

Совершенно очевидно, какие строки изменились. (Часто бывает полезно вырезать часть файла для тестирования. Лучше всего, если эта часть будет достаточно маленькой, чтобы поместиться на экране, но при этом достаточно большой, чтобы включить различные примеры того, что вы хотите изменить. После того как все изменения были успешно применены к тестовому файлу, происходит второй уровень тестирования, когда вы применяете их к полному исходному файлу.)

Следующая правка, которую мы делаем, - это удаление строки, начинающейся с «+» и содержащей подчеркивание строчного принтера. Мы можем просто удалить эту строку с помощью команды delete, d. при написании шаблона, соответствующего этой строке, у нас есть несколько вариантов выбора. Каждая из следующих строк будет соответствовать этой строке:

/^+/
/^+□/
/^+□□*/
/^+□□*__*/

Как видите, каждое последующее регулярное выражение соответствует большему количеству символов. Только путем тестирования вы можете определить, насколько сложным должно быть выражение, чтобы оно соответствовало конкретной строке, а не другим. Чем длиннее шаблон, который вы определяете в регулярном выражении, тем вероятнее, что он не приведет к нежелательным совпадениям. Для этого скрипта выберем третье выражение:

/^+□□*/d

Эта команда удалит любую строку, которая начинается со знака плюс и за которой следует хотя бы один пробел. В шаблоне указываются два пробела, но второй пробел изменен знаком «*», что означает, что второй пробел может быть, а может и отсутствовать.

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

s/^□□*//

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

Мы можем добавить эту команду в сценарий и протестировать его.

$ sed -f sedscr horsefeathers
HORSEFEATHERS SOFTWARE PRODUCT BULLETIN
.LP
DESCRIPTION
.LP
BigOne Computer  offers three  software packages from the  suite
of Horsefeathers  software products  --  Horsefeathers  Business
BASIC, BASIC  Librarian,  and LIDO. These software products  can
fill   your    requirements    for   powerful,    sophisticated,
general-purpose business  software providing you with a base for
software customization or development.
.LP
Horsefeathers  BASIC is  BASIC optimized for use on  the  BigOne
machine with UNIX  or MS-DOS operating systems.  BASIC Librarian
is a full screen program editor, which also provides the ability

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

s/□□*/□/g

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

Давайте протестируем новый скрипт:

$ sed -f sedscr horsefeathers
HORSEFEATHERS SOFTWARE PRODUCT BULLETIN
.LP
DESCRIPTION
.LP
BigOne Computer offers three software packages from the suite
of Horsefeathers software products -- Horsefeathers Business
BASIC, BASIC Librarian, and LIDO. These software products can
fill your requirements for powerful, sophisticated,
general-purpose business software providing you with a base for
software customization or development.
.LP
Horsefeathers BASIC is BASIC optimized for use on the BigOne
machine with UNIX or MS-DOS operating systems. BASIC Librarian
is a full screen program editor, which also provides the ability

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

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

s/\.□□*/.□□/g

Эта команда заменяет точку, за которой следует любое количество пробелов, на точку, за которой следуют два пробела. Следует отметить, что предыдущая команда сокращает несколько пробелов до одного, так что после точки будет найден только один пробел**. Тем не менее, этот шаблон работает независимо от того, сколько пробелов следует за точкой, если есть хотя бы один. (Например, это не повлияет на имя файла формы test.ext, если оно появилось в документе.) Эта команда помещается в конец сценария и проверяется:

$ sed -f sedscr horsefeathers
HORSEFEATHERS SOFTWARE PRODUCT BULLETIN
.LP
DESCRIPTION
.LP
BigOne Computer offers three software packages from the suite
of Horsefeathers software products -- Horsefeathers Business
BASIC, BASIC Librarian, and LIDO. These software products can
fill your requirements for powerful, sophisticated,
general-purpose business software providing you with a base for
software customization or development.
.LP
Horsefeathers BASIC is BASIC optimized for use on the BigOne
machine with UNIX or MS-DOS operating systems. BASIC Librarian
is a full screen program editor, which also provides the ability

Это работает. Вот готовый сценарий:

s/^$/.LP/
/^+□□*/d
s/^□□*//
s/□□*/□/g
s/\.□□*/.□□/g

Как мы уже говорили ранее, следующим этапом будет тестирование скрипта на всем файле (hf.product.bulletin) с помощью testsed и тщательное изучение результатов. Когда мы будем удовлетворены результатами, мы сможем использовать runsed, чтобы сделать изменения постоянными:

$ runsed hf.product.bulletin
done

Выполнив runsed, мы перезаписали исходный файл.

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

* Эта команда также будет соответствовать только одному пробелу. Но поскольку замена также представляет собой одиночный пробел, такой случай, по сути, является «no-op» (пустой командой).

** Таким образом, команда может быть упрощена до: s/\.□/.□□/g

4.4.2 Внесение изменений в набор файлов

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

Пример, который мы рассматриваем в этом разделе, представляет собой сценарий преобразования, предназначенный для изменения различных «машинно-зависимых» терминов в наборе документации UNIX. Один человек просмотрел комплект документации и составил список вещей, которые нужно было изменить. Другой человек работал с этим списком, чтобы создать следующий список замен.

s/ON switch/START switch/g
s/ON button/START switch/g
s/STANDBY switch/STOP switch/g
s/STANDBY button/STOP switch/g
s/STANDBY/STOP/g
s/[cC]abinet [Ll]ight/control panel light/g
s/core system diskettes/core system tape/g
s/TERM=542[05] /TERM=PT200 /g
s/Teletype 542[05]/BigOne PT200/g
s/542[05] terminal/PT200 terminal/g
s/Documentation Road Map/Documentation Directory/g
s/Owner\/Operator Guide/Installation and Operation Guide/g
s/AT&T 3B20 [cC]omputer/BigOne XL Computer/g
s/AT&T 3B2 [cC]omputer/BigOne XL Computer/g
s/3B2 [cC]omputer/BigOne XL Computer/g
s/3B2/BigOne XL Computer/g

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

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

Использование grep для изучения большого количества вводимых данных может быть очень полезным. Например, если вы хотите определить, как «core system diskettes» появляются в документах, вы можете найти его повсюду и детально изучить список. Чтобы быть точным, вы должны выполнить grep для «core», «core system», «system diskettes» и «diskettes», чтобы искать вхождения, разбитые на несколько строк. (Вы также можете использовать сценарий phrase из Главы 6 для поиска вхождений нескольких слов в последовательных строках.) Изучение ввода - лучший способ узнать, что должен делать ваш сценарий.

В некотором смысле написание сценария похоже на выдвижение гипотезы на основе определенного набора фактов. Вы пытаетесь доказать справедливость гипотезы, увеличивая количество данных, с которыми вы ее проверяете. Если вы собираетесь запускать сценарий для нескольких файлов, используйте testsed для запуска сценария на нескольких десятках файлов после того, как вы протестировали его на меньшем образце. Затем сравните временные файлы с оригиналами, чтобы убедиться, что ваши предположения верны. Сценарий может немного отличаться, и вы можете его исправить. Чем больше времени вы тратите на тестирование, что на самом деле является довольно интересной работой, тем меньше шансов, что вы потратите свое время на решение проблем, вызванных неудачным скриптом.

4.4.3 Извлечение содержимого файла

Один из типов приложения sed используется для извлечения соответствующего материала из файла. Таким образом, sed работает так же, как grep, с дополнительным преимуществом, заключающимся в том, что ввод может быть изменен до вывода. Этот тип сценария является хорошим кандидатом на роль сценария оболочки.

Вот два примера: извлечение определения макроса из пакета макросов и отображение структуры документа.

4.4.3.1 Извлечение определения макроса

Макросы troff определяются в пакете макросов, часто это один файл, расположенный в каталоге, таком как /usr/lib/macros. Определение макроса troff всегда начинается со строки «.de», за которой следует необязательный пробел и одно- или двухбуквенное имя макроса. Определение заканчивается строкой, начинающейся с двух точек (..). Сценарий, который мы покажем в этом разделе, извлекает конкретное определение макроса из пакета макросов. (Это избавляет вас от необходимости находить и открывать файл с помощью редактора и искать строки, которые вы хотите изучить.)

Первым шагом в разработке этого скрипта является написание скрипта, который извлекает определенный макрос, в данном случае макрос BL (Bulleted List - маркированный список) в пакете -mm*.

$ sed -n "/^\.deBL/,/^\.\.$/p" /usr/lib/macros/mmt
.deBL
.if\\n(.$<1 .)L \\n(Pin 0 1n 0 \\*(BU
.if\\n(.$=1 .LB 0\\$1 0 1 0 \\*(BU
.if\\n(.$>1 \{.ie !\w^G\\$1^G .)L \\n(Pin 0 1n 0 \\*(BU 0 1
.el.LB 0\\$1 0 1 0 \\*(BU 0 1 \}
..

Sed вызывается с параметром -n, чтобы он не выводил весь файл целиком. С помощью этой опции sed будет печатать только те строки, которые ему явно указано напечатать с помощью команды print. Сценарий sed содержит два адреса: первый соответствует началу определения макроса «.deBL», а второй - его окончанию «..» в отдельной строке. Обратите внимание, что точки появляются буквально в двух шаблонах и экранируются с помощью обратной косой черты.

Эти два адреса определяют диапазон строк для команды print, p. Именно эта возможность отличает этот вид поискового скрипта от grep, который не может соответствовать диапазону строк.

Мы можем взять эту командную строку и сделать ее более общей, поместив ее в сценарий оболочки. Одним из очевидных преимуществ создания сценария оболочки является то, что это экономит ввод текста. Еще одним преимуществом является то, что сценарий оболочки может быть разработан для более широкого использования. Например, мы можем разрешить пользователю предоставлять информацию из командной строки. В этом случае вместо жесткого кода имени макроса в сценарии sed мы можем использовать аргумент командной строки для его предоставления. Вы можете ссылаться на каждый аргумент командной строки в скрипте оболочки с помощью позиционной нотации: первый аргумент равен $1, второй - $2 и так далее. Вот сценарий getmac:

#!/bin/sh
# getmac -- print mm macro definition for $1
sed -n "/^\.de$1/,/^\.\.$/p" /usr/lib/macros/mmt

Первая строка сценария оболочки заставляет интерпретировать файл как сценарий оболочки Bourne, используя механизм исполняемого интерпретатора «#!», доступный во всех современных системах UNIX. Вторая строка - это комментарий, описывающий название и цель скрипта. Команда sed в третьей строке идентична предыдущему примеру, за исключением того, что «BL» заменяется на «$1», переменную, представляющую собой первый аргумент командной строки. Обратите внимание, что двойные кавычки, окружающие сценарий sed, необходимы. Одиночные кавычки не позволяли бы оболочке интерпретировать «$1».

Этот скрипт, getmac, может быть выполнен следующим образом:

$ getmac BL

где «BL» - это первый аргумент командной строки. Он производит тот же результат, что и в предыдущем примере.

Этот скрипт может быть адаптирован для работы с любым из нескольких пакетов макросов. Следующая версия getmac позволяет пользователю указать имя пакета макросов в качестве второго аргумента командной строки.

#! /bin/sh
# getmac - read macro definition for $1 from package $2
file=/usr/lib/macros/mmt
mac="$1"
case $2 in
    -ms) file="/work/macros/current/tmac.s";;
    -mm) file="/usr/lib/macros/mmt";;
    -man) file="/usr/lib/macros/an";;
esac
sed -n "/^\.de *$mac/,/^\.\.$/p" $file

Что здесь нового, так это оператор case, который проверяет значение $2, а затем присваивает значение file. Обратите внимание, что мы назначаем file значение по умолчанию, поэтому, если пользователь не назначает пакет макросов, выполняется поиск пакета макросов -mm. Кроме того, для наглядности и удобочитаемости переменной mac присваивается значение $1.

При создании этого скрипта мы обнаружили разницу между пакетами макросов в первой строке определения макросов. Макросы -ms включают пробел между «.de» и именем макроса, в то время как -mm и -man этого не делают. К счастью, мы можем изменить шаблон, чтобы приспособить оба случая.

/^\.de□*$mac/

После «.de» мы указываем пробел, за которым следует звездочка, что означает, что этот пробел необязателен.

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

* Мы случайно узнали, что макросы -mm не имеют пробела после команды «.de».

4.4.3.2 Создание схемы

Наш следующий пример не только извлекает информацию; он изменяет ее, чтобы ее легче было читать. Мы создаем сценарий оболочки с именем do.outline, который использует sed для отображения структуры документа. Он обрабатывает строки, содержащие закодированные заголовки разделов, например:

.Ah "Shell Programming"

Используемый нами пакет макросов имеет макрос заголовка главы с именем «Se» и иерархические заголовки с именами «Ah», «Bh» и «Ch». В пакете макросов -mm такими макросами могут быть «H», «H1», «H2», «H3» и т. д. Вы можете адаптировать сценарий к любым макросам или тегам, определяющим структуру документа. Сценарий do.outline предназначен для того, чтобы сделать структуру более наглядной путем печати заголовков в формате схемы с отступом.

Результат do.outline показан ниже:

$ do.outline ch13/sect1
CHAPTER 13 Let the Computer Do the Dirty Work
    A. Shell Programming
        B. Stored Commands
        B. Passing Arguments to Shell Scripts
        B. Conditional Execution
        B. Discarding Used Arguments
        B. Repetitive Execution
        B. Setting Default Values
        B. What We've Accomplished

Он выводит результат на стандартный вывод (конечно, без внесения каких-либо изменений в сами файлы).

Давайте посмотрим, как собрать этот сценарий. Скрипт должен соответствовать строкам, начинающимся с макроса для:

Нам нужно сделать замены в этих строках, заменив макросы текстовым маркером (например, A, B) и добавив соответствующий интервал (с помощью табуляции) для отступа каждого заголовка. (Вспомним, что «•» обозначает символ табуляции.)

Вот основной сценарий:

sed -n '
s/^\.Se /CHAPTER /p
s/^\.Ah /•A. /p
s/^\.Bh /••B. /p' $*

do.outline работает со всеми файлами, указанными в командной строке («$*»). Параметр -n подавляет вывод программы по умолчанию. Сценарий sed содержит три замещающие команды, которые заменяют коды буквами и делают отступ в каждой строке. Каждая замещающая команда модифицируется флагом p, который указывает, что строка должна быть напечатана.

Когда мы тестируем этот скрипт, получаем следующие результаты:

CHAPTER "13" "Let the Computer Do the Dirty Work"
    A. "Shell Programming"
        B. "Stored Commands"
        B. "Passing Arguments to Shell Scripts"

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

s/"//g

Необходимо указать глобальный флаг g, чтобы перехватывать все вхождения в одну строку. Однако главное решение - где поместить эту команду в сценарий. Если мы поместим его в конец скрипта, он удалит кавычки после того, как строка уже была выведена. Мы должны поместить его в начало сценария и выполнить это редактирование для всех строк, независимо от того, выводятся они позже в сценарии или нет.

sed -n '
s/"//g
s/^\.Se /CHAPTER /p
s/^\.Ah /•A. /p
s/^\.Bh /••B. /p' $*

Теперь этот сценарий выдает результат, который был показан ранее.

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

sed -n '
s/[{}]//g
s/\\section/•A. /p
s/\\subsection/••B. /p' $*

4.4.4 Правки на вынос

Давайте рассмотрим приложение, которое показывает sed в роли настоящего потокового редактора, вносящего изменения в канал - изменения, которые никогда не записываются обратно в файл.

На устройстве, похожем на пишущую машинку (включая ЭЛТ), длинное тире набирается как пара дефисов (- -). При наборе он печатается как одинарное длинное тире (—). troff предлагает специальное символьное имя для длинного тире, но набирать «\(em» неудобно.

Следующая команда изменяет два последовательных дефиса на тире.

s/--/\\(em/g

Мы удваиваем обратную косую черту в строке замены для \(em, так как обратная косая черта имеет особое значение для sed.

Возможно, есть случаи, когда мы не хотим, чтобы эта заменяющая команда применялась. Что, если кто-то использует дефисы, чтобы нарисовать горизонтальную линию? Мы можем уточнить эту команду, чтобы исключить строки, содержащие три или более последовательных дефиса. Для этого мы используем следующее модификатор адреса !:

/---/!s/--/\\(em/g

Для понимания этого синтаксиса может потребоваться некоторое время. Отличие состоит в том, что мы используем в шаблоне адрес для ограничения строк, на которые воздействует команда замены, и мы используем ! чтобы изменить смысл сопоставления с образцом. Команда просто говорит: «Если найдешь строку, содержащую три последовательных дефиса, не применяй редактирование». Во всех остальных строках будет применяться команда замены.

Мы можем использовать эту команду в скрипте, который автоматически вставляет за нас длинное тире. Для этого мы будем использовать sed в качестве препроцессора для файла troff. Файл будет обработан sed, а затем передан в troff.

sed '/---/!s/--/\\(em/g' file | troff

Другими словами, sed изменяет входной файл и передает выходные данные непосредственно в troff, без создания промежуточного файла. Изменения вносятся на ходу и не влияют на входной файл. Вы можете спросить, почему бы просто не внести изменения в исходный файл навсегда? Одна из причин заключается в том, что в этом нет необходимости - ввод остается согласованным с тем, что набрал пользователь, но troff по-прежнему дает то, что лучше всего подходит для вывода типографского качества. Кроме того, поскольку он встроен в более крупный сценарий оболочки, преобразование дефисов в длинное тире невидимо для пользователя и не является дополнительным шагом в процессе форматирования.

Мы используем сценарий оболочки с именем format, который для этой цели использует sed. Вот как выглядит сценарий оболочки:

#! /bin/sh
eqn=  pic=  col=
files=  options=  roff="ditroff -Tps"
sed="| sed '/---/!s/--/\\(em/g'"
while [ $# -gt 0 ]
do
   case $1 in
     -E) eqn="| eqn";;
     -P) pic="| pic";;
     -N) roff="nroff"  col="| col"  sed= ;;
     -*) options="$options $1";;
      *) if [ -f $1 ]
         then files="$files $1"
         else echo "format: $1: file not found"; exit 1
         fi;;
   esac
   shift
done
eval "cat $files $sed | tbl $eqn $pic | $roff $options $col | lp"

Этот сценарий назначает и оценивает ряд переменных (с префиксом доллара), которые составляют командную строку, которая отправляется для форматирования и печати документа. (Обратите внимание, что мы установили параметр -N для nroff, чтобы он устанавливал для переменной sed пустую строку, поскольку мы хотим внести это изменение только в том случае, если используем troff. Несмотря на то, что nroff понимает специальный символ \(em, внесение этого изменения не повлияет на результат.)

Замена дефисов на длинное тире - не единственное «красивое» редактирование, которое мы можем сделать при верстке документа. Например, большинство клавиатур не позволяют вводить открывающие и закрывающие кавычки (« и » вместо " и "). В troff вы можете указать открытую кавычку, введя два последовательных знака «обратной кавычки» (``), и закрывающую кавычку, введя две последовательные одинарные кавычки (''). Мы можем использовать sed для изменения каждого символа двойных кавычек на пару одинарных открытых или закрывающих кавычек (в зависимости от контекста), которые при наборе будут создавать вид правильных «двойных кавычек».

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

s/^"/``/
s/"$/''/
s/"?□/''?□/g
s/"?$/''?/g
s/□"/□``/g
s/"□/''□/g
s/•"/•``/g
s/"•/''•/g
s/")/'')/g
s/"]/'']/g
s/("/(``/g
s/\["/\[``/g
s/";/'';/g
s/":/'':/g
s/,"/,''/g
s/",/'',/g
s/\."/.\\\&''/g
s/"\./''.\\\&/g
s/\\(em\\^"/\\(em``/g
s/"\\(em/''\\(em/g
s/\\(em"/\\(em``/g
s/@DQ@/"/g

Первая замещающая команда ищет кавычку в начале строки и заменяет ее открытой кавычкой. Вторая команда ищет кавычки в конце строки и заменяет их закрывающими кавычками. Остальные команды ищут кавычки в разных контекстах, до или после знака препинания, пробела, табуляции или длинного тире. Последняя команда позволяет нам вставить настоящую двойную кавычку (") во вход troff, если нам это нужно. Мы помещаем эти команды в сценарий очистки вместе с командой, меняющей дефисы на тире, и вызываем ее в канале, который форматирует и печатает документы с помощью troff.

4.5 Как добраться до страны PromiSed

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

Вот несколько советов:

  1. Знай свой инпут! Внимательно изучите входной файл с помощью команды grep, прежде чем разрабатывать сценарий.
  2. Образец перед покупкой. Начните с небольшой выборки вхождений в тестовом файле. Запустите свой сценарий на образце и убедитесь, что он работает. Помните, также важно убедиться, что сценарий не работает там, где вы этого не хотите. Затем увеличьте размер образца. Попробуйте увеличить сложность ввода.
  3. Подумайте, прежде чем делать. Работайте осторожно, проверяя каждую команду, которую вы добавляете в сценарий. Сравните выходные данные с входным файлом, чтобы увидеть, что изменилось. Докажите себе, что ваш сценарий завершен. Ваш сценарий может работать отлично, основываясь на ваших предположениях о том, что находится во входном файле, но ваши предположения могут быть ошибочными.
  4. Будьте прагматичны! Постарайтесь сделать все, что вы можете, с помощью сценария sed, но он не должен выполнять 100 процентов работы. Если вы столкнулись с трудными ситуациями, проверьте и посмотрите, как часто они возникают. Иногда лучше внести несколько оставшихся правок вручную.

По мере накопления опыта добавляйте в этот список свои собственные «советы по написанию сценариев». Вы также обнаружите, что эти советы одинаково хорошо применимы при работе с awk.

Глава 5.
Основные команды sed

Набор команд sed состоит из 25 команд. В этой главе мы вводим четыре новые команды редактирования: d (удалить), a (добавить), i (вставить) и c (изменить). Мы также рассмотрим способы изменения управления потоком (т. е., определения того, какая команда будет выполняться следующей) в сценарии.

5.1 О синтаксисе команд sed

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

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

[address]command

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

[line-address]command

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

address {
    command1
    command2
    command3
}

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

Когда sed не может понять команду, он печатает сообщение «Command garbled» (команда искажена). Одна тонкая синтаксическая ошибка - это добавление пробела после команды. Это недопустимо; конец команды должен быть в конце строки.

Доказательством этого ограничения является предложение ed с помощью «недокументированной» функции: несколько команд sed могут быть помещены в одну строку, если каждая из них разделена точкой с запятой*. Следующий пример синтаксически корректен:

n;d

Однако установка пробела после команды n приводит к синтаксической ошибке. Поставить пробел перед командой d - это нормально.

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

* Удивительно, но использование точек с запятой для разделения команд не задокументировано в стандарте POSIX.

5.2 Комментарий

Комментарий можно использовать для документирования сценария, описывая его назначение. Начиная с этой главы, наши полные примеры сценариев начинаются со строки комментария. Строка комментария может отображаться как первая строка сценария. В версии sed System V комментарий разрешен только в первой строке. В некоторых версиях, включая sed, работающий под управлением SunOS 4.1.x и GNU sed вы можете размещать комментарии в любом месте скрипта, даже в строке, следующей за командой. Примеры в этой книге будут следовать более ограничительному случаю используемой системы, ограничивая комментарии первой строкой сценария. Однако возможность использовать комментарии для документирования вашего сценария очень ценна, и вы должны использовать ее, если это позволяет ваша версия sed.

Октоторп (#) должен быть первым символом в строке. Синтаксис строки комментария таков:

#[n]

В следующем примере показана первая строка сценария:

# wstar.sed: convert WordStar files

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

Если следующий символ, следующий за # - это n, скрипт не будет автоматически выводить данные. Это эквивалентно указанию параметра командной строки -n. Остальная часть строки, следующей за n, рассматривается как комментарий. В соответствии со стандартом POSIX, #n, используемый таким образом, должен быть первыми двумя символами в файле.

* Однако это не работает с GNU sed (версия 2.05).

5.3 Замена

Мы уже продемонстрировали множество вариантов использования подстановочной команды. Давайте внимательно посмотрим на ее синтаксис:

[address]s/pattern/replacement/flags

где flags, изменяющие подстановку:

n
Число (от 1 до 512), указывающее, что замену следует производить только для n-го вхождения pattern.
g
Глобально внести изменения во все вхождения в буфере шаблона. Иначе заменяется только первое вхождение.
p
Распечатать содержимое буфера шаблона.
w file
Записать содержимое буфера шаблона в file.

Команда замены применяется к строкам, совпадающим с address. Если адрес не указан, она применяется ко всем строкам, которым соответствует pattern, регулярному выражению. Если в качестве адреса указано регулярное выражение, а pattern не указан, команда замены соответствует тому, что соответствует адресу. Это может быть полезно, когда замещающая команда является одной из нескольких команд, применяемых к одному и тому же адресу. Для примера см. раздел «Проверка справочных страниц» далее в этой главе.

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

s!/usr/mail!/usr2/mail!

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

Когда-то компьютеры хранили текст в записях фиксированной длины. Строка заканчивалась после некоторого количества символов (обычно 80), а затем начиналась следующая строка. В данных не было явного символа для обозначения конца одной строки и начала следующей; каждая строка имела одинаковое (фиксированное) количество символов. Современные системы более гибки; они используют специальный символ (называемый новой строкой) для обозначения конца строки. Это позволяет строкам иметь произвольную длину*.

Поскольку новая строка - это просто еще один символ при внутреннем хранении, регулярное выражение может использовать «\n» для соответствия встроенной новой строке. Это происходит, как вы увидите в следующей главе, в особом случае, когда к текущей строке в буфере шаблона добавляется еще одна строка. (См. Главу 2. Понимание основных операций, для обсуждения адресации строк и Главу 3. Понимание синтаксиса регулярных выражений, для обсуждения синтаксиса регулярных выражений.)

Замена - это строка символов, которая заменит то, что соответствует регулярному выражению. (См. раздел «Степень совпадения» в Главе 3.) В разделе replacement только следующие символы имеют особое значение:

&
Заменяется строкой, соответствующей регулярному выражению.
\n
Соответствует n-й подстроке (n - это одна цифра), ранее указанной в pattern с помощью «\(» и «\)».
\
Используется для экранирования амперсанда (&), обратной косой черты (\) и разделителя команды замены, когда они используются буквально в разделе замены. Кроме того, символ может быть использован для экранирования новой строки и создания многострочной строки замены.

Таким образом, помимо метасимволов в регулярных выражениях, sed также имеет метасимволы в замене. Примеры их использования приведены в следующем разделе «Замещающие метасимволы».

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

Числовой флаг может использоваться в редких случаях, когда регулярное выражение повторяется в строке, и замена должна быть произведена только для одного из этих вхождений по позиции. Например, строка, возможно содержащая входные данные tbl, может содержать несколько табуляций. Предположим, что в строке есть три табуляции, и вы хотите заменить вторую табуляцию на «>». Следующая команда замены сделает это:

s/•/>/2

«•» представляет собой фактический символ табуляции, который в противном случае невидим на экране. Если входные данные представляют собой однострочный файл, например следующий:

Column1•Column2•Column3•Column4

выходные данные, полученные при запуске скрипта в этом файле, будут следующими:

Column1•Column2>Column3•Column4

Обратите внимание, что без числового флага команда замены заменит только первую вкладку. (Следовательно «1» можно считать числовым флагом по умолчанию.)

* Ну, более или менее. Многие программы UNIX имеют внутренние ограничения на длину строк, которые они будут обрабатывать. Однако большинство программ GNU не имеют таких ограничений.

5.3.1 Замещающие метасимволы

Замещающими метасимволами являются обратная косая черта (\), амперсанд (&) и \n. Обратная косая черта обычно используется для экранирования других метасимволов, но она также используется для включения новой строки в строку замены. Мы можем сделать вариацию на предыдущем примере, чтобы заменить вторую табуляцию на каждой строке новой строкой.

s/•/\
/2

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

Column1•Column2
Column3•Column4

Другой пример - преобразование файла для troff в формат ввода ASCII для Ventura Publisher. Он преобразует следующую строку для troff:

.Ah "Major Heading"

для подобной строки в Ventura Publisher:

@A HEAD = Major Heading

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

/^\.Ah/{
s/\.Ah */\
\
@A HEAD = /
s/"//g
s/$/\
/
}

Первая команда замены заменяет «.Ah» двумя новыми строками и «@A HEAD =». Обратная косая черта в конце строки необходима, чтобы избежать новой строки. Вторая замена удаляет кавычки. Последняя команда соответствует концу строки в буфере шаблона (а не встроенным новым строкам) и добавляет новую строку после нее.

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

s/ORA/O'Reilly \& Associates, Inc./g

Легко забыть об амперсанде, появляющемся буквально в заменяемой строке. Если бы мы не избежали этого в данном примере, выход был бы «O'Reilly ORA Associates, Inc.».

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

s/UNIX/\\s-2&\\s0/g

Поскольку обратные косые черты также являются заменяющими метасимволами, для вывода одной обратной косой черты необходимы две обратные косые черты. «&» в строке замены относится к «UNIX.» Если входной строкой является:

on the UNIX Operating System.

то команда производит следующую замену:

on the \s-2UNIX\s0 Operating System.

Амперсанд особенно полезен, когда регулярное выражение соответствует вариациям слова. Он позволяет указать строку замены переменной, соответствующую тому, что было фактически сопоставлено. Например, предположим, что вы хотите окружить скобками любую перекрестную ссылку на нумерованный раздел в документе. Другими словами, любая ссылка, такая как «See Section 1.4» или «See Section 12.9», должна быть заключена в круглые скобки, как «(See Section 12.9)». Регулярное выражение может соответствовать различной комбинации чисел, поэтому мы используем «&» в строке замены и окружаем все, что было сопоставлено.

s/See Section [1-9][0-9]*\.[1-9][0-9]*/(&)/

Амперсанд позволяет ссылаться на всё совпадение в строке замены.

Теперь давайте рассмотрим метасимволы, которые позволяют нам выбрать любую отдельную часть строки, которая соответствует, и вспомнить ее в строке замены. Пара экранированных скобок используется в sed, чтобы заключить любую часть регулярного выражения и сохранить ее для вызова. На одну строку приходится до девяти «сэйвов». «\n» используется для вызова части совпадения, которая была сохранена, где n - это число от 1 до 9, ссылающееся на определенную «сохраненную» строку в порядке использования.

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

s/\(See Section \)\([1-9][0-9]*\.[1-9][0-9]*\)/\1\\fB\2\\fP/

Указываются две пары экранированных скобок. Первая захватывает «See Section□» (поскольку это фиксированная строка, она могла быть просто перепечатана в строке замены). Вторая захватывает номер секции. Строка замены вызывает первую сохраненную подстроку как «\1», а вторую как «\2», которая окружена запросами жирным шрифтом.

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

$ cat test1
first:second
one:two
$ sed 's/\(.*\):\(.*\)/\2:\1/' test1
second:first
two:one

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

5.3.1.1 Исправление записей индекса

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

.XX "sed, substitution command"

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

Sed может быть большим подспорьем в выполнении этих правок в группе файлов. Можно просто создать список правок в скрипте sed, а затем запустить его на всех файлах. Ключевым моментом является то, что заменяющая команда нуждается в адресе, ограничивающем ее строками, начинающимися на «.XX». Ваш скрипт не должен вносить изменения в сам текст.

Предположим, что мы хотели изменить запись индекса выше на «sed, substitute command». Следующая команда сделает это:

/^\.XX /s/sed, substitution command/sed, substitute command/

Адрес совпадает со всеми строками, начинающимися с «.XX » и только на этих строках он пытается произвести замену. Вы можете задаться вопросом, почему бы не указать более короткое регулярное выражение? Например:

/^\.XX /s/substitution/substitute/

Ответ заключается просто в том, что могут быть и другие записи, которые правильно используют слово «substitution» и которые мы не хотели бы менять.

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

#! /bin/sh
# index.edit -- compile list of index entries for editing.
grep "^\.XX" $* | sort -u |
sed '
s/^\.XX \(.*\)$/\/^\\.XX \/s\/\1\/\1\//'

Скрипт оболочки index.edit использует grep для извлечения всех строк, содержащих индексные записи, из любого количества файлов, указанных в командной строке. Он передает этот список через sort, которая с помощью опции -u сортирует и удаляет дубликаты записей. Затем список передается в sed, и однострочный сценарий sed создает команду подстановки.

Давайте посмотрим на это более внимательно. Вот отдельно регулярное выражение:

^\.XX \(.*\)$

Оно соответствует всей строке и сохраняет индексную запись для повторного использования. Вот отдельно строка замены:

\/^\\.XX \/s\/\1\/\1\/

Она генерирует команду замены, начинающуюся с адреса: косая черта, за которой следуют две обратные косые черты - для вывода одной обратной косой черты для защиты точки в «.XX» - затем идет пробел, затем еще одна косая черта для завершения адреса. Затем мы выводим букву «s», за которой следует косая черта, а затем вызываем сохраненную часть для использования в качестве регулярного выражения. Затем следует еще одна косая черта, и мы снова вызываем сохраненную подстроку в качестве строки замены. Наконец, косая черта завершает команду.

Когда index.edit запускается на файле, он создает листинг, похожий на этот:

$ index.edit ch05
/^\.XX /s/"append command(a)"/"append command(a)"/
/^\.XX /s/"change command"/"change command"/
/^\.XX /s/"change command(c)"/"change command(c)"/
/^\.XX /s/"commands:sed, summary of"/"commands:sed, summary of"/
/^\.XX /s/"delete command(d)"/"delete command(d)"/
/^\.XX /s/"insert command(i)"/"insert command(i)"/
/^\.XX /s/"line numbers:printing"/"line numbers:printing"/
/^\.XX /s/"list command(l)"/"list command(l)"/

Эти выходные данные могут быть записаны в файл. Затем вы можете удалить записи, которые не нуждаются в изменении, и внести изменения, отредактировав строку замены. На этом этапе вы можете использовать этот файл в качестве сценария sed для исправления записей индекса во всех файлах документов.

При создании большой книги с большим количеством записей вы можете снова использовать grep для извлечения определенных записей из выходных данных index.edit и направить их в свой собственный файл для редактирования. Это избавит вас от необходимости пробираться через многочисленные записи.

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

5.4 Удаление

Ранее мы показывали примеры команды delete (d). Она принимает адрес и удаляет содержимое буфера шаблона, если строка совпадает с адресом.

Команда delete также является командой, которая может изменить поток управления в скрипте. Это происходит потому, что после его выполнения никакие другие команды не выполняются в «пустом» буфере шаблона*. Команда delete приводит к тому, что считывается новая строка ввода и новый проход по сценарию редактирования начинается сверху. (В этом поведении она аналогична команде next, с которой вы столкнетесь позже в этой главе.)

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

/^$/d

Еще одним использованием команды delete может быть удаление определенных запросов troff, таких как добавление интервалов, разрыв страницы и включение и выключение режима заполнения:

/^\.sp/d
/^\.bp/d
/^\.nf/d
/^\.fi/d

Эти команды удаляют целую строку. Например, первая команда удалит строку «.sp 1» или «.sp .03v».

Команда delete может быть использована для удаления диапазона строк. В предыдущей главе приведен пример удаления всех таблиц из файла путем удаления строк между макросами .TS и .TE. Существует также команда Delete (D), используемая для удаления части многострочного буфера шаблона. Эта расширенная команда представлена в следующей главе.

* Документация UNIX гласит «никакие дальнейшие команды не предпринимаются на трупе удаленной строки». R.I.P.

5.5 Добавление, вставка и изменение

Команды append (a), insert (i) и change (c) предоставляют функции редактирования, которые обычно выполняются с помощью интерактивного редактора, например vi. Вам может показаться странным использование этих же команд для «ввода» текста с помощью неинтерактивного редактора. Синтаксис этих команд необычен для sed, поскольку они должны быть заданы в нескольких строках. Синтаксис следующий:

append [line-address]a\
    text
insert [line-address]i\
    text
change [address]c\
    text

Команда insert помещает предоставленный текст перед текущей строкой в буфер шаблона. Команда append помещает его после текущей строки. Команда change заменяет содержимое буфера шаблона на предоставленный текст.

Каждая из этих команд требует обратной косой черты, следующей за ней, чтобы избежать первого конца строки. Текст должен начинаться со следующей строки. Чтобы ввести несколько строк текста, каждая следующая строка должна заканчиваться обратной косой чертой, за исключением самой последней. Например, следующая команда insert вставляет две строки текста в строку, соответствующую «<Larry's Address>»:

/<Larry's Address>/i\
4700 Cross Court\
French Lick, IN

Кроме того, если текст содержит буквальную обратную косую черту, добавьте дополнительную обратную косую черту, чтобы заэкранировать ее*.

Команды append и insert могут быть применены только к одному адресу строки, а не к диапазону строк. Однако команда change может обращаться к целому ряду строк. В этом случае он заменяет все адресованные строки одной копией текста. Другими словами, она удаляет каждую строку в диапазоне, но предоставленный текст выводится только один раз. Например, следующий сценарий при запуске файла, содержащего почтовое сообщение:

/^From /,/^$/c\
<Mail Header Removed>

удаляет весь заголовок почтового сообщения и заменяет его строкой «<Mail Header Removed>». Обратите внимание, что вы увидите противоположное поведение, когда команда изменения является одной из группы команд, заключенных в фигурные скобки, которые действуют на диапазон строк. Например, следующий сценарий:

/^From /,/^$/{
s/^From //p
c\
<Mail Header Removed>
}

выведет «<Mail Header Removed>» для каждой строки в диапазоне. Таким образом, в то время как первый пример выводит текст один раз, этот пример будет выводить его 10 раз, если в диапазоне есть 10 строк.

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

Команды insert и append не влияют на содержимое буфера шаблона. Предоставленный текст не будет соответствовать никакому адресу в последующих командах скрипта, и эти команды не могут повлиять на текст. Независимо от того, какие изменения происходят, чтобы изменить буфер шаблона, предоставленный текст все равно будет выведен соответствующим образом. Это также верно, когда выходные данные по умолчанию подавлены - предоставленный текст будет выведен, даже если буфер шаблона не является таковым. Кроме того, предоставленный текст не влияет на внутренний счетчик строк sed.

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

1i\
.so macros\
.ds CH First Draft

После того как sed выполнит эту команду, шаблон остается неизменным. Новый текст выводится перед текущей строкой. Последующая команда не может успешно соответствовать «macros» или «First Draft».

Вариант предыдущего примера показывает, что команда append добавляет строку в конец файла:

$a\
End of file

$ - это символ адресации строки, соответствующий последней строке файла. Предоставленный текст будет выведен после текущей строки, поэтому он станет последней строкой в выводе. Обратите внимание, что даже если выводится только одна строка, предоставленный текст должен начинаться сам по себе и не может находиться в той же строке, что и команда append.

В следующем примере показаны команды insert и append, используемые в том же скрипте. Задача здесь состоит в том, чтобы добавить несколько запросов troff перед макросом, который инициализирует список, и несколько после макроса, который закрывает список.

/^\.Ls/i\
.in 5n\
.sp .3
/^\.Le/a\
.in 0\
.sp .3

Команда insert помещает две строки перед макросом .Ls, а команда append - две строки после макроса .Le.

Команду insert можно использовать, чтобы поместить пустую строку перед текущей строкой, а команду append, чтобы вставить ее после, оставив строку, следующую за ней, пустой.

Команда change заменяет содержимое буфера шаблона предоставленным вами текстом. Фактически, она удаляет текущую строку и помещает предоставленный текст на ее место. Ее можно использовать, когда вы хотите сопоставить строку и полностью заменить ее. Допустим, например, что файл содержит множество явных запросов на интервалы troff с разным количеством интервалов. Взгляните на следующую серию:

.sp 1.5
.sp
.sp 1
.sp 1.5v
.sp .3v
.sp 3

Если бы вы хотели изменить все аргументы на «.5», вероятно, проще использовать команду change, чем пытаться сопоставить все отдельные аргументы и сделать правильную замену.

/^\.sp/c\
.sp .5

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

* В оригинальной документации UNIX говорится, что любые начальные табуляции или пробелы в предоставленном тексте исчезнут при выводе. Это относится и к более старым версиям, таким как SunOS 4.1.x и /usr/ucb/sed на Solaris. System V и GNU sed не удаляют начальные пробелы. Если они исчезают в вашей системе, решение состоит в том, чтобы поставить обратную косую черту в начале строки, предшествующей первой табуляции или пробелу. Обратная косая черта не выводится.

5.6 Список

Команда list (l) отображает содержимое буфера шаблона, показывая непечатаемые символы как двузначные коды ASCII. По функциям она похожа на команду list (:l) в vi. Вы можете использовать эту команду для обнаружения «невидимых» символов во входных данных*.

$ cat test/spchar
Here is a string of special characters: ^A  ^B
^M ^G

$ sed -n -e "l" test/spchar
Here is a string of special characters: \01 \02
\15 \07

$ # test with GNU sed too
$ gsed -n -e "l" test/spchar
Here is a string of special characters: \01  \02
\r \a

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

Вы не можете сопоставить символ по значению ASCII (равно как и восьмеричные значения) в sed**. Вместо этого вы должны найти комбинацию клавиш в vi, чтобы произвести его. Используйте CTRL-V, чтобы процитировать символ. Например, вы можете сопоставить символ ESC (^[). Посмотрите на следующий сценарий:

# list line and replace ^[ with "Escape"
l
s/^[/Escape/

Вот однострочный тестовый файл:

The Great ^[ is a movie starring Steve McQueen.

Запуск скрипта приводит к следующему результату:

The Great \33 is a movie starring Steve McQueen.
The Great Escape is a movie starring Steve McQueen.

GNU sed производит такой результат:

The Great \1b is a movie starring Steve McQueen.
The Great Escape is a movie starring Steve McQueen.

Символ ^[ был сделан в vi с помощью сочетания CTRL-V и затем нажатия клавиши ESC.

* GNU sed отображает определенные символы, такие как возврат каретки, с использованием управляющих последовательностей ANSI C вместо прямого восьмеричного представления. По-видимому, это легче понять тем, кто знаком с C (или awk, как мы увидим позже в книге).

** Однако вы можете сделать это в awk.

5.6.1 Удаление непечатаемых символов из файлов nroff

Форматор UNIX nroff производит вывод для строчных принтеров и ЭЛТ-дисплеев. Для достижения таких специальных эффектов, как выделение жирным шрифтом, он выводит символ, за которым следует backspace, а затем снова выводит тот же символ. Его образец в текстовом редакторе может выглядеть так:

N^HN^HN^HNA^HA^HA^HAM^HM^HM^HME^HE^HE^HE

Слово «NAME» будет выделено жирным шрифтом. Для каждого вывода символа есть три зачеркивания. Точно так же подчеркивание достигается путем вывода подчеркивания, символа возврата и затем подчеркиваемого символа. В следующем примере слово «file» окружено последовательностью для его подчеркивания.

_^Hf_^Hi_^Hl_^He

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

s/.^H//g

Она удаляет любой символ, предшествующий backspace, вместе с самим backspace. В случае подчеркивания «.» соответствует подчеркиванию; для жирного начертания она соответствует символу зачеркивания. Поскольку она применяется повторно, несколько вхождений символа перекрытия удаляются, оставляя один символ для каждой последовательности. Обратите внимание, что ^H вводится в vi нажатием CTRL-V, а затем CTRL-H.

Пример приложения «деформирует» созданную nroff страницу руководства man, которая есть в более старой системе System V UNIX *. Если вы хотите получить доступ к отформатированным страницам с помощью текстового редактора, вам нужна чистая версия. (Во многом это схожая проблема с проблемой, которую мы решили при преобразовании файла текстового редактора в предыдущей главе.) Отформатированная страница руководства man, сохраненная в файле, выглядит следующим образом:

^[9     who(1)                                  who(1)
^[9 N^HN^HN^HNA^HA^HA^HAM^HM^HM^HME^HE^HE^HE
      who - who is on the system?
  S^HS^HS^HSY^HY^HY^HYN^HN^HN^HNO^HO^HO^HOP^HP^HP^HPS^HS^HS^HSI^HI
      who [-a] [-b] [-d] [-H] [-l] [-p] [-q] [-r] [-s] [-t] [-T]
      [-u] [_^Hf_^Hi_^Hl_^He]
          who am i
          who am I
  D^HD^HD^HDE^HE^HE^HES^HS^HS^HSC^HC^HC^HCR^HR^HR^HRI^HI^HI^HIP^HP
      who can list the user's name, terminal line, login time,
      elapsed time since activity occurred on the line, and the
...

Помимо удаления выделений жирным шрифтом и подчеркивающих последовательностей, существуют странные escape-последовательности, которые производят подачу форм или различные другие функции принтера. Вы можете увидеть последовательность «^[9» в верхней части отформатированной man-страницы. Эту экранирующую последовательность можно просто удалить:

s/^[9//g

Еще раз, символ ESC вводится в vi, нажатием CTRL-V с последующим нажатием клавиши ESC. Число 9 - литерал. Есть также то, что выглядит как ведущие пробелы, которые обеспечивают левое поле и отступ. При дальнейшем рассмотрении оказывается, что перед заголовком, таким как «NAME», стоят пробелы, а перед каждой строкой текста - отдельная табуляция. Кроме того, есть табуляции, которые неожиданно появляются в тексте, которые имеют отношение к тому, как nroff оптимизируется для отображения на ЭЛТ-экране.

Чтобы исключить левое поле и ненужные табуляции, мы добавим две команды к нашим предыдущим двум:

# sedman -- deformat nroff-formatted manpage
s/.^H//g
s/^[9//g
s/^[□•]*//g
s/•/ /g

Третья команда ищет любое количество табуляций или пробелов в начале строки. (Табуляция обозначается знаком «•», а пробел - знаком «□».) Последняя команда ищет табуляцию и заменяет ее одним пробелом. Запуск этого скрипта на нашем примере вывода man-страницы приводит к получению файла, который выглядит следующим образом:

who(1)                                         who(1)
NAME
who - who is on the system?
SYNOPSIS
who [-a] [-b] [-d] [-H] [-l] [-p] [-q] [-r] [-s] [-t] [-T]
[-u] [file]
who am i
who am I
DESCRIPTION
who can list the user's name, terminal line, login time,
elapsed time since activity occurred on the line, and the
...

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

* Некоторое время многие поставщики System V UNIX предоставляли только предварительно отформатированные страницы руководства. Это позволяло команде man быстро отображать информацию, а не форматировать ее, но отсутствие исходника troff на страницах руководства затрудняло исправление ошибок документации. К счастью, большинство поставщиков современных систем UNIX предоставляют исходные коды для своих руководств.

5.7 Преобразование

Команда преобразования своеобразна не только потому, что она наименее мнемоническая из всех команд sed. Эта команда преобразует каждый символ по позиции в строке abc в его эквивалент в строке xyz *. У нее следующий синтаксис:

[address]y/abc/xyz/

Замена производится по положению символа. Поэтому, она не имеет понятия о «слове». Таким образом, «a» заменяется на «x» в любом месте строки, независимо от того, следует ли за ним буква «b». Одно из возможных применений этой команды - замена строчных букв на их заглавные аналоги.

y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/

Эта команда влияет на все содержимое буфера шаблона. Если вы хотите преобразовать одно слово в строке ввода, вы можете сделать это с помощью буфера хранения. См. Главу 6 для получения более подробной информации о том, как использовать буфер хранения. (Процесс не тривиален: вы выводите строку до слова, которое хотите изменить, удаляете эту часть строки, копируете строку после слова в буфер хранения, преобразуете слово, а затем добавляете содержимое буфера хранения обратно в буфер шаблона.)

* Эта команда создана по образцу команды UNIX tr, которая переводит символы. Это полезная команда сама по себе; подробности см. в вашей локальной документации. Несомненно, команда sed y была бы названа t, если бы буква t уже не была занята (о команде test см. Глава 6, Расширенные команды sed).

5.8 Печать

Команда печати (p) вызывает вывод содержимого буфера шаблона. Она не очищает буфер шаблона и не изменяет поток управления в скрипте. Однако она часто используется перед командами (d, N, b), которые изменяют управление потоком. Если вывод по умолчанию не подавлен (-n), команда print вызовет вывод дубликатов строк. Ее можно использовать, когда выход по умолчанию подавлен или когда управление потоком через программу избегает достижения нижней части сценария.

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

#n Print line before and after changes.
/^\.Ah/{
p
s/"//g
s/^\.Ah //p
}

Обратите внимание, что флаг печати предоставляется команде замены. Флаг печати команды замены отличается от команды печати тем, что он обусловлен успешной заменой.

Вот пример выполнения приведенного выше сценария:

$ sed -f sed.debug ch05
.Ah "Comment"
Comment
.Ah "Substitution"
Substitution
.Ah "Delete"
Delete
.Ah "Append, Insert and Change"
Append, Insert and Change
.Ah "List"
List

Каждая затронутая строка печатается дважды.

Дополнительные примеры команды печати мы рассмотрим в следующей главе. См. также команду многострочной печати (P) в следующей главе.

5.9 Печать номера строки

Знак равенства (=), следующий за адресом, выводит номер совпадающей строки. Если вы не отключите автоматический вывод строк, будут напечатаны как номер строки, так и сама строка. Синтаксис таков:

[line-address]=

Эта команда не может работать в диапазоне строк.

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

#n print line number and line with if statement
/
if/{
=
p
}

Обратите внимание, что #n подавляет вывод строк по умолчанию. Теперь давайте посмотрим, как это работает на примере программы, random.c:

$ sed -f sedscr.= random.c
192
        if( rand_type == TYPE_0 ) {
234
        if( rand_type == TYPE_0 ) state[ -1 ] = rand_type;
236
        if( n < BREAK_1 ) {
252
              if( n < BREAK_3 ) {
274
        if( rand_type == TYPE_0 ) state[ -1 ] = rand_type;
303
        if( rand_type == TYPE_0 ) state[ -1 ] = rand_type;

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

5.10 next

Команда next (n) выводит содержимое буфера шаблона, а затем считывает следующую строку ввода, не возвращаясь к началу сценария. Ее синтаксис таков:

[address]n

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

Давайте рассмотрим пример команды next, в котором мы удаляем пустую строку только тогда, когда она следует за шаблоном, совпадающим с предыдущей строкой. В этом случае автор вставил пустую строку после макроса заголовка раздела (.Н1). Мы хотим удалить эту пустую строку, не удаляя все пустые строки в файле. Вот пример файла:

.H1 "On Egypt"

Napoleon, pointing to the Pyramids, said to his troops:
"Soldiers, forty centuries have their eyes upon you."

Следующий сценарий удаляет эту пустую строку:

/^\.H1/{
n
/^$/d
}

Вы можете прочитать этот сценарий следующим образом: «сопоставьте любую строку, начинающуюся со строки .H1, затем выведите эту строку и прочтите следующую. Если она пуста, удалите ее». Фигурные скобки используются для применения нескольких команд по одному и тому же адресу.

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

Вы увидите дополнительные примеры команды n в Главе 6, а также многострочную версию этой команды.

5.11 Чтение и запись файлов

Команды чтения (r) и записи (w) позволяют работать непосредственно с файлами. Обе получают один аргумент - имя файла. Синтаксис следующий:

[line-address]r file
[address]w file

Команда чтения считывает содержимое file в буфер шаблона после адресной строки. Она не может работать с рядом строк. Команда записи записывает содержимое буфера шаблона в file.

Между командой и именем файла должен быть один пробел. (Все, что после этого пробела и до новой строки, принимается за имя файла. Таким образом, ведущие или встроенные пробелы станут частью имени файла.) Команда read не будет жаловаться, если файл не существует. Команда write создаст файл, если он еще не существует; если файл уже существует, команда write будет перезаписывать его каждый раз при вызове скрипта. Если в одном скрипте записывается несколько команд в один и тот же файл, то каждая команда записи добавляется в этот файл. Кроме того, имейте в виду, что вы можете открыть только до 10 файлов на сценарий.

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

sed '$r closing' $* | pr | lp

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

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

/^<Company-list>/r company.list

То есть, когда sed соответствует строке, начинающейся со строки «<Company-list>», он собирается добавить содержимое файла company.list в конец совпадающей строки. Никакая последующая команда не повлияет на строки, прочитанные из файла. Например, вы не можете вносить какие-либо изменения в список компаний, которые вы прочитали в файле. Однако команды, которые обращаются к исходной строке, будут работать. За предыдущей командой может последовать вторая:

/^<Company-list>/d

чтобы удалить исходную строку. Так что если бы входной файл был следующим:

For service, contact any of the following companies:
<Company-list>
Thank you.

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

For service, contact any of the following companies:
        Allied
        Mayflower
        United
Thank you.

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

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

Adams, Henrietta       Northeast
Banks, Freda           South
Dennis, Jim            Midwest
Garvey, Bill           Northeast
Jeffries, Jane         West
Madison, Sylvia        Midwest
Sommes, Tom            South

Писать сценарий для семистрочного файла, конечно, смешно. Тем не менее такой сценарий потенциально может обрабатывать столько имен, сколько вы можете собрать вместе, и является многоразовым.

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

/Northeast$/w region.northeast
/South$/w region.south
/Midwest$/w region.midwest
/West$/w region.west

Все имена продавцов, относящиеся к Северо-восточному региону, будут помещены в файл с именем region.northeast.

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

/Northeast$/{
        s///
        w region.northeast
        }

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

5.11.1 Проверка справочных страниц

Как и многие программы, сценарий sed часто начинается с малого, его легко написать и легко прочитать. При тестировании сценария вы можете обнаружить конкретные случаи, для которых общие правила не применяются. Чтобы учесть это, вы добавляете строки в свой сценарий, делая его более длинным, сложным и полным. В то время как количество времени, которое вы тратите на доработку своего сценария, может свести на нет время, сэкономленное не редактированием вручную, по крайней мере, в течение этого времени ваш ум был занят вашей собственной кажущейся ловкостью рук: «Смотрите! Это сделал компьютер.»

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

******************************************************************

NAME:   DBclose - closes a database

SYNTAX:
        void    DBclose(fdesc)
                DBFILE *fdesc;

USAGE:  fdesc - pointer to database file descriptor

DESC:
DBclose () closes a file when given its database file descriptor.
Your pending writes to that file will be completed before the
file is closed . All of your update locks are removed.
*fdesc becomes invalid.

Other users are not affected when you call DBclose (). Their update
locks and pending writes are not changed.

Note that there is no default file as there is in BASIC.
*fdesc must specify an open file.

DBclose () is analogous to the CLOSE statement in BASIC.

RETURNS:
        There is no return value

******************************************************************

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

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

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

/^\*\**\*$/d

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

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

/^•/s///
/:•/s//:/

Следующая строка, на которую мы натыкаемся, содержит название команды и описание.

NAME:   DBclose - closes a database

Нам нужно заменить ее макросом .Rh 0. Его синтаксис таков:

.Rh 0 "command" "description"

Мы вставляем макрос в начало строки, удаляем дефис и заключаем аргументы в кавычки.

/NAME:/ {
        s//.Rh 0 "/
        s/ - /" "/
        s/$/"/
        }

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

.Rh 0 "DBclose" "closes a database"

Следующая часть, которую мы исследуем, начинается с «SYNTAX». Что нам здесь нужно сделать, так это добавить макрос .Rh плюс несколько дополнительных запросов troff на отступ, изменение шрифта, а также без заполнения и без настройки. (Отступ необходим, потому что мы удалили табуляции в начале строки.) Эти запросы должны идти до и после строк синтаксиса, включая и выключая возможности. Для этого мы определяем адрес, который определяет диапазон строк между двумя шаблонами, меткой и пустой строкой. Затем, используя команду изменения, мы заменяем метку и пустую строку серией запросов на форматирование.

/SYNTAX:/,/^$/ {
        /SYNTAX:/c\
.Rh Syntax\
.in +5n\
.ft B\
.nf\
.na
        /^$/c\
.in -5n\
.ft R\
.fi\
.ad b
        }

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

Следующая часть USAGE состоит из одного или нескольких описаний переменных. Здесь мы хотим отформатировать каждый элемент как абзац с отступом и висящей надписью, выделенной курсивом. Сначала мы выводим макрос .Rh; затем мы ищем строки, состоящие из двух частей, разделенных табуляцией и дефисом. Каждая часть сохраняется с использованием обратной косой черты в круглых скобках и вызывается во время замены.

/USAGE:/,/^$/ {
        /USAGE:/c\
.Rh Usage
        /\(.*\)•- \(.*\)/s//.IP "\\fI\1\\fR" 15n\
\2./
        }

Это хороший пример силы регулярных выражений. Давайте еще раз заглянем вперед и предварительно рассмотрим какими получатся выходные данные образца.

.Rh Usage
.IP "\fIfdesc\fR" 15n
pointer to database file descriptor.

Следующая часть, с которой мы сталкиваемся, - это описание. Мы замечаем, что в этой части для разделения абзацев используются пустые строки. При указании адреса для этой части мы используем следующую метку: «RETURNS».

/DESC:/,/RETURNS/ {
        /DESC:/i\
.LP
        s/DESC: *$/.Rh Description/
        s/^$/.LP/
}

Первое, что мы делаем, это вставляем макрос абзаца, потому что предыдущий раздел USAGE состоял из абзацев с отступом. (Мы могли бы использовать макрос списка переменных из пакета -mm в разделе USAGE; если это так, мы бы вставили .LE в этой точке.) Это делается только один раз, поэтому он привязан к метке «DESC». Затем мы заменяем метку «DESC» макросом .Rh и заменяем все пустые строки в этом разделе макросом абзаца.

Когда мы протестировали эту часть сценария sed на нашем примере файла, она не сработала, потому что после метки DESC был один пробел. Мы изменили регулярное выражение, чтобы искать ноль или больше пробелов после метки. Хотя это сработало для образца файла, когда мы использовали более крупный образец, возникли другие проблемы. Автор непоследовательно использовал ярлык «DESC». Чаще всего это происходило в отдельной строке; иногда, правда, его включали в начало второго абзаца. Поэтому нам пришлось добавить еще один шаблон, чтобы справиться с этим случаем. Он ищет метку, за которой следует пробел и один или несколько символов.

s/DESC: *$/.Rh Description/
s/DESC: \(.*\)/.Rh Description\
\\1/

Во втором случае выводится макрос заголовка ссылки, за которым следует новая строка.

Следующий раздел, обозначенный как «RETURNS», обрабатывается так же, как и раздел SYNTAX.

Мы вносим незначительные изменения в контент, заменяя метку «RETURNS» на «Return Value» и, следовательно, добавляя эту замену:

s/There is no return value\.*/None./

Последнее, что мы делаем, это удаляем оставшиеся пустые строки.

/^$/d

Наш сценарий помещен в файл с именем refsed. Вот он полностью:

# refsed -- add formatting codes to reference pages
/^\*\**\*$/d
/^•/s///
/:•/s//:/
/NAME:/ {
        s//.Rh 0 "/
        s/ - /" "/
        s/$/"/
}
/SYNTAX:/,/^$/ {
        /SYNTAX:/c\
.Rh Syntax\
.in +5n\
.ft B\
.nf\
.na
        /^$/c\
.in -5n\
.ft R\
.fi\
.ad b
}
/USAGE:/,/^$/ {
        /USAGE:/c\
.Rh Usage
        /\(.*\)•- \(.*\)/s//.IP "\\fI\1\\fR" 15n\
\2./
}
/DESC:/,/RETURNS/ {
        /DESC:/i\
.LP
        s/DESC: *$/.Rh Description/
        s/DESC: \(.*\)/.Rh Description\
\1/
        s/^$/.LP/
}
/RETURNS:/,/^$/ {
        /RETURNS:/c\
.Rh "Return Value"
        s/There is no return value\.*/None./
}
/^$/d

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

$ sed -f refsed refpage
.Rh 0 "DBclose" "closes a database"
.Rh Syntax
.in +5n
.ft B
.nf
.na
void    DBclose(fdesc)
        DBFILE *fdesc;
.in -5n
.ft R
.fi
.ad b
.Rh Usage
.IP "\fIfdesc\fR" 15n
pointer to database file descriptor.
.LP
.Rh Description
DBclose() closes a file when given its database file descriptor.
Your pending writes to that file will be completed before the
file is closed. All of your update locks are removed.
*fdesc becomes invalid.
.LP
Other users are not effected when you call DBclose(). Their update
locks and pending writes are not changed.
.LP
Note that there is no default file as there is in BASIC.
*fdesc must specify an open file.
.LP
DBclose() is analogous to the CLOSE statement in BASIC.
.LP
.Rh "Return Value"
None.

5.12 Выход

Команда вызода (q) заставляет sed перестать читать новые строки ввода (и перестать отправлять их на вывод). Ее синтаксис:

[line-address]q

Она может принимать только однострочный адрес. Как только будет достигнуто совпадение с адресом строки, сценарий будет завершен*. Например, следующий однострочный файл использует команду quit для печати первых 100 строк из файла:

$ sed '100q' test
...

Она печатает каждую строку, пока не дойдет до строки 100, и завершится. В этом отношении эта команда работает аналогично UNIX команде head.

Еще одно возможное использование quit - выйти из сценария после того, как вы извлекли из файла то, что хотите. Например, в таком приложении, как getmac (оно представлено в Главе 4, Написание сценариев sed), продолжение сканирования большого файла после того, как sed нашел то, что он ищет, является неэффективным.

Так, например, мы могли бы изменить сценарий sed в сценарии оболочки getmac следующим образом:

sed -n "
/^\.de *$mac/,/^\.\.$/{
p
/^\.\.$/q
}" $file

Группировка команд сохраняет строку:

/^\.\.$/q

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

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

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

for file
do
        sed 10q $file
done

В следующем примере также печатаются первые 10 строк с помощью команды печати и без вывода по умолчанию:

for file
do
        sed -n 1,10p $file
done

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

* Вам нужно быть очень осторожным, чтобы не использовать q в любой программе, которая записывает свои изменения обратно в исходный файл. После выполнения q дальнейший вывод не производится. Ее не следует использовать в любом случае, когда вы хотите отредактировать начало файла и передать оставшуюся часть без изменений. Использование q в этом случае - очень опасная ошибка новичка.

Глава 6.
Расширенные команды sed

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

Расширенные команды делятся на три группы:

  1. Работа с многострочным буфером шаблона (N, D, P).
  2. Использование буфера хранения, чтобы сохранить содержимое буфера шаблона и сделать его доступным для последующих команд (H, h, G, g, x).
  3. Написание сценариев, использующих ветвление и условные инструкции для изменения потока управления (:, b, t).

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

Сценарии в этой главе прерывают или нарушают нормальный поток управления по различным причинам. Они могут захотеть запретить выполнение команд в скрипте, кроме как при определенных обстоятельствах, или предотвратить удаление содержимого буфера шаблона. Изменение потока управления делает сценарий гораздо более трудным для чтения и понимания. На самом деле, сценарии могут быть такими, что их проще писать, чем читать. Когда вы пишете сложный сценарий, вы можете протестировать его, чтобы увидеть, как и почему работают команды. Мы рекомендуем вам протестировать сценарии, представленные в этой главе, и поэкспериментировать, добавляя или удаляя команды, чтобы понять, как работает сценарий. Просмотр результатов для себя поможет вам понять сценарий гораздо лучше, чем если просто читать о нем.

6.1 Многострочный буфер шаблона

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

Sed имеет возможность смотреть на более чем одну строку в буфере шаблона. Это позволяет сопоставлять шаблоны, которые простираются на несколько строк. В этом разделе мы рассмотрим команды, которые создают многострочный буфер шаблона и управляют его содержимым. Все три многострочные команды (N,D,P) соответствуют строчным базовым командам (n,d,p), которые были представлены в предыдущей главе. Например, команда Delete (D) является многострочной версией команды delete (d). Разница заключается в том, что в то время как d удаляет содержимое буфера шаблона, D удаляет только первую строку многострочного буфера шаблона.

6.1.1 Добавить следующую строку

Многострочная команда Next (N) создает многострочный буфер шаблона, считывая новую строку ввода и добавляя ее к содержимому буфера шаблона. Исходное содержимое буфера шаблона и новая входная строка разделяются новой строкой. Встроенный символ новой строки может быть сопоставлен в шаблонах с помощью escape-последовательности «\n». В многострочном буфере шаблона метасимвол «^» соответствует самому первому символу буфера шаблона, а не символу (символам), следующим за любой вложенной новой строкой (строками). Аналогично, «$» соответствует только концу последней новой строки в буфере шаблона, а не какой-либо встроенной новой строке (строкам). После выполнения команды Next управление передается последующим командам в скрипте.

Команда Next отличается от команды next, которая выводит содержимое буфера шаблона, а затем считывает новую строку ввода. Команда next не создает многострочный буфер шаблона.

Для нашего первого примера предположим, что мы хотели бы изменить «Owner and Operator Guide» на «Installation Guide», но мы обнаружили, что фраза появляется в файле на двух строках, разделенных между «Operator» и «Guide».

Например, вот несколько строк примерного текста:

Consult Section 3.1 in the Owner and Operator
Guide for a description of the tape drives
available on your system.

Следующий скрипт ищет «Operator» в конце строки, считывает следующую строку ввода и затем производит замену.

/Operator$/{
N
s/Owner and Operator\nGuide/Installation Guide/
}

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

s/Owner and Operator\nGuide /Installation Guide\
/

Эта команда восстанавливает новую строку после «Installation Guide». Также необходимо сопоставить пробел, следующий за «Guide», чтобы новая строка не начиналась с пробела. Теперь мы можем показать результат:

Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

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

Что делать, если есть и другие случаи «Owner and Operator Guide», которые разбиваются на несколько строк в разных местах? Вы можете изменить регулярное выражение, чтобы искать пробел или новую строку между словами, как показано ниже:

/Owner/{
N
s/Owner *\n*and *\n*Operator *\n*Guide/Installation Guide/
}

Звездочка указывает, что пробел или новая строка необязательны. Хотя это кажется тяжелой работой, и на самом деле есть более общий путь. Мы также изменили адрес, чтобы соответствовать «Owner», первое слово в шаблоне вместо последнего. Мы можем прочитать новую строку в буфер шаблона, а затем использовать команду замены, чтобы удалить встроенную новую строку, где бы она ни находилась.

s/Owner and Operator Guide/Installation Guide/
/Owner/{
N
s/ *\n/ /
s/Owner and Operator Guide */Installation Guide\
/
}

Первая строка соответствует «Owner and Operator Guide», когда она появляется в строке сама по себе. (См. обсуждение после примера о том, почему это необходимо.) Если мы совпадаем со строкой «Owner», мы читаем следующую строку в буфер шаблона и заменяем встроенную новую строку пробелом. Затем мы пытаемся сопоставить весь шаблон и сделать замену, за которой следует новая строка. Этот скрипт будет соответствовать «Owner and Operator Guide» независимо от того, как он разбит на две строки. Вот наш расширенный тестовый файл:

Consult Section 3.1 in the Owner and Operator
Guide for a description of the tape drives
available on your system.

Look in the Owner and Operator Guide shipped with your system.

Two manuals are provided including the Owner and
Operator Guide and the User Guide.

The Owner and Operator Guide is shipped with your system.

Запуск приведенного выше скрипта в файле примера приводит к следующему результату:

$ sed -f sedscr sample
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

Look in the Installation Guide shipped with your system.

Two manuals are provided including the Installation Guide
and the User Guide.

The Installation Guide is shipped with your system.

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

$ sed -f sedscr2 sample
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

Look in the Installation Guide
shipped with your system.
Two manuals are provided including the Installation Guide
and the User Guide.

Видите ли вы эти две проблемы? Самая очевидная проблема заключается в том, что последняя строка не напечатана. Последняя строка соответствует «Owner», и когда N выполняется, нет другой входной строки для чтения, поэтому sed завершает работу (немедленно, даже не выводя строку). Чтобы исправить это, в целях безопасности следует использовать команду Next следующим образом:

$!N

Так последняя строка ($) исключается из следующей команды. Как и в нашем сценарии, сопоставляя «Owner and Operator Guide» в последней строке, мы избегаем сопоставления «Owner» и применения команды N. Однако, если бы слово «Owner» появилось в последней строке, у нас была бы та же проблема, если бы мы не использовали синтаксис «$!N».

Вторая проблема несколько менее заметна. Это связано с появлением во втором абзаце «Owner and Operator Guide». Во входном файле фраза находится в строке сама по себе:

Look in the Owner and Operator Guide shipped with your system.

В выходных данных, показанных выше, пустая строка, следующая за «shipped with your system», отсутствует. Причина этого заключается в том, что эта строка соответствует «Owner», а следующая строка, пустая строка, добавляется к буферу шаблона. Команда замены удаляет встроенную новую строку, и пустая строка фактически исчезает. (Если бы строка не была пустой, новая строка все равно была бы удалена, но текст появился бы в той же строке с надписью «shipped with your system».) Лучшее решение, по-видимому, состоит в том, чтобы избежать чтения следующей строки, когда шаблон может быть сопоставлен на одной строке. Итак, именно поэтому первая инструкция пытается соответствовать случаю, когда строка появляется вся в одной строке.

6.1.1.1 Преобразование файла Interleaf

FrameMaker и Interleaf создают технические издательские пакеты WYSIWYG. Оба они имеют возможность читать и сохранять содержимое документа в формате с кодировкой ASCII, в отличие от их обычного двоичного формата файла. В этом примере мы преобразуем файл Interleaf в формат troff; однако тот же сценарий может быть применен для преобразования файла, закодированного troff, в формат Interleaf. То же самое касается FrameMaker. Оба помещают теги кодирования в файл, окруженный угловыми скобками.

В этом примере наше преобразование демонстрирует влияние команды change на многострочный буфер шаблона. В файле Interleaf «<para>» обозначает абзац. Перед тегом и после него - пустые строки. Посмотрите на пример файла:


<para>

This is a test paragraph in Interleaf style ASCII. Another line
in a paragraph. Yet another.

<Figure Begin>

v.1111111111111111111111100000000000000000001111111111111000000
100001000100100010001000001000000000000000000000000000000000000
000000

<Figure End>

<para>

More lines of text to be found after the figure.
These lines should print.

Этот файл также содержит растровый рисунок, напечатанный как последовательность единиц и нулей. Чтобы преобразовать этот файл в макросы troff, мы должны заменить код «<para>» на макрос (.LP). Однако нужно сделать еще немного, потому что нам нужно удалить пустую строку, следующую за кодом. Есть несколько способов сделать это, но мы воспользуемся командой Next для создания многострочного буфера шаблона, состоящего из «<para>» и пустой строки, а затем воспользуемся командой change, чтобы заменить то, что находится в буфере шаблона, на макрос абзаца. Вот часть скрипта, которая это делает:

//{
        N
        c\
.LP
}

Адрес соответствует строкам с тегом абзаца. Команда Next добавляет следующую строку, которая должна быть пустой, в буфер шаблона. Мы используем команду Next (N) вместо команды next (n), потому что мы не хотим выводить содержимое буфера шаблона. Команда change перезаписывает предыдущее содержимое («<para>», за которым следует новая строка) буфера шаблона, даже если оно содержит несколько строк.

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

/<Figure Begin>/,/<Figure End>/{
        w fig.interleaf
        /<Figure End>/i\
.FG\
<insert figure here>\
.FE
        d
}

Эта процедура сопоставляет строки между «<Figure Begin>» и «<Figure End>» и записывает их в файл с именем fig.interleaf. Каждый раз, когда эта инструкция совпадает, будет выполняться команда delete, удаляя строки, которые были записаны в файл. При совпадении «<Figure End>» вместо рисунка в выводе вставляется пара макросов. Обратите внимание, что последующая команда delete не влияет на вывод текста командой insert. Однако она удаляет «<Figure End>» из буфера шаблона.

Вот весь сценарий:

/<para>/{
        N
        c\
.LP
}
/<Figure Begin>/,/<Figure End>/{
        w fig.interleaf
        /<Figure End>/i\
.FG\
<insert figure here>\
.FE
        d
}
/^$/d

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

Результат выполнения этого скрипта в тестовом файле выдает:

$ sed -f sed.interleaf test.interleaf
.LP
This is a test paragraph in Interleaf style ASCII. Another line
in a paragraph. Yet another.
.FG
<insert figure here>
.FE
.LP
More lines of text to be found after the figure.
These lines should print.

6.1.2 Многострочное удаление

Команда delete (d) удаляет содержимое буфера шаблона и вызывает чтение новой строки ввода с возобновлением редактирования в верхней части скрипта. Команда Delete (D) работает несколько иначе: она удаляет часть буфера шаблона, вплоть до первой встроенной новой строки. Она не вызывает чтения новой строки ввода; вместо этого поток выполнения возвращается к началу сценария, применяя эти инструкции к тому, что остается в буфере шаблона. Мы можем увидеть разницу, написав сценарий, который ищет серию пустых строк и выводит одну пустую строку. В приведенной ниже версии используется команда delete:

# reduce multiple blank lines to one; version using d command
/^$/{
        N
        /^\n$/d
}

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

This line is followed by 1 blank line.

This line is followed by 2 blank lines.


This line is followed by 3 blank lines.



This line is followed by 4 blank lines.




This is the end.

Запуск сценария на тестовом файле дает следующий результат:

$ sed -f sed.blank test.blank
This line is followed by 1 blank line.

This line is followed by 2 blank lines.
This line is followed by 3 blank lines.

This line is followed by 4 blank lines.
This is the end.

Там, где было четное число пустых строк, все пустые строки удалялись. Только когда было нечетное число, оставалась одна пустая строка. Это происходит потому, что команда delete очищает весь буфер шаблона. Как только встречается первая пустая строка, следующая строка считывается, и обе удаляются. Если встречается третья пустая строка, а следующая строка не является пустой, команда delete не применяется, и таким образом выводится пустая строка. Если мы используем многострочную команду Delete (D, а не d), то получим желаемый результат:

$ sed -f sed2.blank test.blank
This line is followed by 1 blank line.

This line is followed by 2 blank lines.

This line is followed by 3 blank lines.

This line is followed by 4 blank lines.

This is the end.

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

6.1.3 Многострочная печать

Команда многострочной печати немного отличается от своего собрата в нижнем регистре. Эта команда выводит первую часть многострочного буфера шаблона до первой встроенной новой строки. После выполнения последней команды в сценарии автоматически выводится содержимое буфера шаблона. (Параметр -n или #n подавляет это действие по умолчанию.) Следовательно, команды печати (P или p) используются, когда вывод по умолчанию подавляется или когда поток управления в сценарии изменяется так, что нижняя часть сценария не достигается. Команда Print часто появляется после команды Next и перед командой Delete. Эти три команды могут создать цикл ввода/вывода, который поддерживает двухстрочный буфер шаблона, но выводит только одну строку за раз. Цель этого цикла - вывести только первую строку в буфере шаблона, а затем вернуться в начало скрипта, чтобы применить все команды к тому, что было второй строкой в буфере шаблона. Без этого цикла при выполнении последней команды в сценарии будут выводиться обе строки в буфере шаблона. Последовательность действий в сценарии, который устанавливает цикл ввода/вывода с помощью команд Next, Print и Delete, проиллюстрирована на Рис. 6.1. Многострочный буфер шаблона создается для соответствия «UNIX» в конце первой строки и «System» в начале второй строки. Если «System UNIX» встречается в двух строках, мы меняем его на «UNIX Operating System». Цикл настроен на возврат к началу сценария и поиск «UNIX» в конце второй строки.

Рис. 6.1: Команды Next, Print и Delete, используемые для настройки цикла ввода/вывода

Команда Next добавляет новую строку ввода к текущей строке в буфере шаблона. После применения команды замены к многострочному буферу шаблона первая часть буфера шаблона выводится командой Print и затем удаляется командой Delete. Это означает, что текущая строка выводится, и новая строка становится текущей строкой. Команда Delete не позволяет сценарию достичь конца, в результате чего будут выведены обе строки и очищено содержимое буфера шаблона. Команда Delete позволяет нам сохранить вторую часть буфера шаблона и передать управление в начало скрипта, где теперь к этой строке можно применить все команды редактирования. Одной из таких команд является команда Next, которая считывает еще одну новую строку в буфер шаблона.

Следующий сценарий реализует тот же цикл:

/UNIX$/{
        N
        /\nSystem/{
        s// Operating &/
        P
        D
        }
}

Команда замены соответствует «\nSystem» и заменяет ее на «Operating \nSystem». Важно сохранить новую строку, иначе в буфере шаблона будет только одна строка. Обратите внимание на порядок команд Print и Delete. Вот наш тестовый файл:

Here are examples of the UNIX
System. Where UNIX
System appears, it should be the UNIX
Operating System.

Запуск скрипта в тестовом файле производит:

$ sed -f sed.Print test.Print
Here are examples of the UNIX Operating
System. Where UNIX Operating
System appears, it should be the UNIX
Operating System.

Цикл ввода/вывода позволяет нам сопоставить появление UNIX в конце второй строки. Его можно было бы пропустить, если бы двухстрочный буфер шаблона выводился нормально.

Если взаимосвязь между командами P и D остается для вас неясной, мы рассмотрим ее еще раз в следующем примере. Вы также можете поэкспериментировать, удалив любую команду из приведенного выше скрипта, или попробовать использовать их собратьев в нижнем регистре.

6.2 Пример для изучения

У Ленни, нашего сотрудника, возникли трудности с преобразованием документа, написанного для Scribe, в наш пакет макросов troff из-за изменения шрифта. Проблемы, с которыми он столкнулся, довольно интересны, помимо задачи, которую он пытался выполнить.

Соглашение Scribe о выделении текста жирным шрифтом:

@f1(put this in bold)

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

$ cat test
I want to see @f1(what will happen) if we put the
font change commands @f1(on a set of lines). If I understand
things (correctly), the @f1(third) line causes problems. (No?).
Is this really the case, or is it (maybe) just something else?

Let's test having two on a line @f1(here) and @f1(there) as
well as one that begins on one line and ends @f1(somewhere
on another line). What if @f1(it is here) on the line?
Another @f1(one).

Пример файла показывает различные контексты, в которых появляются команды изменения шрифта. Сценарий должен соответствовать «@f1(anything)», когда он встречается в одной строке или несколько раз в одной строке или когда он распространяется более чем на одну строку.

Самый простой способ сделать одно совпадение - это:

s/@f1(\(.*\))/\\fB\1\\fR/g

Регулярное выражение соответствует «@f1(.*)» и сохраняет все, что находится внутри скобок, используя \( и \). В разделе замены сохраненная часть совпадения вызывается как «\1».

Поместив эту команду в сценарий sed, мы запустим ее в нашем примере файла.

$ sed -f sed.len test
I want to see \fBwhat will happen\fR if we put the
font change commands \fBon a set of lines\fR. If I understand
things (correctly), the \fBthird) line causes problems. (No? \fR.
Is this really the case, or is it (maybe) just something else?

Let's test having two on a line \fBhere) and @f1(there\fR as
well as one that begins on one line and ends @f1(somewhere
on another line). What if \fBit is here\fR on the line?
Another \fBone\fR.

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

Поскольку регулярное выражение всегда делает возможным самое длинное совпадение, «.*» соответствует всем символам от «@f1(» до последней закрывающей скобки в строке. Другими словами, промежуток, обозначенный «.*» заканчивается последней заключительной скобкой, которую он находит, а не первой.

Мы можем решить эту проблему, изменив регулярное выражение «.*» до нуля или более вхождений любого символа, кроме «)».

[^)]*

Каретка (^) в классе символов изменяет смысл операции таким образом, что она соответствует всем символам, кроме указанных в скобках. Вот как выглядит пересмотренная команда:

s/@f1(\([^)]*\))/\\fB\1\\fR/g

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

I want to see \fBwhat will happen\fR if we put the
font change commands \fBon a set of lines\fR. If I understand
things (correctly), the \fBthird\fR line causes problems. (No?).
Is this really the case, or is it (maybe) just something else?

Let's test having two on a line \fBhere\fR and \fBthere\fR as
well as one that begins on one line and ends @f1(somewhere
on another line). What if \fBit is here\fR on the line?
Another \fBone\fR.

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

/@f1(/,/)/{
        s/@f1(/\\fB/g
        s/)/\\fR/g
}

Он попытался решить проблему сопоставления события по нескольким строкам, указав диапазон строк. Вот результат выполнения скрипта в тестовом файле: $ sed -f sed .len test . len

$ sed -f sed.len test.len
I want to see \fBwhat will happen\fR if we put the
font change commands \fBon a set of lines\fR. If I understand
things (correctly, the \fBthird) line causes problems. (No?\fR.
Is this really the case, or is it (maybe) just something else?

Let's test having two on a line \fBhere) and (there\fR as
well as one that begins on one line and ends \fBsomewhere
on another line\fR. What if \fBit is here\fR on the line?
Another \fBone\fR.

Совпадение строк, содержащих «)», приводит к нежелательному совпадению строк, содержащих только круглые скобки. Решение для сопоставления шаблона более чем в одной строке заключается в создании многострочного буфера шаблона. Если мы совпадаем с «@f1(» и не находим закрывающей скобки, нам нужно прочитать (N) другую строку в буфер и попытаться сделать то же самое совпадение, что и в первом случае (\n представляет новую строку).

s/@f1(\([^)]*\))/\\fB\1\\fR/g
/@f1(.*/{
        N
        s/@f1(\(.*\n[^)]*\))/\\fB\1\\fR/g
}

Мы можем это проверить:

$ sed -f sednew test
I want to see \fBwhat will happen\fR if we put the
font change commands \fBon a set of lines\fR. If I understand
things (correctly), the \fBthird\fR line causes problems. (No?).
Is this really the case, or is it (maybe) just something else?

Let's test having two on a line \fBhere\fR and \fBthere\fR as
well as one that begins on one line and ends \fBsomewhere
on another line\fR. What if @f1(it is here) on the line?
Another \fBone\fR.

Как видите, мы уловили все, кроме предпоследнего изменения шрифта. Команда N считывает вторую строку в буфер шаблона. Скрипт сопоставляет шаблон в двух строках, а затем выводит обе строки из буфера шаблона. А как насчет второй строки? Нужна возможность применить к ней все команды сценария сверху вниз. Теперь, возможно, вы понимаете, почему нам нужно настроить многострочный цикл ввода/вывода, подобный тому, который обсуждался в предыдущем разделе. Мы добавляем в скрипт многострочный Print и многострочный Delete.

# Scribe font change script.
s/@f1(\([^)]*\))/\\fB\1\\fR/g
/@f1(.*/{
        N
        s/@f1(\(.*\n[^)]*\))/\\fB\1\\fR/g
        P
        D
}

Это можно перевести так: после замены двух строк распечатайте первую строку, а затем удалите ее из буфера шаблона. Когда вторая часть остается в буфере шаблона, управление переходит к началу скрипта, где мы видим, остался ли в строке «@f1(».

Исправленный сценарий соответствует всем вхождениям в файле примера. Однако это не идеально, так что о Ленни мы еще услышим.

6.3 Храни эту строку

Буфер шаблона - это буфер, содержащий текущую входную строку. Существует также выделенный буфер, называемый буфером хранения (hold space). Содержимое буфера шаблона можно скопировать в буфер хранения, а содержимое буфера хранения - в буфер шаблона. Группа команд позволяет перемещать данные между буфером хранения и буфером шаблона. Буфер хранения используется для временного хранения, вот и все. Отдельные команды не могут обращаться к буферу хранения или изменять его содержимое.

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

Команда Сокращение Функция
Сохранить
(Hold)
h или H Скопировать или добавить содержимое буфера шаблона в буфер хранения.
Получить
(Get)
g или G Скопировать или добавить содержимое буфера хранения в буфер шаблона.
Обменять
(Exchange)
x Поменять местами содержимое буфера хранения и буфера шаблона.

Каждая из этих команд может принимать адрес, определяющий одну строку или диапазон строк. Команды hold (h, H) перемещают данные в буфер хранения, а команды get (g, G) перемещают данные из буфера хранения обратно в буфер шаблона. Разница между версиями одной и той же команды в нижнем и верхнем регистре состоит в том, что команда в нижнем регистре перезаписывает содержимое целевого буфера, а команда в верхнем регистре добавляет к существующему содержимому буфера. Команда hold заменяет содержимое буфера хранения содержимым буфера шаблона. Команда get заменяет содержимое буфера шаблона содержимым буфера хранения.

Команда Hold добавляет к содержимому буфера хранения новую строку и после нее содержимое буфера шаблона. (Новая строка добавляется к буферу хранения, даже если буфер хранения пуст.) Команда Get добавляет к содержимому буфера шаблона новую строку и после нее содержимое буфера хранения.

Команда exchange меняет местами содержимое двух буферов. Она не имеет побочных эффектов ни для одного из буферов.

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

1
2
11
22
111
222

Цель состоит в том, чтобы изменить порядок строк, начинающихся с 1, и строк, начинающихся с 2. Вот как мы используем буфер хранения: мы копируем первую строку в буфер хранения - удерживаем ее и очищаем буфер шаблона. Затем sed считывает вторую строку в буфер шаблона, и мы добавляем строку из буфера хранения в конец буфера шаблона. Посмотрите на сценарий:

# Reverse flip
/1/{
h
d
}
/2/{
G
}

Любая строка, соответствующая «1», копируется в буфер хранения и удаляется из буфера шаблона. Управление переходит в начало скрипта, и строка не печатается. Когда следующая строка считывается, она соответствует образцу «2», и строка, которая была скопирована в буфер хранения, теперь добавляется к буферу шаблона. Затем печатаются обе строки. Другими словами, мы сохраняем первую строку пары и не выводим ее, пока не сопоставим вторую строку.

Вот результат выполнения сценария на примере файла:

$ sed -f sed.flip test.flip
2
1
22
11
222
111

Команда hold, за которой следует команда delete, - довольно распространенное сочетание. Без команды delete элемент управления достигнет нижней части скрипта, и будет выведено содержимое буфера шаблона. Если сценарий использовал команду next (n) вместо команды delete, то также будет выведено содержимое буфера шаблона. Вы можете поэкспериментировать с этим сценарием, полностью удалив команду delete или поместив на ее место команду next. Вы также можете увидеть, что произойдет, если вы используете g вместо G.

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

6.3.1 Трансформация прописных букв

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

Работая над руководством по программированию, мы обнаружили, что названия операторов вводятся непоследовательно. Они должны были быть в верхнем регистре, но некоторые были строчными, а другие - с заглавной буквы. Хотя задача была простой - использовать имя оператора с заглавной буквы, - было около 100 операторов, и казалось весьма утомительным занятием писать такое множество явных замен вида:

s/find the Match statement/find the MATCH statement/g

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

# capitalize statement names
/the .* statement/{
h
s/.*the \(.*\) statement.*/\1/
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
G
s/\(.*\)\n\(.*the \).*\( statement.*\)/\2\1\3/
}

Адрес ограничивает процедуру строками, соответствующими «the .* statement». Давайте посмотрим, что делает каждая команда:

h

Команда hold копирует текущую строку ввода в буфер хранения. Используя образец строки «find the Match statement», мы покажем содержимое буфера шаблона и буфера хранения. После команды h буфер шаблона и буфер хранения идентичны.

Pattern Space: find the Match statement
Hold Space:    find the Match statement
s/.*the \(.*\) statement.*/\1/

Команда замены извлекает имя оператора из строки и заменяет им всю строку.

Pattern Space: Match
Hold Space:    find the Match statement
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/

Команда преобразования заменяет каждую строчную букву на прописную.

Pattern Space: MATCH
Hold Space:    find the Match statement
G

Команда Get добавляет строку, сохраненную в буфере хранения, в буфер шаблона.

Pattern Space: MATCH\nfind the Match statement
Hold Space:    find the Match statement
s/\(.*\)\n\(.*the \).*\( statement.*\)/\2\1\3/

Команда замены находит три совпадения с различными частями буфера шаблона: 1) все символы до встроенной новой строки, 2) все символы, следующие за встроенной новой строкой и до «the» включительно, за которым следует пробел, и 3) все символы, начинающиеся с пробела и последующего «statement» до конца буфера шаблона. Имя оператора в том виде, в каком оно появилось в исходной строке, совпадает, но не сохраняется. Раздел замены этой команды вызывает сохраненные части и собирает их в другом порядке, помещая имя оператора с заглавной буквы между «the» и «statement».

Pattern Space: find the MATCH statement
Hold Space:    find the Match statement

Давайте посмотрим на пробный запуск. Вот наш образец файла:

find the Match statement
Consult the Get statement.
using the Read statement to retrieve data

Запуск сценария в файле примера дает:

find the MATCH statement
Consult the GET statement.
using the READ statement to retrieve data

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

6.3.2 Исправление записей указателя (Часть II)

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

.XX "asterisk (*) metacharacter"

После обработки этой записи исходный index.edit сгенерировал следующую команду замены:

/^\.XX /s/asterisk (*) metacharacter/asterisk (*)
metacharacter/

Хотя он «знает», как экранировать точку перед «.XX», он не защищает метасимвол «*». Проблема в том, что шаблон «(*)» не соответствует совпадению «(*)», и команда замены не может быть применена. Решение состоит в том, чтобы изменить index.edit таким образом, чтобы он искал метасимволы и экранировал их. Есть еще один нюанс: в замещающей строке распознается другой набор метасимволов.

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

#! /bin/sh
# index.edit -- compile list of index entries for editing
#                new version that matches metacharacters
grep "^\.XX" $* | sort -u |
sed '
h
s/[][\\*.]/\\&/g
x
s/[\\&]/\\&/g
s/^\.XX //
s/$/\//
x
s/^\\\.XX \(.*\)$/\/^\\.XX \/s\/\1\//
G
s/\n//'

Команда hold помещает копию текущей записи индекса в буфер хранения. Затем замещающая команда ищет любой из следующих метасимволов: «]», «[», «\», «*» или «.». Это регулярное выражение довольно интересно: 1) если закрывающая скобка является первым символом в символьном классе, она интерпретируется буквально, а не как закрывающий разделитель класса; и 2) из указанных метасимволов только обратная косая черта имеет особое значение в классе символов и должна быть экранирована. Кроме того, нет необходимости экранировать метасимволы «^» и «$», потому что они имеют особое значение только в первой или последней позиции в регулярном выражении что, соответственно, невозможно с учетом структуры записи индекса. После экранирования метасимволов команда обмена меняет местами содержимое буфера шаблона и буфера хранения.

Начиная с новой копии строки, команда замены добавляет обратную косую черту, чтобы экранировать обратную косую черту или амперсанд для заменяющей строки. Затем другая команда замены удаляет «.XX» из строки, а следующая за ней добавляет косую черту (/) в конец строки, подготавливая строку замены, которая выглядит так:

"asterisk (*) metacharacter"/

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

\/^\\.XX \/s\/\1\//

Используя образец записи, буфер шаблона будет иметь следующее содержимое:

/^\.XX /s/"asterisk (\*) metacharacter"/

Затем команда Get берет заменяющую строку в буфере хранения и добавляет ее в буфер шаблона. Поскольку Get также вставляет новую строку, для ее удаления необходима команда замены. В конце выводится следующая строка:

/^\.XX /s/"asterisk (\*) metacharacter"/"asterisk (*) metacharacter"/

6.3.3 Строительные блоки текста

Буфер хранения можно использовать для сбора блока строк перед их выводом. Некоторые запросы и макросы troff являются блочно-ориентированными, так как команды должны окружать блок текста. Обычно код в начале включает формат, а код в конце отключает формат. Код HTML-документов также содержит множество блочноориентированных конструкций. Например, «<p>» начинает абзац, а «</p>» заканчивает его. В следующем примере мы рассмотрим размещение тегов абзацев в стиле HTML в текстовом файле. В этом примере входными данными является файл, содержащий строки переменной длины, образующие абзацы; каждый абзац отделяется от следующего пустой строкой. Следовательно, сценарий должен собрать все строки в буфере хранения, пока не будет обнаружена пустая строка. Содержимое буфера хранения извлекается и окружается тегами абзаца.

Вот сценарий:

/^$/!{
    H
    d
    }
/^$/{
        x
        s/^\n/<p>/
        s/$/<\/p>/
        G
        }

Выполнение сценария на примере файла дает:

<p>My wife won't let me buy a power saw. She is afraid of an
accident if I use one.
So I rely on a hand saw for a variety of weekend projects like
building shelves.
However, if I made my living as a carpenter, I would
have to use a power
saw. The speed and efficiency provided by power tools
would be essential to being productive.<p>

<p>For people who create and modify text files,
sed and awk are power tools for editing.<p>

<p>Most of the things that you can do with these programs
can be done interactively with a text editor. However,
using these programs can save many hours of repetitive
work in achieving the same result.<p>

В основном сценарий состоит из двух частей, соответствующих каждому адресу. Либо мы делаем что-то одно, если строка ввода не пуста, либо другое, если пуста. Если строка ввода не пуста, она добавляется к буферу хранения (с H), а затем удаляется из буфера шаблона. Команда удаления предотвращает вывод строки и очищает буфер шаблона. Управление возвращается в начало сценария, и читается новая строка. Общая идея состоит в том, что мы не выводим ни одной строки текста; он собирается в буфере хранения.

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

Pattern Space: ^$
Hold Space:    \nFor people who create and
               modify text files, \nsed and
               awk are power tools for
               editing.

Пустая строка в буфере шаблона отображается как «^$» - соответствующее ей регулярное выражение. Встроенные символы новой строки представлены в буфере хранения символом «\n». Обратите внимание, что команда Hold помещает новую строку в буфер хранения, а затем добавляет текущую строку в буфер хранения. Даже когда в буфере хранения пусто, команда Hold помещает новую строку перед содержимым буфера шаблона.

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

Pattern Space: \nFor people who create and
               modify text files, \nsed and
               awk are power tools for
               editing.
Hold Space:    ^$

Теперь мы делаем две замены: помещаем «<p>» в начало буфера шаблона и «</p>» в конец. Первая заменяющая команда соответствует «^\n», потому что новая строка находится в начале строки как результат команды Hold. Вторая команда замены соответствует концу буфера шаблона («$» не соответствует никаким встроенным символам новой строки, а соответствует только терминальной новой строке.)

Pattern Space: <p>For people who create and
               modify text files, \nsed and
               awk are power tools for
               editing.</p>
Hold Space:    ^$

Обратите внимание, что встроенная новая строка сохраняется в буфере шаблона. Последняя команда, G, добавляет пустую строку из буфера хранения в буфер шаблона. Достигнув конца скрипта, sed выводит абзац, который мы собрали в буфере хранения и закодировали в буфере шаблона.

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

Этот сценарий также показывает, как использовать адресацию для установки эксклюзивных адресов, в которых строка должна соответствовать одному или другому адресу. Вы также можете настроить адреса для обработки различных исключений на входе и тем самым повысить надежность скрипта. Например, что произойдет в предыдущем сценарии, если последняя строка входного файла не пустая? Все строки, собранные с момента последней пустой строки, не выводятся. Есть несколько способов справиться с этим, но довольно умный - создать пустую строку, которой процедура с пустой строкой будет соответствовать позже в сценарии. Другими словами, если последняя строка содержит строку текста, мы скопируем текст в буфер хранения и очистим содержимое буфера шаблона с помощью команды замены. Мы делаем текущую строку пустой, чтобы она соответствовала процедуре, которая выводит то, что было собрано в буфере шаблона. Вот процедура:

${
/^$/!{
    H
    s/.*//
    }
}

Эта процедура должна быть помещена в сценарий перед двумя процедурами, показанными ранее. Символ адресации «$» соответствует только последней строке в файле. Внутри этой процедуры мы проверяем наличие непустых строк. Если строка пуста, нам не нужно с ней ничего делать. Если текущая строка не пуста, мы добавляем ее в буфер хранения. Это то, что мы делаем в другой процедуре, которая соответствует непустой строке. Затем мы используем команду замены, чтобы создать пустую строку в буфере шаблона.

После выхода из этой процедуры в буфере шаблона остается пустая строка. Она соответствует последующей процедуре для пустых строк, которая добавляет коды абзаца HTML и выводит абзац.

6.4 Расширенные команды управления потоком

Вы уже видели несколько примеров изменений в обычном управлении потоком в sed. В этом разделе мы рассмотрим две команды, которые позволят вам указать, какие части скрипта должны выполняться и когда. Команды branch (b) и test (t) передают управление в сценарии строке, содержащей указанную метку. Если метка не указана, управление передается в конец скрипта. Команда перехода передает управление безоговорочно, в то время как тестовая команда является условной передачей, происходящей только в том случае, если заменяющая команда изменила текущую строку.

Метка - это любая последовательность, содержащая до семи символов*. Метка помещается в строку, начинающуюся с двоеточия:

:mylabel

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

b mylabel

Убедитесь, что вы не оставили пробел после метки.

* Стандарт POSIX говорит, что реализация может допускать более длинные метки, если она того пожелает. GNU sed позволяет использовать метки любой длины.

6.4.1 Ветвление

Команда ветки позволяет передать управление другой строке скрипта.

[address]b[label]

label не является обязательной, и если она не указана, управление передается в конец скрипта. Если метка присутствует, выполнение возобновляется со строки, следующей за меткой.

В Главе 4, Написание сценариев sed мы рассмотрели сценарий набора, который преобразует кавычки и дефисы в их аналоги для набора. Если мы хотим избежать внесения этих изменений в определенные строки, мы могли бы использовать команду ветвления, чтобы пропустить эту часть скрипта. Например, текст внутри созданных компьютером примеров, помеченный макросами .ES и .EE, не должен изменяться. Таким образом, мы могли бы написать предыдущий скрипт так:

/^\.ES/,/^\.EE/b
s/^"/``/
s/"$/''/
s/"?□/''?□/g
.
.
.
s/\\(em\\^"/\\(em``/g
s/"\\(em/''\\(em/g
s/\\(em"/\\(em``/g
s/@DQ@/"/g

Поскольку метка не указана, команда ветвления переходит в конец сценария, пропуская все последующие команды.

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

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

Например, если мы используем несколько пакетов макросов, помимо .ES и .EE могут быть и другие пары макросов, которые определяют диапазон строк, которых мы хотим вообще избежать. Так, например, мы можем написать:

/^\.ES/,/^\.EE/b
/^\.PS/,/^\.PE/b
/^\.G1/,/^\.G2/b

Чтобы получить хорошее представление о типах управления потоком, возможных в сценарии sed, давайте рассмотрим несколько простых, но абстрактных примеров. В первом примере показано, как использовать команду ветвления для создания цикла. Как только строка ввода будет прочитана, к строке будут применены command1 и command2; впоследствии, если содержимое буфера шаблона совпадает с шаблоном, то управление будет передано в строку, следующую за меткой «top», а это означает, что command1 и за ней command2 будут выполнены снова.

:top
command1
command2
/pattern/b top
command3

Сценарий выполняет command3, только если шаблон не совпадает. Будут выполнены все три команды, хотя первые две могут выполняться несколько раз.

В следующем примере выполняется command1. Если шаблон совпадает, управление переходит к строке, следующей за меткой «end». Это означает, что command2 пропускается.

command1
/pattern/b dothree
command2
b
:dothree
command3

Во всех случаях выполняются command1 и command3.

Теперь давайте посмотрим, как указать, что выполняется command2 или command3, но не обе одновременно. В следующем скрипте есть две команды ветвления.

command1
/pattern/b dothree
command2
b
:dothree
command3

Первая команда branch передает управление команде command3. Если этот шаблон не совпадает, выполняется command2. Команда ветвления, следующая за command2, отправляет управление в конец скрипта, минуя command3. Первая из команд ветвления зависит от соответствия шаблону; вторая нет. Посмотрев на команду test, мы рассмотрим «реальный» пример.

6.4.2 Команда test

Команда test переходит к метке (или к концу скрипта), если в текущей адресуемой строке была произведена успешная замена. Таким образом, подразумевается условный переход. Ее синтаксис следующий:

[address]t[label]

Если метка label не указана, управление переходит к концу скрипта. Если метка указана, выполнение возобновляется со строки, следующей за меткой.

Давайте посмотрим на пример Тима О'Рейли. Он пытался сгенерировать автоматические записи указателя на основе оценки аргументов в макросе, который создавал верхнюю часть справочной страницы команды. Если были три процитированных аргумента, он хотел сделать что-то иное, чем если бы было два или только один. Задача заключалась в том, чтобы попытаться сопоставить каждый из этих случаев последовательно (3,2,1), а при успешной замене избежать дальнейших сопоставлений. Вот сценарий Тима:

/\.Rh 0/{
s/"\(.*\)" "\(.*\)" "\(.*\)"/"\1" "\2" "\3"/
t
s/"\(.*\)" "\(.*\)"/"\1" "\2"/
t
s/"\(.*\)"/"\1"/
}

Команда test позволяет нам перейти к концу скрипта после того, как была сделана замена. Если в строке .Rh есть три аргумента, команда test после первой заменяющей команды будет истинной, и sed перейдет к следующей строке ввода. Если аргументов меньше трех, подстановка производиться не будет, команда test будет оценена как ложная, и будет выполнена попытка следующей замещающей команды. Это будет повторяться до тех пор, пока не будут использованы все возможности.

Команда test предоставляет функциональные возможности, аналогичные оператору case в языке программирования C или языках программирования оболочки. Вы можете протестировать каждый случай, и когда он подтвердится, вы выйдете из конструкции.

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

/\.Rh 0/{
s/"\(.*\)" "\(.*\)" "\(.*\)"/"\1" "\2" "\3"/
t break
.
.
.
}
:break
more commands

В следующем разделе дается полный пример команды test и использования меток.

6.4.3 Еще один случай

Помните Ленни? Ему было поручено преобразовать документы Scribe в troff. Мы отправили ему следующий сценарий:

# Scribe font change script.
s/@f1(\([^)]*\))/\\fB\1\\fR/g
/@f1(.*/{
N
s/@f1(\(.*\n[^)]*\))/\\fB\1\\fR/g
P
D
}

После использования скрипта он отправил следующее письмо:

Thank you so much! You've not only fixed the script but shown me
where I was confused about the way it works. I can repair the
conversion script so that it works with what you've done, but to be
optimal it should do two more things that I can't seem to get working
at all - maybe it's hopeless and I should be content with what's
there.

First, I'd like to reduce multiple blank lines down to one.
Second, I'd like to make sed match the pattern over more than two
(say, even only three) lines.

Thanks again.

Lenny

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

/^$/{
N
/^\n$/D
}

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

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

# Scribe font change script. New and Improved.
:begin
/@f1(\([^)]*\))/{
s//\\fB\1\\fR/g
b begin
}
/@f1(.*/{
N
s/@f1(\([^)]*\n[^)]*\))/\\fB\1\\fR/g
t again
b begin
}
:again
P
D

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

Вторая часть пытается сопоставить шаблон на нескольких строках. Команда Next создает многострочный буфер шаблона. Команда подстановки пытается найти образец со встроенной новой строкой. В случае успеха команда test передает управление строке, следующей за меткой again. Если подстановки не производится, управление передается строке, следующей за меткой begin, чтобы мы могли читать в другой строке. Это цикл, который вступает в силу, когда мы сопоставили начальную последовательность запроса на изменение шрифта, но еще не нашли конечную последовательность. Sed вернется в цикл и продолжит добавлять строки в буфер шаблона, пока не будет найдено совпадение.

Третья часть - это снова процедура, следующая за меткой again. Первая строка в буфере шаблона выводится, а затем удаляется. Как и в предыдущей версии этого скрипта, мы обрабатываем несколько строк подряд. Управление никогда не достигает нижней части скрипта, но перенаправляется командой Delete в начало скрипта.

6.5 Присоединиться к фразе

Мы рассмотрели все расширенные конструкции sed и теперь готовы рассмотреть сценарий оболочки с именем phrase, который использует почти все из них. Этот сценарий представляет собой универсальную программу, похожую на grep, которая позволяет вам искать серию из нескольких слов, которые могут появиться в двух строках.

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

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

Сценарий предназначен для приема аргументов из командной строки. Первый аргумент - это шаблон поиска. Все остальные аргументы командной строки будут интерпретироваться как имена файлов. Давайте посмотрим на весь сценарий, прежде чем анализировать его:

#! /bin/sh
# phrase -- search for words across lines
# $1 = search string; remaining args = filenames
search=$1
shift
for file
do
sed '
/'"$search"'/b
N
h
s/.*\n//
/'"$search"'/b
g
s/ *\n/ /
/'"$search"'/{
g
b
}
g
D' $file
done

Переменной оболочки с именем search назначается первый аргумент в командной строке, который должен быть шаблоном поиска. В этом сценарии показан другой метод передачи переменной оболочки в сценарий. Здесь мы заключаем ссылку на переменную в двойные кавычки, а затем в одинарные. Обратите внимание, что сам сценарий заключен в одинарные кавычки, которые защищают символы, которые обычно являются специальными для оболочки, от интерпретации. Последовательность пары двойных кавычек внутри пары одиночных кавычек* проверяет, что заключенный аргумент сначала оценивается оболочкой, прежде чем сценарий sed будет оценен sed'ом**.

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

/'"$search"'/b

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

Если одна строка ввода не соответствует шаблону, мы начинаем нашу следующую процедуру, чтобы создать пространство многострочного шаблона. Возможно, что новая строка сама по себе будет соответствовать строке поиска. Может быть неочевидно, почему этот шаг необходим - почему бы просто не поискать паттерн где-нибудь через две строки? Причина в том, что если шаблон действительно совпадает со второй строкой, мы все равно будем выводить пару строк. Другими словами, пользователь увидит строку, предшествующую совпадающей, и может быть сбит с толку. Таким образом, мы выводим вторую строку отдельно, если она соответствует шаблону.

N
h
s/.*\n//
/'"$search"'/b

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

/\n.*'"$search"'/b

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

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

Итак, мы пытаемся сопоставить шаблон во второй строке, и если это не удается, мы пытаемся сопоставить его в двух строках:

g
s/ *\n/ /
/'"$search"'/{
g
b
}

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

Последняя часть скрипта выполняется только в том случае, если шаблон не был сопоставлен.

g
D

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

Вот результат запуска программs на образце файла:

$ phrase "the procedure is followed" sect3
If a pattern is followed by a \f(CW!\fP, then the procedure
is followed for all lines that do not match the pattern.
so that the procedure is followed only if there is no match.

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

* Фактически, это объединение текста в одинарных кавычках с текстом в двойных кавычках с большим количеством текста в одинарных кавычках (и так далее, уф!), Чтобы получить одну большую строку в кавычках. Здесь пригодилось бы быть мастером оболочки.

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

Глава 7.
Написание скриптов для awk

Как упоминалось в предисловии, эта книга описывает POSIX awk; то есть язык awk в соответствии со стандартом POSIX. Прежде чем углубляться в детали, мы расскажем немного истории.

Оригинальный awk был красивым маленьким языком. Впервые он увидел свет в версии 7 UNIX примерно в 1978 году. Он стал популярным, и люди использовали его для серьезного программирования.

В 1985 году первоначальные авторы, видя, что awk используется для более серьезного программирования, чем они когда-либо планировали, решили усовершенствовать язык. (См. Главу 11, Семейство awk, где описывается исходный awk и все, чего в нем не было по сравнению с новым.) Новая версия, наконец, была выпущена для всего мира в 1987 году, и именно эта версия до сих пор встречается в системах SunOS 4.1.x.

В 1989 году для System V Release 4 awk был немного обновлен*. Эта версия стала основой для списка возможностей awk в стандарте POSIX. POSIX прояснил ряд аспектов awk и добавил переменную CONVFMT (которая будет обсуждаться позже в этой главе).

Когда вы будете читать остальную часть этой книги, имейте в виду, что термин awk относится к POSIX awk, а не к какой-либо конкретной реализации, будь то оригинальная от Bell Labs или любая из других, обсуждаемых в главе 11. Однако в тех немногих случаях, когда разные версии имеют принципиальные различия в поведении, это будет указано в основной части обсуждения.

* Были добавлены опция -v, функции tolower() и toupper(), а srand() и printf были очищены. Подробности будут представлены в этой и следующих главах.

7.1 Играем в эту игру

Чтобы написать сценарий awk, вы должны ознакомиться с правилами игры. Правила могут быть изложены просто, и вы найдете их описание в Приложении B, Краткий справочник по awk, а не в этой главе. Цель этой главы не в том, чтобы описать правила, а в том, чтобы показать вам, как играть в эту игру. Таким образом, вы познакомитесь со многими особенностями языка и увидите примеры, иллюстрирующие, как на самом деле работают скрипты. Некоторые люди предпочитают начать с чтения правил, что примерно равносильно обучению использованию программы с ее справочной страницы или обучению устной речи на языке, с помощью просмотра его правила грамматики - непростая задача. Однако хорошее понимание правил очень важно, как только вы начнете регулярно использовать awk. Но чем больше вы используете awk, тем быстрее правила игры становятся второй натурой. Вы изучаете их методом проб и ошибок - тратя много времени на исправление глупой синтаксической ошибки, такой как пропущенный пробел или скобка, вы оказываете магическое воздействие на долговременную память. Таким образом, лучший способ научиться писать сценарии - это начать их писать. По мере того, как вы будете продвигаться в написании сценариев, вы, несомненно, выиграете от чтения правил (и повторного их чтения) в Приложении B или на странице руководства awk или в книге The AWK Programming Language. Вы можете сделать это позже - давайте начнем прямо сейчас.

7.2 Hello, World

Стало традицией знакомство с языком программирования начинать с программы «Hello, world». Демонстрация того, как эта программа работает в awk, покажет, насколько нетрадиционным является awk. На самом деле необходимо показать несколько разных подходов к печати «Hello, world».

В первом примере мы создаем файл с именем test, который содержит одну строку. В этом примере показан сценарий, содержащий оператор print:

$ echo 'this line of data is ignored' > test
$ awk '{ print "Hello, world" }' test
Hello, world

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

А теперь давайте посмотрим на другой пример. Здесь мы используем файл, содержащий строку «Hello, world».

$ cat test2
Hello, world
$ awk '{ print }' test2
Hello, world

В этом примере во входном файле появляется «Hello, world». Достигается тот же результат, потому что оператор print без аргументов просто выводит каждую строку ввода. Если бы были дополнительные строки ввода, они также были бы выведены.

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

Чтобы убедиться в этом, попробуйте ввести командную строку из первого примера, но опустите имя файла. Вы обнаружите, что, поскольку awk ожидает ввода с клавиатуры, он будет ждать, пока вы не передадите ввод для обработки: несколько раз нажмите ENTER, затем введите EOF (CTRL-D в большинстве систем), чтобы сигнализировать об окончании ввода. Каждый раз, когда вы нажимаете ENTER, будет выполняться действие, которое печатает «Hello, world».

Есть еще один способ написать сообщение «Hello, world» и не заставлять awk ждать ввода. Этот метод связывает действие с шаблоном BEGIN. Шаблон BEGIN определяет действия, которые выполняются перед чтением первой строки ввода.

$ awk 'BEGIN { print "Hello, world" }'
Hello, world

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

7.3 Модель программирования Awk

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

Программа awk состоит из того, что мы будем называть основным циклом ввода (main input loop). Цикл - это процедура, которая выполняется снова и снова, пока не появится какое-либо условие, которое ее завершит. Вы не пишете этот цикл, он дан - он существует как структура, в которой будет выполняться код, который вы пишете. Основной цикл ввода в awk - это процедура, которая считывает одну строку ввода из файла и делает ее доступной для обработки. Действия, которые вы пишете для выполнения обработки, предполагают, что есть доступная строка ввода. В другом языке программирования вам нужно будет создать основной цикл ввода как часть вашей программы. Ему придется открывать входной файл и читать по одной строке за раз. Это не обязательно много работы, но это иллюстрирует базовый кратчайший путь awk, который упрощает написание вашей программы.

Основной цикл ввода выполняется столько раз, сколько строк ввода. Как вы видели в примерах «Hello, world», этот цикл не выполняется, пока не появится строка ввода. Он завершается, когда больше нет ввода для чтения.

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

Вы можете думать о сценарии awk как о потенциально состоящем из трех основных частей: что происходит до, что происходит во время и что происходит после обработки ввода. На Рис. 7.1 показано соотношение этих частей в потоке управления сценарием awk.

Рис. 7.1: Поток и управление в сценариях awk

Из этих трех частей основная часть работы выполняется в основном цикле ввода или «что происходит во время обработки». Внутри основного цикла ввода ваши инструкции записываются как последовательность процедур шаблон/действие. Шаблон - это правило для проверки входной строки, чтобы определить, следует ли к ней применить действие. Действия, как мы увидим, могут быть довольно сложными и состоять из операторов, функций и выражений.

Главное помнить, что каждая процедура шаблона/действия находится в основном цикле ввода, который заботится о чтении строки ввода. Написанные вами процедуры будут применяться к каждой строке ввода, по одной строке за раз.

7.4 Соответствие шаблону

Программа «Hello, world» не демонстрирует возможности правил сопоставления с образцом. В этом разделе мы рассмотрим несколько небольших, даже тривиальных примеров, которые, тем не менее, демонстрируют эту центральную особенность сценариев awk.

Когда awk читает строку ввода, он пытается сопоставить каждое правило сопоставления с образцом в сценарии. Только строки, соответствующие определенному шаблону, являются объектом действия. Если действие не указано, печатается строка, соответствующая шаблону (выполнение оператора print является действием по умолчанию). Рассмотрим следующий сценарий:

/^$/ { print "This is a blank line." }

Этот сценарий гласит: если строка ввода пуста, выведите «This is a blank line». Шаблон записывается как регулярное выражение, определяющее пустую строку. Действие, как и большинство из тех, что мы видели до сих пор, содержит один оператор print.

Если мы поместим этот сценарий в файл с именем awkscr и используем входной файл с именем test, содержащий три пустые строки, то следующая команда выполнит сценарий:

$ awk -f awkscr test
This is a blank line.
This is a blank line.
This is a blank line.

(С этого момента мы будем предполагать, что наши сценарии помещены в отдельный файл и вызываются с использованием параметра командной строки -f). Результат говорит нам, что в test есть три пустые строки. Этот сценарий игнорирует непустые строки.

Добавим в скрипт несколько новых правил. Теперь этот сценарий будет анализировать ввод и классифицировать его как целое число, строку или пустую строку.

# test for integer, string or empty line.
/[0-9]+/     { print "That is an integer" }
/[A-Za-z]+/  { print "This is a string" }
/^$/         { print "This is a blank line." }

Общая идея заключается в том, что если строка ввода соответствует любому из этих шаблонов, будет выполнен связанный оператор print. Метасимвол + является частью расширенного набора метасимволов регулярного выражения и означает «один или несколько». Следовательно, строка, содержащая последовательность из одной или нескольких цифр, будет считаться целым числом. Вот пример выполнения со стандартным вводом:

$ awk -f awkscr
4
That is an integer
t
This is a string
4T
That is an integer
This is a string
RETURN
This is a blank line.
44
That is an integer
CTRL-D
$

Обратите внимание, что ввод «4T» был идентифицирован как целое число и как строка. Строка может соответствовать более чем одному правилу. Вы можете написать более строгий набор правил, чтобы строка не соответствовала более чем одному правилу. Вы также можете написать действия, предназначенные для пропуска других частей скрипта.

В этой главе мы будем исследовать использование правил сопоставления с образцом.

7.4.1 Описание вашего сценария

Добавление комментариев при написании сценария - хорошая практика. Комментарий начинается с символа «#» и заканчивается новой строкой. В отличие от sed, awk позволяет оставлять комментарии в любом месте сценария.

NOTE:Если вы вводите свою awk-программу в командной строке, а не помещаете ее в файл, не используйте одинарные кавычки где-либо в вашей программе. Оболочка интерпретирует это и запутается.

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

# blank.awk -- Print message for each blank line.
/^$/ { print "This is a blank line." }

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

# blocklist.awk -- print name and address in block form.
# fields: name, company, street, city, state and zip, phone

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

7.5 Записи и поля

Awk предполагает, что его ввод структурирован, а не является простой бесконечной строкой символов. В простейшем случае каждая строка ввода рассматривается как запись, а каждое слово, разделенное пробелами или табуляцией, - как поле. (Символы, разделяющие поля, часто называются разделителями (delimiters)). Следующая запись в файле names имеет три поля, разделенных пробелом или табуляцией.

John Robinson   666-555-1111

Два или более последовательных пробела и/или табуляции считаются одним разделителем.

7.5.1 Ссылки и разделение полей

Awk позволяет ссылаться на поля в действиях с помощью оператора поля $. За этим оператором следует число или переменная, определяющая положение поля по номеру. «$1» относится к первому полю, «$2» - ко второму полю и так далее. «$0» относится ко всей входной записи. В следующем примере сначала отображается фамилия, а затем - имя, за которым следует номер телефона.

$ awk '{ print $2, $1, $3 }' names
Robinson John 666-555-1111

$1 относится к имени, $2 - к фамилии, $3 - к номеру телефона. Запятые, разделяющие каждый аргумент в операторе print, вызывают вывод пробела между значениями. (Позже мы обсудим разделитель выходных полей (OFS), значение которого выводит запятая и который по умолчанию является пробелом.) В этом примере одна строка ввода формирует одну запись, содержащую три поля: с пробелом между именем и фамилией и табуляцией между фамилией и номером телефона. Если вы хотите получить имя и фамилию как одно поле, вы можете явно установить разделитель полей, чтобы распознавались только табуляции. Тогда awk распознает только два поля в этой записи.

Для ссылки на поле можно использовать любое выражение, которое оценивается как целое число, а не только числа и переменные.

$ echo a b c d | awk 'BEGIN { one = 1; two = 2 }
> { print $(one + two) }'
c

Вы можете изменить разделитель полей с помощью опции -F в командной строке. За ним следует символ-разделитель (сразу или через пробел). В следующем примере разделитель полей заменяется табуляцией.

$ awk -F"\t" '{ print $2 }' names
666-555-1111

«\t» - это escape-последовательность (escape sequence) (обсуждается ниже), которая представляет фактический символ табуляции. Его следует заключить в одинарные или двойные кавычки.

Поля в следующих двух адресных записях разделяются запятыми.

John Robinson,Koren Inc.,978 4th Ave.,Boston,MA 01760,696-0987
Phyllis Chapman,GVE Corp.,34 Sea Drive,Amesbury,MA 01881,879-0900

Программа awk может печатать имя и адрес в блочном формате.

# blocklist.awk -- print name and address in block form.
# input file -- name, company, street, city, state and zip, phone
{
        print "" # output blank line
        print $1 # name
        print $2 # company
        print $3 # street
        print $4, $5     # city, state zip
}

Первый оператор print указывает пустую строку ("") (помните, что print сам по себе выводит текущую строку). Это обеспечивает разделение записей в отчете пустыми строками. Мы можем вызвать этот скрипт и указать, что разделителем полей является запятая, используя следующую команду:

awk -F, -f blocklist.awk names

Создается следующий отчет:

John Robinson
Koren Inc.
978 4th Ave.
Boston  MA 01760

Phyllis Chapman
GVE Corp.
34 Sea Drive
Amesbury MA 01881

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

BEGIN { FS = "," }

Теперь давайте используем его в сценарии для распечатки имен и номеров телефонов.

# phonelist.awk -- print name and phone number.
# input file -- name, company, street, city, state and zip, phone

BEGIN { FS = "," }  # comma-delimited fields

{ print $1 ", " $6 }

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

$ awk -f phonelist.awk names
John Robinson, 696-0987
Phyllis Chapman, 879-0900

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

/MA/ { print $1 ", " $6 }

где MA будет соответствовать почтовой аббревиатуре штата Массачусетс. Тем не менее, мы могли бы сопоставить название компании или какое-то другое поле, в котором появились буквы «MA». Мы можем проверить конкретное поле на соответствие. Оператор тильда (~) позволяет проверить регулярное выражение на соответствие полю.

$5 ~ /MA/   { print $1 ", " $6 }

Вы можете изменить значение правила на противоположное, используя восклицательный знак и тильду (!~).

$5 !~ /MA/   { print $1 ", " $6 }

Это правило будет соответствовать всем тем записям, пятое поле которых не содержит «MA». Еще более сложным правилом сопоставления с образцом было бы правило сопоставления только междугородных телефонных номеров. Следующее регулярное выражение ищет код области.

$6 ~ /1?(-|□)?\(?[0-9]+\)?(□|-)?[0-9]+-[0-9]+/

Это правило соответствует любой из следующих форм:

707-724-0000
(707) 724-0000
(707)724-0000
1-707-724-0000
1 707-724-0000
1(707)724-0000

Регулярное выражение можно расшифровать, разбив его на части. «1?» означает ноль или одно вхождение «1». «(-|□)?» ищет либо дефис, либо пробел в следующей позиции, либо вообще ничего. «\(?» ищет ноль или одну левую круглую скобку; обратная косая черта предотвращает интерпретацию «(» как метасимвола группировки. «[0-9]+» ищет одну или несколько цифр; обратите внимание, что мы выбрали ленивый выход и указаны одна или несколько цифр, а не ровно три. В следующей позиции мы ищем необязательную правую круглую скобку и снова либо пробел, либо дефис, либо вообще ничего. Затем мы ищем одну или несколько цифр «[0-9]+», за которым следует дефис, за которым следует одна или несколько цифр «[0-9]+».

7.5.2 Разделение поля: полная история

Есть три разных способа создать отдельные поля для awk. Первый способ - разделить поля пробелами. Для этого установите FS равным одному пробелу. В этом случае начальные и конечные пробелы (пробелы и/или табуляции) удаляются из записи, а поля разделяются сериями пробелов и/или табуляциями. Поскольку значение по умолчанию для FS - это один пробел, таким образом awk обычно разбивает каждую запись на поля.

Второй метод состоит в том, чтобы иметь некоторые другие отдельные поля, состоящие из одного символа. Например, программы awk для обработки UNIX файла /etc/passwd обычно используют «:» в качестве разделителя полей. Когда FS - любой одиночный символ, каждое появление этого символа отделяет следующее поле. Если есть два последовательных вхождения, поле между ними просто имеет пустую строку в качестве своего значения.

Наконец, если в качестве разделителя полей вы укажете более одного символа, он будет интерпретирован как регулярное выражение. То есть разделителем полей будет «самая длинная левая непустая и неперекрывающаяся подстрока*, соответствующая регулярному выражению. (Фраза «нулевая строка» - это технический жаргон для обозначения того, что мы называем «пустой строкой».) Вы можете увидеть разницу между указанием:

FS = "\t"

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

FS = "\t+"

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

abc\t\tdef

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

FS = "[':\t]"

Любой из трех символов в скобках будет интерпретироваться как разделитель полей.

* The AWK Programming Language [Aho], стр. 60.

7.6 Выражения

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

Выражение оценивается и возвращает значение. Выражение состоит из любой комбинации числовых и строковых констант, переменных, операторов, функций и регулярных выражений. Мы подробно рассмотрели регулярные выражения в Главе 2, Понимание основных операций, и они кратко описаны в Приложении B, Краткий справочник по awk. Функции будут подробно изучены в Главе 9, Функции. В этом разделе мы рассмотрим выражения, состоящие из констант, переменных и операторов.

Есть два типа констант: строковые или числовые («red» или 1). Строка должна быть заключена в кавычки в выражении. Строки могут использовать escape-последовательности, перечисленные в Таблице 7.1.

Таблица 7.1 Escape-последовательности

Последовательность Описание
\a Предупреждение (звонок), обычно символ ASCII BEL
\b Удаление предыдущего символа
\f Перевод страницы
\n Новая строка
\r Возврат каретки
\t Горизонтальная табуляция
\v Вертикальная табуляция
\ddd Символ, представленный в виде восьмеричного числа от 1 до 3 цифр
\xhex Символ, представленный в виде шестнадцатеричного значенияa
\c Любой литеральный символ c (например, \"for")b

a POSIX не предоставляет «\x», но он общедоступен.

b Как и ANSI C, POSIX намеренно оставляет неопределенным то, что вы получаете, помещая обратную косую черту перед любым символом, не указанным в таблице. В большинстве awk вы просто получаете этот символ.

Переменная (variable) - это идентификатор, который ссылается на значение. Чтобы определить переменную, вам нужно только назвать ее и присвоить ей значение. Имя может содержать только буквы, цифры и символы подчеркивания и не может начинаться с цифры. Важно различать регистр в именах переменных: Salary и salary - две разные переменные. Переменные не объявляются; вам не нужно указывать awk, какой тип значения будет храниться в переменной. Каждая переменная имеет строковое и числовое значение, и awk использует соответствующее значение в зависимости от контекста выражения. (Строки, не состоящие из чисел, имеют числовое значение 0.) Переменные не требуют инициализации; awk автоматически инициализирует их пустой строкой, которая действует как 0, если используется как число. Следующее выражение присваивает значение x:

x = 1

x - это имя переменной, = - это оператор присваивания, а 1 - это числовая константа.

Следующее выражение присваивает переменной z строку «Hello»:

z = "Hello"

Пробел - это оператор конкатенации строк. Выражение:

z = "Hello" "World"

объединяет две строки и присваивает «HelloWorld» переменной z.

Оператор знака доллара ($) используется для ссылки на поля. Следующее выражение присваивает значение первого поля текущей входной записи переменной w:

w = $1

В выражениях можно использовать множество операторов. Арифметические операторы перечислены в Таблице 7.2.

Таблица 7.2: Арифметические операторы

Оператор Описание
+ Сложение
- Вычитание
* Умножение
/ Деление
% Деление по модулю
^ Возведение в степень
** Возведение в степеньa

a Это обычное расширение. Этого нет в стандарте POSIX, а часто и в системной документации. Таким образом, его нельзя переносить.

После того как переменной было присвоено значение, на это значение можно ссылаться, используя имя переменной. Следующее выражение добавляет 1 к значению x и присваивает его переменной y:

y = x + 1

Итак, возьмите значение x, прибавьте к нему 1 и поместите результат в переменную y. Выражение:

print y

печатает значение y. Если в скрипте присутствует следующая последовательность операторов:

x = 1
y = x + 1
print y

тогда значение y равно 2.

Мы могли бы сократить эти три утверждения до двух:

x = 1
print x + 1

Обратите внимание, однако, что после оператора print значение x все еще равно 1. Мы не меняли значение x; мы просто добавили к нему 1 и распечатали это значение. Другими словами, если последует третий оператор print x, он выдаст 1. Если бы мы действительно хотели накопить значение в x, мы могли бы использовать оператор присваивания +=. Этот оператор объединяет две операции; он добавляет 1 к x и присваивает новое значение x. В Таблице 7.3 перечислены операторы присваивания, используемые в выражениях awk.

Таблица 7.3 Операторы присваивания

Оператор Описание
++ Добавить 1 к переменной.
-- Вычесть 1 из переменной.
+= Назначить результат сложения.
-= Назначить результат вычитания.
*= Назначить результат умножения.
/= Назначить результат деления.
%= Назначить результат деления по модулю.
^= Назначить результат возведения в степень.
**= Назначить результат возведения в степеньa.

a Как и «**», это обычное расширение, которое также нельзя переносить.

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

# Count blank lines.
/^$/ {
        print x += 1
     }

Хотя мы не инициализировали значение x, мы можем с уверенностью предположить, что его значение равно 0 до тех пор, пока не встретится первая пустая строка. Выражение «x += 1» вычисляется каждый раз, когда сопоставляется пустая строка, и значение x увеличивается на 1. Оператор print печатает значение, возвращаемое выражением. Поскольку мы выполняем оператор print для каждой пустой строки, мы получаем текущее количество пустых строк.

Существуют разные способы написания выражений, некоторые из которых более краткие, чем другие. Выражение «x += 1» более краткое, чем следующее эквивалентное выражение:

x = x + 1

Но ни одно из этих выражений не является таким кратким, как следующее:

++x

«++» - оператор инкремента. («--» - оператор декремента.) Каждый раз, когда вычисляется выражение, значение переменной увеличивается на единицу. Операторы инкремента и декремента могут появляться по обе стороны от операнда как префиксные или постфиксные операторы. Эффект зависит от позиции.

++x   Increment x before returning value (prefix)
x++   Increment x after returning value (postfix)

Например, если наш пример написан так:

/^$/ {
        print x++
     }

Когда совпадает первая пустая строка, выражение возвращает значение «0»; вторая пустая строка возвращает «1» и так далее. Если мы поместим оператор инкремента перед x, то при первом вычислении выражения оно вернет «1».

Давайте реализуем это выражение в нашем примере. Кроме того, вместо того, чтобы печатать счетчик каждый раз, когда сопоставляется пустая строка, мы будем накапливать счет как значение x и печатать только общее количество пустых строк. Шаблон END - это место для оператора print, отображающего значение x после чтения последней строки ввода.

# Count blank lines.
/^$/ {
        ++x
}
END {
        print x
}

Давайте попробуем это на примере файла, в котором есть три пустые строки.

$ awk -f awkscr test
3

Скрипт выводит количество пустых строк.

7.6.1 Среднеарифметические оценки учащихся

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

john 85 92 78 94 88
andrea 89 90 75 90 86
jasper 84 88 80 92 84

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

# average five grades
{ total = $2 + $3 + $4 + $5 + $6
  avg = total / 5
  print $1, avg }

Этот сценарий складывает поля со 2 по 6, чтобы получить общую сумму пяти оценок. Значение total делится на 5 и присваивается переменной avg. («/» - оператор деления.) Оператор print выводит имя и среднее значение учащегося. Обратите внимание, что мы могли бы пропустить присвоение avg и вместо этого вычислить среднее значение как часть оператора print, как показано ниже:

print $1, total / 5

Этот скрипт показывает, насколько легко писать программы на awk. Awk анализирует введенные поля и записи. Вам не нужно читать отдельные символы и объявлять типы данных. Awk сделает это за вас автоматически.

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

$ awk -f grades.awk grades
john 87.4
andrea 86
jasper 85.6

7.7 Системные переменные

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

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

Выходным эквивалентом FS является OFS, который по умолчанию является пробелом. Вскоре мы увидим пример переопределения OFS.

Awk определяет переменную NF как количество полей для текущей входной записи. На самом деле изменение значения NF имеет побочные эффекты. Взаимодействия, которые происходят при изменении $0, полей и NF, являются темной областью, особенно когда NF уменьшается*. С облегчением он создает новые (пустые) поля и перестраивает $0 с полями, разделенными значением OFS. В случае, когда NF уменьшается, gawk и mawk перестраивают запись, и поля, которые были выше нового значения NF, устанавливаются равными пустой строке. Bell Labs awk не меняет $0.

Awk также определяет RS, разделитель записей, как новую строку. RS немного необычный; это единственная переменная, в которой awk обращает внимание только на первый символ значения.

Эквивалент RS - ORS, который по умолчанию также является новой строкой. В следующем разделе «Работа с многострочными записями» мы покажем, как изменить разделитель записей по умолчанию. Awk устанавливает переменную NR равной номеру текущей входной записи. Его можно использовать для нумерации записей в списке. Переменная FILENAME содержит имя текущего входного файла. Переменная FNR полезна при использовании нескольких входных файлов, поскольку она обеспечивает номер текущей записи относительно текущего входного файла.

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

NOTE: До выпуска Bell Labs awk в июне 1996 года версии awk для UNIX в этом отношении не следовали стандарту POSIX. В этих версиях, если вы еще не ссылались на отдельное поле и устанавливаете разделитель полей на другое значение, текущая строка ввода разделяется на поля с использованием нового значения FS. Таким образом, вы должны проверить, как ведет себя ваш awk, и, если это вообще возможно, перейти на правильную версию awk.

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

str = (5.5 + 3.2) " is a nice value"

Здесь результат числового выражения 5.5 + 3.2 (который равен 8.7) должен быть преобразован в строку, прежде чем его можно будет использовать в конкатенации строк. CONVFMT управляет этим преобразованием. Его значение по умолчанию - "%.6g" что является спецификацией формата в стиле printf для чисел с плавающей точкой. Например, изменение CONVFMT на "%d" приведет к преобразованию всех чисел в строки как целые числа. До стандарта POSIX в awk для этой цели использовался OFMT. OFMT выполняет ту же работу, но контролирует преобразование числовых значений при использовании оператора print. Комитет POSIX хотел отделить задачи преобразования вывода от простого преобразования строк. Обратите внимание, что числа, которые являются целыми числами, всегда преобразуются в строки как целые числа, независимо от того, какими могут быть значения CONVFMT и OFMT.

Теперь давайте рассмотрим несколько примеров, начиная с переменной NR. Вот измененная инструкция print для скрипта, который вычисляет средние показатели учащихся:

print NR ".", $1, avg

Запуск исправленного сценария дает следующий результат:

1. john 87.4
2. andrea 86
3. jasper 85.6

После чтения последней строки ввода NR содержит количество прочитанных входных записей. Его можно использовать в действии END, чтобы предоставить сводку отчета. Вот исправленная версия скрипта phonelist.awk.

# phonelist.awk -- print name and phone number.
# input file -- name, company, street, city, state and zip,
phone
BEGIN { FS = ", *" } # comma-delimited fields
{ print $1 ", " $6 }
END {   print ""
        print NR, "records processed." }

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

$ awk -f phonelist2.awk names
John Robinson, 696-0987
Phyllis Chapman, 879-0900
2 records processed.

Разделитель выходного поля (OFS) генерируется, когда запятая используется для разделения аргументов в операторе print. Возможно, вам интересно, какой эффект имеет запятая в следующем выражении:

print NR ".", $1, avg

По умолчанию запятая вызывает вывод пробела (значение по умолчанию OFS). Например, вы можете переопределить OFS как табуляцию в действии BEGIN. Тогда предыдущий оператор print выдаст следующий результат:

$ awk -f grades2.awk grades
1.    john    87.4
2.    andrea  86
3.    jasper  85.6

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

Еще одна часто используемая системная переменная - это NF, которая задает количество полей для текущей записи. Как мы увидим в следующем разделе, вы можете использовать NF, чтобы проверить, имеет ли запись такое же количество полей, как вы ожидаете. Вы также можете использовать NF для ссылки на последнее поле каждой записи. Использование оператора поля «$» и NF дает эту ссылку. Если имеется шесть полей, то «$NF» означает «$6». Дан список имен, например:

John Kennedy
Lyndon B. Johnson
Richard Milhouse Nixon
Gerald R. Ford
Jimmy Carter
Ronald Reagan
George Bush
Bill Clinton

Вы заметите, что фамилии могут иметь разные номера поля для каждой записи. Вы можете напечатать фамилию каждого президента, используя «$NF»**.

Это основные системные переменные, наиболее часто используемые. В Приложении B указано большее их количество, и мы будем вводить новые системные переменные по мере необходимости в следующих главах.

* К сожалению, стандарт POSIX здесь не так полезен, как следовало бы.

** Эта схема не работает для Martin Van Buren; к счастью, в нашем списке только недавние президенты США.

7.7.1 Работа с многострочными записями

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

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

John Robinson
Koren Inc.
978 Commonwealth Ave.
Boston
MA 01760
696-0987

Эта запись имеет шесть полей. Пустая строка разделяет каждую запись.

Чтобы обработать эти данные, мы можем указать многострочную запись, определив разделителем полей символ новой строки, представленный как «\n», и установить разделитель записей на пустую строку, которая обозначает пустую строку.

BEGIN { FS = "\n"; RS = "" }

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

# block.awk - print first and last fields
# $1 = name; $NF = phone number

BEGIN { FS = "\n"; RS = "" }

{ print $1, $NF }

Вот пример запуска:

$ awk -f block.awk phones.block
John Robinson 696-0987
Phyllis Chapman 879-0900
Jeffrey Willis 914-636-0000
Alice Gold (707) 724-0000
Bill Gold 1-707-724-0000

Два поля печатаются в одной строке, потому что разделитель вывода по умолчанию (OFS) остается одним пробелом. Если вы хотите, чтобы поля выводились на отдельных строках, замените OFS на новую строку. Пока вы занимаетесь этим, вы, вероятно, захотите сохранить пустую строку между записями, поэтому вы должны указать в качестве разделителя выходной записи ORS два символа новой строки.

OFS = "\n"; ORS = "\n\n"

7.7.2 Баланс чековой книжки

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

Эта программа предполагает, что вы ввели в файл следующую информацию:

1000
125    Market          125.45
126    Hardware Store  34.95
127    Video Store     7.45
128    Book Store      14.32
129    Gasoline        16.10

Первая строка содержит начальный баланс. Каждая из остальных строк представляет информацию из одного чека: номер чека, описание того, где он был потрачен, и сумма чека. Три поля разделены табуляциями. Использование отрицательных сумм для чеков позволяет положительным суммам представлять депозиты.

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

# checkbook.awk
BEGIN { FS = "\t" }

#1 Expect the first record to have the starting balance.
NR == 1 { print "Beginning Balance: \t" $1

        balance = $1
        next             # get next record and start over
}

#2 Apply to each check record, adding amount from balance.
{
        print $1, $2, $3
        print balance -= $3    # checks have negative amounts 
}

Запустим эту программу и посмотрим на результат:

$ awk -f checkbook.awk checkbook.test
Beginning Balance:      1000
125 Market 125.45
874.55
126 Hardware Store 34.95
839.6
127 Video Store 7.45
832.15
128 Book Store 14.32
817.83
129 Gasoline 16.10
801.73

Отчет сложно читать, но позже мы научимся исправлять формат с помощью оператора printf. Важно убедиться, что сценарий делает то, что мы хотим. Также обратите внимание, что на создание сценария в awk уходит всего несколько минут. На таком языке программирования, как C, написание этой программы займет гораздо больше времени; во-первых, у вас может быть намного больше строк кода; и вы будете программировать на гораздо более низком уровне. Вы можете внести в эту программу любое количество доработок, а доработка программы занимает гораздо больше времени. Дело в том, что с помощью awk вы можете довольно легко изолировать и реализовать базовые функции.

7.8 Операторы сравнения и логические операторы

Операторы сравнения и логические операторы позволяют сравнивать два выражения. Операторы сравнения находятся в Таблице 7.4.

Таблица 7.4: Операторы сравнения

Оператор Описание
< Менее чем
> Больше чем
<= Меньше или равно
>= Больше или равно
== Равно
!= Не равно
~ Совпадает
!~ Не совпадает

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

NF == 5

Это выражение сравнивает значение NF (количество полей для каждой входной записи) с пятью. Если это правда, действие будет выполнено; иначе не будет.

NOTE: Обязательно обратите внимание на то, что оператор сравнения «==» («эквивалентно с») не совпадает с оператором присваивания «=» («равно»). Использование «=» вместо «==» для проверки равенства - распространенная ошибка.

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

NF == 6 { print $1, $6 }

В итоге будут напечатаны только строки с шестью полями.

Противоположностью «==» является «!=» («не равно»). Точно так же вы можете сравнить одно выражение с другим, чтобы узнать, больше ли оно (>) или меньше (<), больше или равно (>=) или меньше или равно (<=). Выражение

NR > 1

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

Регулярные выражения обычно заключаются в косую черту. Их можно рассматривать как константы регулярного выражения, так же как hello - строковую константу. До сих пор мы видели много примеров:

/^$/ { print "This is a blank line." }

Однако вы не ограничены константами регулярных выражений. При использовании с операторами отношения ~ («совпадение») и !~ («нет совпадений»), правая часть выражения может быть любым выражением awk; awk рассматривает его как строку, определяющую регулярное выражение*. Мы уже видели пример оператора ~, используемого в правиле сопоставления для базы данных телефонов:

$5 ~ /MA/    { print $1 ", " $6 }

где значение поля 5 сравнивается с регулярным выражением «MA».

Поскольку любое выражение можно использовать с ~ и !~, регулярные выражения могут передаваться через переменные. Например, в скрипте phonelist мы могли бы заменить «/MA/» на state и иметь процедуру, определяющую значение штата.

$5 ~ state  { print $1 ", " $6 }

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

Булевы операторы позволяют комбинировать серии сравнений. Они перечислены в Таблице 7.5.

Таблица 7.5: Логические операторы

Оператор Описание
|| Логическое ИЛИ (OR)
&& Логическое И (AND)
! Логическое НЕ (NOT)

Для двух или более выражений || указывает, что одно из них должно быть истинным (ненулевым или непустым), чтобы все выражение было истинным. && указывает, что оба выражения должны быть истинными, чтобы вернуть истинное значение.

Следующее выражение:

NF == 6 && NR > 1

утверждает, что количество полей должно быть равно 6 и номер записи должен быть больше 1.

&& имеет более высокий приоритет, чем ||. Можете ли вы сказать, как будет оцениваться следующее выражение?

NR > 1 && NF >= 2 || $1 ~ /\t/

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

(NR > 1 && NF >= 2) || $1 ~ /\t/

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

NR > 1 && (NF >= 2 || $1 ~ /\t/)

Первое условие должно быть истинным и любое из двух других условий должно быть истинным.

Если выражение истинно или ложно, оператор ! инвертирует смысл выражения.

! (NR > 1 && NF > 3)

Это выражение истинно, если выражение в скобках ложно. Этот оператор наиболее полезен с оператором in в awk, чтобы увидеть, нет ли индекса в массиве (как мы увидим позже), хотя он имеет и другие применения.

* Вы также можете использовать строки вместо констант регулярных выражений при вызове функций match(), split(), sub() и gsub().

7.8.1 Получение информации о файлах

Теперь мы рассмотрим несколько сценариев, которые обрабатывают вывод команды UNIX ls. Ниже приводится пример длинного списка, созданного командой ls -l*:

$ ls -l
-rw-rw-rw-  1 dale  project  6041 Jan  1 12:31 com.tmp
-rwxrwxrwx  1 dale  project  1778 Jan  1 11:55 combine.idx
-rw-rw-rw-  1 dale  project  1446 Feb 15 22:32 dang
-rwxrwxrwx  1 dale  project  1202 Jan  2 23:06 format.idx

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

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

ls -l $* | awk 'script'

Переменная $* используется оболочкой и расширяется до всех аргументов, передаваемых из командной строки. (Мы могли бы использовать здесь $1, который передал бы первый аргумент, но передача всех аргументов обеспечивает большую гибкость.) Эти аргументы могут быть именами файлов или каталогов или дополнительными параметрами для команды ls. Если аргументы не указаны, символ «$*» будет пустым и будет указан текущий каталог. Таким образом, вывод команды ls будет направлен в awk, который автоматически прочитает стандартный ввод, поскольку имена файлов не указаны.

Мы бы хотели, чтобы наш awk-скрипт выводил размер и имя файла. То есть вывести поле 5 ($5) и поле 9 ($9).

ls -l $* | awk '{
        print $5, "\t", $9
}'

Если мы поместим вышеуказанные строки в файл с именем fls и сделаем этот файл исполняемым, мы сможем ввести fls как команду.

$ fls
6041    com.tmp
1778    combine.idx
1446    dang
1202    format.idx
$ fls com*
6041    com.tmp
1778    combine.idx

Итак, наша программа берет длинный список и сокращает его до двух полей. Теперь давайте добавим в наш отчет новые функции, предоставив некоторую информацию, которую не предоставляет список ls -l. Мы добавляем размер каждого файла к промежуточной сумме, чтобы получить общее количество байтов, используемых всеми файлами в списке. Мы также можем отслеживать количество файлов и вычислять их общее количество. Добавление этой функции состоит из двух частей. Первая - накопить итоги для каждой строки ввода. Мы создаем переменную sum для накопления размера файлов и переменную filenum для накопления количества файлов в листинге.

{
        sum += $5
        ++filenum
        print $5, "\t", $9
}

В первом выражении используется оператор присваивания +=. Он добавляет значение поля 5 к текущему значению переменной sum. Второе выражение увеличивает текущее значение переменной filenum. Эта переменная используется как счетчик, и каждый раз, когда выражение оценивается, к счетчику добавляется 1.

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

END { print "Total: ", sum, "bytes (" filenum " files)" }

Мы также можем использовать правило BEGIN для добавления заголовков столбцов в отчет.

BEGIN { print "BYTES", "\t", "FILE" }

Теперь мы можем поместить этот сценарий в исполняемый файл с именем filesum и выполнить его как команду из одного слова.

$ filesum c*
BYTES    FILE
882      ch01
1771     ch03
1987     ch04
6041     com.tmp
1778     combine.idx
Total:  12459 bytes (5 files)

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

Хотя основной механизм работает, есть несколько проблем, которые необходимо решить. Первая проблема возникает, когда вы перечисляете весь каталог с помощью команды ls -l. В листинге есть строка, в которой указано общее количество блоков в каталоге. Частичный список (все файлы, начинающиеся с «c») в предыдущем примере не имеет этой строки. Но следующая строка будет включена в вывод, если будет указан полный каталог:

total 555

Сумма блока нас не интересует, потому что программа отображает общий размер файла в байтах. В настоящее время filesum не выводит эту строку; однако он читает эту строку и вызывает увеличение счетчика filenum.

У этого сценария также есть проблема с тем, как он обрабатывает подкаталоги. Посмотрите на следующую строку из ls -l:

drwxrwxrwx   3 dale    project    960 Feb  1 15:47 sed

«d» в качестве первого символа в столбце 1 (права доступа к файлу) указывает, что файл является подкаталогом. Размер этого файла (960 байт) не указывает размер файлов в этом подкаталоге, и поэтому добавление его к общему размеру файла немного вводит в заблуждение. Кроме того, может быть полезно указать, что это каталог.

Если вы хотите перечислить файлы в подкаталогах, укажите параметр -R (рекурсивный) в командной строке. Он будет передан команде ls. Однако список немного отличается, поскольку он идентифицирует каждый каталог. Например, чтобы определить подкаталог old, список ls -lR создает пустую строку, за которой следует:

./old:

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

ls -l $* | awk '
# filesum: list files and total size in bytes
# input: long listing produced by "ls -l"

#1 output column headers
BEGIN { print "BYTES", "\t", "FILE" }

#2 test for 9 fields; files begin with "-"
NF == 9 && /^-/ {

        sum += $5    # accumulate size of file
        ++filenum    # count number of files
        print $5, "\t", $9    # print size and filename
}

#3 test for 9 fields; directory begins with "d"
NF == 9 && /^d/ {
        print "<dir>", "\t", $9 # print <dir> and name
}

#4 test for ls -lR line ./dir:
$1 ~ /^\..*:$/ {
        print "\t" $0 # print that line preceded by tab
}

#5 once all is done,
END {
        # print total file size and number of files
        print "Total: ", sum, "bytes (" filenum " files)"
}'

Правила и связанные с ними действия пронумерованы, чтобы их было легче обсуждать. Список, созданный с помощью ls -l, содержит девять полей для файла. Awk предоставляет количество полей для записи в системной переменной NF. Таким образом, правила 2 и 3 проверяют, что NF равно 9. Это помогает нам избежать совпадения нечетных пустых строк или строки, в которой указано общее количество блоков. Поскольку мы хотим обрабатывать каталоги и файлы по-разному, мы используем другой шаблон для сопоставления с первым символом строки. В правиле 2 мы проверяем «-» в первой позиции строки, которая указывает на файл. Связанное действие увеличивает счетчик файлов и добавляет размер файла к предыдущему итоговому значению. В правиле 3 мы проверяем каталог, обозначенный буквой «d» в качестве первого символа. Связанное действие печатает «<dir>» вместо размера файла. Правила 2 и 3 представляют собой составные выражения, определяющие два шаблона, которые объединяются с помощью оператора &&. Оба шаблона должны совпадать, чтобы выражение было истинным.

Правило 4 проверяет особый случай, созданный листингом ls -lR («./old:»). Есть ряд шаблонов, которые мы можем написать для сопоставления с этой строкой, используя регулярные выражения или выражения сравнения:


NF == 1            Если количество полей равно 1 ...
/^\..*:$/          Если строка начинается с точки, за которой следует
                   любое количество символов, и заканчивается двоеточием...
$1 ~ /^\..*:$/     Если поле 1 соответствует регулярному выражению...

Мы использовали последнее выражение, потому что оно кажется наиболее конкретным. Оно использует оператор соответствия (~) для проверки первого поля на соответствие регулярному выражению. Связанное действие состоит только из оператора print.

Правило 5 - это шаблон END, и его действие выполняется только один раз, печатается сумма размеров файлов, а также количество файлов.

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

* Обратите внимание, что в системе UNIX, производной от Berkeley 4.3BSD, такой как Ultrix или SunOS 4.1.x, команда ls -l выводит отчет из восьми столбцов; используйте ls -lg, чтобы получить тот же формат отчета, что и здесь.

7.9 Форматированная печать

Многие из сценариев, которые мы написали до сих пор, отлично справляются с задачами обработки данных, но вывод не отформатирован должным образом. Это потому, что вы можете сделать очень многое с помощью основного оператора print. А поскольку одна из наиболее распространенных функций awk - создание отчетов, очень важно, чтобы мы могли упорядоченно форматировать наши отчеты. Программа filesum хорошо выполняет арифметические задачи, но отчету не хватает упорядоченного формата.

Awk предлагает альтернативу оператору print, printf, который заимствован из языка программирования C. Оператор printf может выводить простую строку точно так же, как оператор print.

awk 'BEGIN { printf ("Hello, world\n") }'

Основное отличие, которое вы заметите с самого начала, заключается в том, что, в отличие от print, printf не предоставляет автоматически новую строку. Вы должны указать это явно как «\n».

Полный синтаксис оператора printf состоит из двух частей:

printf ( format-expression[, arguments] )

Скобки необязательны. Первая часть - это выражение, описывающее спецификации формата; обычно это строковая константа в кавычках. Вторая часть - это список аргументов, например список имен переменных, которые соответствуют спецификациям формата. Спецификации формата предшествует знак процента (%), а спецификатор - один из символов, показанных в Таблице 7.6. Два основных спецификатора формата: s для строк и d для десятичных целых чисел*.

Таблица 7.6 Спецификаторы формата, используемые в printf

Символ Описание
c ASCII-символ
d Десятичное целое число
i Десятичное целое число. (Добавлено в POSIX)
e Формат с плавающей точкой ([-]d.precisione[+-]dd)
E Формат с плавающей точкой ([-]d.precisionE[+-]dd)
f Формат с плавающей точкой ([-]ddd.precision)
g преобразование e или f, в зависимости от того, какое из них является более коротким, с удалением конечных нулей
G преобразование E или f, в зависимости от того, какое из них является более коротким, с удалением конечных нулей
o Беззнаковое восьмеричное значение
s Строка
u Беззнаковое десятичное значение
x Шестнадцатеричное число без знака. Использует a-f для диапазона от 10 до 15
X Шестнадцатеричное число без знака. Использует A-F для диапазона от 10 до 15
% Литерал %

В этом примере оператор printf используется для вывода для правила 2 программы filesum. Он выводит строку и десятичное значение в двух разных полях:

printf("%d\t%s\n", $5, $9)

Необходимо вывести значение $5, за которым следует табуляция (\t) и $9, а затем новая строка (\n)**. Для каждой спецификации формата вы должны указать соответствующий аргумент.

Этот оператор printf можно использовать для указания ширины и выравнивания полей вывода. Выражение формата может принимать три необязательных модификатора, следующих за «%» и предшествующих спецификатору формата:

%-width.precision format-specifier

width - ширина поля вывода - это числовое значение. Когда вы указываете ширину поля, содержимое поля по умолчанию будет выровнено по правому краю. Вы должны указать «-», чтобы получить выравнивание по левому краю. Таким образом, «%-20s» выводит строку с выравниванием по левому краю в поле шириной 20 символов. Если строка меньше 20 символов, поле будет заполнено пробелами. В следующих примерах символ «|» выводится, чтобы указать фактическую ширину поля. В первом примере текст выравнивается по правому краю:

printf("|%10s|\n", "hello")

Производит:

|     hello|

В следующем примере текст выравнивается по левому краю:

printf("|%-10s|\n", "hello")

Производит:

|hello     |

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

Обратите внимание, что точность по умолчанию для вывода числовых значений составляет «%.6g».

Вы можете указать width и precision (ширину и точность) динамически, используя значения в списке аргументов printf или sprintf. Вы делаете это, указывая звездочки вместо буквальных значений.

printf("%*.*g\n", 5, 3, myvar);

В этом примере ширина равна 5, точность равна 3, а значение для печати будет получено из myvar.

Точность по умолчанию, используемую оператором print при выводе чисел, можно изменить, задав системную переменную OFMT. Например, если вы используете awk для написания отчетов, содержащих долларовые значения, вы можете предпочесть изменить OFMT на «%.2f».

Использование полного синтаксиса выражения формата может решить проблему с правильным выравниванием filesum для получения полей и заголовков. Одна из причин, по которой мы выводим размер файла перед именем файла, заключалась в том, что поля имеют больше шансов выровняться, если они выводятся в таком порядке. Решение, которое предлагает printf, - это возможность фиксировать ширину полей вывода; поэтому каждое поле начинается в том же столбце.

Давайте изменим порядок полей вывода в отчете filesum. Нам нужна минимальная ширина поля, чтобы второе поле начиналось с той же позиции. Вы указываете место ширины поля между % и спецификацией преобразования. «%-15s» указывает минимальную ширину поля в 15 символов, в которой значение выравнивается по левому краю. «%10d» без дефиса выравнивается по правому краю, что и требуется для десятичного значения.

printf("%-15s\t%10d\n", $9, $5)   # print filename and size

Будет создан отчет, в котором данные выровнены по столбцам, а числа выровнены по правому краю. Посмотрите, как оператор printf используется в действии END:

printf("Total: %d bytes  (%d files)\n", sum, filenum)

Заголовок столбца в правиле BEGIN также изменяется соответствующим образом. Теперь при использовании оператора printf filesum выводит следующий результат:

$ filesum g*
FILE           BYTES
g              23
gawk           2237
gawk.mail      1171
gawk.test      74
gawkro         264
gfilesum       610
grades         64
grades.awk     231
grepscript     6
Total: 4680 bytes  (9 files)

* Способ округления в printf обсуждается в Приложении B.

** Сравните этот оператор с оператором print в программе filesum, которая печатает строку заголовка. Оператор print автоматически добавляет новую строку (значение ORS); при использовании printf вы должны указать новую строку, она никогда не предоставляется автоматически.

7.10 Передача параметров в скрипт

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

awk 'script' var=value inputfile

Каждый параметр следует интерпретировать как один аргумент. Поэтому по обе стороны от знака равенства пробелы не допускаются. Таким образом можно передать несколько параметров. Например, если вы хотите определить переменные high и low из командной строки, вы можете вызвать awk следующим образом:

$ awk -f scriptfile high=100 low=60 datafile

Эти две переменные доступны внутри скрипта, и к ним можно обращаться как к любой переменной awk. Если бы вы поместили этот сценарий в скрипт оболочки, тогда вы могли бы передать аргументы командной строки оболочки как значения. (Оболочка делает доступными аргументы командной строки в позиционных переменных - $1 для первого параметра, $2 для второго и т. д.)* Например, посмотрите на версию сценария оболочки для предыдущей команды:

awk -f scriptfile "high=$1" "low=$2" datafile

Если бы этот сценарий оболочки был назван awket, его можно было бы вызвать как:

$ awket 100 60

«100» будет равно $1 и будет передаваться как значение, присвоенное переменной high.

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

awk '{ ... }' directory=$cwd file1 ...
awk '{ ... }' directory=`pwd` file1 ...

«$cwd» возвращает значение переменной cwd, текущий рабочий каталог (только для csh). Во втором примере используются обратные кавычки для выполнения команды pwd и присвоения ее результата переменной directory (это более переносимо).

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

$ awk '{ print NR, $0 }' OFS='. ' names
1. Tom 656-5789
2. Dale 653-2133
3. Mary 543-1122
4. Joe 543-2211

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

Важным ограничением параметров командной строки является то, что они недоступны в процедуре BEGIN. То есть они недоступны до тех пор, пока не будет прочитана первая строка ввода. Почему? Что ж, вот что сбивает с толку. Параметр, переданный из командной строки, обрабатывается как имя файла. Присваивание не происходит до тех пор, пока параметр, если он был именем файла, не будет фактически вычислен.

Посмотрите на следующий сценарий, который устанавливает переменную n в качестве параметра командной строки.

awk 'BEGIN { print n }
{
if (n == 1) print "Reading the first file"
if (n == 2) print "Reading the second file"
}' n=1 test n=2 test2

Есть четыре параметра командной строки: «n=1», «test», «n=2» и «test2». Теперь, если вы помните, что процедура BEGIN - это «то, что мы делаем перед обработкой ввода», вы поймете, почему ссылка на n в процедуре BEGIN ничего не возвращает. Таким образом, оператор print напечатает пустую строку. Если бы первым параметром был файл, а не назначение переменной, файл не открывался бы до тех пор, пока не будет выполнена процедура BEGIN.

Переменной n присваивается начальное значение 1 из первого параметра. Второй параметр предоставляет имя файла. Таким образом, для каждой строки в test условие «n == 1» будет истинным. После того, как входные данные исчерпаны из test, оценивается третий параметр, и он устанавливает n равным 2. Наконец, четвертый параметр предоставляет имя второго файла. Теперь условие «n == 2» в основной процедуре будет истинным.

Одним из следствий способа оценки параметров является то, что вы не можете использовать процедуру BEGIN для тестирования или проверки параметров, которые вводятся в командной строке. Они доступны только после прочтения строки ввода. Вы можете обойти это ограничение, составив правило «NR == 1» и используя его процедуру для проверки присвоения. Другой способ - проверить параметры командной строки в сценарии оболочки перед вызовом awk.

POSIX awk предоставляет решение проблемы определения параметров до чтения любого ввода. Параметр -v** указывает присвоение переменных, которое вы хотите выполнить перед выполнением процедуры BEGIN (то есть до того, как будет прочитана первая строка ввода). Параметр -v должен быть указан перед сценарием командной строки. Например, следующая команда использует параметр -v для установки разделителя записей для многострочных записей.

$ awk -F"\n" -v RS="" '{ print }' phones.block

Для каждого присваивания переменной, передаваемого программе, требуется отдельная опция -v.

Awk также предоставляет системные переменные ARGC и ARGV, которые будут знакомы программистам на C. Поскольку для этого требуется понимание массивов, мы обсудим эту возможность в Главе 8, Условные выражения, циклы и массивы.

* Осторожно! Не путайте параметры оболочки с полевыми переменными awk.

** Параметр -v не входил в состав исходной (1987 г.) версии nawk (до сих пор используется в системах SunOS 4.1.x и некоторых системах System V Release 3.x). Он был добавлен в 1989 году после того, как Брайан Керниган из Bell Labs, авторы GNU awk и авторы MKS awk договорились о способе установки переменных в командной строке, которые будут доступны внутри блока BEGIN. Теперь это часть спецификации POSIX для awk.

7.11 Извлечение информации

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

Список сокращений ниже представляет собой простую базу данных.

$ cat acronyms
BASIC   Beginner's All-Purpose Symbolic Instruction Code
CICS    Customer Information Control System
COBOL   Common Business Oriented Language
DBMS    Data Base Management System
GIGO    Garbage In, Garbage Out
GIRL    Generalized Information Retrieval Language

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

Сценарий оболочки, который мы разрабатываем, называется acro. Он берет первый аргумент из командной строки (имя акронима) и передает его скрипту awk. Сценарий acro следующий:

$ cat acro
#! /bin/sh
# assign shell's $1 to awk search variable
awk '$1 == search' search=$1 acronyms

Первый аргумент, указанный в командной строке оболочки ($1), присваивается переменной с именем search; эта переменная передается в awk-программу как параметр. Параметры, передаваемые программе awk, указываются после раздела скрипта. (Это несколько сбивает с толку, потому что $1 внутри awk-программы представляет первое поле каждой строки ввода, а $1 в оболочке представляет первый аргумент, указанный в командной строке.)

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

$ acro CICS
CICS Customer Information Control System

Обратите внимание, что мы протестировали параметр как строку ($1 == search). Мы также могли бы записать это как соответствие регулярному выражению ($1 ~ search).

7.11.1 В поисках сбоя

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

I have been trying to rewrite a sed/tr/fgrep script that we use quite
a bit here in Perl, but have thus far been unsuccessful...hence this
posting. Having never written anything in perl, and not wishing to
wait for the Nutshell Perl Book, I figured I'd tap the knowledge of this
group.

Basically, we have several files which have the format:

item     info line 1
         info line 2
           .
           .
           .
         info line n

Where each info line refers to the item and is indented by either
spaces or tabs. Each item "block" is separated by a blank line.

What I need to do, is to be able to type:

info glitch filename

Where info is the name of the perl script, glitch is what I want to
find out about, and filename is the name of the file with the
information in it. The catch is that I need it to print the entire
"block" if it finds glitch anywhere in the file, i.e.:

machine          Sun 3/75
                 8 meg memory
                 Prone to memory glitches
                 more info
                 more info

would get printed if you looked for "glitch" along with any other
"blocks" which contained the word glitch.

Currently we are using the following script:

#!/bin/csh -f
#
sed '/^ /\!s/^/@/' $2 | tr '\012@' '@\012' | fgrep -i $1 | tr '@' '\012'

Which is in a word....SLOW.

I am sure Perl can do it faster, better, etc...but I cannot figure it out.

Any, and all, help is greatly appreciated.

Thanks in advance,
Emmett
-------------------------------------------------------------------
Emmett Hogan              Computer Science Lab, SRI International

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

Вот скрипт info с использованием awk *:

awk 'BEGIN { FS = "\n"; RS = "" }
$0 ~ search { print $0 }' search=$1 $2

Получив тестовый файл с несколькими записями, скрипт info был проверен, чтобы увидеть, может ли он найти слово «glitch».

$ info glitch glitch.test
machine          Sun 3/75
                 8 meg memory
                 Prone to memory glitches
                 more info
                 more info

В следующей главе мы рассмотрим условные конструкции, конструкции цикла и массивы.

* Помните, что для этого вам понадобится awk, обеспечивающий семантику POSIX. Он может называться awk, nawk или еще как-нибудь! Проверьте документацию по локальной системе.

Глава 8.
Условные выражения, циклы и массивы

В этой главе рассматриваются некоторые фундаментальные конструкции программирования. Она охватывает все операторы управления на языке программирования awk. Она также охватывает массивы, переменные, которые позволяют хранить серию значений. Если вы впервые сталкиваетесь с такими конструкциями, вы поймете, что даже sed предоставляет возможности условного программирования и цикла. В awk эти возможности гораздо более обобщены, а синтаксис намного проще в использовании. Фактически, синтаксис условных конструкций и конструкций цикла в awk заимствован из языка программирования C. Таким образом, изучая awk и конструкции из этой главы, вы также становитесь на путь изучения языка C.

8.1 Условные операторы

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

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

if ( expression )
action1
[else
action2]

Если expression оценивается как истинное (ненулевое или непустое), выполняется action1. Если указано предложение else и expression принимает значение false (ноль или пусто), выполняется action2. Выражение может содержать арифметические, логические операторы или операторы сравнения, описанные в Главе 7, Написание скриптов для awk.

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

if ( x ) print x

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

if ( x == y ) print x

Помните, что «==» - это оператор отношения, а «=» - оператор присваивания. Мы также можем проверить, соответствует ли x образцу, используя оператор сопоставления с образцом «~»:

if ( x ~ /[yY](es)?/ ) print x

Вот несколько дополнительных синтаксических моментов:

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

Предполагая, что средний балл 65 или выше является проходным, мы могли бы написать следующее условие:

if ( avg >= 65 )
        grade = "Pass"
else
        grade = "Fail"

Значение, присвоенное grade, зависит от того, оценивается ли выражение «avg >= 65» как истинное или как ложное.

Можно использовать несколько условных операторов, чтобы проверить, истинно ли одно из нескольких возможных условий. Например, возможно, учащимся будет выставлена буквенная оценка вместо оценки «удовлетворительно». Вот условие, при котором буквенная оценка зависит от средней оценки учащегося:

if (avg >= 90) grade = "A"
else if (avg >= 80) grade = "B"
else if (avg >= 70) grade = "C"
else if (avg >= 60) grade = "D"
else grade = "F"

Важно понимать, что такие последовательные условные выражения, как это, оцениваются до тех пор, пока одно из них не вернет истинное значение; как только это происходит, остальные условные выражения пропускаются. Если ни одно из условных выражений не является истинным, принимается последнее выражение else, составляющее действие по умолчанию; в этом случае grade присваивается оценка «F».

8.1.1 Условный оператор

Awk предоставляет условный оператор, который можно найти в языке программирования C. Его форма:

expr ? action1 : action2

Предыдущее простое условие if/else можно записать с помощью условного оператора:

grade = (avg >= 65) ? "Pass" : "Fail"

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

8.2 Циклы

Цикл - это конструкция, которая позволяет нам снова и снова выполнять одно или несколько действий. В awk цикл можно указать с помощью операторов while, do или for.

8.2.1 Цикл while

Синтаксис цикла while:

while ( condition )
    action

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

i = 1
while ( i <= 4 ) {
    print $i
    ++i
}

Как и в выражении if, действие, состоящее из более чем одного оператора, должно быть заключено в фигурные скобки. Обратите внимание на роль каждого утверждения. Первый оператор присваивает i начальное значение. Выражение «i <= 4» сравнивает i с 4, чтобы определить, следует ли выполнить действие. Действие состоит из двух операторов: один просто выводит значение поля, обозначенного как «$i», а другой увеличивает i. i - это переменная счетчика, которая используется для отслеживания того, сколько раз мы проходим цикл. Если мы не увеличим значение переменной счетчика или если сравнение никогда не даст ложного результата (например, i > 0), действие будет повторяться без конца.

8.2.2 Цикл Do

Цикл do - это вариант цикла while. Синтаксис цикла do:

do
    action
while ( condition )

Новая строка не обязательна после do. Также новая строка не обязательна после action, если action заканчивается точкой с запятой. Основная особенность этого цикла в том, что условное выражение появляется после действия. Таким образом, действие выполняется хотя бы один раз. Посмотрите на следующий цикл do.

BEGIN {
    do {
        ++x
        print x
    } while ( x <= 4 )
}

В этом примере значение x устанавливается в теле цикла с помощью оператора автоинкремента. Тело цикла выполняется один раз и вычисляется выражение. В предыдущем примере цикла while начальное значение i было установлено перед циклом. Сначала вычислялось выражение, затем один раз выполнялось тело цикла. Обратите внимание на значение x, когда мы запускаем этот пример:

$ awk -f do.awk
1
2
3
4
5

Перед первой оценкой условного выражения x увеличивается до 1. (Это зависит от того факта, что все переменные awk инициализируются нулем.) Тело цикла выполняется пять раз, а не четыре; когда x равно 4, условное выражение истинно, и тело цикла выполняется снова, увеличивая x до 5 и выводя его значение. Только тогда условное выражение оценивается как ложное и цикл завершается. При изменении оператора с «<=» на «<» или меньше, тело цикла будет выполнено четыре раза.

Чтобы иметь в виду разницу между циклом do и циклом while, помните, что цикл do всегда выполняет тело цикла хотя бы один раз. Внизу процедуры вы решаете, нужно ли вам выполнить ее снова.

Для примера рассмотрим программу, которая перебирает поля записи, ссылаясь на столько полей, сколько необходимо, пока их совокупное значение не превысит 100. Причина, по которой мы используем цикл do, заключается в том, что мы будем ссылаться хотя бы на одно из полей. Мы добавляем значение поля к сумме, и если сумма превышает 100, мы не ссылаемся ни на какие другие поля. Мы ссылаемся на второе поле только в том случае, если первое поле меньше 100. Его значение добавляется к сумме, и если сумма превышает 100, мы выходим из цикла. Если она все еще меньше 100, мы выполняем цикл еще раз.

{
    total = i = 0
    do {
        ++i
        total += $i
    } while ( total <= 100 )
    print i, ":", total
}

Первая строка скрипта инициализирует значения двух переменных: total и i. Цикл увеличивает значение i и использует оператор поля для ссылки на конкретное поле. Каждый раз при прохождении цикла он обращается к другому полю. Когда цикл выполняется в первый раз, ссылка на поле получает значение первого поля и присваивает его переменной total. Условное выражение в конце цикла проверяет, превышает ли total 100. Если да, цикл завершается. Затем печатаются значение i - количество полей, на которые мы ссылались и общее количество. (Этот сценарий предполагает, что каждая запись насчитывает не менее 100; в противном случае нам пришлось бы проверить, не превышает ли i количество полей для записи. Мы строим такой тест в примере, представленном в следующем разделе, чтобы показать цикл for.)

Вот тестовый файл, содержащий ряд чисел:

$ cat test.do
45 25 60 20
10 105 50 40
33 5 9 67
108 3 5 4

Запуск сценария в тестовом файле дает следующее:

$ awk -f do.awk test.do
3 : 130
2 : 115
4 : 114
1 : 108

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

8.2.3 Цикл For

Оператор for предлагает более компактный синтаксис, который дает тот же результат, что и цикл while. Хотя это выглядит более сложным, этот синтаксис намного проще в использовании и гарантирует, что вы предоставите все необходимые элементы цикла. Синтаксис цикла for:

for ( set_counter ; test_counter ; increment_counter )
action

Перевод строки после правой круглой скобки необязателен. Цикл for состоит из трех выражений:

set_counter
Устанавливает начальное значение для переменной счетчика.
test_counter
Устанавливает условие, которое проверяется в начале цикла.
increment_counter
Увеличивает счетчик каждый раз в конце цикла, прямо перед повторным тестированием test_counter.

Посмотрите на этот довольно распространенный цикл for, который печатает каждое поле в строке ввода.

for ( i = 1; i <= NF; i++ )
    print $i

Как и в предыдущем примере, i - это переменная, которая используется для ссылки на поле с помощью оператора поля. Системная переменная NF содержит количество полей для текущей входной записи, и мы проверяем ее, чтобы определить, достиг ли i последнего поля в строке. Значение NF - это максимальное количество раз, которое нужно пройти через цикл. Внутри цикла выполняется инструкция print, печатающая каждое поле в отдельной строке. Сценарий, использующий эту конструкцию, может печатать каждое слово в отдельной строке, которая затем может быть запущена через sort | uniq -c, чтобы получить статистику распределения слов для файла.

Вы также можете написать цикл для печати от последнего поля к первому.

for ( i = NF; i >= 1; i-- )
    print $i

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

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

total = $2 + $3 + $4 + $5 + $6
avg = total / 5

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

total = 0
for (i = 2; i <= NF; ++i)
    total += $i
avg = total / (NF - 1)

Мы инициализируем переменную total каждый раз, потому что мы не хотим, чтобы ее значение накапливалось от одной записи к другой. В начале цикла for счетчик i инициализируется значением 2, потому что первое числовое поле - это поле 2. Каждый раз при прохождении цикла значение текущего поля добавляется к total. Когда указана ссылка на последнее поле (i больше NF), мы выходим из цикла и вычисляем среднее значение. Например, если запись состоит из 4 полей, при первом прохождении цикла мы присваиваем total значение $2. В конце цикла i увеличивается на 1, а затем сравнивается с NF, равным 4. Выражение оценивается как истинное, а total увеличивается на значение $3.

Обратите внимание, как мы делим общую сумму на количество полей минус 1, чтобы удалить имя студента из подсчета. Скобки необходимы вокруг «NF - 1», потому что в противном случае приоритет операторов разделил бы total на NF, а затем вычел 1, вместо того, чтобы сначала вычесть 1 из NF.

8.2.4 Вычисление факториалов

Факториал числа - это результат последовательного умножения этого числа на единицу меньше этого числа. Факториал 4 равен 4 × 3 × 2 × 1, или 24. Факториал 5 в 5 раз больше факториала 4 или 5 × 24, или 120. Получение факториала для данного числа может быть выражено с помощью цикла следующим образом :

fact = number
for (x = number - 1 ; x > 1; x--)
    fact *= x

где number - это число, для которого мы выведем факториал fact. Допустим number равно 5. В первый раз в цикле x равно 4. Действие вычисляет «5 * 4» и присваивает значение fact. В следующий раз в цикле x равно 3, и на него умножается 20. Мы проходим цикл до тех пор, пока x не станет равным 1.

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

awk '# factorial: return factorial of user-supplied number
BEGIN {
    # prompt user; use printf, not print, to avoid the newline
    printf("Enter number: ")
}

# check that user enters a number
$1 ~ /^[0-9]+$/ {
    # assign value of $1 to number & fact
    number = $1
    if (number == 0)
        fact = 1
    else
        fact = number
    # loop to multiply fact*x until x = 1
    for (x = number - 1; x > 1; x--)
        fact *= x
    printf("The factorial of %d is %g\n", number, fact)
    # exit -- saves user from typing CRTL-D.
    exit
}

# if not a number, prompt again.
{ printf("\nInvalid entry. Enter a number: ")
}' -

Это интересный пример основного цикла ввода, который запрашивает ввод и считывает ответ из стандартного ввода. Правило BEGIN используется, чтобы предложить пользователю ввести число. Поскольку мы указали, что ввод должен поступать не из файла, а из стандартного ввода, программа остановится после вывода приглашения, а затем будет ждать, пока пользователь наберет число. Первое правило проверяет, был ли введен номер. В противном случае будет применяться второе правило, предлагающее пользователю снова ввести число. Мы настраиваем цикл ввода, который будет продолжать чтение из стандартного ввода, пока не будет найдена действительная запись. См. программу lookup в следующем разделе для другого примера построения цикла ввода.

Вот пример того, как работает программа factorial:

$ factorial
Enter number: 5
The factorial of 5 is 120

Обратите внимание, что результат использует «%g» в качестве формата спецификации преобразования в операторе printf. Это позволяет использовать нотацию с плавающей точкой для выражения очень больших чисел. Взгляните на следующий пример:

$ factorial
Enter number: 33
The factorial of 33 is 8.68332e+36

8.3 Другие операторы, влияющие на управление потоком

Операторы if, while, for и do позволяют изменить нормальный ход выполнения процедуры. В этом разделе мы рассмотрим несколько других операторов, которые также влияют на изменение управления потоком.

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

Рассмотрим, что происходит в следующем фрагменте программы:

for ( x = 1; x <= NF; ++x )
    if ( y == $x ) {
        print x, $x
        break
    }
print

Устанавливается цикл для проверки каждого поля текущей входной записи. Каждый раз при прохождении цикла значение y сравнивается со значением поля, обозначенного как $x. Если результат верен, мы печатаем номер поля и его значение, а затем выходим из цикла. Следующий оператор, который нужно выполнить, - это print. Использование break означает, что нас интересует только первое совпадение в строке и что мы не хотим перебирать остальные поля.

Вот аналогичный пример с использованием оператора continue:

for ( x = 1; x <= NF; ++x ) {
    if ( x == 3 )
        continue
    print x, $x
}

В этом примере выполняется цикл по полям текущей входной записи, выводится номер поля и его значение. Однако (по какой-то причине) мы не хотим печатать третье поле. Условный оператор проверяет переменную счетчика, и если она равна 3, выполняется инструкция continue. Оператор continue передает управление обратно в начало цикла, где переменная счетчика снова увеличивается. Это позволяет избежать выполнения оператора print для этой итерации. Того же результата можно было достичь, просто переписав условное выражение для выполнения print, пока x не равно 3. Дело в том, что вы можете использовать оператор continue, чтобы избежать попадания в нижнюю часть цикла на определенной итерации.

Есть два оператора, которые влияют на основной цикл ввода: next и exit. Оператор next вызывает чтение следующей строки ввода, а затем возобновляет выполнение с начала сценария*. Это позволяет избежать применения других процедур к текущей строке ввода. Типичное использование оператора next - продолжить чтение ввода из файла, игнорируя другие действия в сценарии, пока этот файл не будет исчерпан. Системная переменная FILENAME предоставляет имя текущего входного файла. Таким образом, шаблон можно записать:

FILENAME == "acronyms" {
    action
    next
}
{ print }

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

Оператор exit завершает основной цикл ввода и передает управление правилу END, если оно есть. Если правило END не определено или в правиле END используется оператор exit, сценарий завершается. Ранее мы использовали оператор exit в программе factorial для выхода после прочтения одной строки ввода.

Оператор exit может принимать выражение в качестве аргумента. Значение этого выражения будет возвращено как статус выхода awk. Если выражение не указано, статус выхода равен 0. Если вы задаете значение для начального оператора exit, а затем снова вызываете exit из правила END без значения, используется первое значение. Например:

awk '{
    ...
    exit 5
}
END { exit }'

Здесь статус выхода из awk будет 5.

В следующих разделах вы встретите примеры, в которых используются эти операторы управления потоком.

* Некоторые awk не позволяют использовать next из пользовательской функции; Caveat emptor.

8.4 Массивы

Массив - это переменная, которая может использоваться для хранения набора значений. Обычно значения так или иначе связаны. Доступ к отдельным элементам осуществляется по их индексу в массиве. Каждый индекс заключен в квадратные скобки. Следующий оператор присваивает значение элементу массива:

array[subscript] = value

В awk необязательно объявлять размер массива; вам нужно только использовать идентификатор как массив. Лучше всего это сделать, присвоив значение элементу массива. Например, в следующем примере строка "cherry" присваивается элементу массива с именем flavor.

flavor[1] = "cherry"

Индекс этого элемента массива равен «1». Следующий оператор выводит строку «cherry»:

print flavor[1]

Циклы можно использовать для загрузки и извлечения элементов из массивов. Например, если массив flavor состоит из пяти элементов, вы можете написать цикл для печати каждого элемента:

flavor_count = 5
for (x = 1; x <= flavor_count; ++x)
    print flavor[x]

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

student_avg[NR] = avg

Системная переменная NR используется в качестве индекса для массива, поскольку она увеличивается для каждой записи. Когда первая запись читается, значение avg помещается в student_avg[1]; для второй записи значение помещается в student_avg[2] и так далее. После того, как мы прочитали все записи, у нас есть список средних значений в массиве student_avg. В правиле END мы можем усреднить все эти оценки, написав цикл, чтобы получить общую сумму оценок, а затем разделив ее на значение NR. Затем мы можем сравнить среднее значение каждого учащегося со средним значением по классу, чтобы собрать итоговые данные для количества учащихся со средним или выше среднего и количества учащихся ниже среднего балла.

END {
    for ( x = 1; x <= NR; x++ )
        class_avg_total += student_avg[x]

    class_average = class_avg_total / NR

    for ( x = 1; x <= NR; x++ )
        if (student_avg[x] >= class_average)
            ++above_average
        else
            ++below_average

    print "Class Average: ", class_average
    print "At or Above Average: ", above_average
    print "Below Average: ", below_average
}

Есть два цикла for для доступа к элементам массива. Первый суммирует средние значения, поэтому их можно разделить на количество записей об учащихся. Следующий цикл извлекает среднее значение каждого учащегося, чтобы его можно было сравнить со средним значением класса. Если оно на уровне среднего или выше, мы увеличиваем значение переменной above_average; в противном случае мы увеличиваем значение below_average.

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

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

В большинстве языков программирования индексы массивов исключительно числовые. В этих реализациях массив - это последовательность мест, где хранятся значения. Индексы массива выводятся из порядка, в котором хранятся значения. Нет необходимости отслеживать индексы. Например, индекс первого элемента массива - «1» или первая позиция в массиве.

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

Иногда важно различать числовые и строковые индексы. Например, если вы используете «04» в качестве индекса элемента массива, вы не можете ссылаться на этот элемент, используя «4» в качестве его индекса. Вы увидите, как справиться с этой проблемой, на примере программы date-month, показанной далее в этой главе.

Ассоциативные массивы - отличительная черта awk - очень мощные, они позволяют использовать строку в качестве индекса для другого значения. Например, вы можете использовать слово в качестве указателя к его определению. Если вы знаете слово, вы можете найти его определение.

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

array[$1] = $2

Используя эту технику, мы можем взять наш список сокращений и загрузить его в массив с именем acro.

acro[$1] = $2

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

acro["BASIC"]

возвращает:

Beginner's All-Purpose Symbolic Instruction Code

Для доступа ко всем элементам ассоциативного массива существует специальный синтаксис цикла. Это версия цикла for.

for ( variable in array )
    do something with array[variable]

array - это имя массива, как оно было определено. variable - это любая переменная, которую вы можете рассматривать как временную переменную, подобную счетчику, который увеличивается в обычном цикле for. Для этой переменной каждый раз в цикле устанавливается определенный индекс. (Поскольку variable - это произвольное имя, вы часто видите используемый item, независимо от того, какое имя переменной использовалось для индекса при загрузке массива.) Например, следующий цикл for печатает имя item акронима и определение, на которое ссылаются под этим именем acro[item].

for ( item in acro )
    print item, acro[item]

В этом примере оператор печати печатает текущий нижний индекс (например, «BASIC»), за которым следует элемент массива acro, на который ссылается индекс («Beginner's All-Purpose Symbolic Instruction Code»).

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

Важно помнить, что все индексы массивов в awk являются строками. Даже когда вы используете число в качестве индекса, awk сначала автоматически преобразует его в строку. Вам не нужно беспокоиться об этом, когда вы используете целочисленные индексы, поскольку они преобразуются в строки как целые числа, независимо от того, какое значение может быть OFMT (исходный awk и более ранние версии нового awk) или CONVFMT (POSIX awk). Но если вы используете в качестве индекса действительное число, преобразование числа в строку может повлиять на вас. Например:

$ gawk 'BEGIN { data[1.23] = "3.21"; CONVFMT = "%d"
> printf "<%s>\n", data[1.23] }'
<>

Здесь между угловыми скобками ничего не было напечатано, поскольку во второй раз 1.23 было преобразовано только в 1, а значение data["1"] имеет пустую строку.

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

Теперь давайте вернемся к нашей программе оценки учащихся для примера. Допустим, мы хотели сообщить, сколько студентов получили «A», сколько студентов получили «B» и т. д. Как только мы определим оценку, мы сможем увеличить счетчик для этой оценки. Мы могли бы установить отдельные переменные для каждой буквенной оценки, а затем проверить, какую из них увеличивать.

if ( grade == "A" )
    ++gradeA
else if (grade == "B" )
    ++gradeB
.
.
.

Однако массив значительно упрощает эту задачу. Мы можем определить массив с именем class_grade и просто использовать буквенную оценку (от A до F) в качестве индекса для массива.

++class_grade[grade]

Таким образом, если оценка - «A», то значение class_grade["A"] увеличивается на единицу. В конце программы мы можем распечатать эти значения в правиле END, используя специальный цикл for:

for (letter_grade in class_grade)
    print letter_grade ":", class_grade[letter_grade] | "sort"

Переменная letter_grade каждый раз в цикле ссылается на единственный индекс массива class_grade. Выходной поток направляется в sort, чтобы убедиться, что оценки выходятся в правильном порядке. (Конвейерный вывод для программ обсуждается в Главе 10, Нижний ящик). Поскольку это последнее добавление, которое мы вносим в скрипт grades.awk, мы можем посмотреть полный листинг.

# grades.awk -- average student grades and determine
# letter grade as well as class averages.
# $1 = student name; $2 - $NF = test scores.

# set output field separator to tab.
BEGIN { OFS = "\t" }

# action applied to all input lines
{
    # add up grades
    total = 0
    for (i = 2; i <= NF; ++i)
        total += $i
    
    # calculate average
    avg = total / (NF - 1)
    
    # assign student's average to element of array
    student_avg[NR] = avg
    
    # determine letter grade
    if (avg >= 90) grade = "A"
    else if (avg >= 80) grade = "B"
    else if (avg >= 70) grade = "C"
    else if (avg >= 60) grade = "D"
    else grade = "F"

    # increment counter for letter grade array
    ++class_grade[grade]
    
    # print student name, average and letter grade
    print $1, avg, grade
}
# print out class statistics
END {
    # calculate class average
    for (x = 1; x <= NR; x++)
        class_avg_total += student_avg[x]
    class_average = class_avg_total / NR

    # determine how many above/below average
    for (x = 1; x <= NR; x++)
        if (student_avg[x] >= class_average)
            ++above_average
        else
            ++below_average

    # print results
    print ""
    print "Class Average: ", class_average
    print "At or Above Average: ", above_average
    print "Below Average: ", below_average

    # print number of students per letter grade
    for (letter_grade in class_grade)
        print letter_grade ":", class_grade[letter_grade] | "sort"
}

Вот пример выполнения:

$ cat grades.test
mona 70 77 85 83 70 89
john 85 92 78 94 88 91
andrea 89 90 85 94 90 95
jasper 84 88 80 92 84 82
dunce 64 80 60 60 61 62
ellis 90 98 89 96 96 92
$ awk -f grades.awk grades.test
mona    79      C
john    88      B
andrea  90.5    A
jasper  85      B
dunce   64.5    D
ellis   93.5    A

Class Average: 83.4167
At or Above Average: 4
Below Average: 2
A: 2
B: 2
C: 1
D: 1

* Технический термин, используемый в The AWK Programming Language - «зависит от реализации».

8.4.2 Проверка на принадлежность к массиву

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

item in array

возвращает 1, если array[item] существует, и 0, если нет. Например, следующий условный оператор верен, если строка «BASIC» является индексом массива acro.

if ( "BASIC" in acro )
    print "Found BASIC"

Это верно, если «BASIC» - это индекс, используемый для доступа к элементу acro. Этот синтаксис не может сказать вам, является ли «BASIC» значением элемента acro. Это выражение аналогично написанию цикла для проверки существования такого индекса, хотя указанное выше выражение намного проще написать и гораздо эффективнее выполнить.

8.4.3 Скрипт поиска в глоссарии

Эта программа считывает серию записей глоссария из файла с именем glossary и помещает их в массив. Пользователю предлагается ввести термин из глоссария, и, если он найден, распечатывается определение термина.

Вот программа lookup:

awk '# lookup -- reads local glossary file and prompts user for query

#0
BEGIN { FS = "\t"; OFS = "\t"
    # prompt user
    printf("Enter a glossary term: ")
}

#1 read local file named glossary
FILENAME == "glossary" {
    # load each glossary entry into an array
    entry[$1] = $2
    next
}

#2 scan for command to exit program
$0 ~ /^(quit|[qQ]|exit|[Xx])$/ { exit }

#3 process any non-empty line
$0 != "" {
    if ( $0 in entry ) {
        # it is there, print definition
        print entry[$0]
    } else
        print $0 " not found"
}

#4 prompt user again for another term
{
    printf("Enter another glossary term (q to quit): ")
}' glossary -

Чтобы упростить обсуждение, правила сопоставления с образцом пронумерованы. Рассматривая отдельные правила, мы будем обсуждать их в том порядке, в котором они встречаются в ходе выполнения сценария. Правило #0 - это правило BEGIN, которое выполняется только один раз перед чтением любого ввода. Оно устанавливает FS и OFS на табуляцию, а затем предлагает пользователю ввести элемент глоссария. Ответ будет поступать из стандартного ввода, но он читается после файла glossary.

Правило #1 проверяет, является ли текущее имя файла (значение FILENAME) «glossary» и поэтому применяется только при чтении ввода из этого файла. Это правило загружает записи глоссария в массив:

entry[term] = definition

где $1 - термин, а $2 - определение. Оператор next в конце правила #1 используется для пропуска других правил в сценарии и вызывает чтение новой строки ввода. Итак, пока не будут прочитаны все записи в файле glossary, никакое другое правило не оценивается.

Когда ввод из glossary исчерпан, awk читает из стандартного ввода, потому что в командной строке указан «-». Стандартный ввод - это то, откуда приходит ответ пользователя. Правило #3 проверяет, что строка ввода ($0) не пуста. Это правило должно соответствовать любому типу пользователя. Действие использует in, чтобы увидеть, является ли строка ввода индексом в массиве. Если это так, мы просто распечатываем соответствующее значение. В противном случае сообщаем пользователю, что действительная запись не найдена.

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

Если пользователь решит выйти, введя «q» в качестве следующей строки ввода, будет выполнено правило #2. Шаблон ищет полную строку, состоящую из альтернативных слов или отдельных букв, которые пользователь может ввести для выхода. Знаки «^» и «$» важны, они означают, что строка ввода не содержит других символов, кроме этих; в противном случае будет совпадать «q» в записи глоссария. Обратите внимание, что размещение этого правила в последовательности правил имеет большое значение. Оно должно стоять перед правилами #3 и #4, потому что эти правила будут соответствовать чему угодно, включая слова «quit» и «exit».

Давайте посмотрим, как работает программа. В этом примере мы сделаем копию файла acronyms и будем использовать его в качестве файла glossary.

$ cp acronyms glossary
$ lookup
Enter a glossary term: GIGO
Garbage in, garbage out
Enter another glossary term (q to quit): BASIC
Beginner's All-Purpose Symbolic Instruction Code
Enter another glossary term (q to quit): q

Как видите, программа настроена так, чтобы запрашивать у пользователя дополнительные элементы, пока пользователь не введет «q».

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

8.4.4 Использование split() для создания массивов

Встроенная функция split() может разбирать любую строку на элементы массива. Эта функция может быть полезна для извлечения «подполей» из поля. Синтаксис функции split():

n = split(string, array, separator)

string - это входная строка, которая будет разбита на элементы массива array. Индексы массива начинаются с 1 и идут до n, количества элементов в массиве. Элементы будут разделены на основе указанного символа separator. Если разделитель не указан, используется разделитель полей (FS). separator может быть полным регулярным выражением, а не только одним символом. Разделение массива аналогично разделению поля; см. раздел «Создание ссылок и разделение полей» в Главе 7.

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

z = split($1, fullname, " ")

В качестве разделителя указывается пробел. На имя человека можно ссылаться как:

fullname[1]

а фамилию человека можно обозначать как:

fullname[z]

потому что z содержит количество элементов в массиве. Это работает независимо от того, содержит ли полное имя человека отчество. Если z - это значение, возвращаемое функцией split(), вы можете написать цикл для чтения всех элементов этого массива.

z = split($1, array, " ")
for (i = 1; i <= z; ++i)
    print i, array[i]

В следующем разделе приведены дополнительные примеры использования функции split().

8.4.5 Создание преобразований

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

При работе над индексной программой, показанной в Главе 12, Полнофункциональные приложения, нам нужен был быстрый способ присвоить номерам томов римские цифры. Другими словами, том 4 необходимо было обозначить в указателе как «IV». Поскольку в ближайшее время не предполагалось, что количество томов превысит 10, мы написали сценарий, который принимал в качестве входных данных число от 1 до 10 и преобразовывал его в римскую цифру.

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

echo $1 |
awk '# romanum -- convert number 1-10 to roman numeral

# define numerals as list of roman numerals 1-10
BEGIN {
    # create array named numerals from list of roman numerals
    split("I,II,III,IV,V,VI,VII,VIII,IX,X", numerals,",")
}

# look for number between 1 and 10
$1 > 0 && $1 <= 10 {
    # print specified element
    print numerals[$1]
    exit
}

{
    print "invalid number"
    exit
}'

Этот скрипт определяет список из 10 римских цифр, а затем использует split() для загрузки их в массив с именем numerals. Это делается в действии BEGIN, потому что это нужно сделать только один раз.

Второе правило проверяет, что первое поле входной строки содержит число от 1 до 10. Если да, то это число используется в качестве индекса для массива numerals, извлекая соответствующий элемент. Оператор exit завершает программу. Последнее правило выполняется только при отсутствии действительной записи.

Вот пример того, как это работает:

$ romanum 4
IV

Следуя той же идее, вот скрипт, который преобразует даты в формате «mm-dd-yy» или «mm/dd/yy» в «month day, year».

awk '
# date-month -- convert mm/dd/yy or mm-dd-yy to month day, year

# build list of months and put in array.
BEGIN {
    # the 3-step assignment is done for printing in book
    listmonths = "January,February,March,April,May,June,"
    listmonths = listmonths "July,August,September,"
    listmonths = listmonths "October,November,December"
    split(listmonths, month, ",")
}

# check that there is input
$1 != "" {

# split on "/" the first input field into elements of array
    sizeOfArray = split($1, date, "/")

# check that only one field is returned
    if (sizeOfArray == 1)
        # try to split on "-"
        sizeOfArray = split($1, date, "-")

# must be invalid
    if (sizeOfArray == 1)
        exit

# add 0 to number of month to coerce numeric type
    date[1] += 0

# print month day, year
    print month[date[1]], (date[2] ", 19" date[3])
}'

Этот сценарий читает из стандартного ввода. Действие BEGIN создает массив с именем month, элементы которого являются названиями месяцев года. Второе правило проверяет, что у нас есть непустая строка ввода. Первый оператор в связанном действии разбивает первое поле ввода, ища «/» в качестве разделителя. sizeOfArray содержит количество элементов в массиве. Если awk не удалось проанализировать строку, он создает массив только с одним элементом. Таким образом, мы можем проверить значение sizeOfArray, чтобы определить, есть ли у нас несколько элементов. В противном случае мы предполагаем, что, возможно, в качестве разделителя использовался «-». Если не удается создать массив с несколькими элементами, мы предполагаем, что ввод недопустим, и выходим. Если мы успешно проанализировали ввод, date[1] содержит номер месяца. Это значение можно использовать как индекс в массиве month, вкладывая один массив в другой. Однако перед использованием date[1] мы приводим тип date[1], добавляя к нему 0. Хотя awk правильно интерпретирует «11» как число, ведущие нули могут привести к тому, что число будет рассматриваться как строка. Таким образом, «06» не может быть распознано должным образом без приведения типов. Элемент, обозначенный date[1], используется как индекс month.

Вот пример выполнения:

$ echo "5/11/55" | date-month
May 11, 1955

8.4.6 Удаление элементов массива

Awk предоставляет инструкцию для удаления элемента массива. Синтаксис:

delete array[subscript]

Скобки обязательны. Этот оператор удаляет элемент, проиндексированный как subscript из массива array. В частности, тест in для subscript теперь будет возвращать false. Это отличается от простого присвоения этому элементу пустой строки; в этом случае in все равно будет возвращать true. См. Сценарий lotto в следующей главе для примера использования оператора delete.

8.5 Процессор акронимов

Теперь давайте посмотрим на программу, которая сканирует файл на наличие акронимов. Каждый акроним заменяется полным текстовым описанием и акронимом в круглых скобках. Если в строке имеется ссылка на «BASIC», мы хотели бы заменить ее описанием «Beginner's AllPurpose Symbolic Instruction Code» и поставить акроним в круглых скобках после нее. (Это, вероятно, не самая полезная программа, но методы, используемые в ней, являются общими и имеют много таких применений.)

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

awk '# awkro - expand acronyms
# load acronyms file into array "acro"
FILENAME == "acronyms" {
    split($0, entry, "\t")
    acro[entry[1]] = entry[2]
    next
}

# process any input line containing caps
/[A-Z][A-Z]+/ {
    # see if any field is an acronym
    for (i = 1; i <= NF; i++)
        if ( $i in acro ) {
            # if it matches, add description
            $i = acro[$i] " (" $i ")"
        }
}

{
    # print all lines
    print $0
}' acronyms $*

Давайте сначала посмотрим на это в действии. Вот пример входного файла.

$ cat sample
The USGCRP is a comprehensive
research effort that includes applied
as well as basic research.
The NASA program Mission to Planet Earth
represents the principal space-based component
of the USGCRP and includes new initiatives
such as EOS and Earthprobes.

И вот файл acronyms:

$ cat acronyms
USGCRP  U.S. Global Change Research Program
NASA    National Aeronautic and Space Administration
EOS     Earth Observing System

Теперь мы запускаем программу на примере файла.

$ awkro sample
The U.S. Global Change Research Program (USGCRP) is a comprehensive
research effort that includes applied
as well as basic research.
The National Aeronautic and Space Administration (NASA) program
Mission to Planet Earth
represents the principal space-based component
of the U.S. Global Change Research Program (USGCRP) and includes new
initiatives
such as Earth Observing System (EOS) and Earthprobes.

Мы рассмотрим эту программу в двух частях. Первая часть читает записи из файла acronyms.

# load acronyms file into array "acro"
FILENAME == "acronyms" {
    split($0, entry, "\t")
    acro[entry[1]] = entry[2]
    next
}

Два поля из этих записей загружаются в массив с использованием первого поля в качестве индекса и присвоения второго поля элементу массива. Другими словами, сам акроним является указателем к своему описанию.

Обратите внимание, что мы не меняли разделитель полей, а вместо этого использовали функцию split() для создания массива entry. Затем этот массив используется для создания массива с именем acro.

Вот вторая половина программы:

# process any input line containing caps
/[A-Z][A-Z]+/ {
    # see if any field is an acronym
    for (i = 1; i <= NF; i++)
        if ( $i in acro ) {
            acronym = $i
            # if it matches, add description
            $i = acro[$i] " (" $i ")"
        }
}

{
    # print all lines
    print $0
}

Только строки, содержащие более одной заглавной буквы подряд, обрабатываются первым из двух действий, показанных здесь. Это действие проходит через каждое поле записи. В основе этого раздела лежит условный оператор, который проверяет, является ли текущее поле ($i) индексом массива (acro). Если поле является индексом, мы заменяем исходное значение поля элементом массива и исходным значением в круглых скобках. (Полям могут быть присвоены новые значения, как и обычным переменным.) Обратите внимание, что вставка описания акронима приводит к тому, что строки могут быть слишком длинными. В следующей главе обсуждается функция length(), которую можно использовать для определения длины строки, чтобы вы могли разделить ее, если она слишком длинная.

Теперь мы собираемся изменить программу так, чтобы она производила замену только при первом появлении акронима. После того, как мы его нашли, мы больше не хотим искать этот акроним. Это легко сделать; мы просто удаляем этот акроним из массива.

if ( $i in acro ) {
    # if it matches, add description
    $i = acro[$i] " (" $i ")"
    # only expand the acronym once
    delete acro[acronym]
}

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

sed 's/\([^.,;:!][^.,;:!]*\)\([.,;:!]\)/\1 @@@\2/g'

и один после:

sed 's/ @@@\([.,;:!]\)/\1/g'

Сценарий sed, запускаемый до вызова awk, может просто вставить пробел перед любым знаком препинания, что приведет к его интерпретации как отдельного поля. Также была добавлена строка мусорных символов (@@@), чтобы мы могли легко идентифицировать и восстанавливать знаки препинания. (Сложное выражение, используемое в первой команде sed, гарантирует, что мы уловим регистр более чем одного знака препинания в строке.)

Такое решение, использующее другой инструмент из набора инструментов UNIX, демонстрирует, что не все нужно делать как процедуру awk. Awk тем более ценен, что находится в среде UNIX.

Однако с помощью POSIX awk мы можем реализовать другое решение, в котором используется регулярное выражение для соответствия аббревиатуре. Такое решение можно реализовать с помощью функций match() и sub(), описанных в следующей главе.

8.5.1 Многомерные массивы

Awk поддерживает линейные массивы, в которых индекс каждого элемента массива является одним индексом. Если вы представите линейный массив как строку чисел, двумерный массив представляет строки и столбцы чисел. Вы можете ссылаться на элемент во втором столбце третьей строки как на «array[3, 2]». Двух- и трехмерные массивы являются примерами многомерных массивов. Awk не поддерживает многомерные массивы, но вместо этого предлагает синтаксис для индексов, имитирующих ссылку на многомерный массив. Например, вы можете написать следующее выражение:

file_array[NR, i] = $i

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

file_array[2, 4]

выдаст значение четвертого поля второй записи.

Этот синтаксис не создает многомерный массив. Он преобразуется в строку, которая однозначно идентифицирует элемент в линейном массиве. Компоненты многомерного индекса интерпретируются как отдельные строки (например, «2» и «4») и объединяются вместе, разделенные значением системной переменной SUBSEP. Разделитель компонентов индекса по умолчанию определен как «\034», непечатаемый символ, который редко встречается в тексте ASCII. Таким образом, awk поддерживает одномерный массив, и индекс для нашего предыдущего примера фактически будет «2\0344» (конкатенация 2, значения SUBSEP и 4). Основным следствием этого моделирования многомерных массивов является то, что чем больше массив, тем медленнее он получает доступ к отдельным элементам. Однако вам следует рассчитать это, используя свое собственное приложение, с различными реализациями awk (см. Главу 11, Семейство awk).

Вот пример сценария awk с именем bitmap.awk, который показывает, как загружать и выводить элементы многомерного массива. Этот массив представляет двумерное растровое изображение, имеющее ширину и высоту 12 символов.

BEGIN { FS = ","   # comma-separated fields
    # assign width and height of bitmap
    WIDTH = 12
    HEIGHT = 12
    # loop to load entire array with "O"
    for (i = 1; i <= WIDTH; ++i)
        for (j = 1; j <= HEIGHT; ++j)
            bitmap[i, j] = "O"
}
# read input of the form x,y.
{
    # assign "X" to that element of array
    bitmap[$1, $2] = "X"
}
# at end output multidimensional array
END {
    for (i = 1; i <= WIDTH; ++i){
        for (j = 1; j <= HEIGHT; ++j)
            printf("%s", bitmap[i, j] )
        # after each row, print newline
        printf("\n")
    }
}

Перед тем, как будет прочитан любой ввод, массив bitmap заполняется символом O. Этот массив состоит из 144 элементов. Входными данными для этой программы являются координаты, по одной в каждой строке.

$ cat bitmap.test
1,1
2,2
3,34,4
5,5
6,6
7,7
8,8
9,9
10,10
11,11
12,12
1,12
2,11
3,10
4,9
5,8
6,7
7,6
8,5
9,4
10,3
11,2
12,1

Для каждой координаты программа поместит «X» вместо «O» в качестве этого элемента массива. В конце скрипта тот же цикл, который загружал массив, теперь выводит его. В следующем примере считывается ввод из файла bitmap.test.

$ awk -f bitmap.awk bitmap.test
XOOOOOOOOOOX
OXOOOOOOOOXO
OOXOOOOOOXOO
OOOXOOOOXOOO
OOOOXOOXOOOO
OOOOOXXOOOOO
OOOOOXXOOOOO
OOOOXOOXOOOO
OOOXOOOOXOOO
OOXOOOOOOXOO
OXOOOOOOOOXO
XOOOOOOOOOOX

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

if ((i, j) in array)

Это проверяет, существует ли индекс i, j (фактически, i SUBSEP j) в указанном массиве. Цикл по многомерному массиву такой же, как и с одномерным массивом.

for (item in array)

Вы должны использовать функцию split() для доступа к отдельным компонентам индекса. Таким образом:

split(item, subscr, SUBSEP)

создает массив subscr из индекса item.

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

8.6 Системные переменные, являющиеся массивами

Awk предоставляет две системные переменные, которые являются массивами:

ARGV
Массив аргументов командной строки, исключая сам сценарий и любые параметры, указанные при вызове awk. Количество элементов в этом массиве доступно в ARGC. Индекс первого элемента массива равен 0 (в отличие от всех других массивов в awk, но согласуется с C), а последний - ARGC - 1.
ENVIRON
Массив переменных среды. Каждый элемент массива - это значение в текущей среде, а индекс - это имя переменной среды.

8.6.1 Массив параметров командной строки

Вы можете написать цикл для ссылки на все элементы массива ARGV.

# argv.awk - print command-line parameters
BEGIN { for (x = 0; x < ARGC; ++x)
            print ARGV[x]
        print ARGC
}

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

$ awk -f argv.awk 1234 "John Wayne" Westerns n=44 -
awk
1234
John Wayne
Westerns
n=44
-
6

Как видите, в массиве шесть элементов. Первый элемент - это имя команды, запустившей сценарий. Последним аргументом в данном случае является «-» - имя файла для стандартного ввода. Обратите внимание, что «-f argv.awk» не отображается в списке параметров.

Как правило, значение ARGC будет не менее 2. Если вы не хотите ссылаться на имя программы или имя файла, вы можете инициализировать счетчик значением 1, а затем протестировать ARGC - 1, чтобы избежать ссылки на последний параметр (предполагая, что есть только одно имя файла).

Помните, что если вы вызываете awk из сценария оболочки, параметры командной строки передаются скрипту, а не awk. Вы должны передать параметры командной строки скрипта программе awk внутри скрипта. Например, вы можете передать все параметры командной строки из сценария в awk, используя «$*». Взгляните на следующий скрипт:

awk '
# argv.sh - print command-line parameters
BEGIN {
    for (x = 0; x < ARGC; ++x)
        print ARGV[x]
    print ARGC
}' $*

Этот сценарий оболочки работает так же, как и первый пример вызова awk.

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

# number.awk - test command-line parameters
BEGIN {
    for (x = 1; x < ARGC; ++x)
        if ( ARGV[x] !~ /^[0-9]+$/ ) {
            print ARGV[x], "is not an integer."
            exit 1
        }
}

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

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

awk '# phone - find phone number for person
# supply name of person on command line or at prompt.
BEGIN { FS = ","
    # look for parameter
    if ( ARGC > 2 ){
        name = ARGV[1]
        delete ARGV[1]
    } else {
        # loop until we get a name
        while (! name) {
            printf("Enter a name? ")
            getline name < "-"
        }
    }
}
$1 ~ name {
    print $1, $NF
}' $* phones.data

Мы тестируем переменную ARGC, чтобы увидеть, есть ли более двух параметров. Указав «$*», мы можем передать все параметры из командной строки оболочки внутрь командной строки awk. Если этот параметр был предоставлен, мы предполагаем, что второй параметр, ARGV[1], является тем, который нам нужен, и он назначен переменной name. Затем этот параметр удаляется из массива. Это очень важно, если параметр, который предоставляется в командной строке, не имеет формы "var=value"; в противном случае он будет позже интерпретирован как имя файла. Если указаны дополнительные параметры, они будут интерпретироваться как имена файлов альтернативных баз данных телефонов. Если параметров не более двух, то запрашиваем название. Функция getline обсуждается в Главе 10; используя этот синтаксис, считываем следующую строку из стандартного ввода.

Вот несколько примеров этого скрипта в действии:

$ phone John
John Robinson 696-0987
$ phone
Enter a name? Alice
Alice Gold (707) 724-0000
$ phone Alice /usr/central/phonebase
Alice Watson (617) 555-0000
Alice Gold (707) 724-0000

Первый пример предоставляет имя в командной строке, второй запрашивает пользователя, а третий принимает два параметра командной строки и использует второй как имя файла. (Сценарий не позволит вам указать имя файла, не указав имя человека в командной строке. Однако вы можете разработать тест, который разрешил бы этот синтаксис.)

Поскольку вы можете добавлять и удалять из массива ARGV, есть потенциал для выполнения множества интересных манипуляций. Вы можете поместить имя файла, например, в конец массива ARGV, и он будет открыт, как если бы он был указан в командной строке. Точно так же вы можете удалить имя файла из массива, и оно никогда не будет открыто. Обратите внимание, что если вы добавляете новые элементы в ARGV, вы также должны увеличивать ARGC; awk использует значение ARGC, чтобы узнать, сколько элементов в ARGV он должен обработать. Таким образом, простое уменьшение ARGC не позволит awk проверять последний элемент в ARGV.

В качестве особого случая, если значением элемента ARGV является пустая строка (""), awk пропустит ее и перейдет к следующему элементу.

8.6.2 Массив переменных среды

Массив ENVIRON был добавлен независимо как в gawk, так и в MKS awk. Затем он был добавлен в nawk System V Release 4 и теперь включен в стандарт POSIX для awk. Это позволяет вам получить доступ к переменным среды. Следующий сценарий просматривает элементы массива ENVIRON и распечатывает их.

# environ.awk - print environment variable
BEGIN {
    for (env in ENVIRON)
        print env "=" ENVIRON[env]
}

Индекс массива - это имя переменной. Сценарий генерирует тот же результат, что и команда env (printenv в некоторых системах).

$ awk -f environ.awk
DISPLAY=scribe:0.0
FRAME=Shell 3
LOGNAME=dale
MAIL=/usr/mail/dale
PATH=:/bin:/usr/bin:/usr/ucb:/work/bin:/mac/bin:.
TERM=mac2cs
HOME=/work/dale
SHELL=/bin/csh
TZ=PST8PDT
EDITOR=/usr/bin/vi

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

ENVIRON["LOGNAME"]

Вы также можете изменить любой элемент массива ENVIRON.

ENVIRON["LOGNAME"] = "Tom"

Однако это изменение не влияет на фактическую среду пользователя (то есть, когда awk завершится, значение LOGNAME не будет изменено), а также не влияет на среду, унаследованную программами, которые вызываются из awk через функции getline или system(), которые описаны в Главе 10.

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

Глава 9.
Функции

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

9.1 Арифметические функции

Девять встроенных функций можно отнести к арифметическим. Большинство из них принимают числовой аргумент и возвращают числовое значение. Таблица 9.1 суммирует эти арифметические функции.

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

Функция Awk Описание
cos(x) Возвращает косинус x (x в радианах).
exp(x) Возвращает e в степени x.
int(x) Возвращает усеченное значение x.
log(x) Возвращает натуральный логарифм (по основанию е) числа x.
sin(x) Возвращает синус x (x в радианах).
sqrt(x) Возвращает квадратный корень из x.
atan2(y,x) Возвращает арктангенс y/x в диапазоне от -π до π.
rand() Возвращает псевдослучайное число r, где 0 <= r < 1.
srand(x) Устанавливает начальное число для rand(). Если начальное число не указано, используется текущее время в секундах. Возвращает предыдущее начальное число.

9.1.1 Тригонометрические функции

Тригонометрические функции cos() и sin() работают одинаково, принимая единственный аргумент, который представляет собой размер угла в радианах, и возвращая косинус или синус для этого угла. (Чтобы преобразовать градусы в радианы, умножьте число на π/180). Тригонометрическая функция atan2() принимает два аргумента и возвращает арктангенс их частного. Выражение

atan2(0, -1)

продуцирует π.

Функция exp() использует естественную экспоненту, которая также известна как возведение в степень по основанию e. Выражение

exp(1)

возвращает натуральное число 2,71828, основание натурального логарифма, называемое e. Таким образом, exp(x) - это e в степени x.

Функция log() дает натуральный логарифм x, обратный функции exp(). Функция sqrt() принимает единственный аргумент и возвращает (положительный) квадратный корень из этого аргумента.

9.1.2 Целочисленная функция

Функция int() обрезает числовое значение, удаляя цифры справа от десятичной точки. Взгляните на следующие два утверждения:

print 100/3
print int(100/3)

Результат этих операторов показан ниже:

33.3333
33

Функция int() просто усекает нецелую часть; без округления в большую или меньшую сторону. (Используйте формат printf «%.0f» для округления.)*

* Способ округления в printf обсуждается в Приложении B, Краткий справочник по awk.

9.1.3 Генерация случайных чисел

Функция rand() генерирует псевдослучайное число с плавающей запятой от 0 до 1. Функция srand() устанавливает начальное число или начальную точку для генерации случайных чисел. Если srand() вызывается без аргумента, она использует время дня для генерации начального числа. С аргументом x srand() использует x в качестве начального числа.

Если вы вообще не вызываете srand(), awk будет действовать так, как если бы srand() был вызван с постоянным аргументом перед запуском вашей программы, заставляя вас получать одну и ту же начальную точку каждый раз при запуске вашей программы. Это полезно, если вы хотите воспроизводимого поведения для тестирования, но неуместно, если вы действительно хотите, чтобы ваша программа каждый раз вела себя по-разному. Взгляните на следующий сценарий:

# rand.awk -- test random number generation
BEGIN {
    print rand()
    print rand()
    srand()
    print rand()
    print rand()
}

Мы печатаем результат функции rand() дважды, а затем вызываем функцию srand() перед печатью результата функции rand() еще два раза. Запустим сценарий.

$ awk -f rand.awk
0.513871
0.175726
0.760277
0.263863

Генерируются четыре случайных числа. Теперь посмотрим, что произойдет, когда мы снова запустим программу:

$ awk -f rand.awk
0.513871
0.175726
0.787988
0.305033

Первые два «случайных» числа идентичны числам, сгенерированным при предыдущем запуске программы, в то время как последние два числа различны. Последние два числа отличаются, потому что мы предоставили функции rand() новое начальное число.

Возвращаемое значение функции srand() - это начальное число, которое она использовала. Это можно использовать для отслеживания последовательностей случайных чисел и их повторного запуска при необходимости.

9.1.4 Выбери их

Чтобы показать, как использовать rand(), мы рассмотрим сценарий, который реализует «быстрый выбор» для лотереи. Этот сценарий, названный lotto, выбирает x чисел из ряда чисел от 1 до y. В командной строке можно указать два аргумента: сколько чисел выбрать (по умолчанию 6) и наибольшее число в серии (по умолчанию 30). Используя значения по умолчанию для x и y, сценарий генерирует шесть уникальных случайных чисел от 1 до 30. Числа сортируются для удобства чтения от наименьшего к наибольшему и выводятся. Прежде чем рассматривать сам скрипт, давайте запустим программу:

$ lotto
Pick 6 of 30
9 13 25 28 29 30
$ lotto 7 35
Pick 7 of 35
1 6 9 16 20 22 27

В первом примере значения по умолчанию используются для печати шести случайных чисел от 1 до 30. Во втором примере печатаются семь случайных чисел из от 1 до 35.

Полный сценарий lotto довольно сложен, поэтому, прежде чем рассматривать весь сценарий, давайте рассмотрим сценарий меньшего размера, который генерирует одно случайное число в серии:

awk -v TOPNUM=$1 '
# pick1 - pick one random number out of y
# main routine
BEGIN {
# seed random number using time of day
    srand()
# get a random number
    select = 1 + int(rand() * TOPNUM)
# print pick
    print select
}'

Сценарий оболочки ожидает от командной строки единственный аргумент, который передается в программу как «TOPNUM=$1» с использованием параметра -v. Все действие происходит в процедуре BEGIN. Поскольку в программе нет других операторов, awk завершает работу по завершении процедуры BEGIN.

Основная процедура сначала вызывает функцию srand(), чтобы заполнить генератор случайных чисел. Затем мы получаем случайное число, вызывая функцию rand():

select = 1 + int(rand() * TOPNUM)

Было бы полезно увидеть это выражение в разбивке, чтобы каждая его часть была очевидна.

Оператор Результат
print r = rand() 0.467315
print r * TOPNUM 14.0195
print int(r * TOPNUM) 14
print 1 + int(r * TOPNUM) 15

Поскольку функция rand() возвращает число от 0 до 1, мы умножаем его на TOPNUM, чтобы получить число от 0 до TOPNUM. Затем мы обрезаем число, чтобы удалить дробные значения, а затем добавляем к числу 1. Последнее необходимо, потому что rand() может вернуть 0. В этом примере генерируется случайное число 15. Вы можете использовать эту программу для печати любого отдельного числа, например, для выбора числа от 1 до 100.

$ pick1 100
83

Сценарий lotto должен «выбрать один» несколько раз. По сути, нам нужно настроить цикл for для выполнения функции rand() столько раз, сколько необходимо. Одна из причин, по которой это сложно, заключается в том, что мы должны беспокоиться о дубликатах. Другими словами, номер можно выбрать снова; поэтому мы должны отслеживать уже выбранные числа.

Вот сценарий lotto:

awk -v NUM=$1 -v TOPNUM=$2 '
# lotto - pick x random numbers out of y
# main routine
BEGIN {
# test command line args; NUM = $1, how many numbers to pick
#                      TOPNUM = $2, last number in series
    if (NUM <= 0)
        NUM = 6
    if (TOPNUM <= 0)
        TOPNUM = 30
# print "Pick x of y"
    printf("Pick %d of %d\n", NUM, TOPNUM)
# seed random number using time and date; do this once
    srand()
# loop until we have NUM selections
    for (j = 1; j <= NUM; ++j) {
        # loop to find a not-yet-seen selection
        do {
            select = 1 + int(rand() * TOPNUM)
        } while (select in pick)
        pick[select] = select
    }
# loop through array and print picks.
    for (j in pick)
        printf("%s ", pick[j])
    printf("\n")
}'

В отличие от предыдущей программы, эта ищет два аргумента командной строки, указывая x чисел из y. Основная процедура проверяет, были ли предоставлены эти числа, а если нет, то присваивает значения по умолчанию.

Есть только один массив, pick, для хранения выбранных случайных чисел. Каждое число гарантированно находится в желаемом диапазоне, потому что результат rand() (значение от 0 до 1) умножается на TOPNUM и затем усекается. Суть сценария - это цикл, который повторяется NUM раз для присвоения NUM элементов массиву pick.

Чтобы получить новое не повторяющееся случайное число, мы используем внутренний цикл, который генерирует выборки и проверяет, находятся ли они в массиве pick. (Использование оператора in намного быстрее, чем цикл по массиву, сравнивающий индексы.) В то время как (select in pick), соответствующий элемент уже найден, поэтому выбор является дубликатом, и мы его отклоняем. Если не select in pick, то мы назначаем select элементу массива pick. Это заставит будущие тесты in вернуть true, что приведет к продолжению цикла do.

Наконец, программа перебирает массив pick и печатает элементы. Эта версия сценария lotto упускает одну вещь. Посмотрим, сможете ли вы сказать, что это, если мы снова запустим его:

$ lotto 7 35
Pick 7 of 35
5 21 9 30 29 20 2

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

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

# create a numerically indexed array for sorting
i = 1
for (j in pick)
    sortedpick[i++] = pick[j]

Программа lotto настроена так, чтобы делать все в блоке BEGIN. Никакие входные данные не обрабатываются. Однако вы можете изменить этот сценарий, чтобы прочитать список имен из файла и для каждого имени создать «быстрый выбор».

9.2 Строковые функции

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

Таблица 9.2: Встроенные строковые функции AWK

Функция Awk Описание
gsub(r,s,t) Глобально заменяет s для каждого совпадения регулярного выражения r в строке t. Возвращает количество замен. Если t не указан, по умолчанию используется $0.
index(s,t) Возвращает позицию подстроки t в строке s или ноль, если нет.
length(s) Возвращает длину строки s или длину $0, если строка не указана.
match(s,r) Возвращает либо позицию в s, где начинается регулярное выражение r, либо 0, если вхождений не найдено. Устанавливает значения RSTART и RLENGTH.
split(s,a,sep) Разбирает строку s на элементы массива a, используя разделитель полей sep; возвращает количество элементов. Если sep не указан, используется FS. Разделение массива работает так же, как разделение полей.
sprintf("fmt",expr) Использует спецификацию формата printf для expr.
sub(r,s,t) Заменяет s на первое совпадение регулярного выражения r в строке t. В случае успеха возвращает 1; 0 в противном случае. Если t не указан, по умолчанию используется $0.
substr(s,p,n) Возвращает подстроку строки s в начальной позиции p до максимальной длины n. Если n не указано, используется остальная часть строки из p.
tolower(s) Переводит все символы верхнего регистра в строке s в нижний регистр и возвращает новую строку.
toupper(s) Переводит все символы нижнего регистра в строке s в верхний регистр и возвращает новую строку.

Функция split() была представлена в предыдущей главе при обсуждении массивов.

Функция sprintf() использует те же спецификации формата, что и printf(), которые обсуждаются в Главе 7, Написание скриптов для awk. Она позволяет применять спецификации формата к строке. Вместо того, чтобы печатать результат, sprintf() возвращает строку, которая может быть присвоена переменной. Она может выполнять специализированную обработку входных записей или полей, такую как преобразование символов. Например, в следующем примере функция sprintf() используется для преобразования числа в символ ASCII.

for (i = 97; i <= 122; ++i) {
    nextletter = sprintf("%c", i)
    ...
}

Цикл предоставляет числа от 97 до 122, которые производят символы ASCII от a до z.

И нам остается обсудить три основные встроенные строковые функции: index(), substr() и length().

9.2.1 Подстроки

Функции index() и substr() работают с подстроками. Для строки s index(s,t) возвращает крайнюю левую позицию, где строка t находится в s. Начало строки находится в позиции 1 (что отличается от языка C, где первый символ в строке находится в позиции 0). Взгляните на следующий пример:

pos = index("Mississippi", "is")

Значение pos равно 2. Если подстрока не найдена, функция index() возвращает 0.

Для строки s функция substr(s,p) возвращает символы, начинающиеся с позиции p. В следующем примере создается номер телефона без кода города.

phone = substr("707-555-1111", 5)

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

area_code = substr("707-555-1111", 1, 3)

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

awk '# caps - capitalize 1st letter of 1st word
# initialize strings
BEGIN { upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower = "abcdefghijklmnopqrstuvwxyz"
}

# for each input line
{
# get first character of first word
    FIRSTCHAR = substr($1, 1, 1)
# get position of FIRSTCHAR in lowercase array; if 0, ignore
    if (CHAR = index(lower, FIRSTCHAR))
    # change $1, using position to retrieve
    # uppercase character
    $1 = substr(upper, CHAR, 1) substr($1, 2)
# print record
    print $0
}'

Этот скрипт создает две переменные, upper и lower, состоящие из прописных и строчных букв. Любой символ, который мы находим в lower, можно найти в том же положении в upper. Первый оператор основной процедуры извлекает из первого поля единственный символ, первый. Условный оператор проверяет, можно ли найти этот символ в lower, используя функцию index(). Если CHAR не 0, тогда CHAR можно использовать для извлечения символа верхнего регистра из upper. Существует два вызова функции substr(): первый извлекает заглавную букву, а второй - оставшуюся часть первого поля, извлекая все символы, начиная со второго символа. Значения, возвращаемые обеими функциями substr(), объединяются и присваиваются $1. Назначение поля, как мы делаем здесь, - это новый поворот, но он имеет дополнительное преимущество, заключающееся в том, что запись может выводиться в обычном режиме. (Если присвоение было сделано переменной, вам нужно будет вывести переменную, а затем вывести оставшиеся поля записи.) Оператор print печатает измененную запись. Давайте посмотрим на это в действии:

$ caps
root user
Root user
dale
Dale
Tom
Tom

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

9.2.2 Длина строки

Представляя программу awkro в предыдущей главе, мы отметили, что программа, вероятно, будет выдавать строки, превышающие 80 символов. Ведь описания довольно длинные. Узнать, сколько символов в строке, можно с помощью встроенной функции length(). Например, чтобы оценить длину текущей входной записи, мы указываем length($0). (Как это бывает, если length() вызывается без аргумента, он возвращает длину $0.)

Функция length() часто используется для определения длины текущей входной записи, чтобы определить, нужно ли нам разорвать строку.

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

Глава 13, Сборник сценариев содержит скрипт, который использует функцию length() для разрыва строк шириной более 80 столбцов.

9.2.3 Функции замены

Awk предоставляет две функции замены: sub() и gsub(). Разница между ними в том, что gsub() выполняет свою замену глобально во входной строке, тогда как sub() выполняет только первую возможную замену. Это делает gsub() эквивалентным команде замены sed с флагом g (global).

Обе функции принимают как минимум два аргумента. Первый - это регулярное выражение (окруженное косой чертой), которое соответствует шаблону, а второй аргумент - это строка, заменяющая то, что соответствует шаблону. Регулярное выражение может быть передано переменной, и в этом случае косые черты опускаются. Необязательный третий аргумент определяет строку, которая является целью замены. Если нет третьего аргумента, выполняется замена для текущей входной записи ($0).

Функции замены изменяют указанную строку напрямую. Вы можете ожидать, учитывая способ работы функций, что функция возвращает новую строку, созданную при выполнении замены. Функции замены фактически возвращают количество выполненных замен. sub() всегда будет возвращать 1 в случае успеха; обе возвращают 0 в случае неудачи. Таким образом, вы можете проверить результат, чтобы увидеть, была ли произведена замена.

Например, в следующем примере gsub() используется для замены всех вхождений «UNIX» на «POSIX».

if (gsub(/UNIX/, "POSIX"))
    print

Условный оператор проверяет возвращаемое значение gsub() таким образом, что текущая строка ввода печатается только в случае внесения изменений.

Как и в случае с sed, если в строке подстановки появляется символ «&», он будет заменен строкой, соответствующей регулярному выражению. Используйте «\&» для вывода амперсанда. (Помните, что для вставки буквального «\» в строку вам необходимо ввести два обратных слэша). Также обратите внимание, что awk не «запоминает» предыдущее регулярное выражение, как sed, поэтому вы не можете использовать синтаксис «//» для ссылки на последнее регулярное выражение.

В следующем примере - любое вхождение «UNIX» будет окружено управляющими последовательностями troff для изменения шрифта.

gsub(/UNIX/, "\\fB&\\fR")

Если на входе «the UNIX operating system», на выходе будет «“the \fBUNIX\fR operating system».

В Главе 4, Написание сценариев sed мы представили следующий сценарий sed с именем do.outline:

sed -n '
s/"//g
s/^\.Se /Chapter /p
s/^\.Ah /•A. /p
s/^\.Bh /••B. /p' $*

Теперь вот этот сценарий, переписанный с использованием функций подстановки:

awk '
{
gsub(/"/, "")
if (sub(/^\.Se /, "Chapter ")) print
if (sub(/^\.Ah /, "\tA. ")) print
if (sub(/^\.Bh /, "\t\tB. ")) print
}' $*

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

awk '# do.outline -- number headings in chapter.
{
gsub(/"/, "")
}
/^\.Se/ {
    sub(/^\.Se /, "Chapter ")
    ch = $2
    ah = 0
    bh = 0
    print
    next
}
/^\.Ah/ {
    sub(/^\.Ah /, "\t " ch "." ++ah " ")
    bh = 0
    print
    next
}
/^\.Bh/ {
    sub(/^\.Bh /, "\t\t " ch "." ah "." ++bh " ")
    print
}' $*

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

Номер главы читается как первый аргумент макроса «.Se» и, таким образом, является вторым полем в этой строке. Схема нумерации выполняется путем увеличения переменной при каждой замене. Действие, связанное с заголовком уровня главы, обнуляет счетчики заголовка раздела. Действие, связанное с заголовком верхнего уровня «.Ah», обнуляет счетчик заголовка второго уровня. Очевидно, вы можете создать столько уровней заголовков, сколько вам нужно. Обратите внимание, как мы можем указать объединение строк и переменных в качестве одного аргумента функции sub().

$ do.outline ch02
Chapter 2 Understanding Basic Operations
    2.1 Awk, by Sed and Grep, out of Ed
    2.2 Command-line Syntax
        2.2.1 Scripting
        2.2.2 Sample Mailing List
    2.3 Using Sed
        2.3.1 Specifying Simple Instructions
        2.3.2 Script Files
    2.4 Using Awk
    2.5 Using Sed and Awk Together

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

9.2.4 Преобразование регистра

POSIX awk предоставляет две функции для преобразования регистра символов в строке. Это функции tolower() и toupper(). Каждая из них принимает единственный строковый аргумент и возвращает копию этой строки со всеми символами одного регистра, преобразованными в другой (от верхнего к нижнему и от нижнего к верхнему соответственно). Их использование просто:

$ cat test
Hello, World!
Good-bye CRUEL world!
1, 2, 3, and away we GO!
$ awk '{ printf("<%s>, <%s>\n", tolower($0), toupper($0)) }' test
<hello, world!>, <HELLO, WORLD!>
<good-bye cruel world!>, <GOOD-BYE CRUEL WORLD!>
<1, 2, 3, and away we go!>, <1, 2, 3, AND AWAY WE GO!>

Обратите внимание, что неалфавитные символы оставлены без изменений.

9.2.5 Функция match()

Функция match() позволяет определить, соответствует ли регулярное выражение указанной строке. Она принимает два аргумента: строку и регулярное выражение. (Эта функция сбивает с толку, потому что регулярное выражение находится на второй позиции, тогда как для функций подстановки оно находится на первой позиции.)

Функция match() возвращает начальную позицию подстроки, которая соответствует регулярному выражению. Вы можете считать это близким родственником функции index(). В следующем примере регулярное выражение соответствует любой последовательности заглавных букв в строке «the UNIX operating system».

match("the UNIX operating system", /[A-Z]+/)

Эта функция возвращает значение 5, это позиция символа «U», первой заглавной буквы в строке.

Функция match() также устанавливает две системные переменные: RSTART и RLENGTH. RSTART содержит то же значение, возвращаемое функцией, начальную позицию подстроки. RLENGTH содержит длину строки в символах (а не конечную позицию подстроки). Когда шаблон не совпадает, RSTART устанавливается на 0, а RLENGTH - на -1. В предыдущем примере RSTART равно 5, а RLENGTH равно 4. (Их сложение дает вам позицию первого символа после совпадения.)

Давайте посмотрим на довольно простой пример, который выводит строку, совпадающую с указанным регулярным выражением, демонстрируя «степень совпадения», как описано в Главе 3, Понимание синтаксиса регулярных выражений. Следующий сценарий оболочки принимает два аргумента командной строки: регулярное выражение, которое следует указывать в кавычках, и имя файла для поиска.

awk '# match -- print string that matches line
# for lines match pattern
match($0, pattern) {
    # extract string matching pattern using
    # starting position and length of string in $0
    # print string
    print substr($0, RSTART, RLENGTH)
}' pattern="$1" $2

Первый параметр командной строки передается как значение шаблона. Обратите внимание, что $1 заключен в кавычки, необходимые для защиты любых пробелов, которые могут появиться в регулярном выражении. Функция match() появляется в условном выражении, которое контролирует выполнение единственной процедуры в этом сценарии awk. Функция match() возвращает 0, если шаблон не найден, и ненулевое значение (RSTART), если он найден, что позволяет использовать возвращаемое значение в качестве условия. Если текущая запись соответствует шаблону, то строка извлекается из $0, используя значения RSTART и RLENGTH в функции substr(), чтобы указать начальную позицию извлекаемой подстроки и ее длину. Подстрока печатается. Эта процедура соответствует только первому вхождению в $0.

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

$ match "emp[^ ]*" personnel.txt
employees
employee
employee.
employment,
employer
employment
employee's
employee

Сценарий match может быть полезным инструментом для улучшения вашего понимания регулярных выражений.

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

awk '# lower - change upper case to lower case
# initialize strings
BEGIN { upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        lower = "abcdefghijklmnopqrstuvwxyz"
}

# for each input line
{
# see if there is a match for all caps
    while (match($0, /[A-Z]+/))
        # get each cap letter
        for (x = RSTART; x < RSTART+RLENGTH; ++x) {
            CAP = substr($0, x, 1)
            CHAR = index(upper, CAP)
            # substitute lowercase for upper
            gsub(CAP, substr(lower, CHAR, 1))
        }
# print record
    print $0
}' $*

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

Регулярное выражение соответствует любой последовательности заглавных букв в $0. Если обнаружено совпадение, цикл for выполняет поиск каждого символа в совпавшей подстроке, аналогично тому, что мы делали в программе-примере caps, показанной ранее в этой главе. Она отличается тем, как мы используем системные переменные RSTART и RLENGTH. RSTART инициализирует переменную счетчика x. Он используется в функции substr() для извлечения одного символа за раз из $0, начиная с первого символа, который соответствует шаблону. Добавляя RLENGTH к RSTART, мы получаем позицию первого символа после тех, которые соответствуют шаблону. Вот почему в цикле используется «<» вместо «<=». В конце мы используем gsub() для замены прописной буквы соответствующей строчной буквой*. Обратите внимание, что мы используем gsub() вместо sub(), потому что это дает нам преимущество в выполнении нескольких замен, если в строке есть несколько экземпляров одной и той же буквы.

$ cat test
Every NOW and then, a WORD I type appears in CAPS.
$ lower test
every now and then, a word i type appears in caps.

Обратите внимание, что вы можете изменить регулярное выражение, чтобы избежать совпадения отдельных заглавных букв, сопоставив последовательность из двух или более заглавных символов, используя: «/[A-Z][A-Z]+/». Это также потребовало бы пересмотра способа преобразования строчных букв с помощью gsub(), поскольку он соответствует одному символу в строке.

В нашем обсуждении команды подстановки sed вы видели, как сохранить и вызвать часть строки, совпадающую с шаблоном, используя \( и \), чтобы окружить шаблон, который нужно сохранить, и \n, чтобы вызвать сохраненную строку в замене. шаблон. К сожалению, стандартные функции подстановки в awk не предлагают эквивалентного синтаксиса. Однако функция match() может решить множество таких проблем.

Например, если вы сопоставляете строку с помощью функции match(), вы можете выделить символы или подстроку в начале или конце строки. Учитывая значения RSTART и RLENGTH, вы можете использовать функцию substr() для извлечения символов. В следующем примере мы заменяем второе из двух двоеточий точкой с запятой. Мы не можем использовать gsub() для замены, потому что «/:/» соответствует первому двоеточию, а «/:[^:]*:/» соответствует всей строке символов. Мы можем использовать match() для сопоставления строки символов и для извлечения последнего символа строки.

# replace 2nd colon with semicolon using match, substr
if (match($1, /:[^:]*:/)) {
    before = substr($1, 1, (RSTART + RLENGTH - 2))
    after = substr($1, (RSTART + RLENGTH))
    $1 = before ";" after
}

Функция match() помещается в условный оператор, проверяющий, что совпадение было найдено. Если есть совпадение, мы используем функцию substr() для извлечения подстроки перед вторым двоеточием, а также подстроки после него. Затем мы объединяем before, литерал «;» и after, присваивая их $1.

Вы можете увидеть примеры использования функции match() в Главе 12, Полнофункциональные приложения.

* Вы можете спросить: «Почему бы просто не использовать tolower()?» Хороший вопрос. В некоторых ранних версиях nawk, включая версию для систем SunOS 4.1.x, отсутствуют функции tolower() и toupper(); поэтому полезно знать, как это сделать самому.

9.3 Написание собственных функций

Благодаря пользовательским функциям awk позволяет начинающему программисту сделать еще один шаг к программированию на C*, написав программы, которые используют автономные функции. Когда вы пишете функцию правильно, вы определили программный компонент, который можно повторно использовать в других программах. Реальное преимущество модульности становится очевидным по мере того, как программы растут в размерах или по возрасту, и когда количество программ, которые вы пишете, значительно увеличивается.

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

function name (parameter-list) {
    statements
}

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

parameter-list - это список переменных, разделенных запятыми, которые передаются в качестве аргументов функции при ее вызове. Тело функции состоит из одного или нескольких операторов. Функция обычно содержит оператор return, который возвращает управление той точке сценария, где функция была вызвана; у него часто есть выражение, которое также возвращает значение.

return expression

В следующем примере показано определение функции insert():

function insert(STRING, POS, INS) {
    before_tmp = substr(STRING, 1, POS)
    after_tmp = substr(STRING, POS + 1)
    return before_tmp INS after_tmp
}

Эта функция принимает три аргумента, вставляя одну строку INS в другую строку STRING после символа в позиции POS**. В теле этой функции используется функция substr() для разделения значения STRING на две части. Оператор return возвращает строку, которая является результатом объединения первой части STRING, строки INS и последней части STRING. Вызов функции может появляться везде, где может быть выражение. Таким образом, выражение следующее:

print insert($1, 4, "XX")

Если значение $1 равно «Hello», то эта функция возвращает «HellXXo». Обратите внимание, что при вызове пользовательской функции между именем функции и левой круглой скобкой не должно быть пробелов. Это не относится к встроенным функциям.

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

Когда мы вызываем функцию insert() и указываем $1 в качестве первого аргумента, тогда копия этой переменной передается функции, где она обрабатывается как локальная переменная с именем STRING. Все переменные в списке параметров определения функции являются локальными переменными, и их значения недоступны вне функции. Точно так же аргументы в вызове функции не изменяются самой функцией. Когда функция insert() возвращается, значение $1 не изменяется.

Однако переменные, определенные в теле функции, по умолчанию являются глобальными. Учитывая приведенное выше определение функции insert(), временные переменные before_tmp и after_tmp видны вне функции. Awk предоставляет то, что его разработчики называют «неизящным» средством объявления переменных, локальных по отношению к функции, а именно путем указания этих переменных в списке параметров.

Локальные временные переменные помещаются в конец списка параметров. Это важно; параметры в списке параметров получают свои значения по порядку из значений, переданных в вызове функции. Любые дополнительные параметры, такие как обычные переменные awk, инициализируются пустой строкой. По соглашению локальные переменные отделяются от «реальных» параметров несколькими пробелами. Например, в следующем примере показано, как определить функцию insert() с двумя локальными переменными.

function insert(STRING, POS, INS,   before_tmp, after_tmp) {
    body
}

Если это кажется запутанным***, может помочь следующий сценарий:

function insert(STRING, POS, INS, before_tmp) {
    before_tmp = substr(STRING, 1, POS)
    after_tmp = substr(STRING, POS + 1)
    return before_tmp INS after_tmp
}

# main routine
{
print "Function returns", insert($1, 4, "XX")
print "The value of $1 after is:", $1
print "The value of STRING is:", STRING
print "The value of before_tmp:", before_tmp
print "The value of after_tmp:", after_tmp
}

Обратите внимание, что мы указываем before_tmp в списке параметров. В основной процедуре мы вызываем функцию insert() и выводим ее результат. Затем мы печатаем разные переменные, чтобы увидеть, каковы их значения, если они есть. Теперь давайте запустим приведенный выше сценарий и посмотрим на результат:

$ echo "Hello" | awk -f insert.awk -
Function returns HellXXo
The value of $1 after is: Hello
The value of STRING is:
The value of before_tmp:
The value of after_tmp: o

Функция insert(), как и ожидалось, возвращает «HellXXo». Значение $1 остается таким же после вызова функции, как и раньше. Переменная STRING является локальной для функции и не имеет значения при вызове из основной процедуры. То же верно и для before_tmp, потому что его имя было помещено в список параметров для определения функции. Переменная after_tmp, которая не была указана в списке параметров, имеет значение, букву «o».

Как показывает этот пример, $1 передается в функцию «по значению». Это означает, что при вызове функции создается копия значения, и функция управляет копией, а не оригиналом. Однако массивы передаются «по ссылке». То есть функция не работает с копией массива, а передает сам массив. Таким образом, любые изменения, которые функция вносит в массив, видны вне функции. (Это различие между «скалярными» переменными и массивами также справедливо для функций, написанных на языке C.) В следующем разделе будет представлен пример функции, которая работает с массивом.

* Или программированию на любом другом традиционном языке высокого уровня.

** Мы использовали соглашение о присвоении нашим параметрам всех имен в верхнем регистре. В основном это сделано для облегчения понимания. На практике это, вероятно, не очень хорошая идея, так как становится намного проще случайно вызвать конфликт параметра с системной переменной.

*** В документации это называется синтаксической заплаткой.

9.3.1 Написание функции сортировки

Ранее в этой главе мы представили сценарий lotto для выбора x случайных чисел из ряда y чисел. Этот сценарий не сортировал список выбранных номеров. В этом разделе мы разработаем функцию sort для элементов массива.

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

sort(sortedpick, NUM)

В определении функции перечислены два аргумента и три локальные переменные, используемые в функции.

# sort numbers in ascending order
function sort(ARRAY, ELEMENTS, temp, i, j) {
    for (i = 2; i <= ELEMENTS; ++i) {
        for (j = i; ARRAY[j-1] > ARRAY[j]; --j) {
            temp = ARRAY[j]
            ARRAY[j] = ARRAY[j-1]
            ARRAY[j-1] = temp
        }
    }
    return
}

В теле функции реализована сортировка вставкой. Этот алгоритм сортировки очень прост. Мы перебираем каждый элемент массива и сравниваем его с предшествующим ему значением. Если первый элемент больше второго, первый и второй элементы меняются местами*. Чтобы фактически поменять местами значения, мы используем временную переменную для хранения копии значения, пока мы перезаписываем оригинал. Цикл продолжает заменять соседние элементы местами, пока все не будут в порядке. В конце функции мы используем оператор return, чтобы просто вернуть управление**. Функция не должна передавать массив обратно в основную процедуру, потому что сам массив изменяется и к нему можно получить прямой доступ.

Вот положительное доказательство:

$ lotto 7 35
Pick 7 of 35
6 7 17 19 24 29 35

Фактически, многие сценарии, которые мы разработали в этой главе, можно было превратить в функции. Например, если бы у нас была только оригинальная версия nawk 1987 года, мы могли бы написать наши собственные функции tolower() и toupper().

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

# grade.sort.awk -- script for sorting student grades
# input: student name followed by a series of grades

# sort function -- sort numbers in ascending order
function sort(ARRAY, ELEMENTS, temp, i, j) {
    for (i = 2; i <= ELEMENTS; ++i)
        for (j = i; ARRAY[j-1] > ARRAY[j]; --j) {
            temp = ARRAY[j]
            ARRAY[j] = ARRAY[j-1]
            ARRAY[j-1] = temp
        }
    return
}

# main routine
{
# loop through fields 2 through NF and assign values to
# array named grades
for (i = 2; i <= NF; ++i)
    grades[i-1] = $i

# call sort function to sort elements

sort(grades, NF-1)

# print student name
printf("%s: ", $1)

# output loop
for (j = 1; j <= NF-1; ++j)
    printf("%d ", grades[j])
printf("\n")
}

Обратите внимание, что процедура сортировки идентична предыдущей версии. В этом примере, как только мы отсортировали оценки, мы просто выводим их:

$ awk -f grade.sort.awk grades.test
mona: 70 70 77 83 85 89
john: 78 85 88 91 92 94
andrea: 85 89 90 90 94 95
jasper: 80 82 84 84 88 92
dunce: 60 60 61 62 64 80
ellis: 89 90 92 96 96 98

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

* Сначала мы должны протестировать это j-1 in ARRAY, чтобы убедиться, что мы не упали с передней части массива.

** return здесь необязателен; «падение с конца» (falling off the end) функции будет иметь тот же эффект. Поскольку функции могут иметь возвращаемые значения, рекомендуется всегда использовать оператор return.

9.3.2 Поддержка библиотеки функций

Вы можете поместить полезную функцию в отдельный файл и сохранить ее в центральном каталоге. AWK допускает многократное использование параметра -f для указания более одного файла программы*. Например, мы могли бы написать предыдущий пример так, чтобы функция сортировки была помещена в отдельный файл от основной программы grade.awk. Следующая команда определяет оба программных файла:

$ awk -f grade.awk -f /usr/local/share/awk/sort.awk grades.test

Эта команда предполагает, что grade.awk находится в рабочем каталоге и что функция сортировки определена в sort.awk в каталоге /usr/local/share/awk.

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

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

* Версия nawk для SunOS 4.1.x не поддерживает несколько файлов сценариев. Этой функции не было и в исходной версии nawk 1987 года. Она была добавлена в 1989 году и теперь является частью POSIX awk.

9.3.3 Другой пример сортировки

Ленни, наш производственный редактор, вернулся с еще одной просьбой.

Dale:

The last section of each Xlib manpage is called "Related Commands"
(that is the argument of a .SH) and it's followed by a list of commands
(often 10 or 20) that are now in random order. It'd be more
useful and professional if they were alphabetized. Currently, commands
are separated by a comma after each one except the last, which has a
period.

The question is: could awk alphabetize these lists? We're talking
about a couple of hundred manpages. Again, don't bother if this is a
bigger job than it seems to someone who doesn't know what's involved.

Best to you and yours,

Lenny

Чтобы понять, о чем он говорит, ниже показана упрощенная версия справочной страницы Xlib:

.SH "Name"
XSubImage - create a subimage from part of an image.
.
.
.
.SH "Related Commands"
XDestroyImage, XPutImage, XGetImage,
XCreateImage, XGetSubImage, XAddPixel,
XPutPixel, XGetPixel, ImageByteOrder.

Вы можете видеть, что названия связанных команд появляются в нескольких строках после заголовка. Вы также можете видеть, что они не расположены в определенном порядке.

Сортировать список связанных команд на самом деле довольно просто, учитывая, что мы уже рассмотрели сортировку. Структура программы несколько интересна, так как мы должны прочитать несколько строк после сопоставления заголовка «Related Commands».

Глядя на ввод, очевидно, что список связанных команд - последний раздел в файле. Все остальные строки, кроме этих, мы хотим напечатать как есть. Ключ должен соответствовать всем строкам от заголовка «Related Commands» до конца файла. Наш сценарий может состоять из четырех подходящих правил:

  1. Заголовок «Related Commands»
  2. Строки после этого заголовка
  3. Все остальные строки
  4. После прочтения всех строк (END)

Большая часть «действия» происходит в процедуре END. Здесь мы сортируем и выводим список команд. Вот сценарий:

# sorter.awk -- sort list of related commands
# requires sort.awk as function in separate file
BEGIN { relcmds = 0 }

#1 Match related commands; enable flag x
/\.SH "Related Commands"/ {
    print
    relcmds = 1
    next
}

#2 Apply to lines following "Related Commands"
(relcmds == 1) {
    commandList = commandList $0
}

#3 Print all other lines, as is.
(relcmds == 0) { print }

#4 now sort and output list of commands
END {
# remove leading spaces and final period.
    gsub(/, */, ",", commandList)
    gsub(/\. *$/, "", commandList)
# split list into array
    sizeOfArray = split(commandList, comArray, ",")
# sort
    sort(comArray, sizeOfArray)
# output elements
    for (i = 1; i < sizeOfArray; i++)
        printf("%s,\n", comArray[i])
    printf("%s.\n", comArray[i])
}

После сопоставления заголовка «Related Commands» мы печатаем эту строку и затем устанавливаем флаг, переменную relcmds, которая указывает, что необходимо собрать последующие строки ввода*. Вторая процедура фактически собирает каждую строку в переменную commandList. Третья процедура выполняется для всех остальных строк, просто распечатывая их.

Когда все строки ввода прочитаны, выполняется процедура END, и мы знаем, что наш список команд завершен. Перед разделением команд на поля мы удаляем любое количество пробелов после запятой. Затем мы удаляем последнюю точку и любые конечные пробелы. Наконец, мы создаем массив comArray с помощью функции split(). Мы передаем этот массив в качестве аргумента функции sort(), а затем печатаем отсортированные значения.

Эта программа генерирует следующий вывод:

$ awk -f sorter.awk test
.SH "Name"
XSubImage - create a subimage from part of an image.
.SH "Related Commands"
ImageByteOrder,
XAddPixel,
XCreateImage,
XDestroyImage,
XGetImage,
XGetPixel,
XGetSubImage,
XPutImage,
XPutPixel.

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

Поскольку в этом сценарии предполагается, что функция sort() существует в отдельном файле, ее необходимо вызывать с использованием нескольких параметров -f:

$ awk -f sort.awk -f sorter.awk test

где функция sort() определена в файле sort.awk.

* Функция getline, представленная в следующей главе, обеспечивает более простой способ управления чтением строк ввода.

Глава 10.
Нижний ящик

Эта глава является доказательством того, что не всему есть свое место. Некоторые вещи просто не подходят, как бы вы их ни организовали. В этой главе собраны такие вещи. Заманчиво называть ее «Продвинутые темы», как будто для объяснения ее организации (или отсутствия организации), но некоторые читатели могут почувствовать, что им нужно добиться большего, прежде чем читать ее. Поэтому мы назвали его «Нижний ящик», думая об организации комода с нижним бельем, носками и другими повседневными вещами в верхних ящиках и более тяжелыми вещами, которые используются реже, например свитерами, в нижних ящиках. Все это одинаково доступно, но вам придется наклониться, чтобы достать вещи в нижнем ящике. Требуется немного больше усилий, чтобы что-то получить, вот и все.

В этой главе мы рассмотрим ряд тем, в том числе следующие:

10.1 Функция getline

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

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

1 Если функция смогла прочитать строку.
0 Если функция встречает конец файла.
-1 Если возникнет ошибка.

NOTE: Хотя getline называется функцией и она возвращает значение, ее синтаксис напоминает оператор. Не пишите getline(); ее синтаксис не допускает скобок.

В предыдущей главе в качестве примера мы использовали исходный файл справочной страницы. Макросы -man обычно помещают текстовый аргумент в следующую строку. Хотя макрос - это шаблон, который вы используете для поиска строки, на самом деле это следующая обрабатываемая вами строка. Например, чтобы извлечь имя команды из справочной страницы, следующий пример сопоставляет заголовок «Name», читает следующую строку и печатает ее первое поле:

# getline.awk -- test getline function
/^\.SH "?Name"?/ {
    getline # get next line
    print $1 # print $1 of new line.
}

Шаблон соответствует любой строке с «.SH», за которым следует «Name», которое может быть заключено в кавычки. После сопоставления этой строки мы используем getline для чтения следующей строки ввода. Когда новая строка считывается, getline присваивает ей $0 и разбирает ее в поля. Также устанавливаются системные переменные NF, NR и FNR. Таким образом, новая строка становится текущей строкой, и мы можем обратиться к «$1» и получить первое поле. Обратите внимание, что предыдущая строка больше не доступна как $0. Однако при необходимости вы можете назначить строку, прочитанную с помощью getline, переменной и не менять $0, как мы вскоре увидим.

Вот пример, который показывает, как работает предыдущий скрипт, распечатывая первое поле строки, следующей за «.SH Name».

$ awk -f getline.awk test
XSubImage

Программа sorter.awk, которую мы продемонстрировали в конце Главы 9, Функции, могла бы использовать getline для чтения всех строк после заголовка «Related Commands». Мы можем протестировать возвращаемое значение getline в цикле while, чтобы прочитать несколько строк из ввода. Следующая процедура заменяет первые две процедуры в программе sorter:

# Match "Related Commands" and collect them
/^\.SH "?Related Commands"?/ {
    print
    while (getline > 0)
        commandList = commandList $0
}

Выражение «getline > 0» будет истинным, пока getline успешно читает строку ввода. Когда доходит до конца файла, getline возвращает 0, и цикл завершается.

10.1.1 Чтение ввода из файлов

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

getline < "data"

Хотя имя файла может быть передано через переменную, обычно оно указывается как строковая константа, которую необходимо заключать в кавычки. Символ «<» совпадает с символом перенаправления ввода оболочки и не будет интерпретироваться как символ «меньше чем». Мы можем использовать цикл while для чтения всех строк из файла, проверяя наличие конца файла для выхода из цикла. В следующем примере открывается файл data и печатаются все его строки:

while ( (getline < "data") > 0 )
    print

(Мы заключаем в скобки, чтобы избежать путаницы; «<» - это перенаправление, а «>» - сравнение возвращаемого значения.) Ввод также может поступать из стандартного ввода. Вы можете использовать getline после приглашения пользователя ввести информацию:

BEGIN { printf "Enter your name: "
    getline < "-"
    print
}

Этот пример кода печатает приглашение «Enter your name:» (используется printf, потому что мы не хотим, чтобы после приглашения возвращалась каретка), а затем вызывает getline для сбора ответа пользователя*. Ответ присваивается $0, и оператор print выводит это значение.

* По крайней мере одно время SGI-версии nawk не поддерживали использование «-» с getline для чтения со стандартного ввода. Caveat emptor.

10.1.2 Присвоение ввода переменной

Функция getline позволяет назначить входную запись переменной. Имя переменной предоставляется в качестве аргумента. Таким образом, следующий оператор считывает следующую строку ввода в переменную input:

getline input

Присвоение ввода переменной не влияет на текущую строку ввода; то есть $0 не затрагивается. Новая строка ввода не разделяется на поля, и, следовательно, переменная NF также не изменяется. Это увеличивает счетчики записей NR и FNR.

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

BEGIN { printf "Enter your name: "
    getline name < "-"
    print name
}

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

name = getline    # wrong

считая, что таким образом возвращаемое значение getline присваивается переменной name.

10.1.3 Чтение ввода из канала

Вы можете выполнить команду и передать вывод в getline. Например, посмотрите на следующее выражение:

"who am i" | getline

Это выражение устанавливает значение «$0» для вывода команды who am i.

dale    ttyC3    Jul 18 13:37

Строка разбирается на поля и устанавливается системная переменная NF. Точно так же вы можете присвоить результат переменной:

"who am i" | getline me

Назначая вывод переменной, вы избегаете установки $0 и NF, но строка не разделяется на поля.

Следующий скрипт представляет собой довольно простой пример передачи вывода команды в getline. Он использует выходные данные команды who am i для получения имени пользователя. Затем он ищет имя в /etc/passwd, распечатывая пятое поле этого файла, полное имя пользователя:

awk '# getname - print users fullname from /etc/passwd
BEGIN { "who am i" | getline
    name = $1
    FS = ":"
}
name ~ $1 { print $5 }
' /etc/passwd

Команда выполняется из процедуры BEGIN, и она предоставляет нам имя пользователя, которое будет использоваться для поиска записи пользователя в /etc/passwd. Как объяснялось выше, who am i выводит одну строку, которой getline присваивает значение $0. Затем $1, первое поле этого вывода, присваивается name.

В качестве разделителя полей используется двоеточие (:), чтобы мы могли получить доступ к отдельным полям в записях в файле /etc/passwd. Обратите внимание, что FS устанавливается после getline, иначе это повлияет на синтаксический анализ вывода команды.

Наконец, основная процедура предназначена для проверки того, что первое поле соответствует name. Если да, то печатается пятое поле записи. Например, когда Дейл запускает этот сценарий, он печатает «Dale Dougherty».

Когда вывод команды передается по каналу в getline и содержит несколько строк, getline читает строку за раз. При первом вызове getline считывается первая строка вывода. Если вы вызовете его снова, он прочитает вторую строку. Чтобы прочитать все строки вывода, вы должны настроить цикл, который выполняет getline до тех пор, пока не будет больше вывода. Например, в следующем примере цикл while используется для чтения каждой строки вывода и присвоения ее следующему элементу массива who_out:

while ("who" | getline)
    who_out[++i] = $0

Каждый раз, когда вызывается функция getline, она читает следующую строку вывода. Однако команда who выполняется только один раз.

Следующий пример ищет в документе строку «@date» и заменяет его сегодняшней датой:

# subdate.awk -- replace @date with todays date
/@date/ {
    "date +'%a., %h %d, %Y'" | getline today
    gsub(/@date/, today)
}
{ print }

Команда date, используя свои параметры форматирования*, обеспечивает дату, а getline присваивает ее переменной today. Функция gsub() заменяет каждый экземпляр «@date» на сегодняшнюю дату.

Этот скрипт можно использовать для вставки даты в письмо:

To: Peabody
From: Sherman
Date: @date

I am writing you on @date to
remind you about our special offer.

Все строки входного файла будут переданы как есть, за исключением строк, содержащих «@date», которые заменяются сегодняшней датой:

$ awk -f subdate.awk subdate.test
To: Peabody
From: Sherman
Date: Sun., May 05, 1996

I am writing you on Sun., May 05, 1996 to
remind you about our special offer.

* Более старые версии date не поддерживают параметры форматирования. В частности, в системах SunOS 4.1.x; там вы должны использовать /usr/5bin/date. Проверьте вашу локальную документацию.

10.2 Функция close()

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

Мы увидим пример функции close() в разделе «Работа с несколькими файлами» далее в этой главе.

10.3 Функция system()

Функция system() выполняет команду, указанную в виде выражения*. Однако это не делает вывод команды доступным для обработки в программе. Она возвращает статус завершения выполненной команды. Сценарий ожидает завершения команды, прежде чем продолжить выполнение. В следующем примере выполняется команда mkdir:

BEGIN { if (system("mkdir dale") != 0)
    print "Command Failed" }

Функция system() вызывается из оператора if, который проверяет ненулевой статус выхода. Выполнение программы дважды дает один успех и один сбой:

$ awk -f system.awk
$ ls dale
$ awk -f system.awk
mkdir: dale: File exists
Command Failed

При первом запуске создается новый каталог, и system() возвращает статус выхода 0 (успех). Во второй раз, когда команда выполняется, каталог уже существует, поэтому mkdir не работает и выдает сообщение об ошибке. Сообщение «Command Failed» создается awk.

В наборе команд Berkeley UNIX есть небольшая, но полезная команда для пользователей troff с именем soelim, названная так потому, что она «удаляет» строки «.so» из входного файла troff. (.so - это запрос на включение или «источник» содержимого указанного файла.) Если у вас есть более старая система System V, в которой нет soelim, вы можете использовать следующий сценарий awk для его создания:

/^\.so/ { gsub(/"/, "", $2)
    system("cat " $2)
    next
    }
{ print }

Этот сценарий ищет «.so» в начале строки, удаляет все кавычки, а затем использует system() для выполнения команды cat и вывода содержимого файла. Этот вывод объединяется с остальными строками в файле, которые просто выводятся на стандартный вывод, как в следующем примере.

$ cat soelim.test
This is a test
.so test1
This is a test
.so test2
This is a test.
$ awk -f soelim.awk soelim.test
This is a test
first:second
one:two
This is a test
three:four
five:six
This is a test.

Мы не проверяем статус завершения команды. Таким образом, если файл не существует, сообщения об ошибках объединяются с выводом:

$ awk -f soelim.awk soelim.test
This is a test
first:second
one:two
This is a test
cat: cannot open test2
This is a test.

Мы могли бы захотеть проверить возвращаемое значение функции system() и сгенерировать сообщение об ошибке для пользователя. Эта программа также очень упрощена: она не обрабатывает экземпляры «.so», вложенные во включенный файл. Подумайте, как вы могли бы реализовать версию этой программы, которая обрабатывала бы вложенные запросы «.so».

Этот пример представляет собой функцию, предлагающую вам ввести имя файла. Он использует функцию system() для выполнения команды test, чтобы убедиться, что файл существует и доступен для чтения:

# getFilename function -- prompts user for filename,
# verifies that file exists and returns absolute pathname.
function getFilename(file) {
    while (! file) {
        printf "Enter a filename: "
        getline < "-" # get response
        file = $0
        # check that file exists and is readable
        # test returns 1 if file does not exist.
        if (system("test -r " file)) {
            print file " not found"
            file = ""
        }
    }
    if (file !~ /^\//) {
        "pwd" | getline # get current directory
        close("pwd")
        file = $0 "/" file
    }
    return file
}

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

Команда test -r возвращает 0, если файл существует и доступен для чтения, и 1, если нет. Как только определено, что имя файла допустимо, мы проверяем имя файла, чтобы увидеть, начинается ли оно с символа «/», что означает, что пользователь предоставил абсолютный путь. Если этот тест не проходит, мы используем функцию getline, чтобы получить вывод команды pwd и добавить его к имени файла. (Следует признать, что сценарий не пытается иметь дело с записями «./» или «../», хотя тесты могут быть легко разработаны для их сопоставления.) Обратите внимание на два использования функции getline: первое получает ответ пользователя и второй выполняет команду pwd.

* Функция system() смоделирована на основе одноименной стандартной библиотечной функции C.

10.4 Генератор команд на основе меню

В этом разделе мы рассмотрим общее использование функций system() и getline для реализации генератора команд на основе меню. Цель этой программы - дать неискушенным пользователям простой способ выполнять длинные или сложные команды UNIX. Меню используется, чтобы предложить пользователю описание задачи, которую необходимо выполнить, что позволяет пользователю выбрать по номеру любой вариант меню для выполнения.

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

Формат файла команд меню содержит заголовок меню в качестве первой строки в файле. Последующие строки содержат два поля: первое - это описание действия, которое нужно выполнить, а второе - это командная строка, которая его выполняет. Пример показан ниже:

$ cat uucp_commands
UUCP Status Menu
Look at files in PUBDIR:find /var/spool/uucppublic -print
Look at recent status in LOGFILE:tail /var/spool/uucp/LOGFILE
Look for lock files:ls /var/spool/uucp/*.LCK

Первым шагом в реализации генератора команд на основе меню является чтение файла команд меню. Мы читаем первую строку этого файла и присваиваем ее переменной title. Остальные строки содержат два поля и считываются в два массива: один для пунктов меню, а другой - для команд, которые должны быть выполнены. Цикл while используется вместе с getline для чтения по одной строке из файла.

BEGIN { FS = ":"
if ((getline < CMDFILE) > 0)
    title = $1
else
    exit 1
while ((getline < CMDFILE) > 0) {
    # load array
    ++sizeOfArray
    # array of menu items
    menu[sizeOfArray] = $1
    # array of commands associated with items
    command[sizeOfArray] = $2
    }
...
}

Внимательно изучите синтаксис выражения, проверенного оператором if и циклом while.

(getline < CMDFILE) > 0

Переменная CMDFILE - это имя файла команды меню, которое передается как параметр командной строки. Два символа угловых скобок имеют совершенно разные функции. Символ «<» интерпретируется функцией getline как оператор перенаправления ввода. Затем значение, возвращаемое функцией getline, проверяется, чтобы убедиться, что оно больше («>») 0. Оно специально заключено в круглые скобки, чтобы прояснить это. Другими словами, сначала оценивается «getline < CMDFILE», а затем его возвращаемое значение сравнивается с 0.

Эта процедура находится в шаблоне BEGIN. Однако есть одна загвоздка. Поскольку мы намеревались передать имя файла меню в качестве параметра командной строки, переменная CMDFILE обычно не определяется и не доступна в шаблоне BEGIN. Другими словами, следующая команда работать не будет:

awk script CMDFILE="uucp_commands" -

потому что переменная CMDFILE не будет определена, пока не будет прочитана первая строка ввода.

К счастью, awk предоставляет параметр -v для обработки именно такого случая. Использование опции -v гарантирует, что переменная будет установлена немедленно и, таки образом, доступна в шаблоне BEGIN.

awk -v CMDFILE="uucp_commands" script

Если в вашей версии awk нет опции -v, вы можете передать значение CMDFILE как переменную оболочки. Создайте сценарий оболочки для выполнения awk и определите в нем CMDFILE. Затем измените строку, которая читает CMDFILE в сценарии invoke (см. ниже), следующим образом:

while ((getline < '"$CMDFILE"') > 0 ) {

После загрузки файла команд меню программа должна отобразить меню и запросить пользователя. Это реализовано как функция, потому что нам нужно вызвать ее в двух местах: из шаблона BEGIN для первоначального запроса пользователя и после обработки ответа пользователя, чтобы можно было сделать другой выбор. Вот функция display_menu():

function display_menu() {
    # clear screen -- comment out if clear does not work
    system("clear")
    # print title, list of items, exit item, and prompt
    print "\t" title
    for (i = 1; i <= sizeOfArray; ++i)
        printf "\t%d. %s\n", i, menu[i]
    printf "\t%d. Exit\n", i
    printf("Choose one: ")
}

Первое, что мы делаем, это используем функцию system() для вызова команды очистки экрана. (В моей системе это делает clear; в других это может быть cls или другая команда. Закомментируйте строку, если вы не можете найти такую команду.) Затем мы печатаем заголовок и каждый из элементов в нумерованном списке. Последний пункт всегда - «Exit». Наконец, мы предлагаем пользователю сделать выбор.

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

# Applies the user response to prompt
{
    # test value of user response
    if ($1 > 0 && $1 <= sizeOfArray) {
        # print command that is executed
        printf("Executing ... %s\n", command[$1])
        # then execute it.
        system(command[$1])
        printf("<Press RETURN to continue>")
        # wait for input before displaying menu again
        getline
    }
    else
        exit
    # re-display menu
    display_menu()
}

Сначала мы проверяем диапазон ответа пользователя. Если ответ выходит за пределы допустимого диапазона, мы просто выходим из программы. Если это допустимый ответ, мы получаем команду из массива command, отображаем ее, а затем выполняем с помощью функции system(). Пользователь видит результат выполнения команды на экране, за которым следует сообщение «<Press RETURN to continue>». Цель этого сообщения - дождаться завершения работы пользователя перед очисткой экрана и повторным отображением меню. Функция getline заставляет программу ждать ответа. Обратите внимание, что мы ничего не делаем с ответом. Функция display_menu() вызывается в конце этой процедуры для повторного отображения меню и запроса другой строки ввода.

awk -v CMDFILE="uucp_commands"  '# invoke -- menu-based
                                 # command generator
# first line in CMDFILE is the title of the menu
# subsequent lines contain: $1 - Description;
# $2 Command to execute
BEGIN { FS = ":"
    # process CMDFILE, reading items into menu array
    if ((getline < CMDFILE) > 0)
        title = $1
    else
        exit 1
    while ((getline < CMDFILE) > 0) {
        # load array
        ++sizeOfArray
        # array of menu items
        menu[sizeOfArray] = $1
        # array of commands associated with items
        command[sizeOfArray] = $2
    }
    # call function to display menu items and prompt
    display_menu()
}
# Applies the user response to prompt
{
    # test value of user response
    if ($1 > 0 && $1 <= sizeOfArray) {
        # print command that is executed
        printf("Executing ... %s\n", command[$1])
        # then execute it.
        system(command[$1])
        printf("<Press RETURN to continue>")
        # wait for input before displaying menu again
        getline
    }
    else
        exit
    # re-display menu
    display_menu()
}

function display_menu() {
    # clear screen -- if clear does not work, try "cls"
    system("clear")
    # print title, list of items, exit item, and prompt
    print "\t" title
    for (i = 1; i <= sizeOfArray; ++i)
        printf "\t%d. %s\n", i, menu[i]
    printf "\t%d. Exit\n", i
    printf("Choose one: ")
}' -

Когда пользователь запускает программу, отображается следующий вывод:

UUCP Status Menu
    1. Look at files in PUBDIR
    2. Look at recent status in LOGFILE
    3. Look for lock files
    4. Exit
Choose one:

Пользователю предлагается ввести номер пункта меню. Любые символы, кроме чисел от 1 до 3, приведут к выходу из меню. Например, если пользователь вводит «1», чтобы увидеть список файлов в общедоступном каталоге uucp, на экране отображается следующий результат:

Executing ...find /var/spool/uucppublic -print
/var/spool/uucppublic
/var/spool/uucppublic/dale
/var/spool/uucppublic/HyperBugs
<Press RETURN to continue>

Когда пользователь нажимает клавишу ENTER, меню снова отображается на экране. Пользователь может выйти из программы, выбрав «4».

Эта программа действительно является оболочкой для выполнения команд. Любая последовательность команд (даже другие программы awk) может быть выполнена путем изменения файла команд меню. Другими словами, часть программы, которая может измениться больше всего, извлекается из самой программы и сохраняется в отдельном файле. Это позволяет легко изменять и расширять список меню нетехническим пользователям.

10.5 Направление вывода в файлы и каналы

Вывод любого оператора print или printf может быть направлен в файл с помощью операторов перенаправления вывода «>» или «>>». Например, следующий оператор записывает текущую запись в файл data.out:

print > "data.out"

Имя файла может быть любым выражением, результатом которого является допустимое имя файла. Файл открывается при первом использовании оператора перенаправления, а последующие его применения добавляют данные в файл. Разница между «>» и «>>» такая же, как и между операторами перенаправления оболочки. Правая угловая скобка («>») очищает файл при его открытии, а «>>» сохраняет все, что содержит файл, и добавляет к нему данные.

Поскольку оператор перенаправления «>» совпадает с оператором сравнения, существует вероятность путаницы, когда вы указываете выражение в качестве аргумента команды print. Правило состоит в том, что «>» будет интерпретироваться как оператор перенаправления, когда он появляется в списке аргументов для любого из операторов печати. Чтобы использовать «>» в качестве оператора сравнения в выражении, которое появляется в списке аргументов, заключите выражение или список аргументов в круглые скобки. Например, в следующем примере условное выражение заключено в круглые скобки, чтобы убедиться, что выражение сравнения оценивается правильно:

print "a =", a, "b =", b, "max =", (a > b ? a : b) > "data.out"

Условное выражение определяет, больше ли a, чем b; если да, то значение a печатается как максимальное; в противном случае используется значение b.

10.5.1 Направление вывода в канал

Вы также можете направить вывод в канал. Команда

print | command

открывает канал при первом выполнении и отправляет текущую запись в качестве входных данных для этой команды. Другими словами, команда вызывается только один раз, но каждое выполнение команды print предоставляет другую строку ввода.

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

{# words.awk - strip macros then get word count
sub(/^\.../,"")
print | "wc -w"
}

Удаляя коды форматирования, мы получаем более точное количество слов.

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

awk '{ # words -- strip macros
sub(/^\.../,"")
print
}' $* |
# get word count
wc -w

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

Помните, что одновременно может быть открыто только определенное количество каналов. Используйте функцию close(), чтобы закрыть канал, когда вы закончите его использовать.

10.5.2 Работа с несколькими файлами

Файл открывается всякий раз, когда вы читаете или записываете в файл. Каждая операционная система имеет ограничение на количество файлов, которые может открывать запущенная программа. Более того, каждая реализация awk может иметь внутреннее ограничение на количество открытых файлов; это число может быть меньше установленного системой*. Чтобы у вас не закончились открытые файлы, в awk есть функция close(), которая позволяет вам закрыть открытый файл. Закрытие файлов, которые вы закончили обрабатывать, позволяет вашей программе позже открывать больше файлов.

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

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

print $0 > $1

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

Если вы не закроете свои файлы, такая программа в конечном итоге исчерпает доступные открытые файлы и должна будет сдаться. Пример, который мы рассмотрим, работает, потому что он использует функцию close(), так что вы не столкнетесь с какими-либо ограничениями на открытые файлы.

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

.nr X 0

(Хотя они по большей части использовали макросы -man, начало man-страницы было странно закодировано, что немного усложняло задачу.) Строка, которая предоставляет имя файла, выглядит так:

.if \nX=0 .ds x}  XDrawLine "" "Xlib - Drawing Primitives"

Пятое поле в этой строке «XDrawLine» содержит имя файла. Возможно, единственная трудность при написании сценария состоит в том, что первая строка не та, которая содержит имя файла. Поэтому мы собираем строки в массив, пока не получим имя файла. Получив имя файла, мы выводим массив и с этого момента просто записываем каждую строку ввода в новый файл. Вот сценарий man.split:

# man.split -- split up a file containing X manpages.
BEGIN { file = 0; i = 0; filename = "" }

# First line of new manpage is ".nr X 0"
# Last line is blank
/^\.nr X 0/,/^$/ {
    # this conditional collects lines until we get a filename.
    if (file == 0)
        line[++i] = $0
    else
        print $0 > filename

    # this matches the line that gives us the filename
    if ($4 == "x}") {
        # now we have a filename
        filename = $5
        file = 1
        # output name to screen
        print filename
        # print any lines collected
        for (x = 1; x <= i; ++x){
            print line[x] > filename
        }
        i = 0
    }

    # close up and clean up for next one
    if ($0 ~ /^$/) {
        close(filename)
        filename = ""
        file = 0
        i = 0
    }
}

Как видите, мы используем переменную file в качестве флага, чтобы сообщить, есть ли у нас допустимое имя файла и можем ли мы писать в файл. Первоначально file равен 0, а текущая строка ввода сохраняется в массиве. Переменная i - это счетчик, используемый для индексации массива. Когда мы встречаем строку, которая устанавливает имя файла, мы устанавливаем file равным 1. Имя нового файла выводится на экран, чтобы пользователь мог получить некоторую обратную связь о ходе выполнения скрипта. Затем мы перебираем массив и выводим его в новый файл. Когда будет прочитана следующая строка ввода, file будет установлен в 1, и оператор print выведет его в указанный файл.

* Gawk будет пытаться сделать вид, что у вас открыто больше файлов, чем установлено системой, путем закрытия и повторного открытия файлов по мере необходимости. Несмотря на то, что gawk «умный», все же эффективнее закрыть файлы, когда вы закончите с ними.

10.6 Создание колоночных отчетов

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

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

Вот два образца записей клиентов из файла заказов клиентов:

Charlotte Webb
P.O N61331 97 Y 045    Date: 03/14/97
#1 3 7.50
#2 3 7.50
#3 1 7.50
#4 1 7.50
#7 1 7.50

Martin S. Rossi
P.O NONE    Date: 03/14/97
#1 2 7.50
#2 5 6.75

Каждый заказ занимает несколько строк, а пустая строка отделяет один заказ от другого. Первые две строки содержат имя клиента, номер заказа на покупку и дату заказа. Каждая последующая строка идентифицирует товар по порядковому номеру, номеру заказа и цене товара.

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


awk '/^#/ {
    amount = $2 * $3
    printf "%s %6.2f\n", $0, amount
    next
}
{ print }' $*

Основная процедура влияет только на строки, соответствующие шаблону. Она умножает второе поле на третье, присваивая значение переменной amount. Спецификатор %f функции printf используется для печати числа с плавающей точкой; «6.2» определяет минимальную ширину поля шесть и точность до двух. Точность - это количество цифр справа от десятичной точки; по умолчанию для %f их шесть. Мы печатаем текущую запись вместе со значением переменной amount. Если в этой процедуре печатается строка, следующая строка считывается из стандартного ввода. Строки, не соответствующие шаблону, просто пропускаются. Давайте посмотрим, как работает addem:

$ addem orders
Charlotte Webb
P.O N61331 97 Y 045    Date: 03/14/97
#1 3 7.50 22.50
#2 3 7.50 22.50
#3 1 7.50  7.50
#4 1 7.50  7.50
#7 1 7.50  7.50

Martin S. Rossi
P.O NONE    Date: 03/14/97
#1 2 7.50 15.00
#2 5 6.75 33.75

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

Наш новый скрипт начнется с установки разделителей полей и записей:

BEGIN { FS = "\n"; RS = "" }

Каждая запись имеет переменное количество полей в зависимости от того, сколько элементов было заказано. Сначала мы проверяем, что входная запись имеет как минимум три поля. Затем создается цикл for для чтения всех полей, начинающихся с третьего поля.

NF >= 3 {
for (i = 3; i <= NF; ++i) {

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

В следующей части скрипта каждое поле разбивается на подзначения. $i предоставит значение текущего поля, которое будет разделено на элементы массива order:

sv = split($i, order, " ")
if (sv == 3) {
    procedure
} else
    print "Incomplete Record"
} # end for loop

Количество элементов, возвращаемых функцией, сохраняется в переменной sv. Это позволяет нам проверить наличие трех подзначений. Если их нет, выполняется инструкция else, выводящая сообщение об ошибке на экран.

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

title = order[1]
copies = order[2]
price = order[3]

Затем мы выполняем группу арифметических операций над этими значениями:

amount = copies * price
total_vol += copies
total_amt += amount
vol[title] += copies
amt[title] += amount

Мы накапливаем эти значения до тех пор, пока не будет прочитана последняя входная запись. Процедура END распечатывает отчет.

Вот полная программа:

$ cat addemup
#! /bin/sh
# addemup -- total customer orders
awk 'BEGIN { FS = "\n"; RS = "" }
NF >= 3 {
    for (i = 3; i <= NF; ++i) {
        sv = split($i, order, " ")
        if (sv == 3) {
            title = order[1]
            copies = order[2]
            price = order[3]
            amount = copies * price
            total_vol += copies
            total_amt += amount
            vol[title] += copies
            amt[title] += amount
        } else
            print "Incomplete Record"
    }
}

END {
    printf "%5s\t%10s\t%6s\n\n", "TITLE", "COPIES SOLD", "TOTAL"
    for (title in vol)
        printf "%5s\t%10d\t$%7.2f\n", title, vol[title], amt[title]
    printf "%s\n", "-------------"
    printf "\t%s%4d\t$%7.2f\n", "Total ", total_vol, total_amt
}' $*

Мы определили два массива с одним и тем же индексом. Нам нужен только один цикл for для чтения обоих массивов.

addemup, генератор отчетов о заказах, выдает следующий результат:

$ addemup orders
TITLE  COPIES SOLD     TOTAL

   #1           5    $  37.50
   #2           8    $  56.25
   #3           1    $  7.50
   #4           1    $  7.50
   #7           1    $  7.50
-------------
    Total      16    $ 116.25

10.7 Отладка

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

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

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

Второй класс ошибок - это ошибка, при которой программа не может выполнить или завершить выполнение. Это может произойти из-за синтаксической ошибки и невозможности заставить awk выплюнуть вам код, который он не может интерпретировать. Многие синтаксические ошибки являются результатом опечатки или отсутствия скобок. Синтаксические ошибки обычно приводят к появлению сообщений об ошибках, которые помогают указать на проблему. Однако иногда программа может вызвать сбой awk (или «core dump»), не выдавая разумного сообщения об ошибке*. Это также может быть вызвано синтаксической ошибкой, но могут быть проблемы, специфичные для машины. У нас было несколько более крупных скриптов, которые сбрасывали дамп на одной машине, а на другой работали без проблем. Например, вы можете столкнуться с ограничениями, установленными для awk для этой конкретной реализации. См. раздел «Ограничения» далее в этой главе.

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

* Это указывает на плохую реализацию awk. Дампы ядра очень редки в современных версиях awk.

10.7.1 Сделайте копию

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

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

Мы рекомендуем вам формализовать этот процесс и пойти так далеко, чтобы использовать систему управления исходным кодом, такую как SCCS (Source Code Control System), RCS (Revision Control System) или CVS (Concurrent Versioning System, совместимая с RCS). Последние два находятся в свободном доступе на любом FTP-зеркале GNU.

10.7.2 Фотографии до и после

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

Распространенная проблема - определить, когда и где в программе происходит присвоение переменной. Первый метод атаки - использование оператора print для вывода значения переменной в различных точках программы. Например, обычно используется переменная в качестве флага, чтобы определить, что произошло определенное условие. В начале программы флаг может быть установлен на 0. В одной или нескольких точках программы значение этого флага может быть установлено на 1. Проблема состоит в том, чтобы найти, где на самом деле происходит изменение. Если вы хотите проверить флаг в определенной части программы, используйте операторы print до и после присваивания. Например:

print flag, "before"
if (! $1) {
    .
    .
    .
    flag = 1
}
print flag, "after"

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

print $2
sub(/ *\(/, "(", $2)
print $2

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

10.7.3 Выяснение, в чем проблема

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

Если программа имеет несколько конструкций ветвления, вы можете обнаружить, что строка ввода проходит через одну из ветвей. Проверьте, что входные данные достигают части программы. Например, при отладке программы masterindex, описанной в Главе 12, Полнофункциональные приложения, мы хотели знать, обрабатывалась ли запись, содержащая слово «retrieving», в определенной части программы. Мы вставили следующую строку в ту часть программы, где, по нашему мнению, она должна встретиться:

if ($0 ~ /retrieving/) print ">> retrieving" > "/dev/tty"

При запуске программы, если она встречает строку «retrieving», она распечатывает сообщение. («>>» используется как пара символов, которые мгновенно привлекают внимание к выводу; «!!» также подойдет.)

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

if (PRIMARY)
    print (">>PRIMARY:", PRIMARY)
else
    if (SECONDARY)
        print (">>SECONDARY:", SECONDARY)
    else
        print (">>TERTIARY:", TERTIARY)

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

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

$INDEXDIR/input.idx $FILES |
sort -bdf -t: +0 -1 +1 -2 +3 -4 +2n -3n | uniq |
$INDEXDIR/pagenums.idx | tee page.tmp |
$INDEXDIR/combine.idx |
$INDEXDIR/format.idx

Добавив «tee page.tmp», мы можем записать вывод программы pagenums.idx в файл с именем page.tmp. Тот же вывод также передается по конвейеру в combine.idx.

10.7.4 Комментирование вслух

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

code#% if ( thisFails )
    print "I give up"

Использование комментария здесь исключает условное выражение, поэтому оператор print выполняется безоговорочно.

10.7.5 Руби и жги

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

Вы можете использовать метод «руби и жги», чтобы узнать, как работает программа. Сначала запустите исходную программу на образце ввода, сохранив вывод. Начните с удаления части программы, которую вы не понимаете. Затем запустите измененную программу на вводе образца и сравните вывод с исходным. Посмотрите, что изменилось.

10.7.6 Защищити свой скрипт

Существуют типы ошибок ввода и несоответствия, которые могут вызвать ошибки в вашем скрипте. Вы, вероятно, не учли, что на ошибки пользователя укажут как на проблемы с вашей программой. Таким образом, неплохо было бы окружить вашу основную программу «защитными» процедурами, предназначенными для перехвата несогласованных входных записей и предотвращения неожиданного сбоя программы. Например, вы можете захотеть проверить каждую входную запись перед ее обработкой, убедившись, что существует правильное количество полей или что тип данных, который вы ожидаете, найден в определенном поле.

Еще один аспект применения защитных методов - обработка ошибок. Другими словами, что вы хотите, чтобы произошло, когда программа обнаружит ошибку? Хотя в некоторых случаях вы можете продолжить выполнение программы, в других случаях может быть предпочтительнее, чтобы программа выводила сообщение об ошибке и/или останавливалась.

Также уместно признать, что сценарии awk обычно ограничиваются областью быстрых исправлений, программ, которые решают конкретную проблему, а не решают класс проблем, с которыми сталкиваются многие разные пользователи. Из-за природы этих программ не обязательно, чтобы они были профессионального качества. Таким образом, нет необходимости писать программы, на 100% защищающие пользователя. Во-первых, защитное программирование отнимает много времени и часто утомительно. Во-вторых, как любители, мы можем писать программы, которые работают так, как мы ожидаем; профессионал должен писать для аудитории и должен учитывать их ожидания. Вкратце, если вы пишете сценарий для использования другими, подумайте, как его можно использовать и с какими проблемами могут столкнуться его пользователи, прежде чем считать программу завершенной. Если нет, то, возможно, тот факт, что сценарий работает - даже для очень узкого набора обстоятельств - достаточно хорош, и все, на что есть время.

10.8 Ограничения

В любой реализации awk есть фиксированные ограничения. Единственная проблема в том, что документация о них редко сообщает. В Таблице 10.1 перечислены ограничения, описанные в The AWK Programming Language. Эти ограничения зависят от реализации, но для большинства систем они являются хорошими приблизительными показателями.

Таблица 10.1: Ограничения

Предмет ограничения Лимит
Количество полей на запись 100
Символов на входную запись 3000
Символов на выходную запись 3000
Символов в поле 1024
Символов на строку printf 3000
Символы в литеральной строке 400
Символы в классе символов 400
Открытые файлы 15
Открытые каналы 1

NOTE: Несмотря на ограничение каналов в Таблице 10.1, опыт показывает, что большинство awk позволяют иметь более одного открытого канала.

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

Выход за эти пределы может вызвать непредвиденные проблемы со скриптами. Разрабатывая примеры для первого издания этой книги, Дейл подумал, что он напишет программу поиска, которая сможет искать слово или последовательность слов в одном абзаце. Идея заключалась в том, чтобы прочитать документ как серию многострочных записей, и если какое-либо из полей содержало поисковый запрос, распечатать запись, которая была абзацем. Его можно использовать для поиска в почтовых файлах, где абзацы разделяются пустыми строками. Полученная программа работала с небольшими тестовыми файлами. Однако при попытке работы с файлами большего размера программа сбрасывала дамп, поскольку обнаружила абзац, длина которого превышала максимальный размер входной записи, равный 3000 символов. (Фактически, файл содержал включенное почтовое сообщение, в котором пустые строки в сообщении были помечены знаком «>».) Таким образом, при чтении нескольких строк как одной записи лучше быть уверенным, что вы не ожидаете записи длиннее 3000 символы. Между прочим, нет конкретного сообщения об ошибке, которое предупреждает вас о том, что проблема заключается в размере текущей записи.

К счастью, gawk и mawk (см. Главу 12, Полнофункциональные приложения) не имеют таких маленьких ограничений; например, количество полей в записи ограничено в gawk максимальным значением, которое может храниться в C long, и, конечно, записи могут быть длиннее 3000 символов. Эти версии позволяют иметь больше открытых файлов и каналов.

В последних версиях Bell Labs awk есть две опции, -mfN и -mrN, которые позволяют вам установить максимальное количество полей и максимальный размер записи в командной строке как экстренный способ обойти ограничения по умолчанию. (Реализации Sed также имеют свои собственные ограничения, которые не задокументированы. Опыт показал, что большинство версий sed для UNIX имеют ограничение в 99 или 100 команд замены (s).)

10.9 Вызов awk с помощью синтаксиса #!

Знак «#!» - это альтернативный синтаксис для вызова awk из сценария оболочки. Его преимущество заключается в том, что вы можете указать параметры awk и имена файлов в командной строке сценария оболочки. Синтаксис символов «#!» распознается в современных системах UNIX, но обычно не встречается в старых системах System V. Лучший способ использовать этот синтаксис - поместить следующую строку в качестве первой строки* сценария оболочки:

#!/bin/awk -f

«#!», за ним следует путь к вашей версии awk, а затем - опция -f. После этой строки вы указываете сценарий awk:

#!/bin/awk -f
{ print $1 }

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

Несколько лет назад в сети прошла интересная дискуссия об использовании символа «#!», который прояснил, как это работает. Обсуждение было вызвано наблюдением пользователя 4.2BSD о том, что сценарий оболочки ниже не работает:

#!/bin/awk
{ print $1 }

в то время как этот работает:

#!/bin/sh
/bin/awk '{ print $1 }'

Два ответа, которые мы видели, были написаны Крисом Тором и Гаем Харрисом, и мы постараемся обобщить их объяснения. Первый сценарий завершается неудачно, потому что он передает имя файла сценария в качестве первого параметра (argv[1] в C), а awk интерпретирует его как входной файл, а не файл сценария. Поскольку, в итоге, сценарий не предоставлен, awk выдает сообщение об ошибке синтаксиса. Другими словами, если имя сценария оболочки - «myscript», то первый сценарий выполняется как:

/bin/awk myscript

Если в сценарий была добавлена опция -f, он выглядит так:

#!/bin/awk -f
{ print $1 }

Вы вводите следующую команду:

$ myscript myfile

И она выполняется так, как если бы вы набрали:

/bin/awk -f myscript myfile

NOTE: Вы можете поставить только один параметр в строку с "#!". Эта строка обрабатывается непосредственно ядром UNIX; он не обрабатывается оболочкой и, следовательно, не может содержать произвольные конструкции оболочки.

Символ «#!» позволяет создавать сценарии оболочки, которые прозрачно передают параметры командной строки в awk. Другими словами, вы можете передавать параметры awk из командной строки, которая вызывает сценарий оболочки.

Например, мы демонстрируем передачу параметров, изменяя наш пример сценария awk на ожидание параметра n:

{ print $1*n }

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

$ myscript n=4 myfile

Это избавляет нас от необходимости передавать "$1" в качестве переменной оболочки и присваивать ее n в качестве параметра awk внутри скрипта оболочки.

masterindex, описанный в Главе 12, использует символ «#!» для вызова awk. Если ваша система не поддерживает этот синтаксис, вы можете изменить сценарий, удалив знак «#!», заключив весь сценарий в одинарные кавычки и завершив сценарий символом «$*», который расширяется до всех параметров командной строки оболочки.

Что ж, мы почти вычистили этот нижний ящик. Материал в этой главе во многом связан с тем, как awk взаимодействует с операционной системой UNIX, вызывает другие утилиты, открывает и закрывает файлы и использует каналы. И мы обсудили некоторые, по общему признанию, грубые методы отладки скриптов awk.

Мы рассмотрели все возможности языка программирования awk. Мы сосредоточились на спецификации POSIX для awk, лишь изредка упоминая о реальных реализациях awk. В следующей главе рассматриваются различия между версиями awk. Глава 12 посвящена разделению двух больших и сложных приложений: проверки орфографии документа и программы индексации. Глава 13, Сборник сценариев, представляет различные пользовательские программы, которые предоставляют дополнительные примеры того, как писать программы.

* Обратите внимание, что используемый путь зависит от системы.

Глава 11.
Семейство awk

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

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

11.1 Оригинальный awk

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

11.1.1 Escape-последовательности

В исходном awk V7 были только символы «\t», «\n», «\» и, конечно же, «\\». Большинство поставщиков UNIX добавили символы «\b» и «\r» и «\f» - все или некоторые.

11.1.2 Возведение в степень

Возведение в степень (с использованием операторов ^, ^=, ** и **=) отсутствует в старой awk.

11.1.3 Условное выражение языка Си

Условное выражение с тремя аргументами, присутствующее в C, - «expr1 ? expr2 : expr3» - отсутствует в старой awk. Вы должны прибегнуть к простому старому условию if-else.

11.1.4 Переменные как логические шаблоны

Вы не можете использовать значение переменной в качестве логического шаблона.

flag { print "..." }

Вместо этого вы должны использовать выражение сравнения.

flag != 0 { print "..." }

11.1.5 Подделка динамических регулярных выражений

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

$ cat awkro2
#! /bin/sh
# assign shell's $1 to awk search variable
search=$1
awk '$1 ~ /'"$search"'/' acronyms

Первая строка скрипта выполняет присвоение переменной до вызова awk. Чтобы оболочка расширяла переменную внутри процедуры awk, мы заключаем ее в одинарные, а затем в двойные кавычки*. Таким образом, awk никогда не увидит переменную оболочки и будет оценивать ее как постоянную строку.

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

search=$1
awk '$1 ~ /'"${search:-.*}"'/' acronyms

Выражение «$search:-.*» указывает оболочке использовать значение search, если оно определено; в противном случае используйте «.*» в качестве значения. Здесь «.*» - это синтаксис обычного выражения, определяющий любую строку символов; поэтому все записи печатаются, если в командной строке нет записи. Поскольку все это заключено в двойные кавычки, оболочка не выполняет подстановочные знаки для «.*».

* Фактически, это объединение текста в одинарных кавычках с текстом в двойных кавычках с большим количеством текста в одинарных кавычках для получения одной большой строки в кавычках. Этот трюк использовался ранее, в Главе 6.

11.1.6 Поток управления

В POSIX awk, если программа имеет только процедуру BEGIN и ничего больше, awk завершит работу после выполнения этой процедуры. Исходная awk отличается; он выполнит процедуру BEGIN, а затем перейдет к обработке ввода, даже если нет операторов шаблон-действие. Вы можете принудительно завершить работу awk, указав /dev/null в командной строке в качестве аргумента файла данных или используя команду exit.

Кроме того, процедуры BEGIN и END, если они есть, должны быть в начале и в конце программы соответственно. Кроме того, вы можете иметь только по одной из них.

11.1.7 Разделение полей

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

11.1.8 Массивы

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

Точно так же in не является оператором в исходной awk; вы не можете использовать if(item in array), чтобы узнать, присутствует ли элемент. К сожалению, это заставляет вас перебирать каждый элемент в массиве, чтобы увидеть, присутствует ли нужный вам индекс.

for (item in array) {
    if (item == searchkey) {
        process array[item]
        break
    }
}

11.1.9 Функция getline

В исходном awk V7 не было getline. Если ваш awk действительно старый, то getline может вам не подойти. Некоторые поставщики имеют простейшую форму getline, которая считывает следующую запись из обычного входного потока и устанавливает $0, NF и NR (FNR отсутствует, см. ниже). Все другие формы getline недоступны.

11.1.10 Функции

Исходный awk имел лишь ограниченное количество встроенных строковых функций. (См. Таблицу 11.1).

Таблица 11.1: Встроенные строковые функции оригинальной awk

Функции Awk Описание
index(s,t) Возвращает позицию подстроки t в строке s или ноль, если она отсутствует.
length(s) Возвращает длину строки s или длину $0, если строка не указана.
split(s,a,sep) Разбирает строку s на элементы массива a, используя разделитель полей sep; возвращает количество элементов. Если sep не указан, используется FS. Разделение массива работает так же, как разделение поля.
sprintf("fmt",expr) Использует printf формат для спецификации expr.
substr(s,p,n) Возвращает подстроку строки s в начальной позиции p до максимума длина n. Если n не указано, используется остальная часть строки из p.

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

Таблица 11.2: Встроенные арифметические функции оригинальной awk

Функции Awk Описание
exp(x) Возвращает e в степени x.
int(x) Возвращает усеченное значение x.
log(x) Возвращает натуральный логарифм (по основанию e) числа x.
sqrt(x) Возвращает квадратный корень из x.

Одна из самых приятных возможностей awk - возможность определять свои собственные функции - также недоступна в исходной awk.

11.1.11 Встроенные переменные

В исходном awk встроены только переменные, показанные в Таблице 11.3.

Таблица 11.3: Исходные системные переменные awk

Переменная Описание
FILENAME Текущее имя файла
FS Разделитель полей (пробел)
NF Количество полей в текущей записи
NR Номер текущей записи
OFMT Формат вывода для чисел (%.6g)
OFS Разделитель полей вывода (пробел)
ORS Разделитель выходной записи (новая строка)
RS Разделитель записей (новая строка)

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

11.2 Свободно доступные awk

Существует три версии awk, исходный код которых находится в свободном доступе. Это Bell Labs awk, GNU awk и mawk Майкла Бреннана. В этом разделе обсуждаются расширения, общие для двух или более из них, а затем подробно рассматривается каждая версия и описывается, как ее получить.

11.2.1 Общие расширения

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

* Как сопровождающий gawk и автор многих расширений, описанных здесь и в разделе о gawk ниже, мое мнение о полезности этих расширений может быть необъективным. ☺ Вы должны сделать свою собственную оценку. [A.R.]

11.2.1.1 Удаление всех элементов массива

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

delete array

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

for (i in data)
    delete data[i]

С расширенной версией оператора delete вы можете просто использовать

delete data

Это особенно полезно для массивов с большим количеством индексов; эта версия значительно быстрее, чем та, которая использует цикл.

Несмотря на то, что в нем больше нет элементов, вы не можете использовать имя массива как простую переменную. Бывших массивов не бывает!

Это расширение появилось сначала в gawk, затем в mawk и awk от Bell Labs.

11.2.1.2 Получение отдельных символов

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

Точно так же, если третьим аргументом функции split() является пустая строка, каждый символ в исходной строке станет отдельным элементом целевого массива. Без этих расширений вам придется использовать повторные вызовы функции substr() для получения отдельных символов.

Это расширение появилось сначала в mawk, затем в gawk и awk от Bell Labs.

11.2.1.3 Очистка буферизованного вывода

В версии Bell Labs awk 1993 года была представлена новая функция, отсутствующая в стандарте POSIX, - fflush(). Как и в close(), аргумент функции fflush() - это имя открытого файла или канала. В отличие от close(), функция fflush() работает только с выходными файлами и каналами.

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

Эта функция появилась сначала в awk лаборатории Bell Labs, затем в gawk и mawk.

11.2.1.4 Специальные имена файлов

В любой версии awk вы можете писать напрямую в специальный файл UNIX, /dev/tty, который является именем пользовательского терминала. Это можно использовать для направления пользователю подсказок или сообщений, когда вывод программы направлен в файл:

printf "Enter your name:" >"/dev/tty"

Это напечатает «Enter your name:» прямо на терминал, независимо от того, куда направлен стандартный вывод и стандартная ошибка.

Три бесплатных awk поддерживают несколько специальных имен файлов, перечисленных в Таблице 11.4.

Таблица 11.4: Исходные системные переменные awk

Имя файла Описание
/dev/stdin Стандартный ввод (не mawk)a
/dev/stdout Стандартный вывод
/dev/stderr Стандартная ошибка

a На странице руководства mawk рекомендуется использовать «-» для стандартного ввода, который является наиболее переносимым.

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

Специальные файлы /dev/stdin, /dev/stdout и /dev/stderr возникли в V8 UNIX. Gawk был первым, кто разработал специальное распознавание этих файлов, за ним последовали mawk и Bell Labs awk.

Функция printerr()

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

Следующая функция printerr() помогает создавать согласованные сообщения об ошибках пользователя. Она печатает слово «ERROR», за которым следует предоставленное сообщение, номер записи и текущая запись. В следующем примере вывод выводится на /dev/tty:

function printerr (message) {
    # print message, record number and record
    printf("ERROR:%s (%d) %s\n", message, NR, $0) > "/dev/tty"
}

Если вывод программы отправляется на экран терминала, то сообщения об ошибках будут смешиваться с выводом. Вывод «ERROR» поможет пользователю распознать сообщения об ошибках.

В UNIX стандартным местом назначения сообщений об ошибках является стандартный поток ошибок. Обоснование записи в стандартный поток ошибок такое же, как указано выше. Чтобы явно записать в стандартный поток ошибок, вы должны использовать запутанный синтаксис «cat 1>&2», как в следующем примере:

print "ERROR" | "cat 1>&2"

Это направляет вывод оператора print в канал, который выполняет команду cat. Вы также можете использовать функцию system() для выполнения команды UNIX, такой как cat или echo, и направить ее вывод в стандартный поток ошибок.

Когда доступен специальный файл /dev/stderr это становится намного проще:

print "ERROR" > "/dev/stderr"  # recent awks only
11.2.1.5 Оператор nextfile

Оператор nextfile аналогичен оператору next, но работает на более высоком уровне. При выполнении nextfile текущий файл данных удаляется, и обработка начинается с начала сценария с использованием первой записи следующего файла. Это полезно, когда вы знаете, что вам нужно обработать только часть файла; нет необходимости настраивать цикл для пропуска записей с помощью next.

Оператор nextfile возник в gawk, а затем был добавлен в awk лаборатории Bell Labs. Он будет доступен в mawk, начиная с версии 1.4.

11.2.1.6 Разделитель записей регулярных выражений (gawk и mawk)

Gawk и mawk позволяют RS быть полным регулярным выражением, а не просто отдельным символом. В этом случае записи разделяются самым длинным текстом во входных данных, который соответствует регулярному выражению. Gawk также устанавливает RT (признак конца записи) на фактический входной текст, соответствующий RS. Пример этого приведен ниже.

Возможность сделать RS регулярным выражением впервые появилась в mawk, а позже была добавлена в gawk.

11.2.2 Bell Labs awk

Bell Labs awk, конечно же, является прямым потомком исходной awk V7 и «новой» awk, которая впервые стала доступной с System V Release 3.1. Исходный код доступен бесплатно через анонимный FTP на хосте netlib.bell-labs.com. Он находится в файле /netlib/research/awk.bundle.Z. Это сжатый архивный файл оболочки. Обязательно используйте режим «binary» или «image» для передачи файла. Эта версия awk требует компилятора ANSI C.

Было несколько различных версий; мы будем идентифицировать их здесь в соответствии с годом, когда они стали доступны.

Первая версия новой awk стала доступной в конце 1987 года. В ней было почти все, что мы описали в предыдущих четырех главах (хотя есть сноски, указывающие на то, что недоступно). Эта версия все еще используется в системах SunOS 4.1.x и некоторых системах UNIX System V Release 3.

В 1989 году в System V Release 4 было добавлено несколько новых вещей. Единственная разница между этой версией и POSIX awk заключается в том, что POSIX использует CONVFMT для преобразования чисел в строку, тогда как версия 1989 года по-прежнему использовала OFMT. Новые функции:

В 1993 году Брайан Керниган из Bell Labs смог выпустить исходный код своего awk. На этом этапе стала доступна CONVFMT и добавлена функция fflush(), описанная выше. Релиз с исправлением ошибок был выпущен в августе 1994 года.

В июне 1996 года Брайан Керниган выпустил еще один релиз. Его можно получить либо с указанного выше FTP-сайта, либо через веб-браузер с веб-страницы доктора Кернигана (http://cm.bell-labs.com/who/bwk), где эта версия обозначается как «единственный настоящий awk». Эта версия добавляет несколько функций, которые возникли в gawk и mawk, описанных ранее в этой главе в разделе «Общие расширения».

11.2.3 GNU awk (gawk)

Версия awk, gawk проекта GNU Free Software Foundation реализует все функции awk POSIX и многие другие. Это, пожалуй, самая популярная из свободно доступных реализаций; gawk используется в системах Linux, а также в различных других свободно доступных UNIX-подобных системах, таких как NetBSD и FreeBSD.

Исходный код gawk доступен через анонимный FTP* на хосте ftp.gnu.org. Он находится в файле /gnu/gawk/gawk-3.0.4.tar.gz (к тому времени, когда вы это прочитаете, там может быть более поздняя версия). Это tar-файл, сжатый с помощью программы gzip, исходный код которой находится в том же каталоге. По всему миру существует множество сайтов, которые «зеркалируют» файлы с основного сайта распространения GNU; Если вы знаете такие, которые находятся ближе к вам, следует получить файлы оттуда. Обязательно используйте режим «binary» или «image» для передачи файла(ов).

Помимо общих расширений, перечисленных ранее, gawk имеет ряд дополнительных функций. Мы исследуем их в этом разделе.

* Если у вас нет доступа к Интернету и вы хотите получить копию gawk, свяжитесь с Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Телефонный номер: 617-542-5942 , номер факса - 617-542-2652.

11.2.3.1 Параметры командной строки

В Gawk есть несколько очень полезных параметров командной строки. Как и в большинстве программ GNU, эти параметры прописаны и начинаются с двух тире «--».

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

Хотя POSIX awk позволяет иметь несколько экземпляров параметра -f, нет простого способа использовать библиотечные функции из программы командной строки. Опция --source в gawk делает это возможным.

gawk --source 'script' -f mylibs.awk file1 file2

В этом примере программа запускается в script, который может использовать функции awk из файла mylibs.awk. Входные данные поступают из файлов file1 и file2.

11.2.3.2 Путь поиска программы awk

Gawk позволяет вам указать переменную окружения с именем AWKPATH, которая определяет путь поиска для файлов программы awk. По умолчанию он определен как .:/usr/local/share/awk. Таким образом, если имя файла указано с параметром -f, поиск будет выполняться в двух каталогах по умолчанию, начиная с текущего каталога. Обратите внимание: если имя файла содержит «/», поиск не выполняется.

Например, если mylibs.awk был файлом функций awk в /usr/local/share/awk, а myprog.awk был программой в текущем каталоге, мы запускаем gawk следующим образом:

gawk -f myprog.awk -f mylibs.awk datafile1

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

11.2.3.3 Продолжение строки

Gawk позволяет разрывать строки после «?» или «:». Вы также можете продолжить строки через символы новой строки, используя обратную косую черту.

$ gawk 'BEGIN { print "hello, \
> world" }'
hello, world
11.2.3.4 Расширенные регулярные выражения

Gawk предоставляет несколько дополнительных операторов регулярных выражений. Они являются общими для большинства программ GNU, работающих с регулярными выражениями. Расширенные операторы перечислены в Таблице 11.5.

Таблица 11.5: Расширенные регулярные выражения Gawk

Специальные операторы Использование
\w Соответствует любому символу, входящему в состав слова (буква, цифра или знак подчеркивания).
\W Соответствует любому символу, не входящему в состав слова.
\< Соответствует пустой строке в начале слова.
\> Соответствует пустой строке в конце слова.
\y Соответствует пустой строке в начале или в конце слова (граница слова). В других программах GNU используется «\b», но это уже занято.
\B Соответствует пустой строке в слове.
\‘ Соответствует пустой строке в начале буфера. Это то же самое, что и строка в awk, и, следовательно, то же самое, что и ^. Он предоставляется для совместимости с GNU Emacs и другим программным обеспечением GNU.
\' Соответствует пустой строке в конце буфера. Это то же самое, что и строка в awk, и, следовательно, то же самое, что и $. Он предоставляется для совместимости с GNU Emacs и другим программным обеспечением GNU.

Вы можете рассматривать «\w» как сокращение для обозначения (POSIX) [[:alnum:]_] и «\W» как сокращение для [^[:alnum:]_]. В следующей таблице приведены примеры соответствия четырех средних операторов, позаимствованные из Effective AWK Programming.

Таблица 11.6: Расширенные регулярные выражения Gawk

Выражение Совпадает Не совпадает
\<away away stowaway
stow\> stow stowaway
\balls?\y ball или balls ballroom или baseball
\Brat\B crate dirty rat
11.2.3.5 Терминаторы записи регулярного выражения

Помимо разрешения RS быть регулярным выражением, gawk устанавливает переменную RT (терминатор записи) на фактический входной текст, который соответствует значению RS.

Вот простой пример Майкла Бреннана, демонстрирующий мощь переменных RS и RT в gawk. Как мы видели, одним из наиболее распространенных способов использования sed является его команда замены (s/old/new/g). Установив RS в соответствие с шаблоном, а ORS в текст замены, простой оператор print может напечатать неизмененный текст, за которым следует текст замены.

$ cat simplesed.awk
# simplesed.awk --- do s/old/new/g using just print
#    Thanks to Michael Brennan for the idea
#
# NOTE! RS and ORS must be set on the command line
{
    if (RT == "")
        printf "%s", $0
    else
        print
}

Есть один полезный совет; в конце файла RT будет пустым, поэтому мы используем оператор printf для печати записи*. Мы могли бы запустить программу вот так.

$ cat simplesed.data
"This OLD house" is a great show.
I like shopping for old things at garage sales.
$ gawk -f simplesed.awk RS="old|OLD" ORS="brand new" simplesed.data
"This brand new house" is a great show.
I like shopping for brand new things at garage sales.

* См. Effective AWK Programming [Robbins], раздел 16.2.8, для более детальной версии описания этой программы.

11.2.3.6 Разделение полей

Помимо обычного способа, которым awk позволяет вам разбивать ввод на записи и запись на поля, gawk дает вам некоторые дополнительные возможности.

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

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

FIELDWIDTHS = "5 6 8 3"

Здесь запись имеет четыре поля: $1 - это пять символов, $2 - шесть символов и т. д. Присвоение значения FIELDWIDTHS заставляет gawk начать использовать его для разделения полей. Присвоение значения FS заставляет gawk возвращаться к обычному механизму разделения полей. Используйте FS = FS, чтобы это произошло без необходимости сохранять значение FS в дополнительной переменной.

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

11.2.3.7 Дополнительные специальные файлы

Gawk имеет ряд дополнительных специальных имен файлов, которые он интерпретирует внутренне. Все специальные имена файлов перечислены в Таблице 11.7.

Таблица 11.7: Особые имена файлов Gawk

Описание Имя файла
/dev/stdin Стандартный ввод.
/dev/stdout Стандартный вывод.
/dev/stderr Стандартная ошибка.
/dev/fd/n Файл указан как файловый дескриптор n.
Устаревшее имя файла Описание
/dev/pid Возвращает запись, содержащую ID процесса.
/dev/ppid Возвращает запись, содержащую ID родительского процесса.
/dev/pgrpid Возвращает запись, содержащую ID группы процессов.
/dev/user Возвращает запись с реальными и эффективными ID пользователей, реальными и эффективными ID групп и, если возможно, любыми дополнительными ID групп.

Первые три были описаны ранее. Четвертое имя файла обеспечивает доступ к любому дескриптору открытого файла, который мог быть унаследован от родительского процесса gawk (обычно оболочки). Вы можете использовать дескриптор файла 0 для стандартного ввода, 1 для стандартного вывода и 2 для стандартного вывода ошибок.

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

Например, вы могли бы использовать PROCINFO["pid"] для получения идентификатора текущего процесса вместо использования getline pid < "/dev/pid". Проверьте документацию gawk, чтобы узнать, доступен ли PROCINFO и поддерживаются ли эти имена файлов.

11.2.3.8 Дополнительные переменные

В Gawk есть еще несколько системных переменных. Они перечислены в Таблице 11.8.

Таблица 11.8: Дополнительные системные переменные gawk

Переменная Описание
ARGIND Индекс текущего входного файла в ARGV.
ERRNO Сообщение с описанием ошибки в случае сбоя getline или close().
FIELDWIDTHS Список чисел, разделенных пробелами, описывающих ширину полей ввода.
IGNORECASE Если не равно нулю, совпадения с образцом и сравнения строк не зависят от регистра.
RT Значение входящего текста, которое соответствует RS.

Мы уже видели переменную терминатора записи, RT, поэтому перейдем к другим переменным, которые мы еще не рассмотрели.

Все сопоставления с образцом и сравнение строк в awk чувствительны к регистру. Gawk представил переменную IGNORECASE, чтобы вы могли указать, что регулярные выражения интерпретируются без учета символов верхнего или нижнего регистра. Начиная с версии 3.0 gawk, сравнение строк также может выполняться без учета регистра.

Значение IGNORECASE по умолчанию равно нулю, что означает, что сопоставление с образцом и сравнение строк выполняется так же, как в традиционном awk. Если для IGNORECASE задано ненулевое значение, то различия в регистре игнорируются. Это относится ко всем местам, где используются регулярные выражения, включая разделитель полей FS, разделитель записей RS и все сравнения строк. Это не относится к индексам массивов.

Интересны еще две переменные gawk. ARGIND автоматически устанавливается gawk как индекс текущего имени входного файла в ARGV. Эта переменная позволяет отслеживать, насколько далеко вы находитесь в списке имен файлов.

Наконец, если возникает ошибка при перенаправлении для getline или во время close(), gawk устанавливает ERRNO в строку, описывающую ошибку. Это дает возможность предоставлять описательные сообщения об ошибках, когда что-то идет не так.

11.2.3.9 Дополнительные функции

В Gawk есть одна дополнительная строковая функция и две функции для работы с текущей датой и временем. Они перечислены в Таблице 11.9.

Таблица 11.9: Дополнительные функции gawk

Функция gawk Описание
gensub(r,s,h,t) Если h - строка, начинающаяся с g или G, глобально заменяет s на r в t. В противном случае h является числом: заменяет h-е вхождение. Возвращает новое значение, t не изменилось. Если t не указан, по умолчанию используется $0.
systime() Возвращает текущее время дня в секундах с начала эпохи UNIX (00:00, 1 января 1970 г. по UTC).
strftime(format,timestamp) Форматирует временную метку (той же формы, что и systime()) в соответствии с format. Если timestamp отсутствует, использует текущее время. Если отсутствует и format, использует формат по умолчанию, вывод которого аналогичен команде date.
11.2.3.10 Общая функция замены

В версии 3.0 gawk появилась новая общая функция замены, названная gensub(). У функций sub() и gsub() есть некоторые проблемы.

По всем этим причинам gawk ввел функцию gensub(). Функция принимает не менее трех аргументов. Первый - это регулярное выражение для поиска. Второй - строка замены. Третий - флаг, который контролирует, сколько замен должно быть выполнено. Четвертый аргумент, если он присутствует, - это исходная строка, которую нужно изменить. Если он не указан, используется текущая входная запись ($0).

В шаблоне могут быть подшаблоны, ограниченные круглыми скобками. Например, он может содержать «/(part) (one|two|three)/». В строке замены обратная косая черта, за которой следует цифра, представляет текст, соответствующий n-ному подшаблону.

$ echo part two | gawk '{ print gensub(/(part) (one|two|three)/, "\\2", "g") }'
two

Флаг - это либо строка, начинающаяся с g или G, и в этом случае замена происходит глобально, либо это число, указывающее, что n-ное вхождение должно быть заменено.

$ echo a b c a b c a b c | gawk '{ print gensub(/a/, "AA", 2) }'
a b c AA b c a b c

Четвертый аргумент - это строка, в которой нужно внести изменение. В отличие от sub() и gsub() целевая строка не изменяется. Вместо этого новая строка - это значение, возвращаемое функцией gensub().

$ gawk '
BEGIN { old = "hello, world"
    new = gensub(/hello/, "goodbye", 1, old)
    printf("<%s>, <%s>\n", old, new)
}'
<hello, world>, <goodbye, world>

* Полное обсуждение дано в Effective AWK Programming [Robbins], раздел 12.3. Детали не для слабонервных.

11.2.3.11 Тайм-менеджмент для программистов

Программы AWK очень часто используются для обработки файлов журналов, создаваемых различными программами. Часто каждая запись в файле журнала содержит отметку времени, указывающую, когда была создана запись. Для краткости и точности метка времени записывается как результат системного вызова UNIX time(2), который представляет собой количество секунд, прошедших с полуночи 1 января 1970 года по UTC. (Эту дату часто называют «эпохой UNIX».) Чтобы упростить создание и обработку записей файлов журнала с такими отметками времени, в gawk есть две функции: systime() и strftime().

Функция systime() в первую очередь предназначена для генерации меток времени для записи в журнал. Предположим, например, что мы используем сценарий awk для ответа на запросы CGI к нашему серверу WWW. Мы можем записывать каждый запрос в файл журнала.

{
...
printf("%s:%s:%d\n", User, Host, systime()) >> "/var/log/cgi/querylog"
...
}

Такая запись может выглядеть как

arnold:some.domain.com:831322007

Функция strftime()* упрощает преобразование меток времени в даты, удобочитаемые человеком. Строка формата аналогична той, что используется sprintf(); она состоит из буквального текста, смешанного со спецификациями формата для различных компонентов даты и времени.

$ gawk 'BEGIN { print strftime("Today is %A, %B %d, %Y") }'
Today is Sunday, May 05, 1996

Список доступных форматов довольно длинный. См. вашу локальную страницу руководства по strftime(3) и полный список в документации по gawk. Наш гипотетический файл журнала CGI может обрабатываться этой программой:

# cgiformat --- process CGI logs
# data format is user:host:timestamp
#1
BEGIN { FS = ":"; SUBSEP = "@" }

#2
{
# make data more obvious
    user = $1; host = $2; time = $3
# store first contact by this user
    if (! ((user, host) in first))
        first[user, host] = time
# count contacts
    count[user, host]++
# save last contact
    last[user, host] = time
}

#3
END {
# print the results
    for (contact in count) {
        i = strftime("%y-%m-%d %H:%M", first[contact])
        j = strftime("%y-%m-%d %H:%M", last[contact])
        printf "%s -> %d times between %s and %s\n",
            contact, count[contact], i, j
    }
}

Первый шаг - установить FS в «:», чтобы правильно разделить поле. Мы также используем изящный трюк и устанавливаем разделитель индексов на «@», чтобы массивы индексировались строками «user@host».

На втором этапе мы смотрим, видим ли мы этого пользователя впервые. Если да (их нет в массиве first), мы их добавляем. Затем мы увеличиваем количество подключений. Наконец, мы сохраняем метку времени этой записи в массиве last. Этот элемент перезаписывается каждый раз, когда мы видим новое соединение пользователя. Все в порядке; в итоге мы получим последнее (самое последнее) соединение, сохраненное в массиве.

Процедура END форматирует данные для нас. Она просматривает массив count, форматируя отметки времени в массивах first и last для печати. Рассмотрим файл журнала со следующими записями.

$ cat /var/log/cgi/querylog
arnold:some.domain.com:831322007
mary:another.domain.org:831312546
arnold:some.domain.com:831327215
mary:another.domain.org:831346231
arnold:some.domain.com:831324598

Вот что дает выполнение программы:

$ gawk -f cgiformat.awk /var/log/cgi/querylog
mary@another.domain.org -> 2 times between 96-05-05 12:09 and 96-05-05 21:30
arnold@some.domain.com -> 3 times between 96-05-05 14:46 and 96-05-05 15:29

* Эта функция создана по образцу одноименной функции в ANSI C.

11.2.4 Awk Майкла (mawk)

Третья свободно доступная awk - это mawk, написанная Майклом Бреннаном. Эта программа совместима снизу вверх с POSIX awk, а также имеет несколько расширений. Она надежная и работает очень хорошо. Исходный код mawk находится в свободном доступе через анонимный FTP с ftp.whidbey.net. Он находится в /pub/brennan/mawk1.3.3.tar.gz. (К тому времени, как вы это прочитаете, там может быть более поздняя версия.) Это также tar-файл, сжатый с помощью программы gzip. Обязательно используйте «binary» или «image» режим для передачи файла.

Основными преимуществами Mawk являются его скорость и надежность. Хотя у него меньше возможностей, чем у gawk, он почти всегда превосходит его*. Помимо систем UNIX, mawk также работает под MS-DOS.

Описанные выше общие расширения также доступны в mawk.

* Преимущества Gawk в том, что он имеет больший набор функций, он перенесен на большее количество систем, отличных от UNIX, и поставляется с гораздо более обширной документацией.

11.3 Коммерческие awk

Также существует несколько коммерческих версий awk. В этом разделе мы рассмотрим те, о которых нам известно.

11.3.1 MKS awk

Mortice Kern Systems (MKS) в Ватерлоо, Онтарио (Канада)* поставляет awk как часть набора инструментов MKS для MS-DOS/Windows, OS/2, Windows 95 и Windows NT. Версия MKS реализует POSIX awk. Он имеет следующие расширения:

* Mortice Kern Systems, 185 Columbia Street West, Waterloo, Ontario N2L 5Z5, Canada. Телефон: 1-800265-2797 в Северной Америке, 1-519-884-2251 в других странах. URL: http://www.mks.com/.

11.3.2 Thompson Automation awk (tawk)

Thompson Automation Software* создает версию awk (tawk)** для MS-DOS/Windows, Windows 95 и NT и Solaris. Tawk интересен по нескольким причинам. Во-первых, в отличие от других версий awk, которые являются интерпретаторами, tawk является компилятором. Во-вторых, tawk поставляется с экранно-ориентированным отладчиком, написанным на awk! Исходные коды отладчика включены. В-третьих, tawk позволяет связать вашу скомпилированную программу с произвольными функциями, написанными на C. Tawk получил восторженные отзывы в группе новостей comp.lang.awk.

Tawk поставляется с интерфейсом awk, который действует как POSIX awk, компилируя и выполняя вашу программу. Однако вы можете скомпилировать свою программу в отдельный исполняемый файл. Компилятор tawk фактически компилируется в компактную промежуточную форму. Промежуточное представление связано с библиотекой, которая выполняет программу при ее запуске, и именно во время компоновки другие подпрограммы C могут быть интегрированы с программой awk.

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

* Thompson Automation Software, 5616 SW Jefferson, Portland OR 97221 США. Телефон: 1-800-944-0139 в США, 1-503-224-1639 в других местах.

** Майкл Бреннан на справочной странице mawk(1) делает следующее заявление: «Разработчики языка AWK постоянно проявляют недостаток воображения при наименовании своих программ».

11.3.2.1 Расширения языка Tawk

В этом разделе представлен «подробный список» новых функций tawk. Полное их рассмотрение выходит за рамки этой книги; документация tawk прекрасно их представляет. Надеюсь, к настоящему времени вы уже достаточно знакомы с awk, чтобы оценить ценность этих функций. Там, где это уместно, мы сопоставим функцию tawk с аналогичной функцией gawk.

Ух! Это довольно длинный список, но эти возможности расширяют возможности программирования на awk.

* Признаюсь, я не вижу в этом реальной пользы. [A.R.]

11.3.2.2 Дополнительные встроенные функции tawk

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

Из этого списка становится ясно, что tawk предоставляет хорошую альтернативу C и Perl для серьезных задач программирования. Например, функции экрана и функции внутреннего состояния используются для реализации отладчика tawk в awk.

11.3.3 Videosoft VSAwk

Videosoft* продает программное обеспечение под названием VSAwk, которое привносит программирование в стиле awk в среду Visual Basic. VSAwk - это элемент управления Visual Basic, который работает в режиме, управляемом событиями. Как и awk, VSAwk предоставляет вам действия по запуску и очистке и разбивает входную запись на поля, а также дает возможность писать выражения и вызывать встроенные функции awk.

VSAwk напоминает UNIX awk в основном своей моделью обработки данных, а не синтаксисом. Тем не менее интересно наблюдать, как люди применяют концепции awk к среде, предоставляемой весьма отличающимся языком.

* С компанией Videosoft можно связаться по адресу: 2625 Alcatraz Avenue, Suite 271, Berkeley CA 94705, США. Телефон: 1-510-704-8200. Факс: 1-510-843-0174. Их сайт http://www.videosoft.com.

11.4 Эпилог

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

Глава 12.
Полнофункциональные приложения

В этой главе представлены два сложных приложения, которые объединяют большинство возможностей языка программирования awk. Первая программа, spellcheck, обеспечивает интерактивный интерфейс для программы spell UNIX. Второе приложение, masterindex, представляет собой пакетную программу для создания указателя книги или набора книг. Даже если вас не интересует конкретное приложение, вам следует изучить эти более крупные программы, чтобы почувствовать объем проблем, которые может решить программа awk.

12.1 Интерактивная проверка орфографии

Программа UNIX spell выполняет адекватную работу по обнаружению орфографических ошибок в документе. Однако для большинства людей она выполняет только половину работы. Она не поможет вам исправить слова с ошибками. Новички в spell обнаруживают, что записывают слова с ошибками, а затем используют текстовый редактор для изменения документа. Более опытные пользователи создают сценарий sed для автоматического внесения изменений.

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

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

$ spellcheck ch00
Use local dict file? (y/n)y

Если файл словаря не указан в командной строке, а файл с именем dict существует в текущем каталоге, то пользователя спрашивают, следует ли использовать локальный словарь. Затем spellcheck запускает spell с использованием местного словаря.

Running spell checker ...

Используя список «неправильно написанных» слов, отображаемый spell, spellcheck предлагает пользователю исправить их. Перед отображением первого слова отображается список ответов, в которых описываются возможные действия.

Responses:
    Change each occurrence,
    Global change,
    Add to Dict,
    Help,
    Quit
    CR to ignore:
1 - Found SparcStation (C/G/A/H/Q/):a

Первое слово, найденное spell, - «SparcStation». Ответ «а» (за которым следует возврат каретки) добавляет это слово в список, который будет использоваться для обновления словаря. Второе слово явно написано с ошибкой, и вводится ответ «g», чтобы изменить глобально:

2 - Found languauge (C/G/A/H/Q/):g
Globally change to:language
Globally change languauge to language? (y/n):y
> and a full description of its scripting language.
1 lines changed. Save changes? (y/n)y

После того, как пользователю будет предложено ввести правильное написание и подтвердить ввод, вносятся изменения, и отображается каждая затронутая строка, перед которой стоит знак «>». Затем пользователя просят одобрить эти изменения перед их сохранением. В словарь также добавляется третье слово:

3 - Found nawk (C/G/A/H/Q/):a

Четвертое слово - это неправильное написание слова «utilities».

4 - Found utlitities (C/G/A/H/Q/):c
These utlitities have many things in common, including
      ^^^^^^^^^^
Change to:utilities
Change utlitities to utilities? (y/n):y
Two other utlitities that are found on the UNIX system
          ^^^^^^^^^^
Change utlitities to utilities? (y/n):y
>These utilities have many things in common, including
>Two other utilities that are found on the UNIX system
2 lines changed. Save changes? (y/n)y

Пользователь вводит «c», чтобы изменить каждое вхождение. Этот ответ позволяет пользователю увидеть строку с ошибкой и затем внести изменения. После того, как пользователь внес каждое изменение, отображаются измененные строки, и пользователя просят подтвердить сохранение изменений.

Неясно, написано ли пятое слово с ошибкой или нет, поэтому пользователь вводит «c», чтобы просмотреть строку.

5 - Found xvf (C/G/A/H/Q/):c
tar xvf filename
    ^^^
Change to:RETURN

Убедившись, что это не орфографическая ошибка, пользователь вводит возврат каретки, чтобы проигнорировать слово. Как правило, при использовании spell встречается много слов, которые не содержат ошибок, поэтому возврат каретки означает игнорирование слова.

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

Save corrections in ch00 (y/n)? y
Make changes to dictionary (y/n)? y

Если пользователь отвечает «n», исходный файл и словарь остаются без изменений.

Теперь давайте посмотрим на сценарий spellcheck.awk, который можно разделить на четыре части:

Мы рассмотрим каждый из этих разделов программы.

12.1.1 Процедура BEGIN

Процедура BEGIN для spellcheck.awk является большой. Это тоже несколько необычно.

# spellcheck.awk -- interactive spell checker
#
# AUTHOR: Dale Dougherty
#
# Usage: nawk -f spellcheck.awk [+dict] file 
# (Use spellcheck as name of shell program) 
# SPELLDICT = "dict" 
# SPELLFILE = "file"

# BEGIN actions perform the following tasks: 
#   1) process command line arguments
#   2) create temporary filenames
#   3) execute spell program to create wordlist file
#   4) display list of user responses

BEGIN { 
# Process command line arguments
# Must be at least two args -- nawk and filename
    if (ARGC > 1) {
    # if more than two args, second arg is dict 
        if (ARGC > 2) {
        # test to see if dict is specified with "+"  
        # and assign ARGV[1] to SPELLDICT
            if (ARGV[1] ~ /^\+.*/) 
                SPELLDICT = ARGV[1]
            else 
                SPELLDICT = "+" ARGV[1]
        # assign file ARGV[2] to SPELLFILE 
            SPELLFILE = ARGV[2]
        # delete args so awk does not open them as files
            delete ARGV[1]
            delete ARGV[2]
        }
    # not more than two args
        else {
        # assign file ARGV[1] to SPELLFILE 
            SPELLFILE = ARGV[1]
        # test to see if local dict file exists
            if (! system ("test -r dict")) {
            # if it does, ask if we should use it
                printf ("Use local dict file? (y/n)")   
                getline reply < "-"
            # if reply is yes, use "dict" 
                if (reply ~ /[yY](es)?/){
                    SPELLDICT = "+dict"
                }
            }
        }
    } # end of processing args > 1 
    # if args not > 1, then print shell-command usage 
    else {
        print "Usage: spellcheck [+dict] file"
        exit 1
    }
# end of processing command line arguments

# create temporary file names, each begin with sp_
    wordlist = "sp_wordlist"
    spellsource = "sp_input"
    spellout = "sp_out"

# copy SPELLFILE to temporary input file
    system("cp " SPELLFILE " " spellsource)

# now run spell program; output sent to wordlist
    print "Running spell checker ..."
    if (SPELLDICT)
        SPELLCMD = "spell " SPELLDICT " "
    else
        SPELLCMD = "spell "
    system(SPELLCMD spellsource " > " wordlist )

# test wordlist to see if misspelled words turned up
    if ( system("test -s " wordlist ) ) {
    # if wordlist is empty, (or spell command failed), exit
        print "No misspelled words found."
        system("rm " spellsource " " wordlist)
        exit
    }   

# assign wordlist file to ARGV[1] so that awk will read it. 
    ARGV[1] = wordlist

# display list of user responses 
    responseList = "Responses: \n\tChange each occurrence," 
    responseList = responseList "\n\tGlobal change," 
    responseList = responseList "\n\tAdd to Dict,"  
    responseList = responseList "\n\tHelp," 
    responseList = responseList "\n\tQuit" 
    responseList = responseList "\n\tCR to ignore: "
    printf("%s", responseList)

} # end of BEGIN procedure

Первая часть процедуры BEGIN обрабатывает аргументы командной строки. Она проверяет, что ARGC больше единицы, для продолжения программы. То есть в дополнение к nawk необходимо указать имя файла. В этом файле указывается документ, который будет анализировать spell. В качестве второго аргумента можно указать необязательное имя файла словаря. Скрипт spellcheck следует интерфейсу командной строки spell, хотя ни один из скрытых параметров spell не может быть вызван из командной строки spellcheck. Если словарь не указан, сценарий выполняет тестовую команду, чтобы проверить, существует ли файл dict. Если это так, пользователю предлагается разрешить использование его в качестве файла словаря.

После обработки аргументов мы удаляем их из массива ARGV. Это сделано для предотвращения их интерпретации как аргументов имени файла.

Вторая часть процедуры BEGIN устанавливает некоторые временные файлы, потому что мы не хотим работать напрямую с исходным файлом. В конце программы у пользователя будет возможность сохранить или отменить работу, выполненную во временных файлах. Все временные файлы начинаются с «sp_» и удаляются перед выходом из программы.

Третья часть процедуры выполняет spell и создает список слов. Мы проверяем, существует ли этот файл и что в нем что-то есть, прежде чем продолжить. Если по какой-либо причине программа spell не работает или слов с ошибками не обнаружено, файл списка слов будет пуст. Если этот файл существует, мы назначаем имя файла вторым элементом массива ARGV. Это необычный, но допустимый способ указать имя входного файла, который будет обрабатывать awk. Обратите внимание, что этот файл не существовал при запуске awk! Имя файла документа, указанное в командной строке, больше не входит в массив ARGV. Мы не будем читать файл документа, используя основной цикл ввода awk. Вместо этого цикл while читает файл, чтобы найти и исправить слова с ошибками.

Последняя задача в процедуре BEGIN - определить и отобразить список ответов, которые пользователь может ввести при отображении слова с ошибкой. Этот список отображается один раз в начале программы, а также когда пользователь вводит «Help» в основном запросе. Помещение этого списка в переменную позволяет нам обращаться к нему из разных точек программы, если это необходимо, без сохранения дубликатов. Назначение responseList можно было бы сделать проще, но длинную строку нельзя было бы распечатать в этой книге. (Нельзя разбивать строку на две строки.)

12.1.2 Процедура Main

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

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

# main procedure, executed for each line in wordlist.
#   Purpose is to show misspelled word and prompt user
#   for appropriate action.

{
# assign word to misspelling
    misspelling = $1 
    response = 1
    ++word
# print misspelling and prompt for response
    while (response !~ /(^[cCgGaAhHqQ])|^$/ ) {
        printf("\n%d - Found %s (C/G/A/H/Q/):", word, misspelling)
        getline response < "-"
    }
# now process the user's response
# CR - carriage return ignores current word 
# Help
    if (response ~ /[Hh](elp)?/) {
    # Display list of responses and prompt again.
        printf("%s", responseList)
        printf("\n%d - Found %s (C/G/A/Q/):", word, misspelling)
        getline response < "-"
    }
# Quit
    if (response ~ /[Qq](uit)?/) exit
# Add to dictionary
    if ( response ~ /[Aa](dd)?/) { 
        dict[++dictEntry] = misspelling
    }
# Change each occurrence
    if ( response ~ /[cC](hange)?/) {
    # read each line of the file we are correcting
        newspelling = ""; changes = ""
        while( (getline < spellsource) > 0){
        # call function to show line with misspelled word
        # and prompt user to make each correction 
            make_change($0)
        # all lines go to temp output file
            print > spellout
        }   
    # all lines have been read 
    # close temp input and temp output file
        close(spellout)
        close(spellsource)
    # if change was made
        if (changes){ 
        # show changed lines
            for (j = 1; j <= changes; ++j)
                print changedLines[j]
            printf ("%d lines changed. ", changes) 
        # function to confirm before saving changes
            confirm_changes()
        }
    }
# Globally change
    if ( response ~ /[gG](lobal)?/) {
    # call function to prompt for correction
    # and display each line that is changed.
    # Ask user to approve all changes before saving.
        make_global_change()
    }   
} # end of Main procedure

Первое поле каждой строки ввода из wordlist содержит слово с ошибкой, и ему назначается misspelling. Мы создаем цикл while, внутри которого мы показываем пользователю слово с ошибкой и запрашиваем ответ. Посмотрите внимательно на регулярное выражение, которое проверяет значение response:

while (response !~ /(^[cCgGaAhHqQ])|^$/)

Пользователь может выйти из этого цикла, только введя любую из указанных букв или введя символ возврата каретки - пустую строку. Использование регулярных выражений для тестирования пользовательского ввода очень помогает при написании простой, но гибкой программы. Пользователь может ввести одну букву «c» в нижнем или верхнем регистре или слово, начинающееся с «c», например «Change».

Остальная часть основной процедуры состоит из условных операторов, которые проверяют конкретный ответ и выполняют соответствующее действие. Первый ответ - это «help», который снова отображает список ответов, а затем повторно отображает подсказку.

Следующий ответ - «quit». Действие, связанное с quit, - это exit, которое выходит из основной процедуры и переходит к процедуре END.

Если пользователь вводит «add», слово с ошибкой помещается в массив dict и будет добавлено как исключение в локальный словарь.

Ответы «Change» и «Global» запускают настоящую работу программы. Важно понимать, чем они отличаются. Когда пользователь вводит «c» или «change», отображается первое вхождение слова с ошибкой в документе. Затем пользователю предлагается внести изменения. Это происходит для каждого случая в документе. Когда пользователь вводит «g» или «global», пользователю предлагается сразу же внести изменения, и все изменения вносятся сразу без запроса пользователя на подтверждение каждого из них. Эту работу в основном выполняют две функции, make_change() и make_global_change(), которые мы рассмотрим в последнем разделе. Это все действительные ответы, кроме одного. Возврат каретки означает игнорирование слова с ошибкой и переход к следующему слову в списке. Это действие по умолчанию для основного цикла ввода, поэтому для него не нужно устанавливать никаких условий.

12.1.3 Процедура END

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

Цель процедуры END - позволить пользователю подтвердить любое постоянное изменение документа или словаря.

# END procedure makes changes permanent.
# It overwrites the original file, and adds words
# to the dictionary.
# It also removes the temporary files.

END {
# if we got here after reading only one record, 
# no changes were made, so exit.
    if (NR <= 1) exit
# user must confirm saving corrections to file
    while (saveAnswer !~ /([yY](es)?)|([nN]o?)/ ) {
        printf "Save corrections in %s (y/n)? ", SPELLFILE
        getline saveAnswer < "-"
    }
# if answer is yes then mv temporary input file to SPELLFILE
# save old SPELLFILE, just in case
    if (saveAnswer ~ /^[yY]/) {
        system("cp " SPELLFILE " " SPELLFILE ".orig")
        system("mv " spellsource " " SPELLFILE)
    }
# if answer is no then rm temporary input file
    if (saveAnswer ~ /^[nN]/)
        system("rm " spellsource) 

# if words have been added to dictionary array, then prompt
# to confirm saving in current dictionary. 
    if (dictEntry) {
        printf "Make changes to dictionary (y/n)? "
        getline response < "-"
        if (response ~ /^[yY]/){
        # if no dictionary defined, then use "dict"
            if (! SPELLDICT) SPELLDICT = "dict"
        
        # loop through array and append words to dictionary
            sub(/^\+/, "", SPELLDICT)
            for ( item in dict )
                print dict[item] >> SPELLDICT
            close(SPELLDICT)
        # sort dictionary file 
            system("sort " SPELLDICT "> tmp_dict")
            system("mv " "tmp_dict " SPELLDICT)
        }
    }
# remove word list
    system("rm sp_wordlist")
} # end of END procedure

Процедура END начинается с условного оператора, который проверяет, что количество записей меньше или равно 1. Это происходит, когда программа spell не создает список слов или когда пользователь вводит «quit» после просмотра только первой записи. Если это так, процедура END завершается, так как работы для сохранения нет.

Затем мы создаем цикл while, чтобы спросить пользователя о сохранении изменений, внесенных в документ. Он требует, чтобы пользователь ответил на приглашение «y» или «n». Если ответ - «y», временный входной файл заменяет исходный файл документа. Если ответ «n», временный файл удаляется. Никакие другие ответы не принимаются.

Затем мы проверяем, есть ли что-нибудь в массиве dict. Его элементами являются слова, которые нужно добавить в словарь. Если пользователь одобряет их добавление в словарь, эти слова добавляются к текущему словарю, как определено выше, или, если нет, к локальному файлу dict. Поскольку словарь должен быть отсортирован для чтения по spell, выполняется команда sort с выводом, отправленным во временный файл, который затем копируется поверх исходного файла.

12.1.4 Вспомогательные функции

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

Когда пользователь хочет «Change each occurrence» (изменить каждое вхождение) в документе, основная процедура имеет цикл while, который читает документ по одной строке за раз. (Эта строка становится $0). Она вызывает функцию make_change(), чтобы проверить, содержит ли строка слово с ошибкой. Если это так, отображается строка, и пользователю предлагается ввести правильное написание слова.

# make_change -- prompt user to correct misspelling 
#        for current input line.  Calls itself
#        to find other occurrences in string.
#   stringToChange -- initially $0; then unmatched substring of $0
#   len -- length from beginning of $0 to end of matched string 
# Assumes that misspelling is defined. 

function make_change (stringToChange, len,  # parameters
    line, OKmakechange, printstring, carets)    # locals
{
# match misspelling in stringToChange; otherwise do nothing 
  if ( match(stringToChange, misspelling) ) {
  # Display matched line 
    printstring = $0
    gsub(/\t/, " ", printstring)
    print printstring
    carets = "^"
    for (i = 1; i < RLENGTH; ++i)
        carets = carets "^"
    if (len)
        FMT = "%" len+RSTART+RLENGTH-2 "s\n"
    else
        FMT = "%" RSTART+RLENGTH-1 "s\n"
    printf(FMT, carets)
  # Prompt user for correction, if not already defined
    if (! newspelling) {
        printf "Change to:"
        getline newspelling < "-"
    }
  # A carriage return falls through
  # If user enters correction, confirm  
    while (newspelling && ! OKmakechange) {
        printf ("Change %s to %s? (y/n):", misspelling, newspelling)
        getline OKmakechange < "-"
        madechg = ""
    # test response
        if (OKmakechange ~ /[yY](es)?/ ) {
        # make change (first occurrence only)
            madechg = sub(misspelling, newspelling, stringToChange)
        }
        else if ( OKmakechange ~ /[nN]o?/ ) {
            # offer chance to re-enter correction 
            printf "Change to:"
            getline newspelling < "-"
            OKmakechange = ""
        }
    } # end of while loop

   # if len, we are working with substring of $0
    if (len) {
    # assemble it
        line = substr($0,1,len-1)
        $0 = line stringToChange
    }
    else {
        $0 = stringToChange
        if (madechg) ++changes
    }

   # put changed line in array for display
    if (madechg) 
        changedLines[changes] = ">" $0

   # create substring so we can try to match other occurrences
    len += RSTART + RLENGTH
    part1 = substr($0, 1, len-1)
    part2 = substr($0, len)
   # calls itself to see if misspelling is found in remaining part 
    make_change(part2, len) 

  } # end of if

} # end of make_change()

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

Two other utlitities that are found on the UNIX system
          ^^^^^^^^^^

Текущая строка ввода копируется в printstring, потому что необходимо изменить строку для отображения. Если строка содержит какие-либо табуляции, каждая табуляция в этой копии строки временно заменяется одним пробелом. Это решает проблему выравнивания каретки при наличии табуляций. (Табуляция считается одним символом при определении длины строки, но на самом деле при отображении занимает больше места, обычно от пяти до восьми символов.)

После отображения строки функция предлагает пользователю ввести поправку. Затем она отображает введенные пользователем данные и запрашивает подтверждение. Если исправление одобрено, вызывается функция sub() для внесения изменения. Если не одобрено, пользователю дается еще один шанс ввести правильное слово.

Помните, что функция sub() изменяет только первое вхождение в строке. Функция gsub() изменяет все вхождения в строке, но мы хотим, чтобы пользователь мог подтверждать каждое изменение. Следовательно, мы должны попытаться сопоставить слово с ошибкой с оставшейся частью строки. И мы должны иметь возможность сопоставить следующее вхождение независимо от того, было ли изменено первое вхождение.

Для этого make_change() разработана как рекурсивная функция; она вызывает себя для поиска дополнительных вхождений в той же строке. Другими словами, при первом вызове make_change() она просматривает все $0 и находит первое слово с ошибкой в этой строке. Затем она разбивает строку на две части: первая часть содержит символы до конца первого вхождения, а вторая часть содержит символы, которые следуют непосредственно до конца строки. Затем она вызывает себя, чтобы попытаться найти слово с ошибкой во второй части. При рекурсивном вызове функция принимает два аргумента.

make_change(part2, len)

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

Функция make_change() также собирает массив строк, которые были изменены.

# put changed line in array for display
    if (madechg)
        changedLines[changes] = ">" $0

Переменная madechg будет иметь значение, если функция sub() была успешной. $0 (две части были воссоединены) присваивается элементу массива. Когда все строки документа прочитаны, основная процедура проходит через этот массив, чтобы отобразить все измененные строки. Затем она вызывает функцию confirm_changes(), чтобы спросить, следует ли сохранить эти изменения. Она копирует временный выходной файл поверх временного входного файла, сохраняя нетронутыми исправления, сделанные для текущего слова с ошибками.

Если пользователь решает сделать «Global change», для этого вызывается функция make_global_change(). Эта функция похожа на функцию make_change(), но проще, потому что мы можем вносить глобальные изменения в каждую строку.

# make_global_change --
#       prompt user to correct misspelling 
#       for all lines globally.  
#       Has no arguments
# Assumes that misspelling is defined. 

function make_global_change(    newspelling, OKmakechange, changes)
{
# prompt user to correct misspelled word
   printf "Globally change to:"
   getline newspelling < "-"

# carriage return falls through
# if there is an answer, confirm 
   while (newspelling && ! OKmakechange) {
        printf ("Globally change %s to %s? (y/n):", misspelling,
                newspelling)
        getline OKmakechange < "-"
    # test response and make change
        if (OKmakechange ~ /[yY](es)?/ ) {
        # open file, read all lines 
            while( (getline < spellsource) > 0){
            # if match is found, make change using gsub
            # and print each changed line.
                if ($0 ~ misspelling) {
                    madechg = gsub(misspelling, newspelling)
                    print ">", $0
                    changes += 1  # counter for line changes
                }
            # write all lines to temp output file
                print > spellout
            } # end of while loop for reading file

        # close temporary files
            close(spellout)
            close(spellsource)
        # report the number of changes  
            printf ("%d lines changed. ", changes) 
        # function to confirm before saving changes
            confirm_changes()
        } # end of if (OKmakechange ~ y) 

    # if correction not confirmed,  prompt for new word
        else if ( OKmakechange ~ /[nN]o?/ ){
            printf "Globally change to:"
            getline newspelling < "-"
            OKmakechange = ""
        }

  } # end of while loop for prompting user for correction

} # end of make_global_change()

Эта функция предлагает пользователю ввести поправку. Цикл while настроен для чтения всех строк документа и применения функции gsub() для внесения изменений. Основное отличие состоит в том, что все изменения вносятся сразу - пользователю не предлагается их подтвердить. Когда все строки были прочитаны, функция отображает строки, которые были изменены, и вызывает confirm_changes(), чтобы пользователь утвердил этот пакет изменений перед их сохранением.

Функция confirm_changes() - это процедура, вызываемая для утверждения изменений, сделанных при вызове функции make_change() или make_global_change().

# confirm_changes --  
#       confirm before saving changes

function confirm_changes(  savechanges) {
# prompt to confirm saving changes
    while (! savechanges ) {
        printf ("Save changes? (y/n)")
        getline savechanges < "-"
    }
# if confirmed, mv output to input
    if (savechanges ~ /[yY](es)?/)
        system("mv " spellout " " spellsource) 
}

Причина создания этой функции - предотвратить дублирование кода. Его цель - просто потребовать от пользователя подтвердить изменения перед заменой старой версии файла документа (spellsource) новой версией (spellout).

12.1.5 Скрипт spellcheck

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

AWKLIB=/usr/local/awklib
nawk -f $AWKLIB/spellcheck.awk $*

Этот сценарий устанавливает переменную оболочки AWKLIB, которая указывает расположение сценария spellcheck.awk. Символ «$*» заменяется на все параметры командной строки, следующие за именем сценария. Затем эти параметры доступны для awk.

Одна из интересных особенностей этой проверки орфографии - то, как мало делается в сценарии оболочки*. Вся работа выполняется на языке программирования awk, включая выполнение 10 команд UNIX. Мы используем согласованный синтаксис и одни и те же конструкции, делая все это в awk. Когда вам приходится выполнять часть работы в оболочке, а часть в awk, это может сбивать с толку. Например, вы должны помнить о различиях в синтаксисе условных выражений if и способах ссылки на переменные. Современные версии awk предоставляют настоящую альтернативу оболочке для выполнения команд и взаимодействия с пользователем. Полный листинг spellcheck.awk можно найти в Главе 12, Полнофункциональные приложения.

* UNIX Text Processing (Dougherty and O'Reilly, Howard W. Sams, 1987) представляет средство проверки орфографии на основе sed, которое во многом полагается на оболочку. Интересно сравнить две версии.

12.2 Создание отформатированного индекса

Процесс создания индекса обычно состоит из трех этапов:

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

Макрос Описание
.XX Производит общие записи индекса.
.XN Создает перекрестные ссылки «see» или «see also».
.XB Создает полужирный текст на странице с указанием основной ссылки.
.XS Начинает диапазон страниц для записи.
.XE Завершает диапазон страниц для записи.

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

"primary [ : secondary [ ; tertiary ]]"

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

Вот запись только с первичным ключом:

.XX "XView"

Следующие две записи указывают вторичный ключ:

.XX "XView: reserved names"
.XX "XView, packages"

Самые сложные записи содержат третичные ключи:

.XX "XView: objects; list"
.XX "XView: objects; hierarchy of"

Наконец, есть два типа перекрестных ссылок:

.XN "error recovery: (see error handling)"
.XX "mh mailer: (see also xmh mailer)"

Запись «see» отсылает человека к другой записи указателя. «see also» обычно используется, когда в данном случае есть записи для «mh mailer», но имеется соответствующая информация, каталогизированная под другим именем. Только записи «see» не имеют связанных номеров страниц.

Когда документ обрабатывается troff, создаются следующие индексные записи:

XView     42
XView: reserved names    43
XView, packages 43
XView: objects; list of 43
XView: objects; hierarchy of    44
XView, packages 45
error recovery: (See error handling)
mh mailer: (see also xmh mailer)    46

Эти записи служат входными данными для программы индексирования. Каждая запись (кроме записей «see») состоит из ключа и номера страницы. Другими словами, запись делится на две части, и первая часть, ключ, также может быть разделена на три части. Когда эти записи обрабатываются программой индексирования и вывод форматируется, записи для «XView» объединяются следующим образом:

XView, 42
    objects; hierarchy of, 44;
        list of, 43
    packages, 43,45
    reserved names, 43

Для этого программа индексации должна:

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

XView   42:I
XView: reserved names   43:I
XView: objects; list of 43:I

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

XView, I:42; II:55,69,75
    objects; hierarchy of, I:44;
        list of, I:43; II: 56
    packages, I:43,45
    reserved names, I:43

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

12.2.1 Программа masterindex

Из-за длины и сложности этого приложения для индексирования* в нашем описании представлена более крупная структура программы. Используйте комментарии в самой программе, чтобы понять, что происходит в программе построчно.

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

input.idx | sort | pagenums.idx | combine.idx | format.idx

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

input.idx
Стандартизирует формат записей и меняет их порядок.
sort
Сортирует записи по ключу, объему и номеру страницы.
pagenums.idx
Объединяет записи с одинаковым ключом, создавая список номеров страниц.
combine.idx
Объединяет последовательные номера страниц в диапазон.
format.idx
Подготавливает отформатированный индекс для экрана или обработки troff.

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

* Истоки этой программы индексирования восходят к копии программы индексирования, написанной на языке awk Стивом Тэлботтом. Я изучил эту программу, разобрав ее, и внес в нее некоторые изменения для поддержки последовательной нумерации страниц в дополнение к нумерации страниц раздела. Это была программа, которую я описал в UNIX Text Processing. Зная эту программу, я написал программу индексирования, которая могла работать с записями указателя, созданными Microsoft Word, и генерировать указатель с использованием нумерации страниц раздела. Позже нам понадобился главный указатель для нескольких книг из нашей серии X Window System. Я воспользовался этим как возможностью переосмыслить нашу программу индексирования и переписать ее с помощью nawk, чтобы она поддерживала индексы как для одной, так и для нескольких книг. The AWK Programming Language содержит пример индексной программы, которая меньше, чем показанная здесь, и может быть отправной точкой, если она окажется для вас слишком сложной. Однако это не касается ключей. Эта программа индексации является упрощенной версией программы, описанной в Bell Labs Computing Science Technical Report 128, Tools for Printing Indexes, октябрь 1986 г., Брайаном Керниганом и Джоном Бентли. [D.D.]

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

12.2.2 Стандартизация ввода

Этот сценарий input.idx ищет различные типы записей и стандартизирует их для упрощения обработки последующими программами. Кроме того, он автоматически поворачивает элементы указателя, содержащие тильду (~) (См. раздел «Вращение двух деталей» далее в этой главе.)

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

Вот код программы input.idx:

#!/work/bin/nawk -f
# ------------------------------------------------
# input.idx -- standardize input before sorting
# Author:  Dale Dougherty
# Version 1.1   7/10/90
# 
# input is "entry" tab "page_number"
# ------------------------------------------------
BEGIN { FS = "\t"; OFS = "" }

#1 Match entries that need rotating that contain a single tilde
   # $1 ~ /~[^~]/  # regexp does not work and I do not know why 
$1 ~ /~/ && $1 !~ /~~/ { 
   # split first field into array named subfield 
    n = split($1, subfield, "~")
    if (n == 2) {
    # print entry without "~" and then rotated
        printf("%s %s::%s\n", subfield[1], subfield[2], $2)
        printf("%s:%s:%s\n", subfield[2], subfield[1], $2)
    }
    next
}# End of 1

#2 Match entries that contain two tildes 
$1 ~ /~~/ { 
   # replace ~~ with ~  
    gsub(/~~/, "~", $1)
} # End of 2

#3  Match entries that use "::" for literal ":". 
$1 ~ /::/ { 
   # substitute octal value for "::"
    gsub(/::/, "\\72", $1) 
}# End of 3

#4 Clean up entries 
{
   # look for second colon, which might be used instead of ";"
    if (sub(/:.*:/, "&;", $1)) {
        sub(/:;/, ";", $1)  
    }
   # remove blank space if any after colon.
    sub(/: */, ":", $1)
   # if comma is used as delimiter, convert to colon. 
    if ( $1 !~ /:/ ) {
    # On see also & see, try to put delimiter before "(" 
        if ($1 ~ /\([sS]ee/) {
            if (sub(/, *.*\(/, ":&", $1)) 
                sub(/:, */, ":", $1)
            else
                sub(/  *\(/, ":(", $1)
        }
        else { # otherwise, just look for comma
            sub(/, */, ":", $1)
        }
    }
    else {
        # added to insert semicolon in "See"
        if ($1 ~ /:[^;]+ *\([sS]ee/) 
            sub(/  *\(/, ";(", $1)
    }   
}# End of 4

#5 match See Alsos and fix for sort at end
$1 ~ / *\([Ss]ee +[Aa]lso/ { 
  # add "~zz" for sort at end
    sub(/\([Ss]ee +[Aa]lso/, "~zz(see also", $1) 
    if ($1 ~ /:[^; ]+ *~zz/) {
        sub(/ *~zz/, "; ~zz", $1)
    }
  # if no page number
    if ($2 == "") {
        print $0 ":" 
        next
    }
    else {
    # output two entries: 
    # print See Also entry w/out page number
        print $1 ":"
    # remove See Also 
        sub(/ *~zz\(see also.*$/, "", $1) 
        sub(/;/, "", $1)
    # print as normal entry
        if ( $1 ~ /:/ )
            print $1 ":" $2
        else
            print $1 "::" $2
        next
    }
}# End of 5

#6 Process entries without page number (See entries)
(NF == 1 || $2 == "" || $1 ~ /\([sS]ee/) { 
   # if a "See" entry
    if ( $1 ~ /\([sS]ee/ ) { 
        if ( $1 ~ /:/ ) 
            print $1 ":"
        else {  
            print $1 ":" 
        }
        next
    }
    else {  # if not a See entry, generate error
        printerr("No page number")
        next
    }
}# End of 6

#7 If the colon is used as the delimiter 
$1 ~ /:/ { 
   # output entry:page
    print $1 ":" $2
    next
}# End of 7

#8  Match entries with only primary keys.
{
    print $1 "::" $2
}# End of 8

# supporting functions
# 
# printerr -- print error message and current record
#       Arg: message to be displayed

function printerr (message) {
    # print message, record number and record
    printf("ERROR:%s (%d) %s\n", message, NR, $0) > "/dev/tty"
}

Этот сценарий состоит из ряда правил сопоставления с образцом для распознавания различных типов ввода. Обратите внимание, что запись может соответствовать более чем одному правилу, если действие, связанное с правилом, не вызывает оператор next.

Описывая этот сценарий, мы будем ссылаться на правила по номерам. Правило 1 вращает записи, содержащие тильду, и производит две выходные записи. Функция split() создает массив с именем subfield, который содержит две части составной записи. Две части печатаются в исходном порядке, а затем меняются местами для создания второй выходной записи, в которой вторичный ключ становится первичным.

Поскольку мы используем тильду как специальный символ, мы должны предоставить некоторый способ ввода тильды. Мы реализовали соглашение, согласно которому две последовательные тильды переводятся в одну тильду. Правило 2 имеет дело с этим случаем, но обратите внимание, что шаблон для правила 1 гарантирует, что за первой тильдой, которой он соответствует, не следует другая тильда*.

Порядок правил 1 и 2 в сценарии имеет значение. Мы не можем заменить «~~» на «~» до тех пор, пока не завершится процедура поворота записи.

Правило 3 работает аналогично правилу 2; оно позволяет использовать «::» для вывода литерала «:» в индекс. Однако, поскольку мы используем двоеточие в качестве разделителя ввода на протяжении всего ввода в программу, мы не можем допустить, чтобы оно появлялось в записи как окончательный вывод до самого конца. Таким образом, мы заменяем последовательность «::» значением ASCII двоеточия в восьмеричном формате. (Программа format.idx отменит замену.)

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

В «базовом» синтаксисе первичный и вторичный ключи разделяются двоеточием. Вторичный и третичный ключи разделяются точкой с запятой. Тем не менее, программа также распознает второе двоеточие вместо точки с запятой как разделитель между вторичным и третичным ключами. Она также распознает, что если двоеточие не указано в качестве разделителя, то в качестве разделителя между первичным и вторичным ключами можно использовать запятую. (Частично это было сделано для совместимости с более ранней программой, в которой в качестве разделителя использовалась запятая.) Функция sub() ищет первую запятую в строке и заменяет ее двоеточием. Это правило также пытается стандартизировать синтаксис записей «see» и «see also». Для записей, разделенных двоеточием, правило 4 удаляет пробелы после двоеточия. Вся работа выполняется с помощью функции sub().

Правило 5 касается записей «see also». Мы добавляем произвольную строку «~zz» к записям «see also», чтобы они сортировались в конце списка вторичных ключей. Сценарий pagenums.idx, позже в конвейере, удалит «~zz» после того, как записи будут отсортированы.

Правило 6 соответствует записям, в которых не указан номер страницы. Единственная действительная запись без номера страницы содержит ссылку «see». Это правило выводит записи «see» с «:» в конце, чтобы указать пустое третье поле. Все остальные записи генерируют сообщение об ошибке через функцию printerr(). Эта функция уведомляет пользователя о том, что конкретная запись не имеет номера страницы и не будет включена в вывод. Это один из методов стандартизации ввода - выбросить то, что вы не можете правильно интерпретировать. Однако крайне важно уведомить пользователя, чтобы он или она могли исправить запись.

Правило 7 выводит записи, содержащие разделитель двоеточия. Его действие использует next, чтобы избежать достижения правила 8.

Наконец, правило 8 сопоставляет записи, содержащие только первичный ключ. Другими словами, разделителя нет. Мы выводим «::», чтобы указать пустое второе поле. Это часть содержимого нашего файла test. Мы будем использовать его для создания примеров в этом разделе.

$ cat test
XView: programs; initialization 45
XV_INIT_ARGS~macro      46
Xv_object~type 49
Xv_singlecolor~type     80
graphics: (see also server image)
graphics, XView model   83
X Window System: events 84
graphics, CANVAS_X_PAINT_WINDOW 86
X Window System, X Window ID for paint window  87
toolkit (See X Window System).
graphics: (see also server image)
Xlib, repainting canvas 88
Xlib.h~header file      89

Когда мы запускаем этот файл через input.idx, он производит:

$ input.idx test
XView:programs; initialization:45
XV_INIT_ARGS macro::46
macro:XV_INIT_ARGS:46
Xv_object type::49
type:Xv_object:49
Xv_singlecolor type::80
type:Xv_singlecolor:80
graphics:~zz(see also server image):
graphics:XView model:83
X Window System:events:84
graphics:CANVAS_X_PAINT_WINDOW:86
X Window System:X Window ID for paint window:87
graphics:~zz(see also server image):
Xlib:repainting canvas:88
Xlib.h header file::89
header file:Xlib.h:89

Каждая запись теперь состоит из трех полей, разделенных двоеточиями. В образце выходных данных вы можете найти примеры записей только с первичным ключом, записей с первичным и вторичным ключами, а также записей с первичным, вторичным и третичным ключами. Вы также можете найти примеры чередующихся записей, повторяющихся записей и записи «see also».

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

* В первом издании Дейл писал: «В качестве дополнительной благодарности, пришлите мне письмо, если вы сможете выяснить, почему закомментированное регулярное выражение непосредственно перед правилом 1 не работает. Я использовал сложное выражение в крайнем случае». Стыдно признаться, что меня это тоже поставило в тупик. Когда Генри Спенсер включил свет, он был ослепляющим: «Причина, по которой закомментированное регулярное выражение не работает, заключается в том, что оно не делает то, что думал автор. ☺ Оно ищет тильду, за которой следует символ без тильды... но за второй тильдой комбинации ~~ обычно следует не-тильда! Использование /[^~]~[^~]/, вероятно, сработает». Я подключил это регулярное выражение к программе, и оно отлично работало. [A.R.]

12.2.3 Сортировка записей

Теперь вывод, созданный input.idx, готов к сортировке. Самый простой способ отсортировать записи - использовать стандартную программу UNIX sort, а не писать собственный сценарий. Помимо сортировки записей, мы хотим удалить все дубликаты и для этой задачи мы используем программу uniq.

Это командная строка, которую мы используем:

sort -bdf -t: +0 -1 +1 -2 +3 -4 +2n -3n | uniq

Как видите, мы используем несколько параметров с командой sort. Первая опция -b указывает, что начальные пробелы игнорируются. Параметр -d указывает сортировку по словарю, в которой символы и специальные символы игнорируются. -f указывает, что строчные и прописные буквы должны складываться вместе; другими словами, они должны рассматриваться как один и тот же символ для целей сортировки. Следующий аргумент, пожалуй, самый важный: -t: указывает программе использовать двоеточие в качестве разделителя полей для ключей сортировки. Параметры «+», которые следуют далее, определяют количество полей, которые нужно пропустить с начала строки. Поэтому, чтобы указать первое поле в качестве первичного ключа сортировки, мы используем «+0». Точно так же параметры «-» указывают конец ключа сортировки. «-1» указывает, что первичный ключ сортировки заканчивается в первом поле или в начале второго поля. Второе поле сортировки - вторичный ключ. Четвертое поле («+3»), если оно существует, содержит номер тома. Последний ключ для сортировки - это номер страницы; для этого требуется числовая сортировка (если бы мы не указали sort, что этот ключ состоит из чисел, то за номером 1 будет следовать 10 вместо 2). Обратите внимание, что мы сортируем номера страниц после сортировки номеров томов. Таким образом, все номера страниц для тома I сортируются по порядку перед номерами страниц для тома II. Наконец, мы направляем вывод в uniq, чтобы удалить идентичные записи. Обрабатывая вывод из input.idx, команда sort производит:

graphics:CANVAS_X_PAINT_WINDOW:86
graphics:XView model:83
graphics:~zz(see also server image):
header file:Xlib.h:89
macro:XV_INIT_ARGS:46
toolkit:(See X Window System).:
type:Xv_object:49
type:Xv_singlecolor:80
X Window System:events:84
X Window System:X Window ID for paint window:87
Xlib:repainting canvas:88
Xlib.h header file::89
XView:programs; initialization:45
XV_INIT_ARGS macro::46
Xv_object type::49
Xv_singlecolor type::80

12.2.4 Обработка номеров страниц

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

PRIMARY:SECONDARY:PAGE:VOLUME

Четвертый - необязательный. Пока мы рассматриваем только индекс для отдельной книги, в которой нет номеров томов. Помните, что записи теперь отсортированы.

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

PRIMARY = $1
SECONDARY = $2
PAGE = $3
if (PRIMARY == prevPRIMARY)
    if (SECONDARY == prevSECONDARY)
        print PAGE
    else
        print PRIMARY:SECONDARY:PAGE
else
    print PRIMARY:SECONDARY:PAGE
prevPRIMARY = PRIMARY
prevSECONDARY = SECONDARY

Давайте посмотрим, как этот код обрабатывает серию записей, начиная с:

XView::18

Первичный ключ не соответствует предыдущему первичному ключу; строка выводится как есть:

XView::18

Следующая запись:

XView:about:3

Когда мы сравниваем первичный ключ этой записи с предыдущей, они совпадают. Когда мы сравниваем вторичные ключи, они различаются; выводим запись как есть:

XView:about:3

Следующая запись:

XView:about:7

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

XView:about:3,7

Следующая запись также соответствует обоим ключам:

XView:about:10

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

XView:about:3,7,10

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

Полный сценарий добавляет дополнительный тест, чтобы проверить, совпадает ли идентификатор тома. Вот полный скрипт pagenums.idx:

#!/work/bin/nawk -f
# ------------------------------------------------
# pagenums.idx -- collect pages for common entries 
# Author:  Dale Dougherty
# Version 1.1   7/10/90
# 
# input should be PRIMARY:SECONDARY:PAGE:VOLUME
# ------------------------------------------------

BEGIN { FS = ":"; OFS = ""}

# main routine -- apply to all input lines
{
   # assign fields to variables
    PRIMARY = $1
    SECONDARY = $2
    PAGE = $3
    VOLUME = $4

   # check for a see also and collect it in array
    if (SECONDARY ~ /\([Ss]ee +[Aa]lso/) {
    # create tmp copy & remove "~zz" from copy 
        tmpSecondary = SECONDARY
        sub(/~zz\([Ss]ee +[Aa]lso */, "", tmpSecondary)
        sub(/\) */, "", tmpSecondary)
    # remove secondary key along with "~zz"
        sub(/^.*~zz\([Ss]ee +[Aa]lso */, "", SECONDARY)
        sub(/\) */, "", SECONDARY)
    # assign to next element of seeAlsoList
        seeAlsoList[++eachSeeAlso] = SECONDARY "; "
        prevPrimary = PRIMARY
    # assign copy to previous secondary key
        prevSecondary = tmpSecondary 
        next
    } # end test for see Also

   # Conditionals to compare keys of current record to previous
   #  record.  If Primary and Secondary keys are the same, only
   #  the page number is printed. 

   # test to see if each PRIMARY key matches previous key
    if (PRIMARY == prevPrimary) {
    # test to see if each SECONDARY key matches previous key
        if (SECONDARY == prevSecondary)
        # test to see if VOLUME matches;
        # print only VOLUME:PAGE
            if (VOLUME == prevVolume)
                printf (",%s", PAGE)
            else {
                printf ("; ")
                volpage(VOLUME, PAGE)
            }
        else{
        # if array of See Alsos, output them now
            if (eachSeeAlso) outputSeeAlso(2)
        # print PRIMARY:SECONDARY:VOLUME:PAGE
            printf ("\n%s:%s:", PRIMARY, SECONDARY)
            volpage(VOLUME, PAGE)
        }
    } # end of test for PRIMARY == prev
    else { # PRIMARY != prev
        # if we have an array of See Alsos, output them now
        if (eachSeeAlso) outputSeeAlso(1)
        if (NR != 1) 
            printf ("\n")
        if (NF == 1){
            printf ("%s:", $0)
        }
        else {
            printf ("%s:%s:", PRIMARY, SECONDARY)
            volpage(VOLUME, PAGE)
        }
    }
    prevPrimary = PRIMARY
    prevSecondary = SECONDARY
    prevVolume = VOLUME

} # end of main routine

# at end, print newline
END { 
   # in case last entry has "see Also"
    if (eachSeeAlso) outputSeeAlso(1)
    printf("\n")
}

# outputSeeAlso function -- list elements of seeAlsoList 
function outputSeeAlso(LEVEL) {
    # LEVEL - indicates which key we need to output
    if (LEVEL == 1)
        printf ("\n%s:(See also ", prevPrimary)
    else {
        sub(/;.*$/, "", prevSecondary)
        printf ("\n%s:%s; (See also ", prevPrimary, prevSecondary)
    }
    sub(/; $/, ".):", seeAlsoList[eachSeeAlso])
    for (i = 1; i <= eachSeeAlso; ++i)
        printf ("%s", seeAlsoList[i]) 
    eachSeeAlso = 0
}

# volpage function -- determine whether or not to print volume info 
#   two args: volume & page
 
function volpage(v, p)
{
   # if VOLUME is empty then print PAGE only 
    if ( v == "" ) 
        printf ("%s", p)
    else
   # otherwise print VOLUME^PAGE
        printf ("%s^%s",v, p)  
}

Помните, прежде всего, что вход в программу сортируется по ее ключам. Номера страниц также расположены по порядку, так что запись «graphics» на странице 7 появляется в вводе перед записью на странице 10. Аналогично, записи для тома I появляются в вводе перед томом II. Следовательно, этой программе не требуется сортировка; она просто сравнивает ключи и, если они совпадают, добавляет номер страницы в список. Таким образом количество записей сокращается.

Этот сценарий также обрабатывает записи «see also». Поскольку записи теперь отсортированы, мы можем удалить специальную последовательность сортировки «~zz». Мы также обрабатываем случай, когда мы можем встретить последовательные записи «see also». Мы не хотим выводить:

Toolkit (see also Xt) (See also XView) (See also Motif).

Вместо этого мы хотели бы объединить их в список, чтобы они выглядели как:

Toolkit (see also Xt; XView; Motif)

Для этого мы создаем массив с именем seeAlsoList. Из SECONDARY мы удаляем круглые скобки, вторичный ключ, если он существует, и «see also», а затем назначаем его элементу seeAlsoList. Мы делаем копию SECONDARY со вторичным ключом и назначаем ее prevSecondary для сравнения со следующей записью.

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

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

X Window System:Xlib:6
XFontStruct structure::317
Xlib::6
Xlib:repainting canvas:88
Xlib.h header file::89,294
Xv_Font type::310
XView::18
XView:about:3,7,10
XView:as object-oriented system:17

Вот пример того, что выводится для основного индекса:

reserved names:table of:I^43
Xt:example of programming interface:I^44,65
Xt:objects; list of:I^43,58; II^40
Xt:packages:I^43,61; II^42
Xt:programs; initialization:I^45
Xt:reserved names:I^43,58
Xt:reserved prefixes:I^43,58
Xt:types:I^43,54,61

Символ «^» используется как временный разделитель между номером тома и списком номеров страниц.

12.2.5 Объединение записей с одинаковыми ключами

Программа pagenums.idx сократила количество записей, которые были такими же, за исключением номера страницы. Теперь мы обработаем записи с одним и тем же первичным ключом. Мы также хотим искать последовательные номера страниц и объединять их в диапазоны.

Скрипт combine.idx очень похож на скрипт pagenums.idx, выполняя еще один проход по индексу, сравнивая записи с тем же первичным ключом. Следующий псевдокод резюмирует это сравнение. (Чтобы упростить это обсуждение, мы опустим третичные ключи и покажем, как сравнивать первичный и вторичный ключи.) После обработки записей с помощью pagenums.idx не существует двух записей, которые имеют одинаковые первичный и вторичный ключи. Следовательно, нам не нужно сравнивать вторичные ключи.

PRIMARY = $1
SECONDARY = $2
PAGE = $3
if (PRIMARY == prevPRIMARY)
    print :SECONDARY:
else
    print PRIMARY:SECONDARY
prevPRIMARY = PRIMARY
prevSECONDARY = SECONDARY

Если первичные ключи совпадают, мы выводим только вторичный ключ. Например, если есть три записи:

XView:18
XView:about:3, 7, 10
XView:as object-oriented system:17

они будут выведены как:

XView:18
:about:3, 7, 10
:as object-oriented system:17

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

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

Чтобы увидеть, являются ли числа последовательными, мы просматриваем массив, сравнивая каждый элемент с 1 + предыдущим элементом.

eachpage[j-1]+1 == eachpage[j]

Другими словами, если добавление 1 к предыдущему элементу дает текущий элемент, то они являются последовательными. Предыдущий элемент становится номером первой страницы в диапазоне, а текущий элемент становится последней страницей в диапазоне. Это выполняется в рамках цикла while, пока условие не станет истинным и номера страниц не будут последовательными. Затем мы выводим номер первой и последней страницы, разделенные дефисом:

23-25

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

Вот полный листинг combine.idx:

#!/work/bin/nawk -f
# ------------------------------------------------
# combine.idx -- merge keys with same PRIMARY key
#       and combine consecutive page numbers
# Author:  Dale Dougherty
# Version 1.1   7/10/90
# 
# input should be PRIMARY:SECONDARY:PAGELIST
# ------------------------------------------------

BEGIN   { FS = ":"; OFS = ""}

# main routine -- applies to all input lines
#  It compares the keys and merges the duplicates.
{
   # assign first field
    PRIMARY=$1
   # split second field, getting SEC and TERT keys.
    sizeOfArray = split($2, array, ";") 
    SECONDARY = array[1]
    TERTIARY = array[2]
   # test that tertiary key exists
    if (sizeOfArray > 1) {
    # tertiary key exists
        isTertiary = 1 
    # two cases where ";" might turn up
    # check SEC key for list of "see also" 
        if (SECONDARY ~ /\([sS]ee also/){
            SECONDARY = $2
            isTertiary = 0
        }
    # check TERT key for "see also" 
        if (TERTIARY ~ /\([sS]ee also/){
            TERTIARY = substr($2, (index($2, ";") + 1))
        }   
    }
    else # tertiary key does not exist
        isTertiary = 0
   # assign third field
    PAGELIST = $3

   # Conditional to compare primary key of this entry to that
   #  of previous entry. Then compare secondary keys.  This 
   #  determines which non-duplicate keys to output.

    if (PRIMARY == prevPrimary) {
        if (isTertiary && SECONDARY == prevSecondary)
            printf (";\n::%s", TERTIARY)
        else
            if (isTertiary)
                printf ("\n:%s; %s", SECONDARY, TERTIARY)
            else
                printf ("\n:%s", SECONDARY)
     }
     else {
        if (NR != 1) 
            printf ("\n")
        if ($2 != "") 
            printf ("%s:%s", PRIMARY, $2)
        else 
            printf ("%s", PRIMARY)

        prevPrimary = PRIMARY
    }

    prevSecondary = SECONDARY
} # end of main procedure

# routine for "See" entries (primary key only)
NF == 1 { printf ("\n") }

# routine for all other entries
#  It handles output of the page number.

NF > 1  {
    if (PAGELIST)
    # calls function numrange() to look for 
    # consecutive page numbers.
        printf (":%s", numrange(PAGELIST))  
    else
        if (! isTertiary || (TERTIARY && SECONDARY)) printf (":")  

} # end of NF > 1

# END procedure outputs newline
END {  printf ("\n") }

# Supporting Functions

# numrange -- read list of Volume^Page numbers, detach Volume
#       from Page for each Volume and call rangeOfPages 
#       to combine consecutive page numbers in the list. 
#   PAGE = volumes separated by semicolons; volume and page
#       separated by ^.

function numrange(PAGE,     listOfPages, sizeOfArray)
{
  # Split up list by volume.
    sizeOfArray = split(PAGE, howManyVolumes,";") 
  # Check to see if more than 1 volume.
    if (sizeOfArray > 1) {

    # if more than 1 volume, loop through list 
        for (i = 1; i <= sizeOfArray; ++i) {
        # for each Volume^Page element, detach Volume 
        # and call rangeOfPages function on Page to
        # separate page numbers and compare to find
        # consecutive numbers.
            if (split(howManyVolumes[i],volPage,"^") == 2)  
                listOfPages = volPage[1] "^" rangeOfPages(volPage[2])
        # collect output in listOfPages
            if (i == 1) 
                result = listOfPages
            else
                result=result ";" listOfPages
        } # end for loop
    }
    else { # not more than 1 volume

    # check for single volume index with volume number 
    # if so, detach volume number.
    # Both call rangeOfPages on the list of page numbers.
        if (split(PAGE,volPage,"^") == 2 )  
        # if Volume^Page, detach volume and then call rangeOfPages 
            listOfPages = volPage[1] "^" rangeOfPages(volPage[2])
        else # No volume number involved 
            listOfPages = rangeOfPages(volPage[1])
        result = listOfPages
    } # end of else

    return result  # Volume^Page list

} # End of numrange function

# rangeOfPages -- read list of comma-separated page numbers,  
#       load them into an array, and compare each one
#       to the next, looking for consecutive numbers.
#   PAGENUMBERS = comma-separated list of page numbers

function rangeOfPages(PAGENUMBERS, pagesAll, sizeOfArray,pages,
                       listOfPages, d, p, j) {
   # close-up space on troff-generated ranges
    gsub(/ - /, ",-", PAGENUMBERS)

   # split list up into eachpage array.
    sizeOfArray = split(PAGENUMBERS, eachpage, ",")
   # if more than 1 page number
    if (sizeOfArray > 1){
    # for each page number, compare it to previous number + 1
        p = 0  # flag indicates assignment to pagesAll 
    # for loop starts at 2
        for (j = 2; j-1 <= sizeOfArray; ++j) {
        # start by saving first page in sequence (firstpage)    
        # and loop until we find last page (lastpage)
            firstpage = eachpage[j-1]
            d = 0  # flag indicates consecutive numbers found  
        # loop while page numbers are consecutive
            while ((eachpage[j-1]+1) == eachpage[j] ||
                    eachpage[j] ~ /^-/) {
            # remove "-" from troff-generated range
                if (eachpage[j] ~ /^-/) {
                    sub(/^-/, "", eachpage[j])
                }
                lastpage = eachpage[j]
            # increment counters
                ++d
                ++j
            } # end of while loop
        # use values of firstpage and lastpage to make range.
            if (d >= 1) {
            # there is a range
                pages = firstpage "-" lastpage 
            }
            else # no range; only read firstpage 
                pages = firstpage 
        # assign range to pagesAll 
            if (p == 0) {
                pagesAll = pages 
                p = 1
            }
            else {
                pagesAll = pagesAll "," pages
            }
        }# end of for loop

    # assign pagesAll to listOfPages
        listOfPages = pagesAll

    } # end of sizeOfArray > 1

    else # only one page 
        listOfPages = PAGENUMBERS

   # add space following comma
    gsub(/,/, ", ", listOfPages)
   # return changed list of page numbers
    return listOfPages
} # End of rangeOfPages function

Этот сценарий состоит из минимальных процедур BEGIN и END. Основная процедура выполняет работу по сравнению первичного и вторичного ключей. Первая часть этой процедуры присваивает поля переменным. Второе поле содержит вторичный и третичный ключи, и мы используем split() для их разделения. Затем мы проверяем наличие третичного ключа и устанавливаем для флага isTertiary значение 1 или 0.

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

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

Различные формы показаны ниже:

primary
primary:secondary
:secondary
:secondary:tertiary
::tertiary
primary:secondary:tertiary

За основной процедурой следуют две дополнительные процедуры. Первая из них выполняется только тогда, когда NF равняется единице. Она касается первой из форм в списке выше. То есть номер страницы отсутствует, поэтому мы должны вывести новую строку, чтобы завершить запись.

Вторая процедура касается всех записей, имеющих номера страниц. Это процедура, в которой мы вызываем функцию для разбора списка номеров страниц и поиска следующих друг за другом страниц. Она вызывает функцию numrange(), основная цель которой - иметь дело с многотомным индексом, где список номеров страниц может выглядеть так:

I^35,55; II^200

Эта функция вызывает split() с использованием разделителя точкой с запятой для разделения каждого тома. Затем мы вызываем split(), используя разделитель «^», чтобы отделить номер тома от списка номеров страниц. Когда у нас есть список страниц, мы вызываем вторую функцию rangeOfPages() для поиска последовательных номеров. В указателе отдельной книги, таком как пример, показанный в этой главе, функция numrange() действительно ничего не делает, кроме вызова rangeOfPages(). Мы обсуждали суть функции rangeOfPages() ранее. Создается массив eachpage, и цикл while используется для просмотра массива, сравнивая элемент с предыдущим. Эта функция возвращает список страниц.

Пример вывода этой программы:

Xlib:6
:repainting canvas:88
Xlib.h header file:89, 294
Xv_Font type:310
XView:18
:about:3, 7, 10
:as object-oriented system:17
:compiling programs:41
:concept of windows differs from X:25
:data types; table of:20
:example of programming interface:44
:frames and subframes:26
:generic functions:21
:Generic Object:18, 24
:libraries:42
:notification:10, 35
:objects:23-24;
:: table of:20;
:: list of:43
:packages:18, 43
:programmer's model:17-23
:programming interface:41
:programs; initialization:45
:reserved names:43
:reserved prefixes:43
:structure of applications:41
:subwindows:28
:types:43
:window objects:25

В частности, обратите внимание на запись «objects» под «XView». Это пример вторичного ключа с несколькими третичными ключами. Это также пример записи с последовательным диапазоном страниц.

12.2.6 Форматирование индекса

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

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

Вот полный листинг для format.idx:

#!/work/bin/nawk -f
# ------------------------------------------------
# format.idx -- prepare formatted index
# Author:  Dale Dougherty
# Version 1.1   7/10/90
# 
# input should be PRIMARY:SECONDARY:PAGE:VOLUME
# Args:  FMT = 0 (default) format for screen
#        FMT = 1 output with troff macros
#        MACDIR = pathname of index troff macro file 
# ------------------------------------------------
BEGIN {     FS = ":"
        upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        lower = "abcdefghijklmnopqrstuvwxyz" 
}

# Output initial macros if troff FMT 
NR == 1 && FMT == 1 {
        if (MACDIR)
            printf (".so %s/indexmacs\n", MACDIR) 
        else
            printf (".so indexmacs\n") 
        printf (".Se \"\" \"Index\"\n") 
        printf (".XC\n") 
} # end of NR == 1
 

# main routine - apply to all lines
# determine which fields to output
{
   # convert octal colon to "literal" colon 
   # make sub for each field, not $0, so that fields are not parsed
    gsub(/\\72/, ":", $1)
    gsub(/\\72/, ":", $2)
    gsub(/\\72/, ":", $3)

   # assign field to variables
    PRIMARY = $1
    SECONDARY = $2
    TERTIARY = ""
    PAGE = $3
    if (NF == 2) {
        SECONDARY = ""
        PAGE = $2
    }
   # Look for empty fields to determine what to output
    if (! PRIMARY) {  
        if (! SECONDARY) {
            TERTIARY = $3
            PAGE = $4   
            if (FMT == 1)
                printf (".XF 3 \"%s", TERTIARY)
            else
                printf ("  %s", TERTIARY)
        }
        else
            if (FMT == 1)
                printf (".XF 2 \"%s", SECONDARY)
            else
                printf ("  %s", SECONDARY)
    }
    else { # if primary entry exists
    
         # extract first char of primary entry
        firstChar = substr($1, 1, 1)
         # see if it is in lower string.
        char = index(lower, firstChar) 
         # char is an index to lower or upper letter 
        if (char == 0)  {
        # if char not found, see if it is upper
            char = index(upper, firstChar)
            if (char == 0)
                char = prevChar
        }
        # if new char, then start group for new letter of alphabet
        if (char != prevChar) {
            if (FMT == 1)
                printf(".XF A \"%s\"\n", substr(upper, char, 1))
            else
                printf("\n\t\t%s\n", substr(upper, char, 1))
            prevChar = char
        }
        # now output primary and secondary entry
        if (FMT == 1)
            if (SECONDARY)
                printf (".XF 1 \"%s\" \"%s", PRIMARY, SECONDARY)
            else
                printf (".XF 1 \"%s\" \"", PRIMARY)
        else
            if (SECONDARY)
                printf ("%s, %s", PRIMARY, SECONDARY)
            else
                printf ("%s", PRIMARY)
    }
    
   # if page number, call pageChg to replace "^" with ":"
   # for multi-volume page lists.
    if (PAGE) {
        if (FMT == 1) {
            # added to omit comma after bold entry
            if (! SECONDARY && ! TERTIARY)
                printf ("%s\"", pageChg(PAGE))
            else
                printf (", %s\"", pageChg(PAGE))
        }
        else
            printf (", %s", pageChg(PAGE))
    }
    else if (FMT == 1)
        printf("\"")
    
    printf ("\n")

} # End of main routine

# Supporting function

# pageChg -- convert "^" to ":" in list of volume^page 
#   Arg: pagelist -- list of numbers 

function pageChg(pagelist) {
     gsub(/\^/, ":", pagelist)
     if (FMT == 1) {
        gsub(/[1-9]+\*/, "\\fB&\\fP", pagelist)
        gsub(/\*/, "", pagelist)
    }
    return pagelist
}# End of pageChg function 

Процедура BEGIN определяет разделитель полей и строки upper и lower. Следующая процедура выводит имя файла, который содержит определения макросов индекса troff. Имя каталога макроса можно задать из командной строки в качестве второго аргумента.

Основная процедура начинается с преобразования «скрытого» двоеточия в буквальное двоеточие. Обратите внимание, что мы применяем функцию gsub() к каждому полю, а не ко всей строке, потому что выполнение последнего приведет к переоценке строки и нарушению текущего порядка полей.

Затем мы присваиваем поля переменным, а затем проверяем, пусто ли поле. Если первичный ключ не определен, мы смотрим, определен ли вторичный ключ. Если определен, выводим. Если нет, то выводим третичный ключ. Если первичный ключ определен, мы извлекаем его первый символ и затем смотрим, найдем ли мы его в строке lower.

firstChar = substr($1, 1, 1)

char = index(lower, firstChar)

Переменная char хранит позицию буквы в строке. Если это число больше или равно 1, то у нас также есть индекс в строке upper. Мы сравниваем каждую запись, и хотя char и prevChar одинаковы, текущая буква алфавита не изменяется. Если они различаются, сначала мы проверяем букву в строке upper. Если char - новая буква, мы выводим центрированную строку, которая идентифицирует эту букву алфавита.

Затем мы смотрим на вывод первичных и вторичных записей. Наконец, список номеров страниц выводится после вызова функции pageChg() для замены «^» в ссылках на страницы тома двоеточием.

Пример вывода экрана, созданного с помощью format.idx, показан ниже:

            X
X Protocol, 6
X Window System, events, 84
  extensibility, 9
  interclient communications, 9
  overview, 3
  protocol, 6
  role of window manager, 9
  server and client relationship, 5
  software hierarchy, 6
  toolkits, 7
  X Window ID for paint window, 87
  Xlib, 6
XFontStruct structure, 317
Xlib, 6
  repainting canvas, 88Xlib.h header file, 89, 294
Xv_Font type, 310
XView, 18
  about, 3, 7, 10
  as object-oriented system, 17
  compiling programs, 41
  concept of windows differs from X, 25
  data types; table of, 20
  example of programming interface, 44
  frames and subframes, 26
  generic functions, 21
  Generic Object, 18, 24

Пример вывода troff, созданного format.idx, показан ниже:

.XF A "X"
.XF 1 "X Protocol" "6"
.XF 1 "X Window System" "events, 84"
.XF 2 "extensibility, 9"
.XF 2 "interclient communications, 9"
.XF 2 "overview, 3"
.XF 2 "protocol, 6"
.XF 2 "role of window manager, 9"
.XF 2 "server and client relationship, 5"
.XF 2 "software hierarchy, 6"
.XF 2 "toolkits, 7"
.XF 2 "X Window ID for paint window, 87"
.XF 2 "Xlib, 6"
.XF 1 "XFontStruct structure" "317"
.XF 1 "Xlib" "6"
.XF 2 "repainting canvas, 88"
.XF 1 "Xlib.h header file" "89, 294"
.XF 1 "Xv_Font type" "310"
.XF 1 "XView" "18"
.XF 2 "about, 3, 7, 10"
.XF 2 "as object-oriented system, 17"

Этот вывод должен быть отформатирован troff для создания печатной версии индекса. Индекс этой книги изначально был составлен с помощью программы masterindex.

12.2.6.1 Сценарий оболочки masterindex

Сценарий оболочки masterindex - это связующее звено, которое объединяет все эти сценарии и вызывает их с соответствующими параметрами на основе командной строки пользователя. Например, пользователь вводит:

$ masterindex -s -m volume1 volume2

чтобы указать, что главный индекс должен быть создан из файлов volume1 и volume2 и что вывод будет отправлен на экран.

Сценарий оболочки masterindex представлен в Приложении C вместе с документацией.

12.3 Дополнительные детали программы masterindex

В этом разделе представлены некоторые интересные детали программы masterindex, которые в противном случае могли бы ускользнуть от внимания. Цель этого раздела - выделить некоторые интересные фрагменты программы и показать, как они решают ту или иную проблему.

12.3.1 Как скрыть специальный символ

Наш первый фрагмент взят из сценария input.idx, задача которого - стандартизировать записи индекса перед их сортировкой. Эта программа принимает в качестве входных данных запись, состоящую из двух полей, разделенных табуляцией: запись указателя и номер ее страницы. Двоеточие используется как часть синтаксиса для обозначения частей записи индекса.

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

#< from input.idx
# convert literal colon to octal value
$1 ~ /::/ {
    # substitute octal value for "::"
    gsub(/::/, "\\72", $1)

«\\72» представляет восьмеричное значение двоеточия. (Вы можете найти это значение, просмотрев таблицу шестнадцатеричных и восьмеричных эквивалентов в файле /usr/pub/ascii). В последнем программном модуле мы используем gsub() для преобразования восьмеричного значения обратно в двоеточие. Вот код из format.idx.

#< from format.idx
# convert octal colon to "literal" colon
# make sub for each field, not $0, so that fields are not parsed
    gsub(/\\72/, ":", $1)
    gsub(/\\72/, ":", $2)
    gsub(/\\72/, ":", $3)

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

12.3.2 Вращение двух частей

Выше мы говорили о синтаксисе двоеточия для разделения первичного и вторичного ключей. Для некоторых типов записей имеет смысл также классифицировать элемент по вторичному ключу. Например, у нас может быть группа программных операторов или пользовательских команд, таких как «sed command». Индексатор может создать две записи: одну для «sed command» и одну для «command: sed». Чтобы упростить кодирование такой записи, мы реализовали соглашение о кодировании, в котором используется символ тильды (~) для обозначения двух частей этой записи, так что первая и вторая части могут быть заменены местами для автоматического создания второй записи*. Таким образом, кодирование следующей индексной записи

.XX "sed~command"

производит две записи:

sed command     43
command: sed    43

Вот код, который меняет записи.

#< from input.idx
# Match entries that need rotating that contain a single tilde
$1 ~ /~/ && $1 !~ /~~/ {
    # split first field into array named subfield
    n = split($1, subfield, "~")
    if (n == 2) {
    # print entry without "~" and then rotated
        printf("%s %s::%s\n", subfield[1], subfield[2], $2)
        printf("%s:%s:%s\n", subfield[2], subfield[1], $2)
    }
    next
}

Правило сопоставления с шаблоном соответствует любой записи, содержащей тильду, но не двум последовательным тильдам, которые указывают на буквальную тильду. Процедура использует функцию split(), чтобы разбить первое поле на два «подполя». Это дает нам две подстроки, одну до и одну после тильды. Исходная запись выводится, а затем выводится повернутая запись, в обоих случаях с использованием оператора printf.

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

#< from input.idx
# Match entries that contain two tildes
$1 ~ /~~/ {
    # replace ~~ with ~
    gsub(/~~/, "~", $1)
}

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

* Идея ротации записей указателей была получена из The AWK Programming Language. Однако там запись автоматически поворачивается там, где находится пробел; тильда используется для предотвращения вращения, «заполняя» пространство. Вместо того, чтобы использовать вращение по умолчанию, мы используем другое соглашение о кодировании, где тильда указывает, где должно произойти вращение.

12.3.3 Поиск замены

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

class: class initialize: (see also methods)

тогда результат:

class: class initialize; (see also methods)

Сформулировать проблему довольно просто - мы хотим изменить второе двоеточие, а не первое. Это довольно легко решить в sed из-за возможности выбора и вызова части того, что соответствует в разделе замены (используя \(...\), чтобы окружить часть, чтобы соответствовать, и \1, чтобы вызвать первую часть). Не имея той же способности в awk, вы должны быть умнее. Одно из возможных решений:

#< from input.idx
# replace 2nd colon with semicolon
if (sub(/:.*:/, "&;", $1))
    sub(/:;/, ";", $1)

Первая замена соответствует всему интервалу между двумя двоеточиями. Замена выполняется тем, что соответствует (&), за которым следует точка с запятой. Эта подстановка происходит в условном выражении, которое оценивает возвращаемое значение функции sub(). Помните, что эта функция возвращает 1, если выполняется подстановка - она не возвращает результирующую строку. Другими словами, если мы делаем первую замену, то делаем вторую. Вторая замена заменяет «:;» на «;». Поскольку мы не можем произвести замену напрямую, мы делаем это косвенно, делая отдельный контекст, в котором появляется второе двоеточие.

12.3.4 Функция сообщения об ошибках

Цель программы input.idx - разрешить вариации (или, в меньшей степени, несогласованности) в кодировании записей указателя. Сведение этих вариантов к одной базовой форме упрощает написание других программ.

С другой стороны, если программа input.idx не может принять запись, она должна сообщить об этом пользователю и отбросить запись, чтобы она не влияла на другие программы. Программа input.idx имеет функцию, используемую для сообщения об ошибках, с именем printerr(), как показано ниже:

function printerr (message) {
    # print message, record number and record
    printf("ERROR:%s (%d) %s\n", message, NR, $0) > "/dev/tty"
}

Эта функция упрощает стандартное сообщение об ошибках. В качестве аргумента она принимает message, которое обычно представляет собой строку, описывающую ошибку. Она выводит это сообщение вместе с номером записи и самой записью. Вывод направляется на пользовательский терминал «/dev/tty». Это хорошая практика, поскольку стандартный вывод программы может быть, как в этом случае, направлен в канал или в файл. Мы также можем отправить сообщение об ошибке в стандартный поток ошибок, например:

print "ERROR:" message " (" NR ") " $0 | "cat 1>&2"

Это открывает канал для cat, а стандартный вывод cat перенаправляется в стандартный поток ошибок. Если вы используете gawk, mawk или Bell Labs awk, вы можете вместо этого сказать:

printf("ERROR:%s (%d) %s\n", message, NR, $0) > "/dev/stderr"

В программе функция printerr() вызывается следующим образом:

printerr("No page number")

Когда возникает эта ошибка, пользователь видит следующее сообщение об ошибке:

ERROR:No page number (612) geometry management:set_values_almost

12.3.5 Обработка записей See Also

Один из типов индексной записи - это «see also». Подобно ссылке «see», он отсылает читателя к другой записи. Однако запись «see also» может также иметь номер страницы. Другими словами, эта запись содержит отдельную информацию, но отсылает читателя к другому месту за дополнительной информацией. Вот несколько примеров записей.

error procedure 34
error procedure (see also XtAppSetErrorMsgHandler) 35
error procedure (see also XtAppErrorMsg)

Первая запись в этом примере имеет номер страницы, а последняя - нет. Когда программа input.idx находит запись «see also», она проверяет, указан ли номер страницы ($2). Если да, она выводит две записи, первая из которых является записью без номера страницы, а вторая - с записью и номером страницы без ссылки «see also».

#< input.idx
# if no page number
    if ($2 == "") {
        print $0 ":"
        next
    }
    else {
    # output two entries:
    # print See Also entry w/out page number
        print $1 ":"
    # remove See Also
        sub(/ *~zz\(see also.*$/, "", $1)
        sub(/;/, "", $1)
    # print as normal entry
        if ( $1 ~ /:/ )
            print $1 ":" $2
        else
            print $1 "::" $2
        next
    }

Следующая проблема, которую необходимо было решить, заключалась в том, как отсортировать записи в правильном порядке. Программа sort, используя предоставленные нами параметры, отсортировала вторичные ключи для записей «see also» вместе в категории «s». (Параметр -d приводит к игнорированию скобок.) Чтобы изменить порядок сортировки, мы изменяем ключ сортировки, добавляя последовательность «~zz» перед ним.

#< input.idx
# add "~zz" for sort at end
    sub(/\([Ss]ee [Aa]lso/, "~zz(see also", $1)

Тильда не интерпретируется сортировкой, но помогает нам идентифицировать строку позже, когда мы ее удалим. Добавление «~zz» гарантирует нам сортировку до конца списка вторичных или третичных ключей.

Скрипт pagenums.idx удаляет строку сортировки из записей «see also». Однако, как мы описали ранее, мы ищем серию записей «see also» для одного и того же ключа и создаем список. Поэтому мы также удаляем то, что одинаково для всех записей, и помещаем саму ссылку в массив:

#< pagenums.idx
# remove secondary key along with "~zz"
    sub(/^.*~zz\([Ss]ee +[Aa]lso */, "", SECONDARY)
    sub(/\) */, "", SECONDARY)
# assign to next element of seeAlsoList
    seeAlsoList[++eachSeeAlso] = SECONDARY "; "

Есть функция, которая выводит список записей «see also», разделяя каждую из них точкой с запятой. Таким образом, вывод записи «see also» в pagenums.idx выглядит так:

error procedure:(see also XtAppErrorMsg;XtAppSetErrorHandler.)

12.3.6 Альтернативные способы сортировки

В этой программе мы решили не поддерживать запросы на шрифт troff и размер точек в индексных записях. Если вы хотите поддерживать специальные escape-последовательности, один из способов сделать это показан в The AWK Programming Language. Для каждой записи возьмите первое поле и добавьте его к записи в качестве ключа сортировки. Теперь, когда есть дубликат первого поля, удалите escape-последовательности из ключа сортировки. После сортировки записей вы можете удалить ключ сортировки. Этот процесс предотвращает нарушение сортировки управляющими последовательностями.

Еще один способ - сделать что-то похожее на то, что мы сделали для записей «see also». Поскольку при сортировке специальные символы игнорируются, мы могли бы использовать программу input.idx для преобразования последовательности изменения шрифта troff, такой как «\fB» в «~~~» и «\fI» в «~~~~» или любую удобную escape-последовательность. Это позволит пройти последовательность через программу sort, не нарушая сортировку. (Этот метод был использован Стивом Тэлботтом в его оригинальном скрипте индексирования.)

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

Глава 13.
Сборник сценариев

В этой главе содержится множество скриптов, созданных пользователями Usenet. Каждая программа представлена автором программы с кратким описанием. Наши комментарии заключены в квадратные скобки [вот так]. Затем отображается полный листинг программ. Если автор не привел пример, мы его генерируем и описываем после листинга. Наконец, в разделе «Примечания к программе» мы кратко поговорим о программе, выделив некоторые интересные моменты. Вот краткое изложение сценариев:

uutot.awk
Отчет статистики UUCP.
phonebill
Отслеживание использования телефона.
combine
Извлечение составных двоичных файлов с uuencoded.
mailavg
Проверка размеров почтовых ящиков.
adj
Настрока строки для текстовых файлов.
readsource
Форматирование исходных файлов troff.
gent
Получение записи termcap.
plpr
lpr препроцессор.
transpose
Транспонирование матрицы.
m1
Очень простой макропроцессор.

13.1 uutot.awk - Отчет статистики UUCP

Предоставил Роджер А. Корнелиус (Roger A. Cornelius)

Вот кое-что, что я написал в nawk в ответ на все версии одной и той же вещи на C, которые некоторое время назад были размещены в alt.sources. По сути, скрипт обобщает статистику соединений uucp (время соединения, пропускная способность, переданные файлы и т. д.). Он поддерживает только файлы журналов в стиле HDB, но будет отображать статистику для каждого сайта или в целом (все сайты). [Это также работает с /usr/spool/uucp/SYSLOG].

Я использую шелл-оболочку, которая для запуска вызывает awk -f, но в этом нет необходимости. Информация об использовании находится в шапке. (Извините за отсутствие комментариев).

# @(#) uutot.awk - display uucp statistics - requires new awk
# @(#) Usage:awk -f uutot.awk [site ...] /usr/spool/uucp/.Admin/xferstats
# Copyright 1989 Roger A. Cornelius (rac@sherpa.uucp)

#   dosome[];       # site names to work for - all if not set
#   remote[];       # array of site names
#   bytes[];        # bytes xmitted by site
#   time[];         # time spent by site
#   files[];        # files xmitted by site
BEGIN {
    doall = 1;
    if (ARGC > 2) {
        doall = 0;
        for (i = 1; i < ARGC-1; i++) {
            dosome[ ARGV[i] ];
            ARGV[i] = "";
        }
    }

    kbyte = 1024    # 1000 if you're not picky
    bang = "!";
    sending = "->";
    xmitting = "->" "|" "<-";

    hdr1 = "Remote     K-Bytes   K-Bytes   K-Bytes " \
        "Hr:Mn:Sc Hr:Mn:Sc AvCPS AvCPS    #    #\n";
    hdr2 = "SiteName      Recv      Xmit     Total     " \
        "Recv     Xmit  Recv  Xmit Recv Xmit\n";
    hdr3 = "-------- --------- --------- --------- -------- " \
        "-------- ----- ----- ---- ----";
    fmt1 = "%-8.8s %9.3f %9.3f %9.3f %2d:%02d:%02.0f " \
        "%2d:%02d:%02.0f %5.0f %5.0f %4d %4d\n";
    fmt2 = "Totals   %9.3f %9.3f %9.3f %2d:%02d:%02.0f " \
        "%2d:%02d:%02.0f %5.0f %5.0f %4d %4d\n";
}
{
    if ($6 !~ xmitting)     # should never be
        next;
    direction = ($6 == sending ? 1 : 2)

    site = substr($1,1,index($1,bang)-1);
    if (site in dosome || doall) {
        remote[site];
        bytes[site,direction] += $7;
        time[site,direction] += $9;
        files[site,direction]++;
    }
}
END {
    print hdr1 hdr2 hdr3;
    for (k in remote) {
        rbyte += bytes[k,2];    sbyte += bytes[k,1];
        rtime += time[k,2];     stime += time[k,1];
        rfiles += files[k,2];   sfiles += files[k,1];
        printf(fmt1, k, bytes[k,2]/kbyte, bytes[k,1]/kbyte,
            (bytes[k,2]+bytes[k,1])/kbyte,
            time[k,2]/3600, (time[k,2]%3600)/60, time[k,2]%60,
            time[k,1]/3600, (time[k,1]%3600)/60, time[k,1]%60,
            bytes[k,2] && time[k,2] ? bytes[k,2]/time[k,2] : 0,
            bytes[k,1] && time[k,1] ? bytes[k,1]/time[k,1] : 0,
            files[k,2], files[k,1]);
    }

    print hdr3
    printf(fmt2, rbyte/kbyte, sbyte/kbyte, (rbyte+sbyte)/kbyte,
        rtime/3600, (rtime%3600)/60, rtime%60,
        stime/3600, (stime%3600)/60, stime%60,
        rbyte && rtime ? rbyte/rtime : 0,
        sbyte && stime ? sbyte/stime : 0,
        rfiles, sfiles);
}

Был создан тестовый файл для проверки программы Корнелиуса. Вот несколько строк, извлеченных из /usr/spool/uucp/.Admin/xferstats (поскольку каждая строка в этом файле слишком длинная для печати на странице, мы разорвали строку, следующую за стрелкой направления, только для отображения):

isla!nuucp S (8/3-16:10:17) (C,126,25) [ttyi1j] ->
                     1131/4.880 secs, 231 bytes/sec
isla!nuucp S (8/3-16:10:20) (C,126,26) [ttyi1j] ->
                     149/0.500 secs, 298 bytes/sec
isla!sue S (8/3-16:10:49) (C,126,27) [ttyi1j] ->
                     646/25.230 secs, 25 bytes/sec
isla!sue S (8/3-16:10:52) (C,126,28) [ttyi1j] ->
                     145/0.510 secs, 284 bytes/sec
uunet!uisla M (8/3-16:15:50) (C,951,1) [cui1a] ->
                     1191/0.660 secs, 1804 bytes/sec
uunet!uisla M (8/3-16:15:53) (C,951,2) [cui1a] ->
                     148/0.080 secs, 1850 bytes/sec
uunet!uisla M (8/3-16:15:57) (C,951,3) [cui1a] ->
                     1018/0.550 secs, 1850 bytes/sec
uunet!uisla M (8/3-16:16:00) (C,951,4) [cui1a] ->
                     160/0.070 secs, 2285 bytes/sec
uunet!daemon M (8/3-16:16:06) (C,951,5) [cui1a] <-
                     552/2.740 secs, 201 bytes/sec
uunet!daemon M (8/3-16:16:09) (C,951,6) [cui1a] <-
                     102/1.390 secs, 73 bytes/sec

Обратите внимание, что есть 12 полей; однако в действительности программа использует только поля 1, 6, 7 и 9. Запуск программы на входных данных примера дает следующие результаты:

$ nawk -f uutot.awk uutot.test
Remote     K-Bytes   K-Bytes   K-Bytes Hr:Mn:Sc Hr:Mn:Sc AvCPS AvCPS    #    #
SiteName      Recv      Xmit     Total     Recv     Xmit  Recv  Xmit Recv Xmit
-------- --------- --------- --------- -------- -------- ----- ----- ---- ----
uunet        0.639     2.458     3.097  0:04:34  2:09:49     2     0    2    4
isla         0.000     2.022     2.022  0:00:00  0:13:58     0     2    0    4
-------- --------- --------- --------- -------- -------- ----- ----- ---- ----
Totals       0.639     4.480     5.119  0:04:34  2:23:47     2     1    2    8

13.1.1 Примечания к программе uutot.awk

Это приложение nawk - отличный пример четко написанной программы awk. Это также типичный пример использования awk для превращения довольно непонятного журнала UNIX в полезный отчет.

Хотя Корнелиус приносит свои извинения за отсутствие комментариев, объясняющих логику программы, использование программы ясно из первоначальных комментариев. Кроме того, он использует переменные для определения шаблонов поиска и макета отчета. Это помогает упростить условные операторы и операторы печати в теле программы. Также помогает то, что переменные имеют имена, которые помогают сразу же распознать их назначение.

Эта программа состоит из трех частей, как мы подчеркивали в Главе 7, Написание скриптов для awk. Она состоит из процедуры BEGIN, в которой определены переменные; тело, в которой обрабатывается каждая строка данных из файла журнала; и процедуры END, в которой генерируется вывод для отчета.

13.2 phonebill - Отслеживание использования телефона

Предоставил Ник Холлоуэй (Nick Holloway)

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

Вход в программу - четыре поля. Первое поле - это дата (не используется). Второе поле - это «band/rate» и используется для поиска длины устройства. Третье поле - это длина звонка. Это может быть «ss», «mm:ss» или «hh:mm:ss». Четвертое поле - это имя звонящего. У нас есть секундомер (старый дешевый цифровой), книга и ручка. Приходящий счет в этот раз проходит через мой сценарий awk. Это касается только стоимости звонков, а не постоянной оплаты.

Цель программы состояла в том, чтобы позволить абонентам вводить минимальный объем информации, и программу можно было использовать для сбора вместе стоимости вызовов для каждого пользователя в одном отчете. Также написано, что если British Telecom изменит свои тарифы, это можно будет легко сделать в верхней части источника (это уже было сделано один раз). Если добавить больше диапазонов или ставок начислений, таблицу можно будет просто расширить (чудеса ассоциативных массивов). Никаких реальных проверок входных данных не проводится. Использование:

phonebill [ file ... ]

Вот (короткий) образец ввода и вывода.

Ввод:
29/05  b/p  5:35      Nick
29/05  L/c  1:00:00   Dale
01/06  L/c  30:50     Nick
Вывод:
Summary for Dale:
        29/05   L/c  1:00:00  11 units
Total: 11 units @ 5.06 pence per unit = $0.56
Summary for Nick:
        29/05   b/p     5:35  19 units
        01/06   L/c    30:50   6 units
Total: 25 units @ 5.06 pence per unit = $1.26

Листинг phonebill следующий:

#!/bin/awk -f
#------------------------------------------------------------------
#   Awk script to take in phone usage - and calculate cost for each
#   person
#------------------------------------------------------------------
#   Author: N.Holloway (alfie@cs.warwick.ac.uk)
#   Date  : 27 January 1989
#   Place : University of Warwick
#------------------------------------------------------------------
#   Entries are made in the form
#   Date   Type/Rate   Length  Name
#
#   Format:
#   Date        : "dd/mm"       - one word
#   Type/Rate   : "bb/rr"  (e.g. L/c)
#   Length      : "hh:mm:ss", "mm:ss", "ss"
#   Name        : "Fred"        - one word (unique)
#------------------------------------------------------------------
#   Charge information kept in array 'c', indexed by "type/rate",
#   and the cost of a unit is kept in the variable 'pence_per_unit'
#   The info is stored in two arrays, both indexed by the name. The
#   first 'summary' has the lines that hold input data, and number 
#   of units, and 'units' has the cumulative total number of units
#   used by name.
#------------------------------------------------------------------

BEGIN \
    {   
        # --- Cost per unit
        pence_per_unit  = 4.40      # cost is 4.4 pence per unit
        pence_per_unit *= 1.15      # VAT is 15%
    
        # --- Table of seconds per unit for different bands/rates
        #     [ not applicable have 0 entered as value ]
        c ["L/c"] = 330 ;  c ["L/s"] = 85.0;  c ["L/p"] = 60.0;
        c ["a/c"] =  96 ;  c ["a/s"] = 34.3;  c ["a/p"] = 25.7;
        c ["b1/c"]= 60.0;  c ["b1/s"]= 30.0;  c ["b1/p"]= 22.5;
        c ["b/c"] = 45.0;  c ["b/s"] = 24.0;  c ["b/p"] = 18.0;
        c ["m/c"] = 12.0;  c ["m/s"] = 8.00;  c ["m/p"] = 8.00;
        c ["A/c"] = 9.00;  c ["A/s"] = 7.20;  c ["A/p"] = 0   ;
        c ["A2/c"]= 7.60;  c ["A2/s"]= 6.20;  c ["A2/p"]= 0   ;
        c ["B/c"] = 6.65;  c ["B/s"] = 5.45;  c ["B/p"] = 0   ;
        c ["C/c"] = 5.15;  c ["C/s"] = 4.35;  c ["C/p"] = 3.95;
        c ["D/c"] = 3.55;  c ["D/s"] = 2.90;  c ["D/p"] = 0   ;
        c ["E/c"] = 3.80;  c ["E/s"] = 3.05;  c ["E/p"] = 0   ;
        c ["F/c"] = 2.65;  c ["F/s"] = 2.25;  c ["F/p"] = 0   ;
        c ["G/c"] = 2.15;  c ["G/s"] = 2.15;  c ["G/p"] = 2.15;
    }

    {
        spu = c [ $2 ]              # look up charge band
        if ( spu == "" || spu == 0 ) {
            summary [ $4 ] = summary [ $4 ] "\n\t" \
                    sprintf ( "%4s  %4s  %7s   ? units",\
                                  $1, $2, $3 ) \
                    " - Bad/Unknown Chargeband"
        } else {
            n = split ( $3, t, ":" )  # calculate length in seconds
            seconds = 0
            for ( i = 1; i <= n; i++ )
            seconds = seconds*60 + t[i]
            u = seconds / spu   # calculate number of seconds
            if ( int( u ) == u )   # round up to next whole unit
            u = int( u )
            else
            u = int( u ) + 1
            units [ $4 ] += u   # store info to output at end
            summary [ $4 ] = summary [ $4 ] "\n\t" \
                    sprintf ( "%4s  %4s  %7s %3d units",\
                                 $1, $2, $3, u )
        }
    }

END \
    {
        for ( i in units ) {        # for each person
            printf ( "Summary for %s:", i ) # newline at start
                                                # of summary
            print summary [ i ]         # print summary details
            # calc cost
            total = int ( units[i] * pence_per_unit + 0.5 )
            printf ( \
            "Total: %d units @ %.2f pence per unit = $%d.%02d\n\n", \
                    units [i], pence_per_unit, total/100, \
                                                   total%100 )
        }
    }

13.2.1 Примечания к программе phonebill

Эта программа - еще один пример создания отчета, который объединяет информацию из простой структуры записи.

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

Основная процедура читает каждую строку журнала пользователя. Она использует второе поле, которое определяет полосу/скорость, чтобы получить значение из массива c. Она проверяет, было ли возвращено положительное значение, а затем обрабатывает это значение к моменту времени, указанному в $3. Количество единиц для этого вызова затем сохраняется в массиве с именем units, индексируемом по имени вызывающего абонента ($4). Это значение накапливается для каждого вызывающего абонента.

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

13.3 combine - Извлечение составных двоичных файлов с uuencoded

Предоставил Рахул Дхеси (Rahul Dhesi)

Из всех сценариев, которые я когда-либо писал, больше всего я горжусь сценарием «combine».

Пока я модерировал comp.binaries.ibm.pc, я хотел предоставить пользователям простой способ извлечения составных двоичных файлов с кодировкой uuencode. Я добавил заголовки BEGIN и END к каждой части, чтобы заключить часть с uuencoded, и предоставил пользователям следующий скрипт:

cat $* | sed '/^END/,/^BEGIN/d' | uudecode

Этот сценарий принимает список имен файлов (по порядку), предоставленный в качестве аргументов командной строки. Он также принимает составные статьи в качестве стандартного ввода.

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

Сценарий вызывает sed, чтобы удалить лишние заголовки и трейлеры, за исключением заголовков в первом входном файле и трейлеров в последнем входном файле. Конечным результатом является то, что часть нескольких входных файлов с uuencoded извлекается и кодируется uudecoded. Каждый входной файл (см. сообщения в comp.binaries.ibm.pc) имеет следующий вид:

headers
BEGIN
uuencoded text
END

У меня есть много других скриптов оболочки, но приведенный выше самый простой и оказался полезным для нескольких тысяч читателей comp.binaries.ibm.pc.

13.3.1 Примечания к программе combine

Это довольно очевидно, но многое дает. Для тех, кто не понимает, как пользоваться этой командой, вот объяснение. Группа новостей Usenet, такая как comp.binaries.ibm.pc, распространяет общедоступные программы и тому подобное. Двоичные файлы, объектный код, созданный компилятором, не могут распространяться как новостные статьи, если они не «закодированы». Программа с именем uuencode преобразует двоичный файл в представление ASCII, которое можно легко распространять. Кроме того, существуют ограничения на размер новостных статей, и большие двоичные файлы разбиваются на серии статей (например, 1 из 3, 2 из 3, 3 из 3). Дхеси разбивал закодированный двоичный файл на управляемые части, а затем добавлял строки BEGIN и END, чтобы разграничить текст, содержащий закодированный двоичный файл.

Читатель этих статей может сохранить каждую статью в файл. Сценарий Дхеси автоматизирует процесс объединения этих статей и удаления посторонней информации, такой как заголовок статьи, а также лишние заголовки BEGIN и END. Его сценарий удаляет строки от первого END до следующего шаблона BEGIN включительно. Он объединяет все отдельные закодированные посылки и направляет их в uudecode, который преобразует представление ASCII в двоичное.

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

13.4 mailavg - Проверка размеров почтовых ящиков

Предоставил Уэс Морган (Wes Morgan)

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

#! /bin/sh
#
# mailavg - average size of files in /usr/mail
#
# Written by Wes Morgan, morgan@engr.uky.edu, 2 Feb 90
ls -Fs /usr/mail | awk '
   { if(NR != 1) {
       total += $1; 
       count += 1;
       size = $1 + 0; 
       if(size == 0) zercount+=1;
       if(size > 0 && size <= 10) tencount+=1;
       if(size > 10 && size <= 19) teencount+=1;
       if(size > 20 && size <= 50) uptofiftycount+=1;
       if(size > 50) overfiftycount+=1;
       }
   }
   END { printf("/usr/mail has %d mailboxes using %d blocks,", count,total) 
         printf("average is %6.2f blocks\n", total/count)
         printf("\nDistribution:\n")
         printf("Size      Count\n")
         printf(" O           %d\n",zercount)
         printf("1-10         %d\n",tencount)
         printf("11-20        %d\n",teencount)
         printf("21-50        %d\n",uptofiftycount)
         printf("Over 50      %d\n",overfiftycount)
       }'
exit 0

Вот пример вывода mailavg:

$ mailavg
/usr/mail has 47 mailboxes using 5116 blocks,
average is 108.85 blocks
Distribution:
Size      Count
 O           1
1-10         13
11-20        1
21-50        5
Over 50      27

13.4.1 Примечания к программе mailavg

Эта административная программа похожа на программу filesum из Главы 7. Она обрабатывает вывод команды ls.

Условное выражение «NR != 1» можно было бы поместить за пределы основной процедуры в качестве шаблона. Хотя логика та же самая, использование выражения в качестве шаблона проясняет, как осуществляется доступ к процедуре, что упрощает понимание программы.

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

13.5 adj - Настрока строки для текстовых файлов

Предоставил Норман Джозеф (Norman Joseph)

[Поскольку автор использовал свою программу для форматирования своего почтового сообщения перед его отправкой, мы сохраняем разрывы строк и абзацы с отступом, представляя его здесь в качестве примера программы. Эта программа похожа на программу BSD fmt.]

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

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

Теперь я знаю, что могу использовать nroff для фильтрации моего документа и корректировки строк, но у него ужасные значения по умолчанию (IMHO) для такого простого текста. Итак, с целью отточить свои навыки nawk, я написал adj.nawk и сопутствующую обертку сценария оболочки adj.

Вот синтаксис nawk фильтра adj:

adj [-l|c|r|b] [-w n] [-i n] [files ...]

Опции:

-l
Строки выровнены влево, неровные справа (по умолчанию).
-c
Строки центрируются.
-r
Строки выровнены вправо, влево рваные.
-b
Линии выровнены вправо и влево.
-w n
Устанавливает ширину строки в n символов (по умолчанию 70).
-i n
Устанавливает начальный отступ в n символов (по умолчанию 0).

Итак, когда я закончу с этим письмом (я использую vi), я дам команду :%!adj -w73 (мне нравится, когда мои строки немного длиннее), и все разрывы и соединения будут выполняться программой (так, как задумал добрый Господь :-). Отступы и пустые строки сохраняются, а после знаков препинания в конце предложения ставятся два пробела.

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

Программа примечательна использованием назначения параметров командной строки и некоторыми новыми функциями awk (nawk), такими как встроенные функции сопоставления и разделения, а также использованием функций поддержки.

#! /bin/sh
#
# adj - adjust text lines
#
# usage: adj [-l|c|r|b] [-w n] [-i n] [files ...]
#
# options:
#    -l    - lines are left adjusted, right ragged (default)
#    -c    - lines are centered
#    -r    - lines are right adjusted, left ragged
#    -b    - lines are left and right adjusted
#    -w n  - sets line width to  characters (default: 70)
#    -i n  - sets initial indent to  characters (default: 0)
#
# note:
#    output line width is -w setting plus -i setting
#
# author:
#    Norman Joseph (amanue!oglvee!norm)

adj=l
wid=70
ind=0

set -- `getopt lcrbw:i: $*`
if test $? != 0
then
    printf 'usage: %s [-l|c|r|b] [-w n] [-i n] [files ...]' $0
    exit 1
fi

for arg in $*
do
    case $arg in
    -l) adj=l;  shift;;
    -c) adj=c;  shift;;
    -r) adj=r;  shift;;
    -b) adj=b;  shift;;
    -w) wid=$2;  shift 2;;
    -i) ind=$2;  shift 2;;
    --) shift;  break;;
    esac
done

exec nawk -f adj.nawk type=$adj linelen=$wid indent=$ind $*

Вот сценарий adj.nawk, который вызывается сценарием оболочки adj.

# adj.nawk -- adjust lines of text per options
#
# NOTE:  this nawk program is called from the shell script "adj"
#    see that script for usage & calling conventions
#
# author:
#    Norman Joseph (amanue!oglvee!norm)

BEGIN  {
    FS = "\n"
    blankline  = "^[ \t]*$"
    startblank = "^[ \t]+[^ \t]+"
    startwords = "^[^ \t]+"
}

$0 ~ blankline {
    if ( type == "b" )
        putline( outline "\n" )
    else
        putline( adjust( outline, type ) "\n" )
    putline( "\n" )
    outline = ""
}

$0 ~ startblank {
    if ( outline != "" ) {
        if ( type == "b" )
            putline( outline "\n" )
        else
            putline( adjust( outline, type ) "\n" )
    }

    firstword = ""
    i = 1
    while ( substr( $0, i, 1 ) ~ "[ \t]" ) {
        firstword = firstword substr( $0, i, 1 )
        i++
    }
    inline = substr( $0, i )
    outline = firstword

    nf = split( inline, word, "[ \t]+" )

    for ( i = 1;  i <= nf;  i++ ) {
        if ( i == 1 ) {
            testlen = length( outline word[i] )
        } else {
            testlen = length( outline " " word[i] )
            if ( match( ".!?:;", "\\" substr( outline,
                    length( outline ), 1 )) )
                testlen++
        }

        if ( testlen > linelen ) {
            putline( adjust( outline, type ) "\n" )
            outline = ""
        }

        if ( outline == "" )
            outline = word[i]
        else if ( i == 1 )
            outline = outline word[i]
        else {
            if ( match( ".!?:;", "\\" substr( outline,
                   length( outline ), 1 )) )
                outline = outline "  " word[i]     # 2 spaces
            else
                outline = outline " " word[i]      # 1 space
        }
    }
}

$0 ~ startwords  {
    nf = split( $0, word, "[ \t]+" )

    for ( i = 1;  i <= nf;  i++ ) {
        if ( outline == "" )
            testlen = length( word[i] )
        else {
            testlen = length( outline " " word[i] )
            if ( match( ".!?:;", "\\" substr( outline,
                   length( outline ), 1 )) )
                testlen++
        }

        if ( testlen > linelen ) {
            putline( adjust( outline, type ) "\n" )
            outline = ""
        }

        if ( outline == "" )
            outline = word[i]
        else {
            if ( match( ".!?:;", "\\" substr( outline,
                   length( outline ), 1 )) )
                outline = outline "  " word[i]     # 2 spaces
            else
                outline = outline " " word[i]      # 1 space
        }
    }
}

END  {
    if ( type == "b" )
        putline( outline "\n" )
    else
        putline( adjust( outline, type ) "\n" )
}


#
# -- support functions --
#

function putline( line,    fmt )
{
    if ( indent ) {
        fmt = "%" indent "s%s"
        printf( fmt, " ", line )
    } else
        printf( "%s", line )
}


function adjust( line, type,    fill, fmt )
{
    if ( type != "l" )
        fill = linelen - length( line )

    if ( fill > 0 ) {
        if        ( type == "c" ) {
            fmt = "%" (fill+1)/2 "s%s"
            line = sprintf( fmt, " ", line )
        } else if ( type == "r" ) {
            fmt = "%" fill "s%s"
            line = sprintf( fmt, " ", line )
        } else if ( type == "b" ) {
            line = fillout( line, fill )
        }
    }

    return line
}


function fillout( line, need,    i, newline, nextchar, blankseen )
{
    while ( need ) {
        newline = ""
        blankseen = 0

        if ( dir == 0 ) {
            for ( i = 1;  i <= length( line );  i++ ) {
                nextchar = substr( line, i, 1 )
                if ( need ) {
                    if ( nextchar == " " ) {
                        if ( ! blankseen ) {
                            newline = newline " "
                            need--
                            blankseen = 1
                        }
                    } else {
                        blankseen = 0
                    }
                }
                newline = newline nextchar
            }

        } else if ( dir == 1 ) {
            for ( i = length( line );  i >= 1;  i-- ) {
                nextchar = substr( line, i, 1 )
                if ( need ) {
                    if ( nextchar == " " ) {
                        if ( ! blankseen ) {
                            newline = " " newline
                            need--
                            blankseen = 1
                        }
                    } else {
                        blankseen = 0
                    }
                }
                newline = nextchar newline
            }
        }

        line = newline

        dir = 1 - dir
    }

    return line
}

13.5.1 Примечания к программе adj

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

Сценарий оболочки adj выполняет все настройки параметров, хотя это можно было сделать, прочитав ARGV в действии BEGIN. Использование оболочки для установки параметров командной строки, вероятно, проще для тех, кто уже знаком с оболочкой.

Отсутствие комментариев в сценарии adj.awk делает этот сценарий более трудным для чтения, чем некоторые другие. Процедура BEGIN присваивает переменным три регулярных выражения: blankline, startblank, startwords. Это хороший метод (который вы увидите в спецификациях lex), потому что регулярные выражения могут быть трудными для чтения, а имя переменной дает понять, с чем она соответствует. Помните, что современные awk позволяют вам передавать регулярное выражение в виде строки в переменной.

Есть три основные процедуры, которым можно присвоить имя по соответствующей переменной. Первый - это blankline, процедура, которая обрабатывает собранный текст, когда встречается пустая строка. Второй - startblank, который обрабатывает строки, начинающиеся с пробела (пробелы или табуляции). Третий - это startwords, обрабатывающая строку текста. Основная процедура - прочитать строку текста и определить, сколько слов в этой строке поместится с учетом ширины строки, вывести те, которые подойдут, и сохранить те, которые не попадут в переменную outline. При чтении следующей строки ввода содержимое outline должно быть выведено до вывода этой строки.

Функция adjust() выполняет работу по выравниванию текста на основе параметра командной строки, указывающего тип формата. Все типы, кроме «l» (с выравниванием влево, с неровностями вправо), должны быть заполнены. Следовательно, первое, что делает эта функция, - это вычисляет, сколько требуется «заполнения», вычитая длину текущей строки из указанной длины строки. Она отлично использует функцию sprintf() для позиционирования текста. Например, чтобы центрировать текст, значение fill (плюс 1) делится на 2, чтобы определить количество отступов, необходимых с каждой стороны строки. Эта сумма передается через переменную fmt в качестве аргумента функции sprintf():

fmt = "%" (fill+1)/2 "s%s"
line = sprintf( fmt, " ", line )

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

Если текст выровнен по правому краю, само значение fill используется для заполнения поля. Наконец, если тип формата - «b» (блок), то вызывается функция fillout, чтобы определить, где добавить пробелы, которые будут заполнять строку.

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

Кстати, мы не уверены, почему FS, разделитель полей, установлен на новую строку в процедуре BEGIN. Это означает, что разделители полей и записей одинаковы (т.е. $0 и $1 одинаковы). Функция split() вызывается, чтобы разбить строку на поля с использованием табуляции или пробелов в качестве разделителя.

nf = split( $0, word, "[ \t]+" )

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

nf = split( $0, word, "[ \t]+" )

Было бы более эффективно использовать синтаксический анализ полей по умолчанию.

Наконец, использование функции match() для поиска знаков препинания неэффективно; было бы лучше использовать index().

13.6 readsource - Форматирование исходных файлов troff

Предоставил Мартин Вайцель (Martin Weitzel)

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

Поскольку я использую troff для обработки текста, в текст должно быть легко включить исходные источники. Но есть некоторые символы (особенно « » и «.» и «,» в начале строки), которые я должен убрать, чтобы предотвратить интерпретацию troff.

Мне часто нужны отрывки из исходников, а не полный файл. Еще мне нужен механизм для установки разрывов страниц. Что ж, возможно, я перфекционист, но я не хочу, чтобы функция C была напечатана почти полностью на одной странице, а на следующей появлялись только две последние строки. Поскольку я часто меняю документы, я не могу охотиться за «красивыми» разрывами страниц - это нужно делать автоматически.

Чтобы решить этот набор проблем, я написал фильтр, который предварительно обрабатывает любой источник для включения в виде текста в troff. Это программа awk, которую я отправляю с этим письмом. [Он не назвал его, поэтому здесь он назван readsource.]

Весь процесс можно автоматизировать с помощью make-файлов. Я включаю предварительно обработанную версию источников в мои документы troff и делаю форматирование зависимым от этих предварительно обработанных файлов. Эти файлы снова зависят от их оригиналов, поэтому, если я «заставлю» документ напечатать его, предварительно обработанные источники будут проверены, чтобы увидеть, актуальны ли они; в противном случае они будут созданы новыми из своих оригиналов.

Моя программа содержит полное описание в виде комментариев. Но поскольку описание больше подходит для меня, чем для других, я дам вам еще несколько советов. По сути, программа просто охраняет некоторые символы, например, «\» превращается в «\e», а «\&» пишется перед каждой строкой. Табуляции могут быть расширены до пробелов (для этого есть переключатель), и вы даже можете генерировать номера строк перед каждой строкой (можно выбрать переключатель). Формат этих номеров строк можно установить с помощью переменной окружения.

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

Последняя функция, правильная установка разрывов страниц, немного сложна. Здесь эволюционировал метод, который я называю «здесь-вы-можете-сломать». Эти точки отмечены специальной линией (я использую «/*!» В программах на C и «#!» в awk, оболочке, make-файлах и т. д.). То, как отмечены точки, не имеет большого значения, у вас могут быть свои собственные соглашения, но должна быть возможность дать регулярное выражение, которое соответствует именно этой линии и никаким другим (например, если ваши источники написаны так, что разрыв страницы допустим везде, где у вас есть пустая строка, вы можете указать это очень легко, так как все, что вам нужно, это регулярное выражение для пустых строк).

Перед всеми отмеченными строками будет вставлена специальная последовательность, которая снова задается переменной окружения. В troff я использую технику открытия «дисплея» (.DS) перед включением такого предварительно обработанного текста и вставки закрытия (.DE) и нового открытого (.DS) отображения везде, где я бы принял разрыв страницы. После этого troff собирает столько строк, сколько умещается на текущей странице. Я полагаю, что существуют подходящие методы для других текстовых процессоров.

#! /bin/sh
# Copyright 1990 by EDV-Beratung Martin Weitzel, D-6100 Darmstadt
# ==================================================================
# PROJECT:  Printing Tools
# SH-SCRIPT:    Source to Troff Pre-Formatter
# ==================================================================

#!
# ------------------------------------------------------------------
# This programm is a tool to preformat source files, so that they
# can be included (.so) within nroff/troff-input. Problems when
# including arbitrary files within nroff/troff-input occur on lines,
# starting with dot (.) or an apostrophe ('), or with the respective
# chars, if these are changed, furthermore from embedded backslashes.
# While changing the source so that non of the above will cause
# any problems, some other usefull things can be done, including
# line numbering and selecting interesting parts.
# ------------------------------------------------------------------
#!
  USAGE="$0 [-x d] [-n] [-b pat] [-e pat] [-p pat] [file ...]"
#
# SYNOPSIS:
# The following options are supported:
#   -x d    expand tabs to "d" spaces
#   -n  number source lines (see also: NFMT)
#   -b pat  start output on a line containing "pat",
#       including this line (Default: from beginning)
#   -e pat  end output on a line containing "pat"
#       excluding this line (Default: upto end)
#   -p pat  before lines containing "pat", page breaks
#       may occur (Default: no page breaks)
# "pat" may be an "extended regular expression" as supported by awk.
# The following variables from the environment are used:
#   NFMT    specify format for line numbers (Default: see below)
#   PBRK    string, to mark page breaks. (Default: see below)
#!
# PREREQUISITS:
# Common UNIX-Environment, including awk.
#
# CAVEATS:
# "pat"s are not checked before they are used (processing may have
# started, before problems are detected).
# "NFMT" must contain exactly one %d-format specifier, if -n
# option is used.
# In "NFMT" and "PBRK", embedded doublequotes must be guarded with
# a leading backslash.
# In "pat"s, "NFMT" and "PBRK" embedded TABs and NLs must be written
# as \t and \n. Backslashes that should "go thru" to the output as
# such, should be doubled. (The latter is only *required* in a few
# special cases, but it does no harm the other cases).
# 
# Must run from Bourne shell [dpd]
#!
# BUGS:
# Slow - but may serve as prototype for a faster implementation.
# (Hint: Guarding backslashes the way it is done by now is very
# expensive and could also be done using sed 's/\\/\\e/g', but tab
# expansion would be much harder then, because I can't imagine how
# to do it with sed. If you have no need for tab expansion, you may
# change the program. Another option would be to use gsub(), which
# would limit the program to environments with nawk.)
# 
# Others bugs may be, please mail me.
#!
# AUTHOR:   Martin Weitzel, D-6100 DA (martin@mwtech.UUCP)
#
# RELEASED:     25. Nov 1989, Version 1.00
# ------------------------------------------------------------------

#! CSOPT
# ------------------------------------------------------------------
#   check/set options
# ------------------------------------------------------------------

xtabs=0 nfmt= bpat= epat= ppat=
for p
do
case $sk in
1) shift; sk=0; continue
esac
case $p in
-x) shift;
    case $1 in
    [1-9]|1[0-9]) xtabs=$1; sk=1;;
    *) { >&2 echo "$0: bad value for option -x: $1"; exit 1; }
    esac
    ;;
-n) nfmt="${NFMT:-<%03d>\   }"; shift ;;
-b) shift; bpat=$1; sk=1 ;;
-e) shift; epat=$1; sk=1 ;;
-p) shift; ppat=$1; sk=1 ;;
--) shift; break ;;
*)  break
esac
done

#! MPROC
# ------------------------------------------------------------------
#   now the "real work"
# ------------------------------------------------------------------

nawk '
#. prepare for tab-expansion, page-breaks and selection
BEGIN {
    if (xt = '$xtabs') while (length(sp) < xt) sp = sp " ";
    PBRK = "'"${PBRK-'.DE\n.DS\n'}"'"
    '${bpat:+' skip = 1; '}'
}
#! limit selection range
{
    '${epat:+' if (!skip && $0 ~ /'"$epat"'/) skip = 1; '}'
    '${bpat:+' if (skip && $0 ~ /'"$bpat"'/) skip = 0; '}'
    if (skip) next;
}
#! process one line of input as required
{
    if ( xt && $0 ~ "\t" )
        gsub(/\t/, sp)
    if ($0 ~ "\\") 
        gsub(/\\/, "\\e")
}
#! finally print this line
{
    '${ppat:+' if ($0 ~ /'"$ppat"'/) printf("%s", PBRK); '}'
    '${nfmt:+' printf("'"$nfmt"'", NR) '}'
    printf("\\&%s\n", $0);
}
' $*

В качестве примера того, как это работает, мы запустили readsource, чтобы извлечь часть его собственной программы.

$ readsource -x 3 -b "process one line" -e "finally print" readsource
\&#! process one line of input as required
\&{
\&   line = ""; ll = 0;
\&   for (i = 1; i <= length; i++) {
\&       c = substr($0, i, 1);
\&       if (xt && c == "\\et") {
\&           # expand tabs
\&           nsp = 8 - ll % xt;
\&           line = line substr(sp, 1, nsp);
\&           ll += nsp;
\&       }
\&       else {
\&           if (c == "\\e\\e") c = "\\e\\ee";
\&           line = line c;
\&           ll++;
\&       }
\&   }
\&}

13.6.1 Примечания к программе readsource

Эта программа, во-первых, весьма полезна, так как помогла нам подготовить списки для этой книги. Автор действительно растягивает (старый) awk до предела, используя переменные оболочки для передачи информации в скрипт. Он выполняет свою работу, но это довольно непонятно.

Программа действительно работает медленно. Мы учли предложение автора и изменили способ замены табуляции и обратной косой черты в программе. Исходная программа использует дорогостоящее посимвольное сравнение, получая символ с помощью функции substr(). (Это процедура, извлеченная из приведенного выше примера.) Ее производительность указывает на то, насколько дорого обходится awk для чтения строки по одному символу за раз, что очень просто в C.

Запуск readsource сам по себе произвел следующие длительности выполнения:

$ timex readsource -x 3 readsource > /dev/null
real        1.56
user        1.22
sys         0.20

Процедуру, изменяющую способ обработки табуляции и обратной косой черты, можно переписать в nawk, чтобы использовать функцию gsub():

#! process one line of input as required
{
    if ( xt && $0 ~ "\t" )
        gsub(/\t/, sp)
    if ($0 ~ "\\")
        gsub(/\\/, "\\e")
}

Последняя процедура требует небольшого изменения, замены переменной line на «$0». (Мы не используем временную переменную line). Версия nawk производит:

$ timex readsource.2 -x 3 readsource > /dev/null
real        0.44
user        0.10
sys         0.22

Разница довольно заметная.

Еще одним последним ускорением может стать использование index() для поиска обратных косых черт:

#! process one line of input as required
{
    if ( xt && index($0, "\t") > 0 )
        gsub(/\t/, sp)
    if (index($0, "\\") > 0)
        gsub(/\\/, "\\e")
}

13.7 gent - Получение записи termcap

Предоставил Том Кристиансен (Tom Christiansen)

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

$ gent vt100

извлекает запись vt100 из termcap, а:

$ gent eagle /etc/disktab

получает запись eagle с disktab. Теперь я знаю, что это можно было сделать на C или Perl, но я сделал это очень давно. Это также интересно тем, как скрипт передает параметры в сценарий sed. Знаю, знаю: тоже надо было писать на sh, а не на csh.

#!/bin/csh -f

set argc = $#argv


set noglob
set dollar = '$'
set squeeze = 0
set noback="" nospace=""

rescan:
    if ( $argc > 0 && $argc < 3 ) then
        if ( "$1" =~ -* ) then
            if ( "-squeeze" =~ $1* ) then
                set noback='s/\\//g' nospace='s/^[  ]*//'
                set squeeze = 1
                shift
                @ argc --
                goto rescan 
            else 
                echo "Bad switch: $1"
                goto usage
            endif
        endif

        set entry = "$1"
        if ( $argc == 1 ) then
            set file = /etc/termcap
        else
            set file = "$2"
        endif
    else
        usage:
            echo "usage: `basename $0` [-squeeze] entry [termcapfile]"
            exit 1
    endif


sed -n -e \
"/^${entry}[|:]/ {\
    :x\
    /\\${dollar}/ {\
    ${noback}\
    ${nospace}\
    p\
    n\
    bx\
    }\
    ${nospace}\
    p\
    n\
    /^  / {\
        bx\
    }\
    }\
/^[^    ]*|${entry}[|:]/ {\
    :y\
    /\\${dollar}/ {\
    ${noback}\
    ${nospace}\
    p\
    n\
    by\
    }\
    ${nospace}\
    p\
    n\
    /^  / {\
        by\
    }\
    }" < $file

13.7.1 Примечания к программе gent

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

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

Этот сценарий можно упростить несколькими способами. Прежде всего, два регулярных выражения не кажутся необходимыми для соответствия записи. Первый соответствует имени записи в начале строки; второй совпадает с ним в другом месте строки. Циклы, помеченные x и y, идентичны, и даже если бы были необходимы два регулярных выражения, мы могли бы перейти к одному и тому же циклу.

13.8 plpr - lpr препроцессор

Предоставил Том Ван Раалте (Tom Van Raalte)

Я подумал, что вы можете использовать следующий сценарий в офисе. Это препроцессор для lpr, который отправляет вывод на «лучший» принтер. [Этот сценарий оболочки написан для системы BSD или Linux, и вы должны использовать эту команду вместо lpr. Он считывает вывод команды lpq, чтобы определить, доступен ли конкретный принтер. Если нет, он проверяет список принтеров, чтобы узнать, какой из них доступен, а какой наименее занят. Затем он вызывает lpr для отправки задания на этот принтер.]

#!/bin/sh
#
#set up temp file
TMP=/tmp/printsum.$$
LASERWRITER=${LASERWRITER-ps6}
#Check to see if the default printer is free?
#
#
FREE=`lpq -P$LASERWRITER | awk '
{ if ($0 == "no entries") 
  {
    val=1
    print val
    exit 0
  }
  else
  {
    val=0
    print val
    exit 0
  }
}'`
#echo Free is $FREE
#
#If the default is free then $FREE is set, and we print and exit.
#
if [ $FREE -eq 1 ] 
then
    SELECT=$LASERWRITER
#echo selected $SELECT
    lpr -P$SELECT $*
    exit 0
fi
#echo Past the exit
#
#Now we go on to see if any of the printers in bank are free.  
#
BANK=${BANK-$LASERWRITER}
#echo bank is $BANK
#
#If BANK is the same as LASERWRITER, then we have no choice.
#otherwise, we print on the one that is free, if any are free.
#
if [ "$BANK" =  "$LASERWRITER" ] 
then
    SELECT=$LASERWRITER
    lpr -P$SELECT $*
    exit 0
fi
#echo past the check bank=laserprinter
#
#Now we check for a free printer.
#Note that $LASERWRITER is checked again in case it becomes free
#during the check.
#
#echo now we check the other for a free one
for i in $BANK $LASERWRITER
do
FREE=`lpq -P$i | awk '
{ if ($0 == "no entries") 
  {
    val=1
    print val
    exit 0
  }
  else
  {
    val=0
    print val
    exit 0
  }
}'`
if [ $FREE -eq 1 ]
then
#   echo in loop for $i
    SELECT=$i
#   echo select is $SELECT
#   if [ "$FREE" != "$LASERWRITER" ]
#   then
#          echo "Output redirected to printer $i"
#   fi
    lpr -P$SELECT $*
    exit 0
fi
done
#echo done checking for a free one
# 
#If we make it here then no printers are free.  So we 
#print on the printer with the least bytes queued.
#
#
for i in $BANK $LASERWRITER
do
val=`lpq -P$i | awk ' BEGIN {
    start=0;
}
/^Time/ {
    start=1; 
    next;
}
(start == 1){
    test=substr($0,62,20);
    print test;
} ' | awk '
BEGIN {
    summ=0;
}
{
    summ=summ+$1;
}
END {
    print summ;
}'`
echo "$i $val" >> $TMP
done

SELECT=`awk '(NR==1) {
    select=$1;
    best=$2
}
($2 < best) {
    select=$1; 
    best=$2} 
END {
    print select
}
' $TMP `
#echo $SELECT
#
rm $TMP
#Now print on the selected printer
#if [ $SELECT != $LASERWRITER ]
#then
#   echo "Output redirected to printer $i"
#fi
lpr -P$SELECT $*
trap 'rm -f $TMP; exit 99' 2 3 15

13.8.1 Примечания к программе plpr

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

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

13.9 transpose - Транспонирование матрицы

Предоставил Джефф Клэр (Geoff Clare)

transpose выполняет транспонирование матрицы на входе. Я написал это, когда увидел в сети сценарий для выполнения этой работы и подумал, что он ужасно неэффективен. Я выложил свой как альтернативу со сравнениями по времени. Если я правильно помню, оригинал хранил все элементы по отдельности и использовал вложенный цикл с printf для каждого элемента. Мне сразу стало очевидно, что будет намного быстрее построить строки транспонированной матрицы «на лету».

В моем сценарии ${1+"$@"} используется для указания имен файлов в командной строке awk, поэтому, если файлы не указаны, awk будет читать стандартный ввод. Это намного лучше, чем простой $*, который не может обрабатывать имена файлов, содержащие пробелы.

#! /bin/sh
# Transpose a matrix: assumes all lines have same number
# of fields

exec awk '
NR == 1 {
    n = NF
    for (i = 1; i <= NF; i++)
        row[i] = $i
    next
}
{
    if (NF > n)
        n = NF
    for (i = 1; i <= NF; i++)
        row[i] = row[i] " " $i
}
END {
    for (i = 1; i <= n; i++)
        print row[i]
}' ${1+"$@"}

Это тестовый файл:

1 2 3 4
5 6 7 8
9 10 11 12

Теперь мы запускаем файл transpose.

$ transpose test
1 5 9
2 6 10
3 7 11
4 8 12

13.9.1 Примечания к программе transpose

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

13.10 m1 - Простой макропроцессор

Предоставил Джон Бентли (Jon Bentley)

Программа m1 является «младшим братом» макропроцессора m4 в системах UNIX. Первоначально она была опубликована в статье m1: A Mini Macro Processor, в Computer Language в июне 1990 г., том 7, номер 6, страницы 47-61. Эту программу мне представил Озан Йигит. Джон Бентли любезно прислал мне свою текущую версию программы, а также черновик своей статьи (у меня возникли проблемы с получением копии опубликованной). Версия этого документа PostScript включена в примеры программ, доступных на FTP-сервере O'Reilly (см. Предисловие). Я написал эти вступительные заметки и приведенные ниже примечания к программе. [A.R.]

Макропроцессор копирует свой ввод на свой вывод, одновременно выполняя несколько заданий. Задачи:

  1. Определить и развернуть макросы. Макросы состоят из двух частей: имени и тела. Все вхождения имени макроса заменяются телом макроса.
  2. Включить файлы. Специальные директивы include в файле данных заменяются содержимым указанного файла. Включаемые файлы обычно могут быть вложенными, при этом один включаемый файл включает другой. Включенные файлы обрабатываются для макросов.
  3. Условное включение и исключение текста. В окончательный вывод могут быть включены различные части текста, часто в зависимости от того, определен макрос или нет.
  4. В зависимости от макропроцессора могут появиться строки комментариев, которые будут удалены из окончательного вывода.

Если вы программист на C или C++, вы уже знакомы со встроенным препроцессором на этих языках. В системах UNIX есть универсальный макропроцессор m4. Это мощная программа, но ее довольно сложно освоить, поскольку определения макросов обрабатываются для расширения во время определения, а не во время расширения. m1 значительно проще, чем m4, что значительно упрощает его изучение и использование.

Вот первый фрагмент, сделанный Джоном на очень простом макропроцессоре. Все, что он делает, это определяет и расширяет макросы. Мы можем назвать это m0a. В этой и следующих программах символ «at» (@) выделяет строки, которые являются директивами, а также указывает на наличие макросов, которые следует раскрыть.

/^@define[ \t]/ {
    name = $2
    $1 = $2 = ""; sub(/^[ \t]+/, "")
    symtab[name] = $0
    next
}
{
    for (i in symtab)
        gsub("@" i "@", symtab[i])
    print
}

Эта версия ищет строки, начинающиеся с «@define». Это ключевое слово - $1, а имя макроса - $2. Остальная часть строки становится телом макроса. Следующая строка ввода затем выбирается с помощью next. Второе правило просто перебирает все определенные макросы, выполняя глобальную замену каждого макроса его телом в строке ввода, а затем выводя строку. Подумайте о компромиссах в этой версии простоты и времени выполнения программы.

В следующей версии (m0b) добавлено включение файлов:

function dofile(fname) {
    while (getline  0) {
        if (/^@define[ \t]/) {        # @define name value
            name = $2
            $1 = $2 = ""; sub(/^[ \t]+/, "")
            symtab[name] = $0
        } else if (/^@include[ \t]/)  # @include filename
            dofile($2)
        else {                        # Anywhere in line @name@
            for (i in symtab)
                gsub("@" i "@", symtab[i])
            print
        }
    }
    close(fname)
}
BEGIN {
    if (ARGC == 2)
        dofile(ARGV[1])
    else
        dofile("/dev/stdin")
}

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

После всего этого введения, вот полноценная программа m1.

#! /bin/awk -f
# NAME
#
# m1
#
# USAGE
#
# awk -f m1.awk [file...]
#
# DESCRIPTION
#
# M1 copies its input file(s) to its output unchanged except as modified by
# certain "macro expressions."  The following lines define macros for
# subsequent processing:
#
#     @comment Any text
#     @@                     same as @comment
#     @define name value
#     @default name value    set if name undefined
#     @include filename
#     @if varname            include subsequent text if varname != 0
#     @unless varname        include subsequent text if varname == 0
#     @fi                    terminate @if or @unless
#     @ignore DELIM          ignore input until line that begins with DELIM
#     @stderr stuff          send diagnostics to standard error
#
# A definition may extend across many lines by ending each line with
# a backslash, thus quoting the following newline.
#
# Any occurrence of @name@ in the input is replaced in the output by
# the corresponding value.
#
# @name at beginning of line is treated the same as @name@.
#
# BUGS
#
# M1 is three steps lower than m4.  You'll probably miss something
# you have learned to expect.
#
# AUTHOR
#
# Jon L. Bentley, jlb@research.bell-labs.com
#

function error(s) {
    print "m1 error: " s | "cat 1>&2"; exit 1
}

function dofile(fname,  savefile, savebuffer, newstring) {
    if (fname in activefiles)
        error("recursively reading file: " fname)
    activefiles[fname] = 1
    savefile = file; file = fname
    savebuffer = buffer; buffer = ""
    while (readline() != EOF) {
        if (index($0, "@") == 0) {
            print $0
        } else if (/^@define[ \t]/) {
            dodef()
        } else if (/^@default[ \t]/) {
            if (!($2 in symtab))
                dodef()
        } else if (/^@include[ \t]/) {
            if (NF != 2) error("bad include line")
            dofile(dosubs($2))
        } else if (/^@if[ \t]/) {
            if (NF != 2) error("bad if line")
            if (!($2 in symtab) || symtab[$2] == 0)
                gobble()
        } else if (/^@unless[ \t]/) {
            if (NF != 2) error("bad unless line")
            if (($2 in symtab) && symtab[$2] != 0)
                gobble()
        } else if (/^@fi([ \t]?|$)/) { # Could do error checking here
        } else if (/^@stderr[ \t]?/) {
            print substr($0, 9) | "cat 1>&2"
        } else if (/^@(comment|@)[ \t]?/) {
        } else if (/^@ignore[ \t]/) { # Dump input until $2
            delim = $2
            l = length(delim)
            while (readline() != EOF)
                if (substr($0, 1, l) == delim)
                    break
        } else {
            newstring = dosubs($0)
            if ($0 == newstring || index(newstring, "@") == 0)
                print newstring
            else
                buffer = newstring "\n" buffer
        }
    }
    close(fname)
    delete activefiles[fname]
    file = savefile
    buffer = savebuffer
}

# Put next input line into global string "buffer"
# Return "EOF" or "" (null string)

function readline(  i, status) {
    status = ""
    if (buffer != "") {
        i = index(buffer, "\n")
        $0 = substr(buffer, 1, i-1)
        buffer = substr(buffer, i+1)
    } else {
        # Hume: special case for non v10: if (file == "/dev/stdin")
        if (getline <file <= 0)
            status = EOF
    }
    # Hack: allow @Mname at start of line w/o closing @
    if ($0 ~ /^@[A-Z][a-zA-Z0-9]*[ \t]*$/)
        sub(/[ \t]*$/, "@")
    return status
}

function gobble(  ifdepth) {
    ifdepth = 1
    while (readline() != EOF) {
        if (/^@(if|unless)[ \t]/)
            ifdepth++
        if (/^@fi[ \t]?/ && --ifdepth <= 0)
            break
    }
}

function dosubs(s,  l, r, i, m) {
    if (index(s, "@") == 0)
        return s
    l = ""  # Left of current pos; ready for output
    r = s   # Right of current; unexamined at this time
    while ((i = index(r, "@")) != 0) {
        l = l substr(r, 1, i-1)
        r = substr(r, i+1)  # Currently scanning @
        i = index(r, "@")
        if (i == 0) {
            l = l "@"
            break
        }
        m = substr(r, 1, i-1)
        r = substr(r, i+1)
        if (m in symtab) {
            r = symtab[m] r
        } else {
            l = l "@" m
            r = "@" r
        }
    }
    return l r
}

function dodef(fname,  str, x) {
    name = $2
    sub(/^[ \t]*[^ \t]+[ \t]+[^ \t]+[ \t]*/, "")  # OLD BUG: last * was +
    str = $0
    while (str ~ /\\$/) {
        if (readline() == EOF)
            error("EOF inside definition")
        # OLD BUG: sub(/\\$/, "\n" $0, str)
        x = $0
        sub(/^[ \t]+/, "", x)
        str = substr(str, 1, length(str)-1) "\n" x
    }
    symtab[name] = str
}

BEGIN { EOF = "EOF"
    if (ARGC == 1)
        dofile("/dev/stdin")
    else if (ARGC >= 2) {
        for (i = 1; i < ARGC; i++)
            dofile(ARGV[i])
    } else
        error("usage: m1 [fname...]")
}

13.10.1 Примечания к программе m1

Программа имеет красивую модульную структуру, с функцией error(), аналогичной той, которая представлена в Главе 11, Семейство awk, и каждая задача четко разделена на отдельные функции.

Основная программа находится в процедуре BEGIN внизу. Она просто обрабатывает либо стандартный ввод, если нет аргументов, либо все файлы, указанные в командной строке.

Обработка высокого уровня происходит в функции dofile(), которая читает по одной строке за раз и решает, что делать с каждой строкой. Массив activefiles отслеживает открытые файлы. Переменная fname указывает текущий файл для чтения данных. Когда отображается директива «@include», dofile() просто рекурсивно вызывает себя для нового файла, как в m0b. Интересно, что включенное имя файла сначала обрабатывается для макросов. Внимательно прочтите эту функцию - здесь есть несколько интересных приемов.

Функция readline() управляет «откатом». После раскрытия макроса макропроцессоры проверяют вновь созданный текст на наличие дополнительных имен макросов. Только после того, как весь развернутый текст обработан и отправлен на выход, программа получает новую строку ввода.

Функция dosubs() фактически выполняет подстановку макросов. Она обрабатывает строку слева направо, заменяя имена макросов их телами. Повторное сканирование новой строки предоставляется логике более высокого уровня, которой совместно управляют readline() и dofile(). Эта версия значительно более эффективна, чем метод грубой силы, используемый в программах m0.

Наконец, функция dodef() обрабатывает определение макросов. Она сохраняет имя макроса из $2, а затем использует sub() для удаления первых двух полей. Новое значение $0 теперь содержит только (первую строку) тело макроса. В статье в Computer Language объясняется, что sub() используется специально, чтобы сохранить пробелы в теле макроса. Простое присвоение пустой строки $1 и $2 приведет к перестроению записи, но все вхождения пробелов будут свернуты в отдельные вхождения значения OFS (один пробел). Затем функция переходит к сбору остальной части тела макроса, обозначенной строками, заканчивающимися знаком «\». Это дополнительное улучшение по сравнению с m0: тела макросов могут иметь длину более одной строки.

Остальная часть программы связана с условным включением или исключением текста; эта часть проста. Что приятно, так это то, что эти условные выражения можно вкладывать друг в друга.

m1 - очень хорошее начало для макропроцессора. Возможно, вы захотите подумать о том, как бы вы могли его расширить; например, разрешив условным операторам иметь предложение «@else»; обработку командной строки для определения макросов; «неопределенные» макросы и другие вещи, которые обычно делают макропроцессоры.

Некоторые другие расширения, предложенные Джоном Бентли:

  1. Добавьте сюда строку оболочки «@shell DELIM», которая считывала бы строки ввода до «DELIM» и отправляла развернутый вывод через конвейер данной команде оболочки.
  2. Добавьте команды «@longdef» и «@longend». Эти команды будут определять макросы с длинными телами, то есть макросы, которые занимают более одной строки, что упрощает логику в dodoef().
  3. Добавьте «@append MacName MoreText», например «.am» в troff. Этот макрос в troff добавляет текст к уже определенному макросу. В m1 это позволит вам добавить к телу уже определенного макроса.
  4. Избегайте использования специального файла V10 /dev/stdin. В системах UNIX * Bell Labs есть специальный файл с именем /dev/stdin, который дает вам доступ к стандартному вводу. Мне приходит в голову, что использование «-» вполне переносимо поможет. Это также не проблема, если вы используете gawk или Bell Labs awk, которые внутренне интерпретируют специальное имя файла /dev/stdin (см. Главу 11).

В заключение, Джон часто использует awk в двух своих книгах, Programming Pearls и More Programming Pearls - Confessions of a Coder (обе опубликованы Addison-Wesley). Обе эти книги прекрасно читаются.

* И в некоторых других системах UNIX.

Приложение A.
Краткий справочник по sed

A.1 Синтаксис командной строки

Синтаксис для вызова sed имеет две формы:

sed [-n][-e] 'command' file(s)
sed [-n] -f scriptfile file(s)

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

Распознаются следующие варианты:

-n
Печатает только строки, указанные с помощью команды p или флага p команды s.
-e cmd
Следующий аргумент - это команда редактирования. Полезно, если указано несколько скриптов.
-f file
Следующий аргумент - файл, содержащий команды редактирования.

Если первая строка сценария - «#n», sed ведет себя так, как если бы была указана опция -n.

Часто используемые сценарии sed обычно вызываются из сценария оболочки. Так как это то же самое для sed и awk, см. раздел «Shell-оболочка для вызова awk» в Приложении B, Краткий справочник по awk.

A.2 Синтаксис команд sed

Команды Sed имеют общий вид:

[address[,address]][!]command [arguments]

Sed копирует каждую строку ввода в буфер шаблона. Инструкции Sed состоят из адресов и команд редактирования. Если адрес команды совпадает со строкой в буфере шаблона, то команда применяется к этой строке. Если у команды нет адреса, она применяется к каждой строке ввода. Если команда изменяет содержимое буфера, последующие адреса команд будут применены к текущей строке в буфере шаблона, а не к исходной строке ввода.

A.2.1 Шаблонная адресация

address может быть либо номером строки, либо pattern, заключенным в косую черту (/pattern/). Шаблон описывается с помощью регулярного выражения. Кроме того, \n может использоваться для сопоставления любой новой строки в буфере шаблона (полученной в результате команды N), но не для новой строки в конце буфера шаблона.

Если шаблон не указан, команда будет применена ко всем строкам. Если указан только один адрес, команда будет применена ко всем строкам, соответствующим этому адресу. Если указаны два адреса, разделенных запятыми, команда будет применена к диапазону строк между первым и вторым адресами включительно. Некоторые команды принимают только один адрес: a, i, r, q и =.

Оператор !, следующий за адресом, заставляет sed применить команду ко всем строкам, которые не соответствуют адресу.

Фигурные скобки ({}) используются в sed для вложения одного адреса в другой или для применения нескольких команд к одному и тому же адресу.

[/pattern/[,/pattern/]]{
command1
command2
}

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

A.2.2 Метасимволы регулярных выражений для sed

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

Обратите внимание, что пустое регулярное выражение «//» совпадает с предыдущим регулярным выражением.

Таблица A.1: Метасимволы шаблонов

Спецсимволы Использование
. Соответствует любому одиночному символу, кроме новой строки.
* Соответствует любому количеству (включая ноль) одиночного символа (включая символ, заданный регулярным выражением), который непосредственно предшествует ему.
[...] Соответствует любому из классов символов, заключенных в квадратные скобки. Все остальные метасимволы теряют свое значение, если они указаны как члены класса. Крышка (^) в качестве первого символа внутри скобок меняет соответствие всем символам, кроме символов новой строки и перечисленных в классе. Дефис (-) используется для обозначения диапазона символов. Закрывающая скобка (]) в качестве первого символа в классе является членом класса.
\{n,m\} Соответствует диапазону вхождений одного символа (включая символ, заданный регулярным выражением), который непосредственно предшествует ему. \{n\} будет соответствовать ровно n вхождениям, \{n,\} будет соответствовать как минимум n вхождениям, а \{n,m\} будет соответствовать любому количеству вхождений от n до m. (только sed и grep).
^ Находит регулярное выражение, которое следует в начале строки. Знак ^ является особенным только тогда, когда он встречается в начале регулярного выражения.
$ Находит предыдущее регулярное выражение в конце строки. Символ $ является особенным только тогда, когда он встречается в конце регулярного выражения.
\ Экранирует следующий специальный символ.
\( \) Сохраняет шаблон, заключенный между «\(» и «\)», в специальное место хранения. Таким образом можно сохранить до девяти шаблонов в одной строке. Их можно «воспроизвести» при замене с помощью управляющих последовательностей от «\1» до «\9».
\n Соответствует n-му шаблону, ранее сохраненному с помощью «\(» и «\)», где n - это число от 1 до 9, а ранее сохраненные шаблоны считаются в строке слева направо.
& Печатает весь совпадающий текст при использовании в строке замены.

A.3 Список команд sed

: :label
Помечает строку в скрипте для передачи управления буквами b или t. label может содержать до семи символов. (Стандарт POSIX говорит, что реализация может разрешить более длинные метки, если она того пожелает. GNU sed позволяет меткам быть любой длины.)
= [address]=
Записывает в стандартный вывод номер адресуемой строки.
a [address]a\ text
Добавляет text после каждой строки, соответствующей address. Если text занимает более одной строки, символы новой строки необходимо «скрыть», поставив перед ними обратную косую черту. text будет завершен первой новой строкой, которая не скрыта таким образом. text недоступен в буфере шаблона, и к нему нельзя применить последующие команды. Результаты этой команды отправляются на стандартный вывод, когда список команд редактирования завершен, независимо от того, что происходит с текущей строкой в буфере шаблона.
b [address1[,address2]]b[label]
Безоговорочно передаетт управление (ветвь) метке :label в другом месте скрипта. То есть команда, следующая за label, является следующей командой, примененной к текущей строке. Если label не указана, управление переходит в конец скрипта, поэтому команды к текущей строке больше не применяются.
c [address1[,address2]]c\ text
Заменить (изменить) строки, выделенные адресом, на text. Если указан диапазон строк, все строки как группа заменяются одной копией text. Новая строка после каждой строки text должна быть экранирована обратной косой чертой, кроме последней строки. Содержимое буфера шаблона, по сути, удаляется, и никакие последующие команды редактирования не могут быть применены к нему (или к text).
d [address1[,address2]]d
Удаляет строку (или строки) из буфера шаблона. Таким образом, строка не передается на стандартный вывод. Считывается новая строка ввода, и редактирование возобновляется с первой команды в скрипте.
D [address1[,address2]]D
Удаляет первую часть (до встроенной новой строки) многострочного буфера шаблона, созданного командой N, и возобновляет редактирование с помощью первой команды в скрипте. Если эта команда очищает буфер шаблона, то читается новая строка ввода, как если бы была выполнена команда d.
g [address1[,address2]]g
Копирует (получает) содержимое буфера хранения (см. команду h или H) в буфер шаблона, удалив предыдущее содержимое.
G [address1[,address2]]G
Добавляет новую строку, за которой следует содержимое буфера хранения (см. команду h или H) к содержимому буфера шаблона. Если буфер хранения пуст, к буферу шаблона все равно добавляется новая строка.
h [address1[,address2]]h
Копирует буфер шаблона в буфер хранения, специальный временный буфер. Предыдущее содержимое буфера хранения стирается.
H [address1[,address2]]H
Добавляет новую строку и содержимое буфера шаблона к содержимому буфера хранения. Даже если буфер хранения пуст, эта команда все равно сначала добавляет новую строку.
i [address]i\ text
Вставляет text перед каждой строкой, соответствующей address. (См. a для подробностей о text.)
l [address1[,address2]]l
Выводит содержимое буфера шаблона, показывая непечатаемые символы как коды ASCII. Переносит длинные строки.
n [address1[,address2]]n
Читает следующую строку ввода в буфер шаблона. Текущая строка отправляется на стандартный вывод. Новая строка становится текущей строкой и увеличивает счетчик строк. Управление передается команде, следующей за n, а не возобновляется в верхней части скрипта.
N [address1[,address2]]N
Добавляет следующую строку ввода к содержимому буфера шаблона; новая строка отделяется от предыдущего содержимого буфера шаблона новой строкой. (Эта команда предназначена для сопоставления шаблонов в двух строках. Используя \n для сопоставления встроенной новой строки, вы можете сопоставить шаблоны в нескольких строках.)
p [address1[,address2]]p
Выводит адресную строку (строки). Обратите внимание, что это может привести к дублированию вывода, если вывод по умолчанию не подавлен с помощью «#n» или параметра командной строки -n. Обычно используется перед командами, которые изменяют управление потоком (d, n, b) и могут предотвратить вывод текущей строки.
P [address1[,address2]]P
Распечатать первую часть (до встроенной новой строки) многострочного буфера шаблона, созданного командой N. То же, что и p, если N не было применено к строке.
q [address]q
Выходит при обнаружении address. Адресная строка сначала записывается в вывод (если вывод по умолчанию не подавляется) вместе с любым текстом, добавленным к ней предыдущими командами a или r.
r [address]r file
Читает содержимое file и добавляет его после содержимого буфера шаблона. Между r и именем файла необходимо поставить ровно один пробел.
s [address1[,address2]]s/pattern/replacement/[flags]

Заменяет replacement на pattern в каждой адресной строке. Если используются адреса шаблона, шаблон // представляет последний указанный адрес шаблона. Могут быть указаны следующие флаги:

n
Заменяет n-й экземпляр /pattern/ в каждой адресной строке. n - любое число от 1 до 512, по умолчанию 1.
g
Заменяет все экземпляры /pattern/ в каждой адресной строке, а не только в первом экземпляре.
p
Выводит строку, если замена выполнена успешно. Если выполнено несколько успешных замен, будет напечатано несколько копий строки.
w file
Записывает строку в file, если была произведена замена. Можно открыть до 10 разных files.
t [address1[,address2]]t[label]
Проверяет, были ли выполнены успешные замены в адресных строках, и если да, переходит к строке, отмеченной как :label. (См. b и :.) Если метка не указана, управление переходит в конец скрипта.
w [address1[,address2]]w label
Добавляет в file содержимое буфера шаблона. Это действие происходит при обнаружении команды, а не при выводе буфера шаблона. Ровно один пробел должен разделять w и имя файла. В скрипте можно открыть до 10 различных файлов. Эта команда создаст файл, если он не существует; если файл существует, его содержимое будет перезаписываться каждый раз при выполнении скрипта. Несколько команд записи, которые направляют вывод в один и тот же файл, добавляются в конец файла.
x [address1[,address2]]x
Обменивает содержимое буфера шаблона на содержимое буфера хранения.
y [address1[,address2]]y /abc/xyz/
Преобразовывает каждый символ по позиции в строке abc в его эквивалент в строке xyz.

Приложение B.
Краткий справочник по awk

В этом приложении описываются возможности языка сценариев awk.

B.1 Синтаксис командной строки

Синтаксис для вызова awk имеет две основные формы:


awk [-v var=value] [-Fre] [--] 'pattern { action }' var=value datafile(s)
awk [-v var=value] [-Fre] -f scriptfile [--] var=value datafile(s)

Командная строка awk состоит из команды, сценария и имени входного файла. Ввод считывается из файла, указанного в командной строке. Если входного файла нет или указано «-», то читается стандартный ввод. Параметр -F устанавливает разделитель полей (FS) в re.

Параметр -v устанавливает для переменной var значение value до выполнения сценария. Это происходит еще до запуска процедуры BEGIN. (См. обсуждение параметров командной строки ниже.)

В соответствии с соглашениями о синтаксическом анализе аргументов POSIX опция «-» отмечает конец параметров командной строки. Используя эту опцию, например, вы можете указать datafile, начинающийся с «-», который иначе можно было бы спутать с опцией командной строки.

Вы можете указать сценарий, состоящий из pattern и action, в командной строке, заключенный в одинарные кавычки. Кроме того, вы можете поместить сценарий в отдельный файл и указать имя scriptfile в командной строке с параметром -f.

Параметры можно передать в awk, указав их в командной строке после сценария. Это включает установку системных переменных, таких как FS, OFS и RS. value может быть литералом, переменной оболочки ($var) или результатом команды ('cmd'); он должен быть заключен в кавычки, если он содержит пробелы или табуляции. Можно указать любое количество параметров.

Параметры командной строки недоступны до тех пор, пока не будет прочитана первая строка ввода, и поэтому к ним нельзя получить доступ в процедуре BEGIN. (В более старых реализациях awk и nawk перед запуском процедуры BEGIN обрабатывались ведущие назначения в командной строке. Это противоречило тому, как все было задокументировано в The AWK Programming Language, который гласит, что они обрабатываются, когда awk открывает их как имена файлов, то есть после процедуры BEGIN. Bell Labs awk был изменен, чтобы исправить это, и опция -v была добавлена в то же время, в начале 1989 года. Теперь это часть POSIX awk.) Параметры оцениваются в том порядке, в котором они появляются в командной строке до тех пор, пока не будет распознано имя файла. Параметры, появляющиеся после этого имени файла, будут доступны, когда будет распознано следующее имя файла.

B.1.1 Скрипт оболочки для вызова awk

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

Вы можете поместить командную строку, вызывающую awk, в файл, присвоив ей имя, определяющее, что делает сценарий. Сделайте этот файл исполняемым (с помощью команды chmod) и поместите его в каталог, где хранятся локальные команды. Имя сценария оболочки можно ввести в командной строке для выполнения сценария awk. Это предпочтительнее для легко используемых и повторно используемых скриптов.

В современных системах UNIX, включая Linux, вы можете использовать синтаксис #! для создания автономных сценариев awk:

#! /usr/bin/awk -f
script

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

B.2 Резюме по языку awk

В этом разделе кратко описывается, как awk обрабатывает входные записи, и описываются различные синтаксические элементы, составляющие awk-программу.

B.2.1 Записи и поля

Каждая строка ввода разбита на поля. По умолчанию разделителем полей является один или несколько пробелов и/или табуляции. Вы можете изменить разделитель полей с помощью параметра командной строки -F. При этом также устанавливается значение FS. Следующая командная строка изменяет разделитель полей на двоеточие:

awk -F: -f awkscr /etc/passwd

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

awk -f awkscr FS=: /etc/passwd

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

{ print $1 }

Разделителем записей по умолчанию является новая строка. Следующая процедура устанавливает FS и RS так, чтобы awk интерпретировал входную запись как любое количество строк вплоть до пустой строки, причем каждая строка является отдельным полем.

BEGIN { FS = "\n"; RS = "" }

Важно знать, что когда RS установлен в пустую строку, новая строка всегда разделяет поля в дополнение к любому значению, которое может иметь FS. Это обсуждается более подробно как в The AWK Programming Language и Effective AWK Programming.

B.2.2 Формат скрипта

Сценарий awk - это набор правил сопоставления с образцом и действий:

pattern { action }

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

{ print }

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

Также могут появиться определения функций:

function name (parameter list) { statements }

Этот синтаксис определяет функцию name, делая доступным список параметров для обработки в теле функции. Переменные, указанные в списке параметров, обрабатываются как локальные переменные внутри функции. Все остальные переменные являются глобальными и доступны вне функции. При вызове пользовательской функции не допускается использование пробела между именем функции и открывающей скобкой. В определении функции разрешены пробелы. Определяемые пользователем функции описаны в Главе 9, Функции.

B.2.2.1 Окончание строки

Строка в сценарии awk завершается новой строкой или точкой с запятой. Использование точки с запятой для размещения нескольких операторов в строке, хотя и разрешено, снижает удобочитаемость большинства программ. Между операторами разрешены пустые строки.

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

if (NF > 1) {
    name = $1
    total += $2
}

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

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

, { && ||

Gawk также позволяет вам продолжить строку после «?» или «:». Литеральные строки не могут быть разделены переносом строки (кроме gawk, где используется символ «\», за которым следует новая строка).

B.2.2.2 Комментарии

Комментарий начинается с символа «#» и заканчивается новой строкой. Он может отображаться отдельно или в конце строки. Комментарии - это описательные замечания, объясняющие работу сценария. Комментарии не могут быть продолжены за строкой, которая завершена обратной косой чертой.

B.2.3 Шаблоны

Шаблон может быть любым из следующих:

/regular expression/
relational expression
BEGIN
END
pattern, pattern
  1. Регулярные выражения используют расширенный набор метасимволов и должны быть заключены в косую черту. Полное обсуждение регулярных выражений см. в Главе 3, Понимание синтаксиса регулярных выражений.
  2. В выражениях сравнения используются операторы сравнения, перечисленные в разделе «Выражения» далее в этой главе.
  3. Шаблон BEGIN применяется до чтения первой строки ввода, а шаблон END применяется после чтения последней строки ввода.
  4. Используйте !, чтобы свести совпадение к противоположному; т.е. для обработки строк, не соответствующих шаблону.
  5. Как и в sed, вы можете адресовать ряд строк:

    pattern, pattern

    Шаблоны, кроме BEGIN и END, можно выразить в составных формах с помощью следующих операторов:

    && Логическое И
    || Логическое ИЛИ

    Версия nawk от Sun (SunOS 4.1.x) не поддерживает обработку регулярных выражений как частей более крупного логического выражения. Например, «/cute/ && /sweet/» или «/fast/ || /quick/» не работают.

    Кроме того, в шаблоне можно использовать условный оператор ? языка C: (pattern ? pattern : pattern).

  6. Образцы можно заключать в скобки, чтобы обеспечить правильную оценку.
  7. Шаблоны BEGIN и END должны быть связаны с действиями. Если написано несколько правил BEGIN и END, они перед применением объединяются в одно правило.

B.2.4 Регулярные выражения

В Таблице B.1 приведены регулярные выражения, описанные в Главе 3. Метасимволы перечислены в порядке их приоритета.

Таблица B.1: Метасимволы регулярных выражений

Спецсимволы Использование
с Соответствует любому буквальному символу c, не являющемуся метасимволом.
\ Экранирует все последующие метасимволы, включая самого себя.
^ Привязывает следующее регулярное выражение к началу строки.
$ Привязывает предшествующее регулярное выражение к концу строки.
. Соответствует любому одиночному символу, включая новую строку.
[...] Соответствует любому из классов символов, заключенных в квадратные скобки. Крышка (^) в качестве первого символа в скобках отменяет соответствие всем символам, кроме перечисленных в классе. Дефис (-) используется для обозначения диапазона символов. Закрывающая скобка (]) в качестве первого символа в классе является членом класса. Все остальные метасимволы теряют свое значение, если они указаны как члены класса, за исключением \, который может использоваться для экранирования ], даже если он не первый.
r1 | r2 Между двумя регулярными выражениями, r1 и r2, позволяет сопоставить любое из регулярных выражений.
(r1)(r2) Используется для объединения регулярных выражений.
r* Соответствует любому числу (включая ноль) регулярного выражения, которое непосредственно ему предшествует.
r+ Соответствует одному или нескольким вхождениям предыдущего регулярного выражения.
r? Соответствует 0 или 1 вхождению предыдущего регулярного выражения.
(r) Используется для группировки регулярных выражений.

Регулярные выражения также могут использовать escape-последовательности для доступа к специальным символам, как определено в разделе «Escape-последовательности» далее в этом приложении.

Обратите внимание, что ^ и $ работают со строками; они не совпадают с символами новой строки, встроенными в запись или строку.

В скобках POSIX допускает специальные обозначения для сопоставления неанглийских символов. Они описаны в Таблице B.2.

Таблица B.2: Возможности списка символов POSIX

Обозначение Объект
[.symbol.] Сопоставление символов. Символ упорядочения - это многосимвольная последовательность, которую следует рассматривать как единое целое.
[=equiv=] Классы эквивалентности. Класс эквивалентности перечисляет набор символов, которые следует считать эквивалентными, например «e» и «è».
[:class:] Классы символов. Ключевые слова класса символов описывают разные классы символов, такие как буквенные символы, управляющие символы и т. д.
[:alnum:] Буквенно-цифровые символы
[:alpha:] Буквенные символы
[:blank:] Пробелы и символы табуляции
[:cntrl:] Управляющие символы
[:digit:] Цифровые символы
[:graph:] Печатные и видимые (не пробельные) символы
[:lower:] Строчные буквы
[:print:] Печатные символы
[:punct:] Знаки пунктуации
[:space:] Пробельные символы
[:upper:] Заглавные буквы
[:xdigit:] Шестнадцатеричные цифры

Обратите внимание, что эти возможности (на момент написания статьи) еще не получили широкого распространения.

B.2.5 Выражения

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

B.2.5.1 Константы

Есть два типа констант: строковые и числовые. Строковая константа должна быть заключена в кавычки, а числовая - нет.

B.2.5.2 Управляющие последовательности

Управляющие последовательности, описанные в Таблице B.3, можно использовать в строках и регулярных выражениях.

Таблица B.3: Управляющие последовательности

Последовательность Описание
\a Предупреждающий символ, обычно символ ASCII BEL
\b Backspace
\f Подача бумаги
\n Новая строка
\r Возврат каретки
\t Горизонтальная табуляция
\v Вертикальная табуляция
\ddd Символ, представленный в виде восьмеричного числа от 1 до 3 цифр
\xhex Символ, представленный в виде шестнадцатеричного значенияa
\c Любой литеральный символ c (например, \"for")b

a POSIX не предоставляет «\x», но он общедоступен.

b Как и ANSI C, POSIX намеренно не определяет, что вы получите, если поставите обратную косую черту перед любым символом, не указанным в таблице. В большинстве awk вы просто получаете этот символ.

B.2.5.3 Переменные

Есть три типа переменных: определяемые пользователем, встроенные и поля. По соглашению, имена встроенных или системных переменных состоят из заглавных букв.

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

Переменную не нужно объявлять или инициализировать. Переменная может содержать строковое или числовое значение. Неинициализированная переменная содержит пустую строку ("") в качестве строкового значения и 0 в качестве числового значения. Awk пытается решить, следует ли обрабатывать значение как строку или число в зависимости от операции.

Присвоение переменной имеет вид:

var = expr

Происходит присваивание значения выражения переменной var. Следующее выражение присваивает переменной x значение 1.

x = 1

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

{ print x }

печатает значение переменной x. В этом случае это будет 1.

См. раздел «Системные переменные» ниже для получения информации о встроенных переменных. Ссылка на переменную поля осуществляется с помощью $n, где n - любое число от 0 до NF, которое ссылается на поле по позиции. Это может быть переменная, например $NF, означающая последнее поле, или константа, например $1, означающая первое поле.

B.2.5.4 Массивы

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

array[index] = value

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

Ассоциативный массив создает «ассоциацию» между индексами и элементами массива. Для каждого элемента массива поддерживается пара значений: индекс элемента и значение элемента. Элементы не хранятся в каком-либо определенном порядке, как в обычном массиве.

Вы можете использовать специальный цикл for для чтения всех элементов ассоциативного массива.

for ( item in array )

Индекс массива доступен как item, в то время как значение элемента массива может указываться как array[item].

Вы можете использовать оператор in, чтобы проверить, существует ли элемент, проверяя, существует ли его индекс.

if (index in array)

проверяет, существует ли array[index], но вы не можете использовать его для проверки значения элемента, на который ссылается array[index].

Вы также можете удалить отдельные элементы массива с помощью оператора delete.

B.2.5.5 Системные переменные

Awk определяет ряд специальных переменных, на которые можно ссылаться или сбрасывать внутри программы, как показано в Таблице B.4 (значения по умолчанию указаны в скобках).

Таблица B.4: Системные переменные Awk

Переменная Описание
ARGC Количество аргументов в командной строке
ARGV Массив, содержащий аргументы командной строки
CONVFMT Формат преобразования строк для чисел (%.6g). (POSIX)
ENVIRON Ассоциативный массив переменных среды.
FILENAME Текущее имя файла
FNR Подобно NR, но относительно текущего файла
FS Разделитель полей (пробел)
NF Количество полей в текущей записи
NR Номер текущей записи
OFMT Формат вывода для чисел (%.6g)
OFS Разделитель полей вывода (пробел)
ORS Разделитель выходной записи (новая строка)
RLENGTH Длина строки, соответствующей функции match()
RS Разделитель записей (новая строка)
RSTART Первая позиция в строке, соответствующая функции match()
SUBSEP Символ-разделитель для индексов массива (\034)
B.2.5.6 Операторы

В Таблице B.5 перечислены операторы в порядке приоритета (от низкого к высокому), доступные в awk.

Таблица B.5: Операторы

Операторы Описание
= += -= *= /= %= ^= **= Присваивание
?: Условное выражение C (Си)
|| Логическое ИЛИ
&& Логическое И
! Соответствие регулярному выражению и отрицанию
< <= > >= != == Операторы сравнения
(пусто) Конкатенация
+ - Сложение, вычитание
* / % Умножение, деление и деление по модулю
+ - ! Унарный плюс и минус и логическое отрицание
^ ** Возведение в степень
++ -- Увеличение и уменьшение, префикс или постфикс
$ Ссылка на поле

NOTE: Хотя «**» и «**=» являются общими расширениями, они не являются частью POSIX awk.

B.2.6 Операторы и функции

Действие заключено в фигурные скобки и состоит из одного или нескольких операторов и/или выражений. Разница между оператором и функцией заключается в том, что функция возвращает значение, а ее список аргументов указывается в круглых скобках. (Формальное синтаксическое различие не всегда верно: printf считается оператором, но его список аргументов может быть заключен в круглые скобки; getline - это функция, которая не использует круглые скобки.)

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

return = function(arg1,arg2)

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

B.3 Список команд awk

Следующий алфавитный список операторов и функций включает все, что доступно в POSIX awk, nawk или gawk. См. Главу 11, Семейство awk, где описаны расширения, доступные в различных реализациях.

atan2()
atan2(y, x)

Возвращает арктангенс y/x в радианах.

break
Выйти из цикла while, for или do.
close()
close(filename-expr)
close(command-expr)

В большинстве реализаций awk вы можете одновременно открывать только ограниченное количество файлов и/или каналов. Таким образом, awk предоставляет функцию close(), которая позволяет закрыть файл или канал. В качестве аргумента она принимает то же выражение, которое открыло канал или файл. Это выражение должно точно совпадать с тем, которое открывало файл или канал, даже пробелы имеют значение.

continue
Начать следующую итерацию цикла while, for или do.
cos()
cos(x)

Возвращает косинус x в радианах.

delete
delete array[element]

Удаляет элемент массива.

do
do
    body
while (expr)

Оператор цикла. Выполнить операторы в body, затем оценить выражение expr и, если оно истинно, снова выполнить body.

exit
exit [expr]

Выйти из скрипта, не читая нового ввода. Правило END, если оно существует, будет выполнено. Необязательное выражение expr становится возвращаемым значением awk.

exp()
exp(x)

Возвращает экспоненту x (e^x).

for
for (init-expr; test-expr; incr-expr) statement

Циклическая конструкция в стиле C. init-expr присваивает начальное значение переменной счетчика. test-expr - это выражение сравнения, которое вычисляется каждый раз перед выполнением инструкции. Когда test-expr ложно, цикл завершается. incr-expr используется для увеличения значения счетчика после каждого прохода.

for (item in array) statement

Специальный цикл, предназначенный для чтения ассоциативных массивов. Для каждого элемента массива выполняется инструкция statement; на элемент может ссылаться array[item].

getline

Считывает следующую строку ввода.

getline [var] [<file]
command | getline [var]

Первая форма считывает ввод из file, а вторая форма считывает вывод command. Обе формы читают по одной строке за раз, и каждый раз, когда инструкция выполняется, она получает следующую строку ввода. Строке ввода присваивается $0, и она разбирается на поля, устанавливая NF, NR и FNR. Если var указана, результат присваивается переменной var и $0 не изменяется. Таким образом, если результат присваивается переменной, текущая строка не изменяется. getline на самом деле является функцией и возвращает 1, если она успешно читает запись, 0, если встречается конец строки, и -1, если по какой-то причине это не удалось.

gsub()
gsub(r, s, t)

Глобально заменяет s для каждого совпадения регулярного выражения r в строке t. Возвращает количество замен. Если t не указан, по умолчанию используется $0.

if
if (expr) statement1
[ else statement2 ]

Условный оператор. Вычислить выражение expr и, если оно истинно, выполнить оператор statement1; если предоставляется предложение else, выполнить оператор statement2, если expr ложно.

index()
index(str, substr)

Возвращает позицию (начиная с 1) подстроки в строке.

int()
int(x)

Возвращает целочисленное значение x путем усечения любых цифр после десятичной точки.

length()
length(str)

Возвращает длину строки или длину $0, если аргумент отсутствует.

log()
log(x)

Вернуть натуральный логарифм (по основанию e) числа x.

match()
match(s, r)

Функция, которая соответствует шаблону, указанному регулярным выражением r, в строке s и возвращает либо позицию в s, где начинается совпадение, либо 0, если вхождений не найдено. Устанавливает значения RSTART и RLENGTH для начала и длины совпадения соответственно.

next
Считывает следующую строку ввода и начинает выполнение сценария с первого правила.
print
print [ output-expr ] [ dest-expr ]

Вычисляет output-expr и направляет его в стандартный вывод, за которым следует значение ORS. Каждое output-expr отделяется значением OFS. dest-expr - необязательное выражение, которое направляет вывод в файл или канал. «> file» направляет вывод в файл, перезаписывая его предыдущее содержимое. «>> file» обавляет вывод в файл, сохраняя его предыдущее содержимое. В обоих случаях файл будет создан, если он еще не существует. «| command» направляет вывод как ввод для системной команды.

printf
printf (format-expr [, expr-list ]) [ dest-expr ]

Альтернативный оператор вывода, заимствованный из языка C. Он может производить форматированный вывод. Его также можно использовать для вывода данных без автоматического создания новой строки. format-expr - это строка спецификаций формата и констант; список спецификаторов формата см. в следующем разделе. expr-list - это список аргументов, соответствующих спецификаторам формата. См. описание команды print для описания dest-expr.

rand()
rand()

Генерация случайного числа от 0 до 1. Эта функция возвращает одну и ту же серию чисел при каждом выполнении скрипта, если только генератор случайных чисел не заполняется с помощью функции srand().

return
return [expr]

Используется в конце пользовательских функций для выхода из функции, возвращает значение выражения.

sin()
sin(x)

Возвращает синус x в радианах.

split()
split(str, array, sep)

Функция, которая разбирает строку на элементы массива с помощью разделителя полей, возвращая количество элементов в массиве. Значение FS используется, если не указан разделитель полей. Разделение массива работает так же, как разделение полей.

sprintf()
sprintf (format-expr [, expr-list ])

Функция, которая возвращает строку, отформатированную в соответствии со спецификацией формата printf. Она форматирует данные, но не выводит их. format-expr - это строка спецификаций и констант формата; список спецификаторов формата см. в следующем разделе. expr-list - это список аргументов, соответствующих спецификаторам формата.

sqrt()
sqrt(x)

Вернуть квадратный корень из x.

srand()
srand(expr)

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

sub()
sub(r, s, t)

Заменяет s на первое совпадение регулярного выражения r в строке t. Возвращает 1 в случае успеха; 0 в противном случае. Если t не указан, по умолчанию используется $0.

substr()
substr(str, beg, len)

Возвращает подстроку строки str в начальной позиции beg и следующих за ней символов до максимальной указанной длины len. Если длина не указана, используется вся оставшаяся часть строки.

system()
system(command)

Функция, которая выполняет указанную команду command и возвращает ее статус. Статус выполненной команды обычно указывает на успех или неудачу. Значение 0 означает, что команда выполнена успешно. Ненулевое значение, положительное или отрицательное, указывает на какой-либо сбой. Подробная информация содержится в документации к выполняемой вами команде. Вывод команды недоступен для обработки в сценарии awk. Используйте «command | getline», чтобы прочитать вывод команды в скрипт.

tolower()
tolower(str)

Преобразовывает все символы верхнего регистра в строке в нижний регистр и возвращает новую строку*.

toupper()
toupper(str)

Преобразовывает все символы нижнего регистра в строке в верхний регистр и возвращает новую строку.

while()
while(expr) statement

Конструкция цикла. Пока expr истинно, выполнять statement.

* Самые ранние версии nawk, такие как SunOS 4.1.x, не поддерживают tolower() и toupper(). Однако теперь они являются частью спецификации POSIX для awk.

B.3.1 Выражения формата, используемые в printf и sprintf

Выражение формата может принимать три необязательных модификатора, следующих за «%» и предшествующих спецификатору формата:

%-width.precision format-specifier

Ширина поля вывода width - это числовое значение. Когда вы указываете ширину поля, содержимое поля по умолчанию будет выровнено по правому краю. Вы должны указать «-», чтобы получить выравнивание по левому краю. Таким образом, «%20s» выводит строку с выравниванием по левому краю в поле шириной 20 символов. Если строка меньше 20 символов, поле будет заполнено пробелами.

Модификатор точности precision, используемый для десятичных значений или значений с плавающей запятой, управляет количеством цифр, которые появляются справа от десятичной запятой. Для строковых форматов он контролирует количество символов в строке для печати.

Вы можете указать width и precision динамически, используя значения в списке аргументов printf или sprintf. Вы делаете это, указывая звездочки, вместо того, чтобы указывать буквальные значения.

printf("%*.*g\n", 5, 3, myvar);

В этом примере ширина равна 5, точность равна 3, а значение для печати будет получено из myvar. Более старые версии nawk могут не поддерживать это.

Обратите внимание, что точность по умолчанию для вывода числовых значений «%.6g». Значение по умолчанию можно изменить, установив системную переменную OFMT. Это влияет на точность, используемую оператором print при выводе чисел. Например, если вы используете awk для написания отчетов, содержащих долларовые значения, вы можете предпочесть изменить OFMT на «%.2f».

Спецификаторы формата, показанные в Таблице B.6, используются с операторами printf и sprintf.

Таблица B.6: Спецификаторы формата, используемые в printf

Символ Описание
c Символ ASCII.
d Целое десятичное число.
i Целое десятичное число. Добавлено в POSIX.
e Формат с плавающей точкой ([-]d.precisione[+ -]dd).
E Формат с плавающей точкой ([-]d.precisionE[+ -]dd).
f Формат с плавающей точкой ([-]ddd.precision).
g e или f преобразование, в зависимости от того, какое из них является самым коротким, с удалением конечных нулей.
G E или f преобразование, в зависимости от того, какое из них является самым коротким, с удалением конечных нулей.
o Восьмеричное значение без знака.
s Строка.
x Шестнадцатеричное число без знака. Использует a-f от 10 до 15.
X Шестнадцатеричное число без знака. Использует A-F от 10 до 15.
% Литерал %.

Часто спецификаторы формата, доступные в системной подпрограмме sprintf(3), доступны в awk.

Способ округления printf и sprintf() часто зависит от системной подпрограммы sprintf(3) на языке C. На многих машинах округление sprintf является «беспристрастным», что означает, что оно не всегда округляет конечную «.5» вверх, вопреки наивным ожиданиям. При беспристрастном округлении «.5» округляется до четного, а не всегда в большую сторону, поэтому 1.5 округляется до 2, но 4.5 округляется до 4. В результате получается, что если вы используете формат, который выполняет округление (например, «%.0f») вам следует проверить, что делает ваша система. Следующая функция выполняет традиционное округление; это может быть полезно, если printf вашего awk выполняет беспристрастное округление.

# round --- do normal rounding
#     Arnold Robbins, arnold@gnu.ai.mit.edu
#     Public Domain
function round(x, ival, aval, fraction)
{
    ival = int(x)     # integer part, int() truncates
    # see if fractional part
    if (ival == x)    # no fraction
        return x
    if (x < 0) {
        aval = -x     # absolute value
        ival = int(aval)
        fraction = aval - ival
        if (fraction >= .5)
            return int(x) - 1    # -2.5 --> -3
        else
            return int(x)        # -2.3 --> -2
    } else {
        fraction = x - ival
        if (fraction >= .5)
            return ival + 1
        else
            return ival
    }
}

Приложение C.
Дополнение к Главе 12

В этом приложении содержатся дополнительные программы и документация для программ, описанных в Главе 12, Полнофункциональные приложения.

C.1 Полный листинг spellcheck.awk

# spellcheck.awk -- interactive spell checker
#
# AUTHOR: Dale Dougherty
#
# Usage: nawk -f spellcheck.awk [+dict] file 
# (Use spellcheck as name of shell program) 
# SPELLDICT = "dict" 
# SPELLFILE = "file"

# BEGIN actions perform the following tasks: 
#   1) process command line arguments
#   2) create temporary filenames
#   3) execute spell program to create wordlist file
#   4) display list of user responses

BEGIN { 
# Process command line arguments
# Must be at least two args -- nawk and filename
    if (ARGC > 1) {
    # if more than two args, second arg is dict 
        if (ARGC > 2) {
        # test to see if dict is specified with "+"  
        # and assign ARGV[1] to SPELLDICT
            if (ARGV[1] ~ /^\+.*/) 
                SPELLDICT = ARGV[1]
            else 
                SPELLDICT = "+" ARGV[1]
        # assign file ARGV[2] to SPELLFILE 
            SPELLFILE = ARGV[2]
        # delete args so awk does not open them as files
            delete ARGV[1]
            delete ARGV[2]
        }
    # not more than two args
        else {
        # assign file ARGV[1] to SPELLFILE 
            SPELLFILE = ARGV[1]
        # test to see if local dict file exists
            if (! system ("test -r dict")) {
            # if it does, ask if we should use it
                printf ("Use local dict file? (y/n)")   
                getline reply < "-"
            # if reply is yes, use "dict" 
                if (reply ~ /[yY](es)?/){
                    SPELLDICT = "+dict"
                }
            }
        }
    } # end of processing args > 1 
    # if args not > 1, then print shell-command usage 
    else {
        print "Usage: spellcheck [+dict] file"
        exit 1
    }
# end of processing command line arguments

# create temporary file names, each begin with sp_
    wordlist = "sp_wordlist"
    spellsource = "sp_input"
    spellout = "sp_out"

# copy SPELLFILE to temporary input file
    system("cp " SPELLFILE " " spellsource)

# now run spell program; output sent to wordlist
    print "Running spell checker ..."
    if (SPELLDICT)
        SPELLCMD = "spell " SPELLDICT " "
    else
        SPELLCMD = "spell "
    system(SPELLCMD spellsource " > " wordlist )

# test wordlist to see if misspelled words turned up
    if ( system("test -s " wordlist ) ) {
    # if wordlist is empty, (or spell command failed), exit
        print "No misspelled words found."
        system("rm " spellsource " " wordlist)
        exit
    }   

# assign wordlist file to ARGV[1] so that awk will read it. 
    ARGV[1] = wordlist

# display list of user responses 
    responseList = "Responses: \n\tChange each occurrence," 
    responseList = responseList "\n\tGlobal change," 
    responseList = responseList "\n\tAdd to Dict,"  
    responseList = responseList "\n\tHelp," 
    responseList = responseList "\n\tQuit" 
    responseList = responseList "\n\tCR to ignore: "
    printf("%s", responseList)

} # end of BEGIN procedure

# main procedure, executed for each line in wordlist.
#   Purpose is to show misspelled word and prompt user
#   for appropriate action.

{
# assign word to misspelling
    misspelling = $1 
    response = 1
    ++word
# print misspelling and prompt for response
    while (response !~ /(^[cCgGaAhHqQ])|^$/ ) {
        printf("\n%d - Found %s (C/G/A/H/Q/):", word, misspelling)
        getline response < "-"
    }
# now process the user's response
# CR - carriage return ignores current word 
# Help
    if (response ~ /[Hh](elp)?/) {
    # Display list of responses and prompt again.
        printf("%s", responseList)
        printf("\n%d - Found %s (C/G/A/Q/):", word, misspelling)
        getline response < "-"
    }
# Quit
    if (response ~ /[Qq](uit)?/) exit
# Add to dictionary
    if ( response ~ /[Aa](dd)?/) { 
        dict[++dictEntry] = misspelling
    }
# Change each occurrence
    if ( response ~ /[cC](hange)?/) {
    # read each line of the file we are correcting
        newspelling = ""; changes = ""
        while( (getline < spellsource) > 0){
        # call function to show line with misspelled word
        # and prompt user to make each correction 
            make_change($0)
        # all lines go to temp output file
            print > spellout
        }   
    # all lines have been read 
    # close temp input and temp output file
        close(spellout)
        close(spellsource)
    # if change was made
        if (changes){ 
        # show changed lines
            for (j = 1; j <= changes; ++j)
                print changedLines[j]
            printf ("%d lines changed. ", changes) 
        # function to confirm before saving changes
            confirm_changes()
        }
    }
# Globally change
    if ( response ~ /[gG](lobal)?/) {
    # call function to prompt for correction
    # and display each line that is changed.
    # Ask user to approve all changes before saving.
        make_global_change()
    }   
} # end of Main procedure

# END procedure makes changes permanent.
# It overwrites the original file, and adds words
# to the dictionary.
# It also removes the temporary files.

END {
# if we got here after reading only one record, 
# no changes were made, so exit.
    if (NR <= 1) exit
# user must confirm saving corrections to file
    while (saveAnswer !~ /([yY](es)?)|([nN]o?)/ ) {
        printf "Save corrections in %s (y/n)? ", SPELLFILE
        getline saveAnswer < "-"
    }
# if answer is yes then mv temporary input file to SPELLFILE
# save old SPELLFILE, just in case
    if (saveAnswer ~ /^[yY]/) {
        system("cp " SPELLFILE " " SPELLFILE ".orig")
        system("mv " spellsource " " SPELLFILE)
    }
# if answer is no then rm temporary input file
    if (saveAnswer ~ /^[nN]/)
        system("rm " spellsource) 

# if words have been added to dictionary array, then prompt
# to confirm saving in current dictionary. 
    if (dictEntry) {
        printf "Make changes to dictionary (y/n)? "
        getline response < "-"
        if (response ~ /^[yY]/){
        # if no dictionary defined, then use "dict"
            if (! SPELLDICT) SPELLDICT = "dict"
        
        # loop through array and append words to dictionary
            sub(/^\+/, "", SPELLDICT)
            for ( item in dict )
                print dict[item] >> SPELLDICT
            close(SPELLDICT)
        # sort dictionary file 
            system("sort " SPELLDICT "> tmp_dict")
            system("mv " "tmp_dict " SPELLDICT)
        }
    }
# remove word list
    system("rm sp_wordlist")
} # end of END procedure

# function definitions

# make_change -- prompt user to correct misspelling 
#        for current input line.  Calls itself
#        to find other occurrences in string.
#   stringToChange -- initially $0; then unmatched substring of $0
#   len -- length from beginning of $0 to end of matched string 
# Assumes that misspelling is defined. 

function make_change (stringToChange, len,  # parameters
    line, OKmakechange, printstring, carets)    # locals
{
# match misspelling in stringToChange; otherwise do nothing 
  if ( match(stringToChange, misspelling) ) {
  # Display matched line 
    printstring = $0
    gsub(/\t/, " ", printstring)
    print printstring
    carets = "^"
    for (i = 1; i < RLENGTH; ++i)
        carets = carets "^"
    if (len)
        FMT = "%" len+RSTART+RLENGTH-2 "s\n"
    else
        FMT = "%" RSTART+RLENGTH-1 "s\n"
    printf(FMT, carets)
  # Prompt user for correction, if not already defined
    if (! newspelling) {
        printf "Change to:"
        getline newspelling < "-"
    }
  # A carriage return falls through
  # If user enters correction, confirm  
    while (newspelling && ! OKmakechange) {
        printf ("Change %s to %s? (y/n):", misspelling, newspelling)
        getline OKmakechange < "-"
        madechg = ""
    # test response
        if (OKmakechange ~ /[yY](es)?/ ) {
        # make change (first occurrence only)
            madechg = sub(misspelling, newspelling, stringToChange)
        }
        else if ( OKmakechange ~ /[nN]o?/ ) {
            # offer chance to re-enter correction 
            printf "Change to:"
            getline newspelling < "-"
            OKmakechange = ""
        }
    } # end of while loop

   # if len, we are working with substring of $0
    if (len) {
    # assemble it
        line = substr($0,1,len-1)
        $0 = line stringToChange
    }
    else {
        $0 = stringToChange
        if (madechg) ++changes
    }

   # put changed line in array for display
    if (madechg) 
        changedLines[changes] = ">" $0

   # create substring so we can try to match other occurrences
    len += RSTART + RLENGTH
    part1 = substr($0, 1, len-1)
    part2 = substr($0, len)
   # calls itself to see if misspelling is found in remaining part 
    make_change(part2, len) 

  } # end of if

} # end of make_change()

# make_global_change --
#       prompt user to correct misspelling 
#       for all lines globally.  
#       Has no arguments
# Assumes that misspelling is defined. 

function make_global_change(    newspelling, OKmakechange, changes)
{
# prompt user to correct misspelled word
   printf "Globally change to:"
   getline newspelling < "-"

# carriage return falls through
# if there is an answer, confirm 
   while (newspelling && ! OKmakechange) {
        printf ("Globally change %s to %s? (y/n):", misspelling,
                newspelling)
        getline OKmakechange < "-"
    # test response and make change
        if (OKmakechange ~ /[yY](es)?/ ) {
        # open file, read all lines 
            while( (getline < spellsource) > 0){
            # if match is found, make change using gsub
            # and print each changed line.
                if ($0 ~ misspelling) {
                    madechg = gsub(misspelling, newspelling)
                    print ">", $0
                    changes += 1  # counter for line changes
                }
            # write all lines to temp output file
                print > spellout
            } # end of while loop for reading file

        # close temporary files
            close(spellout)
            close(spellsource)
        # report the number of changes  
            printf ("%d lines changed. ", changes) 
        # function to confirm before saving changes
            confirm_changes()
        } # end of if (OKmakechange ~ y) 

    # if correction not confirmed,  prompt for new word
        else if ( OKmakechange ~ /[nN]o?/ ){
            printf "Globally change to:"
            getline newspelling < "-"
            OKmakechange = ""
        }

  } # end of while loop for prompting user for correction

} # end of make_global_change()

# confirm_changes --  
#       confirm before saving changes

function confirm_changes(  savechanges) {
# prompt to confirm saving changes
    while (! savechanges ) {
        printf ("Save changes? (y/n)")
        getline savechanges < "-"
    }
# if confirmed, mv output to input
    if (savechanges ~ /[yY](es)?/)
        system("mv " spellout " " spellsource) 
}

C.2 Листинг сценария оболочки masterindex

#! /bin/sh
# 1.1 -- 7/9/90
MASTER=""
FILES=""
PAGE=""
FORMAT=1
INDEXDIR=/work/sedawk/awk/index
#INDEXDIR=/work/index
INDEXMACDIR=/work/macros/current
# Add check that all dependent modules are available.
sectNumber=1
useNumber=1
while [ "$#" != "0" ]; do
   case $1 in
    -m*)    MASTER="TRUE";;
    [1-9])  sectNumber=$1;;
    *,*)    sectNames=$1; useNumber=0;;
    -p*)    PAGE="TRUE";;
    -s*)    FORMAT=0;;
    -*) echo $1 " is not a valid argument";;
    *)  if [ -f $1 ]; then
            FILES="$FILES $1"
        else 
            echo "$1: file not found"
        fi;;
   esac
   shift
done
if [ "$FILES" = "" ]; then
    echo "Please supply a valid filename."
    exit
fi
if [ "$MASTER" != "" ]; then
    for x in $FILES
    do
    if [ "$useNumber" != 0 ]; then
        romaNum=`$INDEXDIR/romanum $sectNumber`
        awk '-F\t' '
            NF == 1 { print $0 } 
            NF > 1 { print $0 ":" volume }
        ' volume=$romaNum $x >>/tmp/index$$ 
        sectNumber=`expr $sectNumber + 1`
    else
        awk '-F\t' '
            NR==1 { split(namelist, names, ","); 
            volname=names[volume]}
            NF == 1 { print $0 } 
            NF > 1 { print $0 ":" volname }
        ' volume=$sectNumber namelist=$sectNames $x >>/tmp/index$$ 
        sectNumber=`expr $sectNumber + 1`
    fi
    done 
    FILES="/tmp/index$$"
fi
if [ "$PAGE" != "" ]; then
    $INDEXDIR/page.idx $FILES
    exit
fi
$INDEXDIR/input.idx $FILES | 
sort -bdf -t:  +0 -1 +1 -2 +3 -4 +2n -3n | uniq | 
$INDEXDIR/pagenums.idx | 
$INDEXDIR/combine.idx | 
$INDEXDIR/format.idx FMT=$FORMAT MACDIR=$INDEXMACDIR
if [ -s "/tmp/index$$" ]; then
    rm /tmp/index$$
fi

C.3 Документация masterindex

Эта документация и следующие за ней примечания принадлежат Дейлу Догерти.

C.3.1 masterindex

Индексирующая программа для одно- и многотомной индексации.

Синопсис
masterindex [-master [volume]] [-page] [-screen] [filename..]
Описание
masterindex создает форматированный индекс на основе записей структурированного индекса, выводимых troff. Если вы не перенаправляете вывод, он попадает на экран.
Параметры

-m или -master указывает, что вы составляете многотомный указатель. Записи указателя для каждого тома должны быть в одном файле, а имена файлов должны быть перечислены последовательно. Если первый файл не является первым томом, укажите номер тома в качестве отдельного аргумента. Номер тома преобразуется в римскую цифру и добавляется ко всем номерам страниц записей в этом файле.

-p или -page создает список записей указателя для каждого номера страницы. Его можно использовать для проверки записей на бумажном носителе.

-s или -screen указывает, что неформатированный индекс будет просматриваться на «экране». По умолчанию вывод, содержащий макросы troff, готовится к форматированию.

Файлы

/work/bin/masterindex

/work/bin/page.idx

/work/bin/pagenums.idx

/work/bin/combine.idx

/work/bin/format.idx

/work/bin/rotate.idx

/work/bin/romanum

/work/macros/current/indexmacs

Смотрите также
Обратите внимание, что эти программы требуют «nawk» (новый awk): nawk(1) и sed(1V).
Ошибки
Новая индексная программа является модульной, вызывая серию более мелких программ. Это должно позволить мне подключать разные модули для реализации новых функций, а также более легко изолировать и устранять проблемы. Записи индекса не должны содержать никаких изменений шрифта troff. Программа их не обрабатывает. Римские цифры больше восьми не будут отсортированы должным образом, что ограничивает индекс из восьми книг. (Программа сортировки отсортирует римские цифры 1-10 в следующем порядке: I, II, III, IV, IX, V, VI, VII, VIII, X).

C.3.2 Справочная информация

Тим О'Рейли рекомендует индекс The Joy of Cooking (JofC) в качестве идеального индекса. Я довольно тщательно изучил индекс JofC и намеревался написать новую программу индексирования, дублирующую его функции. Я не копировал полностью формат JofC, но при желании это можно было сделать довольно легко. Пожалуйста, посмотрите индекс JofC самостоятельно, чтобы изучить его возможности.

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

C.3.3 Записи индекса кодирования

В этом разделе описывается кодирование записей указателя в файле документа. Мы используем макрос .XX для размещения индексных записей в файле. Самый простой случай:

.XX "entry"

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

.XX "primary, secondary"

Два ключа разделяются запятой. У нас также есть макрос .XN для создания ссылок «See» без номера страницы. Это указано как:

.XN "entry (See anotherEntry)"

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

.XX "primary: secondary; tertiary"

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

Еще одна функция - автоматическая ротация первичных и вторичных ключей, если в качестве разделителя используется тильда (~). Итак, следующая запись:

.XX "cat~command"

эквивалентна следующим двум записям:

.XX "cat command"
.XX "command: cat"

Вы можете рассматривать вторичный ключ как классификацию (команда, атрибут, функция и т. д.) первичной записи. Будьте осторожны, чтобы не поменять их местами, так как «command cat» не имеет особого смысла. Чтобы использовать тильду в записи, введите «~~».

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

.XB "cat command"

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

Еще одна особенность индекса JofC заключается в том, что самый первый вторичный ключ появляется в одной строке с первичным ключом. Старая индексная программа помещала любой вторичный ключ в следующую строку. Одним из преимуществ использования способа JofC является то, что записи, содержащие только один вторичный ключ, будут выводиться в одной строке и выглядеть намного лучше. Таким образом, у вас будет «выравнивание строки, определение», а не «определение» в следующей строке. Следующий вторичный ключ будет с отступом. Обратите внимание, что если первичный ключ существует как отдельная запись (с ним связаны номера страниц), ссылки на страницы для первичного ключа будут выводиться в той же строке, а первая вторичная запись будет выводиться в следующей строке. Повторяю, хотя синтаксис трехуровневых записей отличается, эта запись указателя совершенно верна:

.XX "line justification, definition of"

Он также дает тот же результат, что и:

.XX "line justification: definition of"

(Двоеточие исчезает в выводе.) Точно так же вы можете написать запись, например

.XX "justification, lines, defined"

или

.XX "justification: lines, defined"

где запятая между «lines» и «defined» не служит разделителем, а является частью вторичного ключа.

Предыдущий пример можно было бы записать как запись с тремя уровнями:

.XX "justification: lines; defined"

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

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

justification 4-9
    lines 4,6; defined, 5

C.3.4 Выходной формат

Одна вещь, которую я хотел сделать, чего не делала наша предыдущая программа, - это сгенерировать индекс без кодов troff. masterindex имеет три режима вывода: troff, screen и page.

Вывод по умолчанию предназначен для обработки с помощью troff (через fmt). Он содержит макросы, которые определены в /work/macros/current/indexmacs. Эти макросы должны создавать тот же формат индекса, что и раньше, что в значительной степени делалось напрямую через отключение запросов. Вот несколько строк сверху:

$ masterindex ch01
.so /work/macros/current/indexmacs
.Se "" "Index"
.XC
.XF A "A"
.XF 1 "applications, structure of 2; program 1"
.XF 1 "attribute, WIN_CONSUME_KBD_EVENTS 13"
.XF 2 "WIN_CONSUME_PICK_EVENTS 13"
.XF 2 "WIN_NOTIFY_EVENT_PROC 13"
.XF 2 "XV_ERROR_PROC 14"
.XF 2 "XV_INIT_ARGC_PTR_ARGV 5,6"

Две верхние строчки должны быть очевидны. Макрос .XC производит вывод с несколькими столбцами. (Он распечатает два столбца для небольших книг. Он недостаточно умен, чтобы принимать аргументы, определяющие ширину столбцов, но это должно быть сделано.) Макрос .XF имеет три возможных значения для своего первого аргумента. «A» означает, что второй аргумент - это буква алфавита, которая должна выводиться как разделитель. «1» указывает, что второй аргумент содержит первичную запись. «2» указывает на то, что запись начинается с вторичной записи с отступом.

При вызове с аргументом -s программа подготавливает индекс для просмотра на экране (или печати в виде файла ASCII). Опять же, вот несколько строк:

$ masterindex -s ch01
                         A
applications, structure of 2; program 1
attribute, WIN_CONSUME_KBD_EVENTS 13
  WIN_CONSUME_PICK_EVENTS 13
  WIN_NOTIFY_EVENT_PROC 13
  XV_ERROR_PROC 14
  XV_INIT_ARGC_PTR_ARGV 5,6
  XV_INIT_ARGS 6
  XV_USAGE_PROC 6

Очевидно, это полезно для быстрой проверки индекса. Третий тип формата также используется для проверки индекса. Вызывается с помощью -p, он предоставляет постраничный список записей индекса.

$ masterindex -p ch01
Page 1
        structure of XView applications
        applications, structure of; program
        XView applications
        XView applications, structure of
        XView interface
        compiling XView programs
        XView, compiling programs
Page 2
        XView libraries

C.3.5 Сборка основного индекса

Многотомный главный индекс вызывается с помощью параметра -m. Каждый набор записей указателя для определенного тома должен быть помещен в отдельный файл.

$ masterindex -m -s book1 book2 book3
xv_init() procedure II: 4; III: 5
XV_INIT_ARGC_PTR_ARGV attribute II: 5,6
XV_INIT_ARGS attribute I: 6

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

$ masterindex -m 4 -s book4 book5

Об авторах

Дэйл Догерти - президент и главный исполнительный директор Songline Studios, дочерней компании O'Reilly & Associates, специализирующейся на разработке онлайн-контента. Редактор-основатель серии Nutshell, Дейл написал, помимо sed & awk, DOS Meets UNIX (с Тимом О'Рейли), Using UUCP & Usenet (с Грейс Тодино) и Guide to the Pick System.

Арнольд Роббинс, уроженец Атланты, профессиональный программист и технический автор. Он работает с системами UNIX с 1980 года, когда он познакомился с PDP11, работающим под управлением шестой версии UNIX. Он активно пользуется awk с 1987 года, когда он начал работать с gawk, версией awk для проекта GNU. Как член группы голосования POSIX 1003.2 он помог сформировать стандарт POSIX для awk. В настоящее время он поддерживает gawk и документацию к нему. Документация доступна в Free Software Foundation, а также была опубликована SSC как Effective AWK Programming.

Выходные данные

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

Животное, изображенное на обложке sed & awk, - серый тонкий лори. Лори ведут ночной образ жизни, живут на деревьях, бесхвостые приматы с густым мягким мехом и большими круглыми глазами. Они обитают в Южной Индии и на Цейлоне, где живут на деревьях, редко спускаются на землю. Было замечено, что лори мочатся на руки и ноги - считается, что они делают это, чтобы улучшить хватку во время лазания и оставить след запаха.

Маленькое животное, серый тонкий лори обычно имеет размер от 7 до 10 дюймов и весит 12 унций или меньше. Оно питается фруктами, листьями, побегами и маленькими животными, которых ловит вручную.

Эди Фридман разработала обложку этой книги, используя гравюру XIX века из Дуврского архива иллюстраций. Макет обложки был создан с помощью Quark XPress 3.3 с использованием шрифта ITC Garamond. По возможности в наших книгах используется RepKover - прочный и гибкий плоский переплет. Если количество страниц превышает предел RepKover, используется идеальная привязка.

Внутренняя структура была разработана Нэнси Прист и Мэри Джейн Уолш. Текст был подготовлен в SGML с использованием DocBook 2.1 DTD. Печатная версия этой книги была создана путем перевода исходного кода SGML в набор макросов gtroff с использованием фильтра, разработанного в ORA Норманом Уолшем. Стив Тэлботт разработал и написал базовый набор макросов на основе макросов GNU troff -gs; Ленни Мюлльнер адаптировал их к SGML и реализовал дизайн книги. Программа форматирования текста GNU groff версии 1.09 использовалась для создания вывода PostScript. Шрифт текста и заголовка - ITC Garamond Light и Garamond Book; в этой книге используется шрифт постоянной ширины Letter Gothic. Иллюстрации, представленные в книге, были созданы в Macromedia Freehand 5.0 Крисом Рейли.