Разделитель записей
Ключевое слово RS (Record Separator) - это специальная переменная, значение которой равно текущему разделителю записей. Первоначально значение RS устанавливается равным символу перевода строки; это означает, что соседние входные записи отделяются одна от другой переводами строк. Значение переменной RS можно заменить на любой символ c, выполнив в действии оператор присваивания RS = "c".
Разделитель полей
Ключевое слово FS (Field Separator) - это специальная переменная, значение которой равно текущему разделителю полей. Первоначальное значение FS - пробел; это означает, что поля отделяются произвольными непустыми последовательностями пробелов и табуляций. Значение переменной FS можно заменить на любой одиночный символ c, выполнив в действии оператор присваивания FS = "c" или указав в командной строке необязательный аргумент -Fc. Два значения c имеют особый смысл. Присваивание FS = " " делает разделителями полей пробелы и табуляции, а аргумент командной строки F\t - табуляции.
Если в качестве разделителя полей используется символ, отличный от пробела, считается, что с каждой стороны от разделителя имеется по полю. Например, если разделитель полей равен 1, то запись 1XXX1 состоит из трех полей. Первое и третье - пустые. Если разделитель полей - пробел, поля отделяются друг от друга пробелами и табуляциями и ни одно из NF полей не может быть пустым.
Записи, состоящие из нескольких строк
Присваивание RS = " " делает разделителем записей пустую строку, а разделителем полей - непустую последовательность из пробелов, табуляций и, возможно, переводов строк. Такое определение не допускает, чтобы первое или последнее поля в записи были пустыми.
Выходные разделители записей и полей
Значение переменной OFS (Output Field Separator) задает выходной разделитель полей; действие print помещает его между полями. После каждой записи print помещает символ, являющийся значением переменной ORS (Output Record Separator). Первоначально ORS устанавливается равным переводу строки, а OFS - пробелу. Данные значения можно заменить любыми другими цепочками при по- мощи присваиваний, например, ORS = "abc" и OFS = "xyz".
A.out - стандартный заголовок системы UNIX
По умолчанию, вспомогательный заголовок файлов, создаваемых редактором внешних связей системы UNIX, имеет стандартную структуру файлов a.out. Размер этой структуры - 28 байт. Поля вспомогательного заголовка описаны в следующей таблице:
Байты | Описание | Имя | Смысл |
0-1 | short | magic | Магическое число |
2-3 | short | vstamp | Метка версии |
4-7 | long int | tsize | Размер секции команд в байтах |
8-11 | long int | dsize | Размер секции инициализированных данных в байтах |
12-15 | long int | bsize | Размер секции неинициализированных данных в байтах |
16-19 | long int | entry | Точка входа |
20-23 | long int | text_start | Адрес начала команд |
24-27 | long int | data_start | Адрес начала данных |
В то время как магическое число в заголовке файла указывает целевой компьютер, магическое число во вспомогательном заголовке содержит информацию о том, как операционная система на этом компьютере должна выполнять файл. В следующей таблице показаны магические числа, распознаваемые ОС UNIX.
Значение | Смысл |
0410 | Данные располагаются с границы сегмента, следующего за сегментом текста. Сегмент текста защищен от записи |
Аддитивные операции
Выражения с аддитивными операциями + и - группируются слева направо. Выполняются обычные арифметические преобразования. Для каждой из операций допустимы и некоторые дополнительные типы операндов.
аддитивное_выражение: выражение + выражение выражение - выражение
Результат операции + равен сумме ее операндов. Можно складывать указатель на объект в массиве и значение любого целочисленного типа. Последнее во всех случаях преобразуется в адресный сдвиг умножением на размер указуемого объекта. Результатом является указатель того же типа, что и первоначальный, указывающий на другой объект в том же массиве, соответствующим образом сдвинутый относительно первоначального объекта. Так, если P - указатель на объект в массиве, то выражение P+1 - это указатель на следующий объект в массиве. Никакие другие комбинации с исполь- зованием указательных типов недопустимы.
Операция + ассоциативна, и выражения, состоящие из нескольких слагаемых, могут быть переупорядочены компилятором.
Результат операции - равен разности ее операндов. Выполняются обычные арифметические преобразования. Кроме того, значение любого целочисленного типа можно вычесть из указателя, при этом выполняются те же преобразования, что и в случае сложения.
При вычислении разности двух указателей на объекты одного типа результат преобразуется (делением на размер объекта данного типа) к типу int; полученное значение есть число объектов между указуемыми точками. В общем случае это преобразование дает неожиданный результат, если только указатели не соответствуют одному массиву, поскольку указатели, даже для объектов одного типа, не обязательно различаются на число, кратное размеру объекта.
Адреса
Применительно к редактированию внешних связей термин физический адрес трактуется нестандартным образом. Физический адрес секции или имени определяется как смещение относительно начала (нулевого адреса) адресного пространства. Физический адрес объекта не обязательно совпадает с тем адресом, по которому объект будет помещен во время выполнения. Так, в системах со страничной виртуальной памятью адрес есть смещение относительно нулевого адреса виртуальной памяти, которое затем преобразуется аппаратурой и/или операционной системой.
Алгоритм размещения
Выходная секция создается в результате выполнения предложения SECTIONS, или объединения одноименных входных секций или объединения секций .text и .init в выходную секцию .text. В выход- ную секцию включаются несколько (возможно, одна или ни одной) входных. После того, как состав выходной секции определен, ей назначается для размещения участок конфигурируемой виртуальной памяти. ld(1) использует алгоритм, цель которого - минимизиро- вать фрагментацию памяти и таким образом повысить вероятность успешного размещения всех выходных секций, с учетом конфигурации памяти. Этот алгоритм заключается в следующем:
Размещаются все выходные секции, связанные с конкретными адресами.
Размещаются все секции, связанные с именованными областями памяти. Секции, размещаемые на этом или следующем шаге, связываются с первым доступным в (именованной) памяти адресом, с учетом требований выравнивания, если таковое имеются.
Размещаются все остальные выходные секции.
Если, как это предполагается по умолчанию, вся память образует одну непрерывную конфигурируемую область, а предложения SECTIONS отсутствуют, то выходные секции размещаются в том порядке, в котором их создает ld(1). В остальных случаях выходные секции размещаются в том порядке, в котором они определяются, или становятся известными ld(1), - в первой подходящей из доступных областей памяти.
АЛГОРИТМ СИНТАКСИЧЕСКОГО РАЗБОРА
yacc отображает файл спецификаций в процедуру на языке C, которая разбирает входной текст в соответствии с заданной спецификацией. Алгоритм, используемый для того, чтобы перейти от спецификации к C-процедуре, сложен и здесь обсуждаться не будет. Алгоритм же самого разбора относительно прост, знание его облегчит понимание механизма нейтрализации ошибок и обработки неоднозначностей.
yacc порождает алгоритм разбора, использующий конечный автомат со стеком. Кроме того, алгоритм разбора может прочесть и запомнить следующую входную лексему (которая называется предварительно просмотренной). Текущее состояние автомата всегда сохраняется на вершине стека. Состояния конечного автомата задаются небольшими целочисленными значениями. В начальный момент автомат находится в состоянии 0 (стек содержит единственное значение 0) и предварительно просмотренная лексема не прочитана.
В алгоритме имеется всего четыре типа действий - перенос
(shift), свертка (reduce), успех (accept), ошибка (error). Шаг работы алгоритма состоит в следующем:
Основываясь на текущем состоянии автомата, алгоритм выясняет, требуется ли для выбора очередного действия предварительно просмотренная лексема. Если требуется, но не прочитана, алгоритм запрашивает следующую лексему у функции yylex.
Используя текущее состояние и, если нужно, предварительно просмотренную лексему, алгоритм выбирает очередное действие и выполняет его. В результате состояния могут быть помещены в стек или извлечены из него, предварительно просмотренная лексема может быть обработана, а может быть и нет.
Действие перенос - наиболее частое действие алгоритма. В выполнении действия перенос всегда участвует предварительно просмотренная лексема. Например, для состояния 56 может быть определено действие
IF shift 34
что означает: в состоянии 56, если предварительно просмотренная лексема равна IF, текущее состояние (56) помещается в стек, текущим становится состояние 34 (заносится на вершину стека). Предварительно просмотренная лексема очищается.
Действие свертка препятствует неограниченному росту стека. Оно выполняется, когда алгоритм, просмотрев правую часть правила, обнаруживает в обрабатываемых данных ее вхождение, которое и заменяет левой частью. Может понадобиться проанализировать предварительно просмотренную лексему (обычно этого не требуется), чтобы решить, выполнять свертку или нет. Часто подразумеваемым действием (которое обозначается точкой) оказывается именно свертка.
Действия свертка ассоциируются с отдельными грамматическими правилами. Грамматические правила (как и состояния) нумеруются небольшими целыми числами, и это приводит к некоторой путанице. В действии
. reduce 18
имеется в виду грамматическое правило 18, тогда как в действии
IF shift 34
- состояние 34.
Предположим, выполняется свертка для правила
A : x y z ;
Действие свертка зависит от символа в левой части правила (в данном случае A) и числа символов в правой части (в данном случае три). При свертке сначала из стека извлекаются три верхних состояния. (В общем случае число извлекаемых состояний равно числу символов в правой части правила). В сущности, эти состояния были занесены в стек при распознавании x, y и z и больше не нужны. После того, как извлечены эти состояния, на вершине стека оказывается состояние автомата на момент перед началом применения данного правила. Для этого состояния и символа, стоящего в левой части правила, выполняется действие, имеющее результат, аналогичный переносу с аргументом A. Конечный автомат переходит в новое состояние, которое заносится в стек, и разбор продолжается. Имеются, однако, существенные отличия между обработкой символа из левой части правила и обычным переносом лексемы, поэтому это действие называется действием переход (goto). В частности, предварительно просмотренная лексема при переносе очищается, а при переходе не затрагивается. В любом случае, открывшееся на вершине стека состояние содержит действие
A goto 20
выполнение которого приводит к тому, что состояние 20 помещается на вершину стека и становится текущим.
В сущности, действие свертка, извлекая состояния из стека, возвращает разбор к состоянию, в котором было начато применение правой части данного правила. После этого алгоритм разбора поступает так, как будто в данный момент обнаружено не вхождение правой части, а символ, стоящий в левой части правила. Если правая часть правила пуста, ни одно состояние из стека не извлекается. Открывающееся состояние - просто то же самое текущее состояние.
Действие свертка важно также для понимания процесса выполнения действий, заданных пользователем, и передачи значений. При свертке правила перед преобразованием стека выполняются действия, ассоциированные с правилом. Кроме стека, содержащего состояния, параллельно поддерживается еще один стек, который содержит значения, возвращаемые лексическим анализатором и действиями. При выполнении переноса в стек значений заносится внешняя переменная yylval. После выполнения пользовательского действия свертка завершается. При выполнении действия переход в стек значений заносится внешняя переменная yyval. Псевдопеременные $1, $2 и т.д. указывают на элементы стека значений.
Понять два других действия алгоритма значительно проще. Действие успех обозначает, что входной текст прочитан и сопоставлен со спецификацией. Это действие выполняется только в том случае, когда предварительно просмотренная лексема равна маркеру конца. Оно обозначает, что работа алгоритма успешно завершена. Действие ошибка, напротив, представляет ситуацию, когда алгоритм не может дальше продолжать разбор в соответствии со спецификацией. Исходные лексемы (вместе с предварительно просмотренной лексемой) таковы, что никакой последующий текст не приведет к допустимым исходным данным. Алгоритм сообщает об ошибке и пытается восстановить ситуацию и продолжить разбор. Позднее будет обсуждаться нейтрализация, а не просто обнаружение ошибок.
Рассмотрим yacc-спецификацию:
%token DING DONG DELL %% rhyme : sound place ; sound : DING DONG ; place : DELL ;
Когда утилита yacc вызывается с опцией -v, порождается файл с именем y.output, содержащий удобочитаемое описание алгоритма разбора. Ниже приводится файл y.output, соответствующий данной спецификации (статистическая информация в конце файла опущена).
state 0 $accept : _rhyme $end
DING shift 3 . error
rhyme goto 1 sound goto 2
state 1 $accept : rhyme _$end
$end accept . error
state 2
rhyme : sound _place DELL shift 5 . error
place goto 4
state 3 sound : DING _DONG
DONG shift 6 . error
state 4 rhyme : sound place_ (1)
. reduce 1
state 5 place : DELL (3)
. reduce 3
state 6 sound : DING DONG_ (2)
. reduce 2
Здесь приведены действия для каждого состояния, есть описания правил разбора, применяемых в каждом состоянии. Чтобы указать, что уже прочитано, а что еще осталось в каждом правиле, используется знак _. Проследить функционирование алгоритма можно на примере следующей входной цепочки:
DING DONG DELL
В начале текущее состояние есть состояние 0. Чтобы выбрать одно из действий, возможных в состоянии 0, алгоритм разбора должен обратиться к входной цепочке, поэтому первая лексема, DING, считывается и становится предварительно просмотренной. Действие в состоянии 0 над DING - перенос 3, состояние 3 помещается в стек, а предварительно просмотренная лексема очищается. Состояние 3 становится текущим. Читается следующая лексема, DONG, и уже она становится предварительно просмотренной. Действие в состоянии 3 над DING - перенос 6, состояние 6 помещается в стек, а предварительно просмотренная лексема очищается. Теперь стек содержит 0, 3 и 6. В состоянии 6, даже не опрашивая предварительно просмотренную лексему, алгоритм выполняет свертку
sound : DING DONG
по правилу 2. Два состояния, 6 и 3, извлекаются из стека, на вершине которого оказывается состояние 0. В соответствии с описанием состояния 0 выполняется
sound goto 2
Состояние 2 помещается в стек и становится текущим.
В состоянии 2 должна быть прочитана следующая лексема, DELL. Действие - перенос 5, поэтому состояние 2 помещается в стек, который теперь содержит 0, 2 и 5, а предварительно просмотренная лексема очищается. В состоянии 5 возможно единственное действие, свертка по правилу 3. В правой части этого правила один символ, поэтому из стека извлекается единственное состояние 5, а на вершине стека открывается состояние 2. Переход в состоянии 2 к place (левая часть правила 3) приводит к состоянию 4. Теперь стек содержит 0, 2 и 4. В состоянии 4 единственное действие - свертка по правилу 1. В его правой части два символа, поэтому из стека извлекаются два состояния, вновь открывая состояние 0. В состоянии 0 переход к rhyme переводит разбор в состояние 1. В состоянии 1 читается входная цепочка и обнаруживается маркер конца, в файле y.output он обозначается $end. Действие в состоянии 1 (когда найден маркер конца) - успешное завершение разбора.
Читателю настоятельно рекомендуется проследить, как действует алгоритм, если сталкивается с такими некорректными цепочками, как DING DONG DONG, DING DONG, DING DONG DELL DELL и т.д. Несколько минут, затраченных на эти и другие простые примеры, окупятся, когда возникнут вопросы в более сложных случаях.
АНАЛИЗ И ОТЛАДКА ПРОГРАММ
В ОС UNIX имеется несколько команд, помогающих обнаруживать сделанные ошибки или предупреждающих о возможных неприятностях.
Архивные библиотеки
Утилита make предоставляет интерфейс для работы с архивными библиотеками. В файле описаний пользователь может обратиться к элементу библиотеки следующим образом:
библиотека(файл.o)
или
библиотека(точка_входа)
Второй метод является в действительности ссылкой на точку входа в объектном файле, содержащемся в библиотеке. make просматривает библиотеку, обнаруживает точку входа и преобразует ее в корректное имя объектного файла.
Чтобы воспользоваться данной процедурой для поддержания архивной библиотеки, необходим make-файл следующего вида:
projlib:: projlib(pfile1.o) $(CC) -c -O pfile1.c $(AR) $(ARFLAGS) projlib pfile1.o rm pfile1.o projlib:: projlib(pfile2.o) $(CC) -c -O pfile2.c $(AR) $(ARFLAGS) projlib pfile2.o rm pfile2.o . . . и так далее для каждого элемента библиотеки
Это утомительно и чревато ошибками. Очевидно, последовательность команд для включения C-файла в библиотеку повторяется для каждого запуска; единственное отличие заключается в имени файла. (Данное утверждение истинно в большинстве случаев.)
Утилита make предоставляет пользователю правила для построения библиотек. Этим правилам соответствует суффикс .a. Так, правило .c.a - это правило компиляции исходного C-файла, включения его в библиотеку и удаления промежуточного .o-файла. Аналогично, правила .y.a, .s.a и .l.a заново обрабатывают, соответственно, файлы yacc'а, ассемблера и lex'а. Встроенными являются следущие правила для работы с архивами: .c.a, .c~.a, .f.a, .f~.a и .s~.a. (Назначение тильды, ~, будет ниже коротко описано.) В файле описаний пользователь может определить другие необходимые ему правила.
Поскольку встроенные правила для поддержания библиотеки уже определены, упомянутую выше библиотеку, состоящую из двух элементов, можно поддерживать при помощи следующего более короткого make-файла:
projlib: projlib(pfile1.o) projlib(pfile2.o) @echo projlib up-to-date
На самом деле правило .c.a выглядит так:
.c.a: $(CC) -c $(CFLAGS) $< $(AR) $(ARFLAGS) $@ $*.o rm -f $*.o
Здесь макрос $@ обозначает целевой файл (библиотеку projlib); макросы $< и $* устанавливаются равными, соответственно, имени изменившегося C-файла и имени файла без суффикса (pfile1.c и pfile1). Макрос $< (в приведенном выше правиле) можно заменить на $*.c.
Полезно в деталях рассмотреть, как make обрабатывает следующую конструкцию:
projlib: projlib(pfile1.o) @echo projlib up-to-date
Предположим, что объектный файл, содержащийся в библиотеке, по сравнению с pfile1.c устарел. Кроме того, файл pfile1.o отсутствует.
Выполняются следующие действия:
make projlib.
При выполнении make projlib сначала проверить все файлы, от которых зависит projlib.
Архив projlib зависит от элемента projlib(pfile1.o), который необходимо сгенерировать.
Перед тем как генерировать элемент projlib(pfile1.o), проверить все файлы, от которых он зависит. (Таких файлов нет.)
Использовать встроенные правила для того, чтобы попытаться создать projlib(pfile1.o). (Встроенного правила нет.) Отметим, что цепочка projlib(pfile1.o) содержит скобки для того, чтобы указать на суффикс целевого файла .a. В конце имени архива projlib суффикс .a явным образом не написан, но скобки его подразумевают. В этом смысле суффикс .a жестко запаян в make.
Разбить имя projlib(pfile1.o) на projlib и pfile1.o. Определить два макроса: $@(=projlib) и $*(=pfile1).
Попытаться найти правило .X.a и файл $*.X. Первый .X (в списке .SUFFIXES), который удовлетворяет этим условиям, это .c; поэтому выбирается правило .c.a и файл - pfile1.c. Установить $< равным pfile1.c и выполнить данное правило. В действительности make должен откомпилировать pfile1.c.
Обновить библиотеку. Выполнить команду, соответствующую projlib: зависимости, то есть
@echo projlib up-to-date
Следует напомнить, что для того, чтобы pfile1.o зависел от каких-либо файлов, требуется поместить в файл описаний запись, подобную
projlib(pfile1.o): $(INCDIR)/stdio.h pfile1.c
На случай использования подобной конструкции предусмотрен макрос для ссылки на имя элемента архива. Всякий раз, когда вычисляется макрос $@, вычисляется и $%. Если текущего элемента архива нет, $% полагается равным пустой цепочке. Если элемент архива существует, то $% равно выражению в скобках.
Арифметические преобразования
Почти все операции похожи друг на друга способами преобразования операндов и определения типа результата. Используемые при этом правила будем называть правилами "обычных арифметических преобразований". Они состоят в следующем:
Сначала все операнды типов char или unsigned long преобразуются к типу int, а все операнды типов unsigned char или unsigned long преобразуются к типу unsigned int.
Затем, если один из операндов имеет тип double, другой преобразуется к типу double, который и объявляется типом результата.
Иначе, если один из операндов имеет тип unsigned long, другой преобразуется к типу unsigned long, который и объявляется типом результата.
Иначе, если один операнд имеет тип long, а другой - unsigned int, они оба преобразуются к типу unsigned long, который и объявляется типом результата.
Иначе, если один из операндов имеет тип long, другой преобразуется к типу long, который и объявляется типом результата.
Иначе, если один из операндов имеет тип unsigned, другой преобразуется к типу unsigned, который и объявляется типом результата.
Иначе оба операнда преобразуются к типу int, который и объявляется типом результата.
Ассемблер
Этот язык наиболее близок к аппаратуре и поэтому является машинно-зависимым и не является мобильным. Необходимость программирования на ассемблере возникает, как правило, при невозможности воспользоваться языками высокого уровня.
Атрибуты вывода
Рассказывая об addch(), мы упомянули, что эта подпрограмма выводит в stdscr один знак типа chtype. chtype состоит из двух частей: информации о самом символе и информации о наборе атрибутов, связанных с этим символом. Эти атрибуты позволяют отображать символ с повышенной яркостью, подчеркнутым, инвертированным и т.д.
С stdscr всегда связан набор атрибутов, которые автоматически присваиваются каждому выводимому символу. Вы можете изменить текущие значения атрибутов, используя attrset() или другие подпрограммы пакета curses, которые описаны ниже. Приведем здесь список атрибутов и их описания:
A_BLINK - мерцание.
A_BOLD - повышенная яркость или жирный шрифт.
A_DIM - пониженная яркость.
A_REVERSE - инвертированное отображение.
A_STANDOUT - как можно заметнее, насколько это возможно на данном терминале.
A_UNDERLINE - подчеркивание.
A_ALTCHARSET - альтернативная кодировка (см. раздел Линии на экране и прочая графика).
Эти атрибуты можно передавать в качестве аргумента подпрограмме attrset() или ей подобным. Им можно передавать комбинации атрибутов, объединенные операцией дизъюнкции (|).
Примечание
Не каждый терминал может отображать любой из перечисленных атрибутов. Если терминал не может реализовать запрошенный атрибут, curses пытается заменить его похожим, а если и это невозможно, то атрибут игнорируется.
Рассмотрим использование одного из этих атрибутов. Следующий фрагмент программы обеспечивает отображение слова с повышенной яркостью:
. . . printw ("Яркое "); attrset (A_BOLD); printw ("слово"); attrset (0); printw (" бросается в глаза.\n"); . . . refresh ();
Атрибуты можно включать по одному, как в примере: attrset (A_BOLD), или в комбинации. Например, чтобы вывести яркий мерцающий текст, Вы можете использовать attrset (A_BOLD | A_BLINK). Те или иные атрибуты включаются и выключаются подпрограммами attron() и attroff() без какого-либо влияния на остальные атрибуты отображения. attrset (0) отключает все атрибуты.
Заметьте, что в набор атрибутов входит A_STANDOUT, который можно применять для привлечения внимания пользователя. Для физической реализации этого атрибута используется наиболее визуально выразительный способ отображения, возможный на данном терминале. Обычно это повышенная яркость или инверсия. Если нужно просто выделить часть текста, все равно, подсветкой, инверсией или как-либо еще, лучше использовать A_STANDOUT. Для его включения и выключения удобны функции standout() и standend() соответственно. Фактически, standend() отключает все атрибуты.
Кроме перечисленных атрибутов, имеется еще две битовые маски, а именно A_CHARTEXT и A_ATTRIBUTES. Их можно использовать для извлечения только символа или только атрибутов из значения, возвращаемого входящей в curses функцией inch(), путем их конъюнкции (операция & языка C) с этим значением. См. описание inch() в curses(3X).
Приведем описание attrset() и других подпрограмм curses, которые используются для управления атрибутами вывода.
attron( ), attroff( ), attrset( )
СИНТАКСИС | |
#include <curses.h> int attron (attrs) chtype attrs; int attrset (attrs) chtype attrs; int attroff (attrs) chtype attrs; |
ОПИСАНИЕ | |
attron() включает запрошенные атрибуты attrs, сохраняя те, которые уже включены. attrs принадлежит к типу chtype, определяемому во включаемом файле <curses.h>. |
attroff() выключает запрошенные атрибуты attrs, если они включены.
Атрибуты могут объединятся при помощи побитной операции ИЛИ (|).
Все подпрограммы возвращают OK.
ПРИМЕР | |
См. программу highlight в разделе Примеры программ, работающих с curses. |
СИНТАКСИС | |
#include <curses.h> int standout ( ) int standend ( ) |
ОПИСАНИЕ | |
standout() включает атрибут A_STANDOUT и эквивалентен вызову attron (A_STANDOUT). |
Обе подпрограммы всегда возвращают OK.
ПРИМЕР | |
См. программу highlight в разделе Примеры программ, работающих с curses. |
Awk(1)
awk(1) (буквы в названии языка представляют инициалы его авторов) отыскивает во входном файле строки, соответствующие шаблону, описанному в файле спецификаций. Найдя такую строку, awk выполняет действия, также заданные в файле спецификаций. Зачастую программа из двух строк на awk может сделать то же, что и программа на две страницы на таких языках как C или Фортран. В качестве примера рассмотрим следующую задачу. Пусть имеется множество записей, состоящих из двух полей, первое из которых является ключом, а второе содержит число. Требуется каждую группу записей с одним и тем же ключевым значением заменить одной записью с тем же ключом и с числом, равным сумме чисел во всех записях группы. Таким образом, в результирующем наборе записей уже не будет совпадающих значений ключа. Если предположить, что исходный набор записей отсортирован по ключевому полю, то алгоритм решения задачи может выглядеть, например, так:
Прочитать первую запись в рабочую область.
Читать записи, пока не встретится признак конца файла (EOF); при этом:
Если ключ текущей записи совпадает с ключом записи в рабочей области, прибавить к числу в рабочей области число из текущей записи. В противном случае добавить запись из рабочей области к результату и поместить в рабочую область текущую запись.
Если достигнут конец файла, добавить к результату последнюю запись из рабочей области.
Программа на awk, выполняющая те же действия, может быть, например, такой:
{ qty[$1] += $2 } END { for (key in qty) print key, qty[key] }
Отметим, что в отличие от описанного выше алгоритма, для этой программы не требуется, чтобы входной файл был отсортирован.
Bc(1) и dc(1)
bc(1) позволяет использовать терминал как программируемый калькулятор. С помощью bc можно выполнить арифметические вычисления, специфицированные в файле или поступающие со стандартного ввода. В действительности bc является препроцессором к dc(1). Можно пользоваться непосредственно dc, но это трудно, поскольку он работает с обратной польской записью.
BEGIN и END
Ключевое слово BEGIN является специальным шаблоном, сопоставляющимся с началом исходных данных перед тем, как считывается первая запись. Ключевое слово END - специальный шаблон, сопоставляющийся с концом исходных данных после того, как обработана последняя строка. Таким образом, BEGIN и END дают возможность перехватить управление до и после обработки исходных данных, чтобы выполнить инициализационные и заключительные действия.
В следующем примере шаблон BEGIN используется для того, чтобы вывести заголовки столбцов. Программа
BEGIN {print "Country","Area","Population","Continent"} { print }
порождает такой текст:
Country Area Population Continent Russia 8650 262 Asia Canada 3852 24 North America China 3692 866 Asia USA 3615 219 North America Brazil 3286 116 South America Australia 2968 14 Australia India 1269 637 Asia Argentina 1072 26 South America Sudan 968 19 Africa Algeria 920 18 Africa
Формат, в котором выводятся заголовки, не очень хорош; в случаях, когда важно качество внешнего представления, обычно используется printf - он больше подходит для такой задачи.
Напомним также, что секцию BEGIN удобно использовать для переустановки специальных переменных, в частности FS и RS. Пример:
BEGIN { FS= "\t" printf "Country\t\t Area\tPopulation\tContinent\n\n"} { printf "%-10s\t%6d\t%6d\t\t% -14s\n", $1, $2, $3, $4 } END { print "The number of records is", NR }
В этой программе FS устанавливается в секции BEGIN равным табуляции, в результате чего все записи в файле countries содержат ровно четыре поля. Рекомендуется указывать BEGIN первым в последовательности шаблонов, а END - последним.
Беззнаковые
Если операндами операции являются беззнаковое и обычное целое, последнее преобразуется в беззнаковое (и результат операции получается беззнаковым). Результат преобразования есть наименьшее беззнаковое целое, совпадающее по модулю (2размер_слова) со знаковым целым. Если целые числа представляются в дополнительном коде, это преобразование оказывается мысленным - ни один бит реально не изменяется.
Когда беззнаковое короткое целое преобразуется в длинное, полу- чается то же численное значение, то есть преобразование сводит- ся к дополнению слева нулями.
Библиотеки объектных файлов
В ОС UNIX объектные файлы часто объединяют в архивы (библиотеки); по соглашению, имена библиотек имеют расширение .a. Например, в библиотеке libc.a хранятся объектные файлы системных вызовов, описанных в разделе 2 Справочника программиста, а также функций (именно функций, а не макросов), описанных в подразделах 3S и 3C раздела 3. Как правило, библиотека libc.a находится в каталоге /lib. Кроме /lib, часто используется каталог /usr/lib, куда помещают прикладные библиотеки.
Во время редактирования связей в выполняемый файл подгружаются копии объектных модулей из архива. Если редактирование связей выполняется по команде cc, требуемые модули по умолчанию отыскиваются в библиотеке libc.a. Если нужно, чтобы поиск производился в библиотеке libбибл.a, отличной от просматриваемой по умолчанию, следует явно указать нужную библиотеку с помощью опции -lбибл. Например, если Ваша программа использует для управления экраном функции из пакета curses(3X), указание опции
-lcurses
приведет к тому, что редактор связей будет просматривать библиотеки /lib/libcurses.a или /usr/lib/libcurses.a и использовать для разрешения ссылок в Вашей программе ту из них, которую найдет первой.
Чтобы изменить порядок просмотра архивных библиотек, можно использовать опцию -Lкаталог. Если в командной строке опция -L
указана перед опцией -l, то редактор связей сначала будет искать указанную в опции -l архивную библиотеку в заданном каталоге, а уже затем в /lib и /usr/lib. Это особенно удобно при тестировании новой версии функции, уже существующей в стандартном архиве. Использование различных версий библиотек возможно, поскольку, разрешив однажды ссылку, редактор связей прекращает дальнейший поиск. Именно поэтому опция -L должна предшествовать опции -l в командной строке.
Бинарные термы
В терме вида
терм биноп терм
биноп может быть одной из пяти арифметических операций +, -, *
(умножение), / (деление), % (остаток). Бинарная операция применяется к числовым значениям операндов терм1 и терм2, результат также имеет соответствующее числовое значение, которое является предпочтительным, но его можно интерпретировать и как текстовое (см. пункт Числовые константы). Операции *, / и % имеют более высокий приоритет, чем + и -. Все операции левоассоциативны.
Блокировка файлов
Есть несколько способов блокировки файла. Конкретный выбор зависит, в частности, от того, как оставшаяся часть программы будет использовать блокировку. Нужно также учесть проблемы эффективности и мобильности программы. Далее будут описаны два метода: один использует системный вызов fcntl(2), другой - библиотечную функцию lockf(3C) (удовлетворяющую стандарту /usr/group).
Блокировка всего файла на самом деле является частным случаем блокировки сегмента, поскольку и концепция, и результат блокировки одинаковы. Файл блокируется начиная с байта со смещением ноль и кончая максимально возможным размером файла, что нужно для предотвращения блокировок каких-либо сегментов файла. В этом случае значение длины блокировки устанавливается в ноль. Далее приведен фрагмент исходного текста, использующего для достижения цели системный вызов fcntl(2):
#include <fcntl.h> . . . #define MAX_TRY 10 int try; struct flock lck;
try = 0;
/* Заполним структуру, нужную для блокировки сегмента. Адрес структуры передается системному вызову fcntl */
lck.l_type = F_WRLCK; /* Блокировка на запись */ lck.l_whence = 0; /* Смещение от начала будет равно l_start */ lck.l_start = 0L; /* Блокировка от начала файла */ lck.l_len = 0L; /* и до конца */
/* Делаем не более MAX_TRY попыток блокировки */
while (fcntl (fd, F_SETLK, &lck) < 0) { if (errno == EAGAIN errno == EACCES) { /* Могут быть и другие ошибки, при которых надо повторить попытку */ if (++try < MAX_TRY) { (void) sleep (2); continue; } (void) fprintf (stderr, "Файл занят\n"); return; } perror("fcntl"); exit(2); } . . .
Приведенная часть исходного текста пытается заблокировать файл. Попытки повторяются несколько раз до тех пор, пока не случится одно из событий:
Файл удалось заблокировать.
Возникла ошибка.
Попытки прекращены из-за того, что их количество превысило MAX_TRY.
Чтобы выполнить эту же задачу, используя функцию lockf(3C), исходный текст должен иметь следующий вид:
#include <unistd.h> #define MAX_TRY 10 int try; try = 0;
/* Устанавливаем указатель текущей позиции в начало файла */
lseek (fd, 0L, 0);
/* Делаем не более MAX_TRY попыток блокировки */
while (lockf (fd, F_TLOCK, 0L) < 0) { if (errno == EAGAIN errno == EACCES) { /* Могут быть и другие ошибки, при которых надо повторить попытку */ if (++try < MAX_TRY) { (void) sleep (2); continue; } (void) fprintf (stderr, "Файл занят\n"); return; } perror("lockf"); exit(2); } . . .
Надо заметить, что пример с использованием lockf(3C) выглядит проще, однако пример с fcntl(2) демонстрирует дополнительную гибкость. При использовании fcntl(2) можно установить тип блокировки и начало блокируемого сегмента просто путем присваивания нужных значений компонентам структуры. В случае lockf(3C) просто устанавливается блокировка на запись (монопольная); для того, чтобы указать начало блокируемого сегмента, нужен дополнительный системный вызов [lseek(2)].
БЛОКИРОВКА ФАЙЛОВ И СЕГМЕНТОВ
В операционной системе UNIX с каждым файлом связан режим доступа, определяющий, кто может читать, записывать или выполнять данный файл. Режим доступа может быть изменен либо владельцем файла, либо суперпользователем. Режим доступа к каталогу, в котором находится файл, также влияет на возможность работы с файлом. Например, если режим доступа к каталогу позволяет каждому записывать в него, то любой файл из этого каталога может быть удален любым пользователем, даже таким, для которого режим доступа к файлу не позволяет читать, записываать или выполнять этот файл. Если информацию стоит защитить, ее стоит защитить основательно. Если Ваша программа будет использовать блокировку сегментов, то необходимо обеспечить и соответствующие режимы доступа к Вашим каталогам и файлам. Блокировка сегмента, в том числе и сильная, будет защищать только ту часть файла, которая заблокирована. Если не предпринять соответствующие меры предосторожности, другая часть файла может быть испорчена.
Только заранее определенное множество программ и/или пользователей должно иметь возможность читать или записывать в базу данных (файл). Указанной цели можно легко достичь, если взвести бит переустановки идентификатора группы [см. chmod(1)] для программ, работающих с базой данных. После этого только определенное множество программ, поддерживающих протокол блокировки сегментов, смогут иметь доступ к файлам. Примером подобной защиты файлов является утилита mail(1), хотя в данном случае блокировка сегментов не используется. Порождаемые этой утилитой файлы непрочитанных писем могут читать и изменять только адресаты и сама команда mail(1).
Блокировка и разблокирование сегментов
Блокировка сегментов осуществляется практически тем же способом, что и блокировка файлов. Различие состоит только в начальной точке и длине блокируемого участка. Далее мы рассмотрим интересную и практически важную задачу. Есть два сегмента (в одном или двух файлах), которые должны быть изменены одновременно таким образом, чтобы другие процессы не могли получить доступ к промежуточному состоянию изменяемой информации. (Подобные задачи встречаются, например, при необходимости изменить указатели в двунаправленных списках.) Чтобы решить нашу задачу, необходимо ответить на следующие вопросы:
Что нужно блокировать?
Если сегментов для блокировки много, в каком порядке блокировать и разблокировать их?
Что делать в случае удачной блокировки всех нужных сегментов?
Что делать в случае неудачи при попытке блокировки некоторого сегмента?
При управлении блокировкой сегментов мы должны планировать свои действия на случай неудачной попытки блокировки. Эти действия в первую очередь зависят от характера информации, хранящейся в тех сегментах, которые мы пытаемся блокировать. Возможны, например, следующие действия:
Подождать некоторое время и повторить попытку.
Аварийно завершить процедуру и предупредить пользователя.
Перевести процесс в состояние ожидания до тех пор, пока не поступит сигнал о снятии блокировки.
Скомбинировать перечисленные выше действия.
Теперь рассмотрим пример включения элемента в двунаправленный список. Допустим, что сегмент, после которого мы хотим включить новый элемент, уже заблокирован на чтение. Чтобы этот сегмент можно было корректировать, блокировка должна быть снята и уста- новлена снова или ее уровень повышен до блокировки на запись.
Повышение уровня блокировки (обычно от блокировки на чтение до блокировки на запись) разрешается в том случае, если нет других процессов, заблокировавших на чтение ту же часть файла. Наличие процессов, ожидающих возможности заблокировать на запись ту же часть файла, не мешает повышению уровня: эти процессы останутся в состоянии ожидания. Понижение уровня блокировки с блокировки на запись до блокировки на чтение возможно всегда. В обоих случаях просто изменяется значение вида блокировки. По той причине, что функция lockf(3C) (в соответствии со стандартом /usr/group) не устанавливает блокировки на чтение, изменение уровня блокировки в случае использования этой функции невозможно. Ниже приведен пример блокировки сегмента с последующим изменением ее уровня:
struct record { . . . /* Данные сегмента */ . . . long prev; /* Указатель на предыдущий сегмент в списке */ long next; /* Указатель на следующий сегмент в списке */ };
/* Изменение уровня блокировки с использованием fcntl(2). Предполагается, что к тому моменту, когда эта программа будет выполняться, сегменты here и next будут заблокированы на чтение. Если заблокируем на запись here и next, то: блокируем на запись сегмент this; возвращаем указатель на сегмент this. Если любая из попыток блокировки окончится неудачей, то: переустанавливаем блокировку сегментов here и next на чтение; снимаем все остальные блокировки; возвращаем -1. */
long set3lock (this, here, next) long this, here, next; { struct flock lck;
lck.l_type = F_WRLCK; /* Блокируем на запись */ lck.l_whence = 0; /* Смещение от начала будет равно l_start */ lck.l_start = here; lck.l_len = sizeof (struct record);
/* Повышение уровня блокировки сегмента here до блокировки на запись */ if (fcntl (fd, F_SETLKW, &lck) < 0) { return (-1); }
/* Блокируем сегмент this на запись */ lck.l_start = this; if (fcntl (fd, F_SETLKW, &lck) < 0) { /* Блокировка сегмента this не удалась. Понижаем блокировку сегмента here до уровня чтения */ lck.l_type = F_RDLCK; lck.l_start = here; (void) fcntl (fd, F_SETLKW, &lck); return (-1); }
/* Повышение уровня блокировки сегмента next до блокировки на запись */ lck.l_start = next; if (fcntl (fd, F_SETLKW, &lck) < 0) { /* Блокировка сегмента next не удалась. Понижаем блокировку сегмента here до уровня чтения...*/ lck.l_type = F_RDLCK; lck.l_start = here; (void) fcntl (fd, F_SETLKW, &lck); /*...и снимаем блокировку сегмента this */ lck.l_type = F_UNLCK; lck.l_start = this; (void) fcntl (fd, F_SETLKW, &lck); return (-1); /* Не смогли заблокировать */ } return (this); }
Если другие процессы мешают заблокировать нужные сегменты, то процесс перейдет в состояние ожидания, что обеспечивается операцией F_SETLKW. Если же использовать операцию F_SETLK, то в случае невозможности блокировки системный вызов fcntl(2) завершается неудчей. В последнем случае программу нужно было бы изменить для обработки подобной ситуации.
Теперь рассмотрим сходный пример с использованием функции lockf(3C). Так как она не позволяет выполнять блокировку на чтение, под блокировкой будет пониматься блокировка на запись.
/* Повышение уровня блокировки с использованием lockf(3C). Предполагается, что в тот момент, когда программа будет выполняться, сегменты here и next не будут заблокированы. Если блокировка удастся, то: блокируем сегмент this; возвращаем указатель на сегмент this. Если любая из попыток блокировки окончится неудачей, то: разблокируем все остальные сегменты; возвращаем -1. */
#include <unistd.h>
long set3lock (this, here, next) long this, here, next; { /* Блокируем сегмент here */ (void) lseek (fd, here, 0); if (lockf (fd, F_LOCK, sizeof (struct record)) < 0) { return (-1); }
/* Блокируем сегмент this */ (void) lseek (fd, this, 0); if (lockf (fd, F_LOCK, sizeof (struct record)) < 0) { /* Блокировка сегмента this не удалась. Разблокируем here */ (void) lseek (fd, here, 0); (void) lockf (fd, F_ULOCK, sizeof (struct record)); return (-1); }
/* Блокируем сегмент next */ (void) lseek (fd, next, 0); if (lockf (fd, F_LOCK, sizeof (struct record)) < 0) { /* Блокировка сегмента next не удалась. Разблокируем сегмент here...*/ (void) lseek (fd, here, 0); (void) lockf (fd, F_ULOCK, sizeof (struct record)); /*...и разблокируем сегмент this */ (void) lseek (fd, this, 0); (void) lockf (fd, F_ULOCK, sizeof (struct record)); return (-1); /* Не смогли заблокировать */ } return (this); }
Блокировка снимается тем же способом, что и устанавливается, только в случае системного вызова fcntl(2) в качестве режима блокировки указывается F_UNLCK, а при использовании функции lockf(3C) - F_ULOCK. Разблокирование не может быть предотвращено другим процессом. Разблокирование можно производить только в отношении блокировок, ранее установленных тем же процессом. Разблокирование касается только сегментов, которые были заданы в предыдущем примере структурой lck. Можно производить разблокирование или изменять тип блокировки для части ранее блокированного сегмента, однако это может привести к появлению дополнительного элемента в системной таблице блокировок.
Блокировка сегментов и развитие системы UNIX
В настоящее время проведена работа по реализации блокировки файлов и сегментов в распределенной среде UNIX. Компьютер, на котором выполняется процесс, запрашивающий блокировку, может не совпадать с компьютером, на котором находится блокируемый файл. В этом случае работающие на различных компьютерах процессы могут блокировать сегменты файлов, размещенных на одном из этих компьютеров или даже на другом компьютере. Блокировки на сегменты файла фиксируются на том компьютере, где расположен файл. Важно отметить, что механизм выявления/предотвращения тупиковых ситуаций определен только для блокировок, с которыми работают в пределах одного компьютера. Поэтому, чтобы механизм управления тупиковыми ситуациями работал, процесс должен в каждый данный момент времени работать с блокировками, зафиксированными ровно на одном компьютере. Если для процесса требуется вести блокировки на нескольких компьютерах сразу, рекомендуется не применять операций блокировки с ожиданием и выявлять тупиковые ситуации самостоятельно. Если процесс использует операции блокировки с ожиданием, то он должен обеспечить механизм выхода по времени таким образом, чтобы не "зависать" в ожидании получения возможности блокировки.
Более сложные элементы lex'а
lex предоставляет средства, позволяющие обрабатывать входной текст, пропуская его сквозь весьма хитроумные шаблоны. Среди этих средств - соглашения, определяющие, какую спецификацию выбрать, если на первый взгляд подходит более одной; функции, которые трансформируют один сопоставляемый шаблон в другой; использование определений и функций. Прежде чем перейти к упомянутым средствам, убедитесь в своем понимании изложенного выше, изучив пример, затрагивающий сразу несколько из рассмотренных вопросов.
%% -[0-9]+ printf("отрицательное число"); \+?[0-9]+ printf("положительное число"); -0\.[0-9]+ { printf("отрицательная дробь, целая часть отсутствует"); } rail[ ]+road printf("railroad - одно слово"); crook printf("Это crook"); function subprogcount++; G[a-zA-Z]* { yytext [yyleng] = (char) 0; printf("G-слово: %s", yytext); Gstringcount++; }
Первые три правила распознают отрицательные числа, положительные числа и отрицательные дроби от -1 до 0. Использование + в конце каждой спецификации определяет, что рассматриваемое число составлено из одной или более цифр. Символ . является знаком операции, поэтому перед десятичной точкой в третьем правиле указан \. Каждое из трех следующих правил распознает особый шаблон. Спецификация для railroad сопоставляется в случаях, когда между двумя слогами слова вклинивается один или боле пробелов. В случаях railroad и crook можно просто печатать синоним, а не выдавать сообщения. Правило, распознающее function, увеличивает на единицу счетчик. Последнее правило иллюстрирует несколько возможностей:
Скобки задают последовательность действий на нескольких строках.
Действие использует массив yytext[], в который заносится распознанная цепочка символов;
Спецификация использует символ *, обозначающий, что за G может следовать нуль или более букв.
БОЛЕЕ СЛОЖНЫЕ ВОПРОСЫ
В данном разделе обсуждается ряд усовершенствований yacc'а.
Более сложный пример
В этом пункте рассматривается пример грамматики, в которой используются некоторые специфические свойства yacc'а. Калькулятор из предыдущего пункта реконструируется, чтобы обеспечить действия над вещественными числами и над интервалами таких чисел. Калькулятор понимает вещественные константы, арифметические операции +, -, *, /, унарный -, переменные от a до z. Кроме того, он воспринимает интервалы вида
(X, Y)
где X не превосходит Y. Можно использовать 26 переменных типа интервал с именами от A до Z. Калькулятор работает аналогично тому, который был описан в пункте Простой пример; присваивания не возвращают значений и ничего не выводят, остальные выражения выводят результат (вещественный или интервальный).
В данном примере используется ряд интересных особенностей yacc'а и языка C. Интервал представляется структурой, состоящей из левой и правой границ, значения которых имеют тип double. При помощи конструкции typedef этому структурному типу дается имя INTERVAL. В стек значений yacc'а могут быть занесены также значения целого и вещественного типов (целые числа используются в качестве индекса в массиве, содержащем значения переменных). Отметим, что организация вычислений в целом сильно зависит от возможности присваивать структуры и объединения в языке C. Многие действия вызывают функции, возвращающие значения структурного типа.
Стоит отметить использование макроса YYERROR для обработки ошибочных ситуаций, когда делается попытка деления на интервал, содержащий 0, или употребляется интервал, левая граница которого больше правой. Механизм нейтрализации ошибок используется для того, чтобы отбросить остаток некорректной строки.
Кроме смешивания типов в стеке значений данная грамматика демонстрирует любопытный способ отслеживания типа (скалярного или интервального) промежуточных выражений. Отметим, что скаляр может быть автоматически преобразован в интервал, если контекст требует значения интервального типа. Это приводит к большому числу конфликтов, которые обнаруживает в грамматике yacc: 18 конфликтов свертка-перенос и 26 свертка-свертка. Проблему можно рассмотреть на примере двух входных строк
2.5 + (3.5 - 4.)
и
2.5 + (3.5, 4.)
Во втором примере константа 2.5 должна использоваться в интервальнозначном выражении, однако данный факт не известен до тех пор, пока не прочитана запятая. К этому времени константа 2.5 обработана и алгоритм разбора не может уже передумать и вернуться назад. В более общем случае может оказаться необходимым просмотреть вперед неограниченное число лексем, чтобы сообразить, преобразовывать ли скаляр в интервал. Эту проблему обходят, вводя два правила для каждой бинарной интервальнозначной операции: одну на случай, когда левый операнд является скаляром, другую - когда левый операнд является интервалом. Во втором случае правый операнд должен быть интервалом, поэтому преобразование будет выполняться автоматически. Несмотря на эту увертку, остается еще много случаев, в которых преобразование может либо выполняться либо нет, что приводит к конфликтам. Чтобы их разрешить, в начало файла спецификаций помещен список правил, задающих скаляры; такой способ разрешения конфликтов ведет к тому, что скалярные выражения остаются скалярными до тех пор, пока их не заставят стать интервальными.
Демонстрируемый способ обработки множественных типов весьма поучителен и ... непрактичен. Если имеется много типов выражений, а не два, как в данном примере, число правил возрастает лавинообразно, а число конфликтов - еще быстрее. Поэтому в более привычных языках программирования лучше считать информацию о типе частью значения, а не частью грамматики.
В заключение - несколько слов о лексическом анализе вещественных констант. Чтобы преобразовать текст в число двойной точности, используется процедура atof() из стандартной C-библиотеки. Если лексический анализатор обнаруживает ошибку, он возвращает лексему, которая не допускается грамматикой, провоцируя тем самым синтаксическую ошибку и, как следствие, ее нейтрализацию.
%{
#include <stdio.h> #include <ctype.h>
typedef struct interval { double lo, hi; } INTERVAL;
INTERVAL vmul (), vdiv ();
double atof ();
double dreg [26]; INTERVAL vreg [26];
%}
%start lines
%union { int ival; double dval; INTERVAL vval; }
%token <ival> DREG VREG /* индексы в массивах dreg, vreg */
%token <dval> CONST /* константа с плавающей точкой */
%type <dval> dexp /* выражение */
%type <vval> vexp /* интервальное выражение */
/* информация о приоритетах операций */
%left '+' '-' %left '*' '/' %left UMINUS /* наивысший приоритет у унарного минуса */
%% /* начало секции правил */
lines : /* пусто */ | lines line ; line : dexp '\n' { printf ("%15.8f\n", $1); } | vexp '\n' { printf ("(%15.8f, %15.8f)\n", $1.lo, $1.hi); } | DREG '=' dexp '\n' { dreg [$1] = $3; } | VREG '=' vexp '\n' { vreg [$1] = $3; } | error '\n' { yyerrok; } ;
dexp : CONST | DREG { $$ = dreg [$1]; } | dexp '+' dexp { $$ = $1 + $3; } | dexp '-' dexp { $$ = $1 - $3; } | dexp '*' dexp { $$ = $1 * $3; } | dexp '/' dexp { $$ = $1 / $3; } | '-' dexp %prec UMINUS { $$ = -$2; } | '(' dexp ')' { $$ = $2; } ;
vexp : dexp { $$.hi = $$.lo = $1; } | '(' dexp ',' dexp ')' { $$.lo = $2; $$.hi = $4; if ($$.lo > $$.hi) { printf ("нижняя граница больше верхней\n"); YYERROR; } }
| VREG { $$ = vreg[$1]; } | vexp '+' vexp { $$.hi = $1.hi + $3.hi; $$.lo = $1.lo + $3.lo; } | dexp '+' vexp { $$.hi = $1 + $3.hi; $$.lo = $1 + $3.lo; } | vexp '-' vexp { $$.hi = $1.hi - $3.hi; $$.lo = $1.lo - $3.lo; } | dexp '-' vexp { $$.hi = $1 - $3.hi; $$.lo = $1 - $3.lo; }
| vexp '*' vexp { $$ = vmul ($1.lo, $1.hi, $3); } | dexp '*' vexp { $$ = vmul ($1, $1, $3); } | vexp '/' vexp { if (dcheck ($3)) YYERROR; $$ = vdiv ($1.lo, $1.hi, $3); } | dexp '/' vexp { if (dcheck ($3)) YYERROR; $$ = vdiv ($1, $1, $3); } | '-' vexp %prec UMINUS { $$.hi = -$2.lo; $$.lo = -$2.hi; } | '(' vexp ')' { $$ = $2; } ;
%% /* начало секции подпрограмм */
#define BSZ 50 /* размер буфера для числа с плав. точкой */
/* лексический анализ */
int yylex () { register int c;
/* пропустить пробелы */ while ((c = getchar ()) == ' ') ; if (isupper (c)) { yylval.ival = c - 'A'; return (VREG); } if (islower (c)) { yylval.ival = c - 'a'; return (DREG); }
/* проглотить цифры, точки, экспоненты */
if (isdigit (c) c == '.') { char buf [BSZ + 1], *cp = buf; int dot = 0, exp = 0;
for (; (cp - buf) < BSZ; ++cp, c = getchar ()) { *cp = c; if (isdigit (c)) continue; if (c == '.') { if (dot++ exp) return ('.'); /* приводит к синт. ошибке */ continue; } if (c == 'e') { if (exp++) return ('e'); /* приводит к синт. ошибке */ continue; } /* конец числа */ break; } *cp = ' '; if (cp - buf >= BSZ) (void) printf ("константа слишком длинная\n"); else /* возврат последнего прочитанного символа */ ungetc (c, stdin); yylval.dval = atof (buf); return (CONST); } return (c); }
INTERVAL hilo (a, b, c, d) double a, b, c, d; { /* вычисляет минимальный интервал, содержащий a, b, c и d */
/* используется процедурами, вычисляющими * и / */ INTERVAL v;
if (a > b) { v.hi = a; v.lo = b; } else { v.hi = b; v.lo = a; }
if (c > d) { if (c > v.hi) v.hi = c; if (d < v.lo) v.lo = d; } else { if (d > v.hi) v.hi = d; if (c < v.lo) v.lo = c; } return (v); }
INTERVAL vmul (a, b, v) double a, b; INTERVAL v; { return (hilo (a*v.hi, a*v.lo, b*v.hi, b*v.lo)); }
dcheck (v) INTERVAL v; { if (v.hi >= 0. && v.lo <= 0.) { (void) printf ("интервал-делитель содержит 0.\n"); return (1); } return (0); }
INTERVAL vdiv (a, b, v) double a, b; INTERVAL v; { return (hilo (a / v.hi, a / v.lo, b / v.hi, b / v.lo)); }
Целевой компьютер
Компиляторы и редакторы внешних связей создают выполняемые объектные файлы, предназначенные для запуска на определенных компьютерах. В случае использования кросс-компиляторов, на одном компьютере компилируются и редактируются объектные файлы, предназначенные для выполнения на другом компьютере. Термин целевой компьютер обозначает тот компьютер, на котором предполагается выполнять объектный файл. За редким исключением целевой компьютер - это в точности тот же компьютер, на котором создается объектный файл.
Целые константы
Целая константа, состоящая из последовательности цифр, считается восьмеричной, если начинается с 0 (цифра нуль). Восьмеричная константа состоит только из цифр от 0 до 7. Последовательность цифр, которой предшествует 0x или 0X, трактуется как шестнадцатеричное число. Шестнадцатеричные цифры включают символы от a
(или A) до f (или F) со значениями от 10 до 15. В остальных случаях целая константа считается десятичной. Если значение десятичной константы больше, чем максимальное для данной машины знаковое целое число, считается, что она имеет тип long; аналогично считается, что восьмеричная или шестнадцатеричная константа, превосходящая максимальное беззнаковое целое число, имеет тип long. В остальных случаях целые константы имеют тип int.
Cflow(1)
Команда cflow(1) выдает граф внешних ссылок для C-, YACC-, LEX-, а также ассемблерных и объектных файлов. Используя файлы нашего примера, с помощью команды
cflow restate.c oppty.c pft.c rfe.c
можно получить следующий результат:
1 main: int(), <restate.c 12> 2 fprintf: <> 3 exit: <> 4 getopt: <> 5 fopen: <> 6 fscanf: <> 7 printf: <> 8 oppty: float(), <oppty.c 7> 9 pft: float(), <pft.c 7> 10 rfe: float(), <rfe.c 7>
Та же команда с опцией -r заменяет отношение "вызывающий-вызываемый" на обратное. Результат выполнения команды:
1 exit: <> 2 main : <> 3 fopen: <> 4 main : 2 5 fprintf: <> 6 main : 2 7 fscanf: <> 8 main : 2 9 getopt: <> 10 main : 2 11 main: int(), <restate.c 12> 12 oppty: float(), <oppty.c 7> 13 main : 2 14 pft: float(), <pft.c 7> 15 main : 2 16 printf: <> 17 main : 2 18 rfe: float(), <rfe.c 7> 19 main : 2
Команда cflow с опцией -ix включает в результат ссылки на внешние и статические данные. В нашем примере есть только одна такая ссылка - opterr. Результат:
1 main: int(), <restate.c 12> 2 fprintf: <> 3 exit: <> 4 opterr: <> 5 getopt: <> 6 fopen: <> 7 fscanf: <> 8 printf: <> 9 oppty: float(), <oppty.c 7> 10 pft: float(), <pft.c 7> 11 rfe: float(), <rfe.c 7>
Если указать и опцию -r, и опцию -ix, результат будет следующим:
1 exit: <> 2 main : <> 3 fopen: <> 4 main : 2 5 fprintf: <> 6 main : 2 7 fscanf: <> 8 main : 2 9 getopt: <> 10 main : 2 11 main: int(), <restate.c 12> 12 oppty: float(), <oppty.c 7> 13 main : 2 14 opterr: <> 15 main : 2 16 pft: float(), <pft.c 7> 17 main : 2 18 printf: <> 19 main : 2 20 rfe: float(), <rfe.c 7> 21 main : 2
Числовые константы
К числовым относятся десятичные константы и константы с плавающей точкой. Десятичная константа - это непустая последовательность цифр, которая может содержать десятичную точку (не более чем одну); например: 12, 12., 1.2, .12. Константа с плавающей точкой состоит из десятичной константы, за которой следуют символы e (либо E), необязательный знак + или - и непустая последовательность цифр; например: 12e3, 1.2e3, 1.2e-3, 1.2E+3. Максимально допустимые размер и точность числовых констант машинно-зависимы.
Формат числовых констант описан ранее, в разделе Лексемы. Значения таких констант хранятся в виде вещественных чисел. Текстовое значение числовой константы определяется естественным образом. Предпочтительным является числовое значение. Следующая таблица содержит примеры значений числовых констант:
Числовая константа | Числовое значение | Текстовое значение |
0 | 0 | 0 |
1 | 1 | 1 |
.5 | 0.5 | .5 |
.5e2 | 50 | 50 |
Что нужно программе для работы с curses
Если программа использует пакет curses, она должна включать файл <curses.h> и вызывать подпрограммы initscr(), refresh(), или им подобные, а также endwin().
Что нужно программе для работы с terminfo
Как правило, программа, работающая с terminfo, включает файлы и подпрограммы, которые перечислены ниже:
#include <curses.h> #include <term.h> . . . setupterm ((char*) 0, 1, (int*) 0); . . . putp (clear_screen); . . . reset_shell_mode (); exit (0);
Файлы <curses.h> и <term.h> нужны, поскольку они содержат определения текстовых объектов, чисел и флагов, которые используются подпрограммами terminfo. setupterm() занимается инициализацией. Передача ей значений (char*) 0, 1 и (int*) 0 обеспечивает установку разумных режимов. Если setupterm() не может распознать тип используемого терминала, она выводит сообщение об ошибке и завершается. reset_shell_mode() делает примерно то же, что и endwin(), и должна вызываться перед завершением terminfo-программы.
При вызове setupterm() определяются значения глобальных переменных, например clear_screen. Их значения могут быть выведены на экран входящими в terminfo подпрограммами putp() или tputs(), что обеспечивает пользователю дополнительные возможности по контролю терминала. Такую строку не следует выводить подпрограммой printf(3S) из библиотеки языка C, поскольку в ней содержится информация об использовании символов-заполнителей. Программа, пытающаяся вывести ее таким образом, может завершиться аварийно, если терминал требует использования заполнителей, или если он использует протокол xon/xoff.
На уровне terminfo подпрограммы более высокого уровня, например, addch() и getch(), недоступны. Вам придется самостоятельно решать проблему вывода на экран. Список характеристик и их описаний см. в terminfo(4), список подпрограмм terminfo см. в curses(3X).
Что такое curses?
Curses(3X) - это библиотека подпрограмм, которые используются для разработки программ, осуществляющих ввод/вывод на экран терминала в системе UNIX. Эти подпрограммы являются функциями C или макросами. Многие из них напоминают подпрограммы из стандартной библиотеки языка C. Например, имеется подпрограмма printw(), весьма похожая на printf(3S) и подпрограмма getch(), подобная getc(3S). В Вашем банке программа - автоматический кассир может использовать printw() для вывода меню и getch()
для приема Ваших запросов на изъятие сумм (или, что даже лучше, на их вклад). Экранный текстовый редактор - такой, например, как редактор vi(1) системы UNIX, также может использовать эти и другие подпрограммы пакета curses.
Название curses принято из-за того, что данная библиотека подпрограмм оптимизирует движение курсора, то есть минимизирует это движение в процессе обновления экрана. Например, если (используя подпрограммы пакета curses) Вы разработали текстовый редактор и редактируете фразу
curses/terminfo - отличный пакет для работы с экраном
так, чтобы она читалась:
curses/terminfo - лучший пакет для работы с экраном
то программа выведет только 'лучший' вместо 'отличный', остальные символы останутся без изменений. Оптимизация управления курсором называется еще оптимизацией вывода, поскольку минимизируется объем передаваемых данных, то есть вывод.
При оптимизации управления курсором запись на экран производится таким способом, который соответствует терминалу, с которым работает программа, использующая пакет curses. Таким образом, библиотека curses позволяет делать все необходимое на терминалах различных типов. Подпрограммы пакета просматривают базу данных terminfo (подробно описывается ниже), чтобы найти подходящее описание терминала.
Чем будет полезна оптимизация управления курсором Вам и тем, кто будет пользоваться Вашими программами? Во-первых, она сэкономит Ваше время, затрачиваемое на описание того, как именно Вы хотите изменять содержимое экрана. Во-вторых, она сохранит время пользователя за счет уменьшения времени, необходимого для переписывания экрана. В-третьих, она уменьшит загрузку линий связи системы UNIX в период обновления экрана. В-четвертых, Вам не придется задумываться об огромном количестве терминалов, на которых Ваша программа, быть может, будет работать.
Далее приводится текст простой программы, работающей с curses. Она обращается к нескольким подпрограммам curses для того, чтобы передвинуть курсор на середину экрана и вывести цепочку символов BullsEye. Все эти подпрограммы описываются в следующем разделе, который называется Использование подпрограмм пакета curses. Чтобы понять, что делают эти подпрограммы, Вам достаточно взглянуть на их имена.
#include <curses.h>
main () { initscr (); move (LINES/2 - 1, COLS/2 - 4); addstr ("Bulls"); refresh (); addstr ("Eye"); refresh (); endwin (); }
Что такое разделяемая библиотека?
Разделяемая библиотека - это файл, содержащий объектные модули, которые могут одновременно использоваться (разделяться) несколькими процессами. С точки зрения организации как обычная (неразделяемая), так и разделяемая библиотеки являются архивами. Далее, однако, термины архивная библиотека или архив будут использоваться только в неразделяемом случае.
Если при редактировании внешних связей программы используется разделяемая библиотека, то библиотечные модули, соответствующие внешним ссылкам программы, не копируются в выполняемый объектный файл. Вместо этого в объектном файле создается специальная секция .lib, содержащая ссылки на библиотеку. Когда ОС UNIX выполняет получившийся файл, информация из этой секции используется для включения соответствующей библиотеки в адресное пространство процесса.
Тот факт, что содержимое разделяемых библиотек не копируется в выполняемый файл, дает следующие преимущества:
Экономится дисковое пространство.
Выполняемые файлы имеют меньший размер.
Экономится оперативная память.
За счет совместного использования несколькими процессами одной разделяемой библиотеки уменьшается суммарный размер нужной им оперативной памяти.
Упрощается сопровождение выполняемых файлов.
Как было указано, содержимое разделяемой библиотеки помещается в адресное пространство процесса на этапе выполнения. Таким образом, внесение изменений в разделяемую библиотеку фактически изменяет все использующие ее выполняемые файлы, поскольку операционная система предоставит процессам новое содержимое библиотеки. После исправления ошибки в разделяемой библиотеке все процессы автоматически будут использовать скорректированные модули.
Разумеется, архивные библиотеки не обладают этим свойством: изменения в архиве не приводят к изменению выполняемых файлов, так как архивные модули копируется в них не во время выполнения, а на этапе редактирования связей.
Более подробно свойства разделяемых библиотек описываются далее, в разделе Использовать ли разделяемую библиотеку?.
Что такое terminfo?
Под terminfo мы понимаем следующее:
Группу подпрограмм из библиотеки curses, которые управляют некоторыми терминальными функциями. Например, Вы можете использовать их для написания фильтров, или для программирования функциональной клавиатуры, если она на Вашем терминале программируемая. Подпрограммами terminfo могут пользоваться как те программисты, которые пишут на C, так и те, которые работают в shell'е.
Базу данных, содержащую описания многих типов терминалов, с которыми могут работать программы, если они используют curses. Эти описания содержат характеристики терминала и то, как терминал выполняет различные операции, - например, сколько строк и столбцов отображается на экране, как интерпретируются управляющие символы и т.п. Описание каждого терминала компилируется в отдельный файл. Вам следует использовать язык, описываемый в terminfo(4), для создания таких файлов, и команду tic(1M) для их компиляции. Скомпилированные файлы обычно находятся в каталогах /usr/lib/terminfo/?. Эти каталоги имеют имена, состоящие из одного символа - первой буквы названия терминала. Например, описание терминала AT&T Teletype 5425 обычно находится в файле /usr/lib/terminfo/a/att5425.
Далее приводится простая командная процедура, использующая базу данных terminfo.
# Очистить экран и показать позицию 0,0 # tput clear tput cup 0 0 # или tput home echo "<- это позиция 0 0" # # Показать позицию 5,10 # tput cup 5 10 echo "<- это позиция 5 10"
Ctrace(1)
Используя команду ctrace(1), можно проследить выполнение C-программы по операторам. ctrace читает исходный текст программы из файла и добавляет в него операторы печати для вывода значений переменных после каждого выполненного оператора. Результат выполнения команды следует направить во временный .c-файл, который затем использовать как входной для команды cc. Во время выполнения полученного в разультате файла a.out будет генерироваться вывод, в котором содержится много полезной информации о событиях в Вашей программе.
С помощью опций команды ctrace можно ограничить количество итераций при выполнении циклов. В исходный текст программы можно вставить функции, включающие и выключающие трассировку. Таким образом можно организовать трассировку только необходимых участков программы.
При обращении к ctrace можно указать только один файл с C-программой. Поэтому, чтобы проиллюстрировать действие ctrace на нашем примере, необходимо выполнить следующие команды:
ctrace restate.c > ct1.c ctrace oppty.c > ct2.c ctrace pft.c > ct3.c ctrace rfe.c > ct4.c
Здесь имена выходных файлов выбраны совершенно произвольно. Можно использовать любые подходящие имена. Выбранные имена файлов должны оканчиваться на .c, поскольку эти файлы будут использоваться как входные для системы компиляции языка C:
cc -o ctt ct1.c ct2.c ct3.c ct4.c
Затем команда
ctt -opr
выдаст приведенный ниже результат на стандартный вывод (stdout). (Напомним, что программа читает исходные данные из файла info.) Разумеется, этот результат можно направить в какой-нибудь файл или вывести на печать для последующей обработки или изучения.
9 main (argc, argv) 24 if (argc < 2) /* argc == 2 */ 32 opterr = FALSE; /* FALSE == 0 */ /* opterr == 0 */ 34 while ((ch = getopt (argc, argv, "opr")) != EOF) /* argc == 2 */ /* argv == 2147483384 */ /* ch == 111 or 'o' */ { 35 switch (ch) /* ch == 111 or 'o' */ 36 case 'o': 37 oflag = TRUE; /* TRUE == 1 */ /* oflag == 1 */ 38 break; 50 }
34 while ((ch = getopt (argc, argv, "opr")) != EOF) /* argc == 2 */ /* argv == 2147483384 */ /* ch == 112 or 'p' */ { 35 switch (ch) /* ch == 112 or 'p' */ 39 case 'p': 40 pflag = TRUE; /* TRUE == 1 */ /* pflag == 1 */ 41 break; 50 }
34 while ((ch = getopt (argc, argv, "opr")) != EOF) /* argc == 2 */ /* argv == 2147483384 */ /* ch == 114 or 'r' */ { 35 switch (ch) /* ch == 114 or 'r' */ 42 case 'r': 43 rflag = TRUE; /* TRUE == 1 */ /* rflag == 1 */ 44 break; 50 }
34 while ((ch = getopt (argc, argv, "opr")) != EOF) /* argc == 2 */ /* argv == 2147483384 */ /* ch == -1 */ 52 if ((fin = fopen ("info", "r")) == NULL) /* fin == 1052602 */ 59 if (fscanf (fin, "%s%f%f%f%f%f%f", first.pname, &first.ppx, &first.dp, &first.i, &first.c, &first.t, &first.spx) != 7) /* fin == 1052602 */ /* first.pname == 2147483294 */ 68 printf ("Наименование: %s\n", first.pname); /* first.pname == 2147483294 or "Tutti_Frutti" */ Наименование: Tutti_Frutti
70 if (oflag) /* oflag == 1 */ 71 printf ("Приемлемая цена: $%#5.2f\n", oppty (&first)); 5 oppty (ps) 8 return (ps->i/12 * ps->t * ps->dp); /* ps->i == 1074108825 */ /* ps->t == 1073741824 */ /* ps->dp == 1074339512 */ Приемлемая цена: $4321.00
73 if (pflag) /* pflag == 1 */ 74 printf ("Ожидаемая прибыль (потери): $%#7.2f\n", pft (&first)); 5 pft (ps) 8 return (ps->spx - ps->ppx + ps->c); /* ps->spx == 1076101120 */ /* ps->ppx == 1072693248 */ /* ps->c == 1073259479 */ Ожидаемая прибыль (потери): $12345.00
77 if (rflag) /* rflag == 1 */ 78 printf("Фондоотдача: %#3.2f%%\n", rfe (&first)); 5 rfe (ps) 8 return (100 * (ps->spx - ps->c) / ps->spx); /* ps->spx == 1076101120 */ /* ps->c == 1073259479 */ Фондоотдача: 95.00%
/* return */
Используя в качестве примера правильно работающую программу, трудно продемонстрировать возможности ctrace. Интереснее было бы обнаружить с помощью ctrace ошибку. По-видимому, эта утилита наиболее полезна в случае, когда программа выполняется до конца, но ее результат не совпадает с ожидаемым.
Curses(3X)
Хотя в действительности это библиотека C-функций, curses(3X) упоминается здесь, поскольку его функции можно рассматривать как подъязык для выполнения операций с экраном терминала. Если Вы пишете программы, предполагающие организацию экранного интерфейса с пользователем, Вам будет полезно знать возможности этого пакета.
В заключении краткого обзора, напомним, что не следует упускать возможность использовать shell-процедуры.
Cxref(1)
Команда cxref(1) анализирует группу .c-файлов и строит для каждого файла таблицу перекрестных ссылок на автоматические, статические и глобальные имена.
Ниже приводится результат выполнения команды
cxref -c -o cx.op restate.c oppty.c pft.c rfe.c
Этот результат помещается в файл, в нашем примере это cx.op. Опция -c приводит к тому, что результат выполнения команды для четырех указанных файлов сливается в одну общую таблицу перекрестных ссылок.
restate.c:
oppty.c:
pft.c:
rfe.c:
SYMBOL FILE FUNCTION LINE
BUFSIZ /usr/include/stdio.h -- *12 EOF /usr/include/stdio.h -- 43 *44 restate.c -- 34 FALSE restate.c -- *7 16 17 18 32 FILE /usr/include/stdio.h -- *23 L_ctermid /usr/include/stdio.h -- *79 L_cuserid /usr/include/stdio.h -- *80 L_tmpnam /usr/include/stdio.h -- *82 NULL /usr/include/stdio.h -- 40 *41 restate.c -- 52 P_tmpdir /usr/include/stdio.h -- *81 TRUE restate.c -- *6 37 40 43
_IOEOF /usr/include/stdio.h -- *35 _IOERR /usr/include/stdio.h -- *36 _IOFBF /usr/include/stdio.h -- *30 _IOLBF /usr/include/stdio.h -- *37 _IOMYBUF /usr/include/stdio.h -- *34 _IONBF /usr/include/stdio.h -- *33 _IOREAD /usr/include/stdio.h -- *31 _IORW /usr/include/stdio.h -- *38 _IOWRT /usr/include/stdio.h -- *32 _NFILE /usr/include/stdio.h -- 9 *10 67
_SBFSIZ /usr/include/stdio.h -- *15 _base /usr/include/stdio.h -- *20 _bufend() /usr/include/stdio.h -- *51 _bufendtab /usr/include/stdio.h -- *77 _bufsiz() /usr/include/stdio.h -- *52 _cnt /usr/include/stdio.h -- *18 _file /usr/include/stdio.h -- *22 _flag /usr/include/stdio.h -- *21 _iob /usr/include/stdio.h -- *67 restate.c main 25 27 46 53 62
_ptr /usr/include/stdio.h -- *19 argc restate.c -- 9 restate.c main *10 24 34 argv restate.c -- 9 restate.c main *11 26 28 34 47 55 64
c ./recdef.h -- *8 pft.c pft 8 restate.c main 60 rfe.c rfe 8 ch restate.c main *19 34 35
clearerr() /usr/include/stdio.h -- *61 ctermid() /usr/include/stdio.h -- *71 cuserid() /usr/include/stdio.h -- *71 dp ./recdef.h -- *6 oppty.c oppty 8 restate.c main 60 exit() restate.c main *14 29 48 56 65
fclose() /usr/include/stdio.h -- *72 fdopen() /usr/include/stdio.h -- *68 feof() /usr/include/stdio.h -- *62 ferror() /usr/include/stdio.h -- *63 fflush() /usr/include/stdio.h -- *72 fgetc() /usr/include/stdio.h -- *72 fgets() /usr/include/stdio.h -- *71 fileno() /usr/include/stdio.h -- *64 fin restate.c main *13 52 59
first restate.c main * 20 59 60 61 68 71 75 78
fopen() /usr/include/stdio.h -- *68 restate.c main 13 52 fprintf() /usr/include/stdio.h -- *73 restate.c main 25 27 46 53 62
fputc() /usr/include/stdio.h -- *74 fputs() /usr/include/stdio.h -- *75 fread() /usr/include/stdio.h -- *72 freopen() /usr/include/stdio.h -- *68 fscanf() /usr/include/stdio.h -- *75 restate.c main 59 fseek() /usr/include/stdio.h -- *72 ftell() /usr/include/stdio.h -- *69 fwrite() /usr/include/stdio.h -- *72 getc() /usr/include/stdio.h -- *55 getchar() /usr/include/stdio.h -- *59 getopt() restate.c main *15 34 gets() /usr/include/stdio.h -- *71 getw() /usr/include/stdio.h -- *73 i ./recdef.h -- *7 oppty.c oppty 8 restate.c main 60 lint /usr/include/stdio.h -- 54 main() restate.c -- *9 oflag restate.c main *16 37 70
oppty() oppty.c -- *5 restate.c main *22 71 opterr restate.c main *21 32 p /usr/include/stdio.h -- 51 *51 52 *52 55 *55 56 *56 57 58 61 *61 62 *62 63 *63 64 *64
pclose() /usr/include/stdio.h -- *73 pflag restate.c main *17 40 73
pft() pft.c -- *5 restate.c main *22 75 pname ./recdef.h -- *4 restate.c main 59 68 popen() /usr/include/stdio.h -- *68 ppx ./recdef.h -- *5 pft.c pft 8 restate.c main 60 printf() /usr/include/stdio.h -- *73 restate.c main 68 71 74 78
ps oppty.c -- 5 oppty.c oppty *6 8 pft.c -- 5 pft.c pft *6 8 rfe.c -- 5 rfe.c rfe *6 8 putc() /usr/include/stdio.h -- *56 putchar() /usr/include/stdio.h -- *60 puts() /usr/include/stdio.h -- *75 putw() /usr/include/stdio.h -- *74 rec ./recdef.h -- *3 oppty.c oppty 6 pft.c pft 6 restate.c main 20 rfe.c rfe 6 rewind() /usr/include/stdio.h -- *70 rfe() restate.c main *22 78 rfe.c -- *5 rflag restate.c main *18 43 77
scanf() /usr/include/stdio.h -- *75 setbuf() /usr/include/stdio.h -- *70 setvbuf() /usr/include/stdio.h -- *76 sprintf() /usr/include/stdio.h -- *73 spx ./recdef.h -- *10 pft.c pft 8 restate.c main 61 rfe.c rfe 8 sscanf() /usr/include/stdio.h -- *75 stderr /usr/include/stdio.h -- *49 restate.c -- 25 27 46 53 62
stdin /usr/include/stdio.h -- *47 stdout /usr/include/stdio.h -- *48 system() /usr/include/stdio.h -- *76 t ./recdef.h -- *9 oppty.c oppty 8 restate.c main 61 tempnam() /usr/include/stdio.h -- *71 tmpfile() /usr/include/stdio.h -- *68 tmpnam() /usr/include/stdio.h -- *71 ungetc() /usr/include/stdio.h -- *76 vfprintf() /usr/include/stdio.h -- *74 vprintf() /usr/include/stdio.h -- *74 vsprintf() /usr/include/stdio.h -- *74 x /usr/include/stdio.h -- *56 57 58 60 *60
ДЕЙСТВИЯ
Действием в языке awk является последовательность операторов, разделенных переводами строки или точками с запятой. Действия позволяют решать множество задач, связанных с бухгалтерскими расчетами и обработкой цепочек символов.
После того, как lex распознает цепочку, сопоставляемую с регулярным выражением, заданным в начале правила, он ищет в правой части правила действие, которое надо выполнить. Среди возможных видов действий - запись типа обнаруженной лексемы и значения, которое принимает лексема (если таковые имеются); замена одной лексемы на другую; подсчет числа вхождений лексем или типов лексем. Все, что Вы хотите проделать, нужно записать в виде фрагментов программ на языке C. Действие может состоять из произвольного числа операторов. Можно напечатать сообщение с найденным текстом или сообщение, как-то трансформирующее этот текст. Так, чтобы распознать выражение Amelia Earhart и сообщить об этом, можно специфицировать правило:
"Amelia Earhart" printf("нашли Amelia");
Чтобы заменить в тексте длинные медицинские термины их эквивалентными обозначениями, следует воспользоваться правилом
Electroencephalogram printf("EEG");
Если требуется подсчитать число строк в тексте, нужно распознавать признаки конца строк и увеличивать на единицу счетчик строк. lex использует стандартные для языка C управляющие последовательности символов, подобные \n для символа перевода строки. Для подсчета числа строк можно использовать правило
\n lineno++;
где lineno, как и другие C-переменные, описывается в секции определений, которую мы обсудим позднее.
lex сохраняет каждую сопоставленную цепочку в массиве символов yytext[]. Вы можете распечатать содержимое этого массива или манипулировать им, как угодно. Иногда Ваше действие может состоять из двух или большего числа C-операторов и Вы должны (или решили из соображений стиля и ясности) написать их на нескольких строках. Чтобы информировать lex о том, что действие относится к одному правилу, надо просто заключить C-операторы в скобки. Например, чтобы подсчитать общее число всех цепочек цифр во входном тексте, печатать текущее общее число таких цепочек и выводить каждую из них, как только она обнаружена, можно воспользоваться такой записью:
\+?[0-9]+ { digstrgcount++; printf("%d",digstrngcount); yytext [yyleng] = (char) 0; printf("%s",yytext); }
Эта спецификация сопоставляется с цепочками цифр, перед которыми, быть может, стоит знак плюс, поскольку операция ? обозначает, что знак плюс спереди не обязателен. Кроме того, будут учитываться и отрицательные цепочки цифр, так как последовательность, которая следует за знаком минус, -, будет сопоставляться с приведенным шаблоном. В следующем разделе показано, как отличить отрицательные целые числа от положительных.
С каждым грамматическим правилом пользователь может связать действия, которые будут выполняться при применении правила. Действия могут возвращать значения и получать значения, возвращенные предыдущими действиями. Кроме того, если требуется, лексический анализатор может возвращать значения лексем.
Действие - это произвольный оператор на языке C. Следовательно, в действии можно выполнять операции ввода/вывода, вызывать подпрограммы, изменять значения массивов и переменных. Действие задается одним или несколькими операторами в фигурных скобках, { и }. Например, конструкции
A : '(' B ')' { hello (1, "abc"); } ;
и
XXX : YYY ZZZ { (void) printf ("сообщение\n"); flag = 25; } ;
являются грамматическими правилами с действиями.
Для организации взаимосвязи между действиями и процедурой разбора используется знак $ (доллар). Псевдопеременная $$ представляет значение, возвращаемое завершенным действием. Например, действие
{$$ = 1;}
возвращает значение 1; в сущности, это все, что оно делает.
Чтобы получать значения, возвращаемые предыдущими действиями и лексическим анализатором, действие может использовать псевдопеременные $1, $2, ... $n. Они обозначают значения, возвращаемые компонентами от 1-го до n-го, стоящими в правой части правила; компоненты нумеруются слева направо. В случае правила
A : B C D ;
$2 принимает значение, возвращенное C, а $3 - значение, возвращенное D.
Пример с правилом
expr : '(' expr ')' ;
банален. Ожидается, что значение, возвращаемое этим правилом, равно значению expr в скобках. Поскольку первый компонент конструкции - левая скобка, требуемый результат можно получить так:
expr : '(' expr ')' { $$ = $2; } ;
По умолчанию значение правила равно значению первого компонента в нем ($1). Поэтому грамматические правила вида
A : B ;
зачастую не требуют явного указания действия. В предыдущих примерах действия всегда выполнялись в конце применения правила. Иногда же бывает желательно получить управление до того, как правило применено полностью. yacc позволяет писать действие в середине правила так же, как и в конце. Считается, что такое действие возвращает значение, доступное действиям справа от него посредством обычного механизма $. В свою очередь, оно имеет доступ к значениям, возвращаемым действиями слева от него. Таким образом, в правиле, указанном ниже, результатом действия является присваивание переменной x значения 1, а переменной y - значения, которое возвращает C.
Диагностические сообщения при компиляции
C-компилятор генерирует сообщения для тех операторов программы, которые не удалось скомпилировать. Как правило, смысл этих сообщений очевиден, но, как и в большинстве компиляторов для других языков, они часто указывают не сам ошибочный оператор, а оператор, расположенный на некотором расстоянии от него. Например, если Вы случайно поставите точку с запятой в конце условия оператора if, то последующий подоператор else будет отмечен как синтаксическая ошибка. В случае, когда между if и else имеется блок из нескольких операторов, выданный компилятором номер строки синтаксической ошибки будет существенно отличаться от истинного. Другим распространенным источником синтаксических ошибок являются несбалансированные фигурные скобки.
Динамические параметры зависимостей
Параметр имеет смысл только в строке зависимостей make-файла. $$@ обозначает часть текущей строки слева от двоеточия ($@). Кроме того, имеется параметр $$(@F), позволяющий получить доступ к обозначающему файл компоненту $@. Так, в следующей записи:
cat: $$@.c
строка зависимостей в момент выполнения преобразуется в цепочку cat.c. Это полезно для построения большого числа выполняемых файлов, у каждого из которых только один исходный файл. Например, в каталоге, содержащем утилиты системы UNIX, может быть make-файл вида:
CMDS = cat dd echo date cmp comm chown
$(CMDS): $$@.c $(CC) -O $? -o $@
Конечно, это только подмножество всех однофайловых программ. Для программ, состоящих из многих файлов, обычно заводится специальный каталог и формируется отдельный make-файл. Для каждого конкретного файла, требующего специфической процедуры компиляции, в make-файле должна быть предусмотрена особая запись.
Еще одна полезная форма параметра зависимостей - $$(@F). Этот параметр представляет обозначающую файл часть $$@. Он также вычисляется во время выполнения. Его польза становится очевидной, если попытаться поддерживать каталог /usr/include при помощи make-файла, который находится в каталоге /usr/src/head. Файл описаний /usr/src/head/makefile должен выглядеть так:
INCDIR = /usr/include
INCLUDES = \ $(INCDIR)/stdio.h $(INCDIR)/pwd.h $(INCDIR)/dir.h $(INCDIR)/a.out.h
$(INCLUDES): $$(@F) cp $? $@ chmod 0444 $@
Использование этого make-файла должно приводить в полное соответствие содержимое каталога /usr/include при обновлении любого из упомянутых файлов, расположенных в каталоге /usr/src/head.
Длинные целые константы
Десятичная, восьмеричная или шестнадцатеричная целая константа, за которой следует символ l (или L), имеет тип long. Как следует из дальнейшего обсуждения, для процессоров MC68020/30 значения типов int и long неразличимы.
Доступ к значениям завершенных правил
Действие может ссылаться на значения, возвращенные в результате обработки предыдущих правил. Происходит это обычным образом - посредством знака $, за которым следует число.
sent : adj noun verb adj noun { просмотр предложения ... } ; adj : THE { $$ = THE; } | YOUNG { $$ = YOUNG; } . . . ; noun : DOG { $$ = DOG; } | CRONE { if ($0 == YOUNG) { (void) printf ("что???\n"); } $$ = CRONE; } ; . . .
В этом случае число должно быть нулевым или отрицательным. В действии, следующем за словом CRONE, выполняется проверка, что предыдущая лексема - не YOUNG. Очевидно, такая проверка возможна только в случае, когда о предыдущей лексеме есть информация. Иногда с помощью данного механизма, который, правда, к структурным не отнесешь, обходят многие проблемы, в особенности, когда требуется исключить из в основном регулярной структуры всего несколько комбинаций.
Другие компоненты пакета управления терминалом
Как было сказано выше, пакет управления терминалом обычно называют curses/terminfo. Однако, в нем есть и другие компоненты, из которых мы уже упомянули, в частности, tic(1M). Далее приводится полный список компонент, рассматриваемых в этом руководстве:
captoinfo(1M) | Средство для перевода описаний терминалов, созданных под ранними версиями системы UNIX, в формат terminfo. |
curses(3X) infocmp(1M) |
Средство для печати и сравнения скомпилированных описаний терминалов. |
tabs(1) | Средство для установки нестандартных позиций табуляции. |
terminfo(4) tic(1M) |
Компилятор описаний терминалов для базы данных terminfo. |
tput(1) | Средство для установки позиций табуляции на терминале и получения значений его характеристик. |
См. также profile(4), scr_dump(4), term(4) и term(5). Более подробную информацию об этих компонентах см. в Справочнике программиста и Справочнике пользователя.
Еще о строках, столбцах и подпрограмме initscr( )
Определив размеры экрана терминала, initscr() присваивает значения переменным LINES и COLS. Эти значения берутся из переменных terminfo, называемых lines
и columns. Последние, в свою очередь, извлекаются из базы данных terminfo, если не установлены значения переменных окружения $LINES и $COLUMNS.
ЕЩЕ О ТИПАХ
Этот раздел содержит обзор операций, которые можно выполнять над объектами определенных типов.
Еще об экономии памяти
Этот раздел поможет Вам понять, почему Ваши программы, как правило, будут лучше работать при использовании разделяемых библиотек. В нем объясняется:
Как разделяемые библиотеки, в отличие от архивных, экономят память.
Как ОС UNIX работает с разделяемыми библиотеками.
В каких случаях разделяемые библиотеки могут потребовать больше памяти.
Еще об окнах и подпрограмме refresh( )
Как указывалось ранее, подпрограммы curses не обновляют экран, пока не будет вызвана refresh(). Когда refresh() вызывается, все данные, накопленные для вывода, пересылаются на экран текущего терминала.
Эффект использования окна во многом подобен эффекту использования буфера при работе с редактором системы UNIX. Например, когда Вы редактируете файл при помощи vi(1), все изменения содержимого файла отражаются в буфере, а сам файл изменяется только после выдачи команд w или zz. Аналогично, когда Вы вызываете программу, работающую с экраном через пакет curses, изменяется содержимое окна, а сам экран терминала перерисовывается только при вызове refresh().
В <curses.h> содержится описание принятого по умолчанию окна stdscr (стандартное окно), размеры которого совпадают с размерами экрана терминала. В <curses.h> stdscr имеет тип WINDOW*, указатель на структуру языка C, которую пользователь может представлять себе в виде двумерного массива символов, соответствующего экрану терминала. Программа всегда отслеживает как состояние stdscr, так и состояние физического экрана. refresh(), когда вызывается, сравнивает их и посылает на терминал последовательность символов, приводящий его экран в соответствующий содержимому stdscr вид. При этом выбирается один из многих способов сделать это, с учетом характеристик терминала и возможного сходства того, что есть на экране и того, что содержится в окне. Выходной поток оптимизируется таким образом, чтобы он содержал как можно меньше символов. На рисунке ниже показано, что происходит при работе программы, выводящей BullsEye в центре экрана (см. раздел Что такое curses?). Обратите внимание, что, какой бы мусор ни был на экране, он там и останется, пока не будет вызвана refresh(). При этом вызове экран очищается и заполняется текущим содержимым stdscr.
Вместо stdscr Вы можете создавать и использовать другие окна. Окна полезны для одновременного ведения нескольких образов экрана. Например, во многих приложениях для ввода и вывода данных используются два окна: одно для ввода/вывода собственно данных, а другое - для вывода сообщений об ошибках, чтобы эти сообщения не портили содержимое основного окна.
Можно разделить экран на большое количество окон, обновляя по желанию то или иное из них. Если окна перекрываются, на экран выводится содержимое того окна, которое обновлялось позже. Можно также создать одно окно внутри другого (первое мы будем иногда называть подокном). Допустим, разрабатываемое Вами приложение использует в качестве интерфейса с пользователем экранные формы, например, для изображения на экране расписки. Тогда можно использовать меньшие, находящиеся внутри основного, окна для управления доступом к отдельным полям такой формы.
Некоторые подпрограммы пакета curses предназначены для работы с окнами особого типа, которые мы будем называть спецокнами. Спецокно - это такое окно, размер которого не ограничивается размером экрана и которое не связано с каким-либо определенным местом на экране. Их можно применять, если Вам нужны очень большие окна, или же такие, которые нужно отображать на экран частями. Спецокна могут, например, понадобиться при работе с электронными таблицами.
Ниже показаны взаимосвязи между несколькими окнами, подокнами и спецокнами и экраном терминала.
В разделе Работа с окнами описываются подпрограммы, необходимые для создания и использования окон. Если Вы хотите сейчас увидеть программу, работающую с окнами средствами curses, см. программу window в разделе Примеры программ, работающих с curses.
Exec(2)
exec() - это наименование целого семейства функций: execv(), execl(), execle(), execve(), execlp() и execvp(). Все они превращают вызвавший процесс в другой. Отличия между функциями заключаются в способе представления аргументов. Например, функция execl() может быть вызвана следующим образом:
execl ("/bin/prog", "prog", arg1, arg2, (char*) 0);
Аргументы функции execl() имеют следующий смысл:
/bin/prog | Маршрутное имя нового выполняемого файла. |
prog | Имя, которое новый процесс получит как argv[0]. |
arg1, ... | Указатели на цепочки символов, содержащие аргументы программы prog. |
(char *) 0 | Пустая ссылка, отмечающая конец списка аргументов. |
Более подробная информация об этих функциях содержится в Справочнике программиста. Ключевым свойством функций семейства exec является то, что в случае их успешного завершения управление не возвращается, поскольку вызвавший процесс заменен новым. При этом новый процесс наследует у старого его идентификатор и другие характеристики. Если обращение к exec() завершается неудачей, управление возвращается в вызвавшую программу с результатом -1. Причина неудачи может быть установлена по значению переменной errno (см. ниже).
Файл <curses.h>
В файле <curses.h> определяются несколько глобальных переменных и структур данных, а также те из подпрограмм пакета, которые в действительности являются макросами.
Начнем с рассмотрения определяемых в файле переменных и структур данных. В файле <curses.h> определены параметры всех подпрограмм, входящих в пакет curses. Кроме того, определяются целочисленные переменные LINES и COLS, которым при выполнении программы назначаются значения соответственно вертикального и горизонтального размера экрана. Это делается при вызове описываемой ниже подпрограммы initscr(). В файле определяются также константы OK и ERR. Большинство подпрограмм curses возвращают OK при нормальном завершении и ERR при возникновении ошибки.
Примечание
LINES и COLS являются внешними (глобальными) переменными, представляющими размеры экрана. В пользовательском окружении можно установить значения двух подобных им переменных, $LINES и $COLUMNS. Программы curses используют значения этих переменных окружения для определения размера экрана. В этой главе при ссылках на переменные окружения используется знак $, чтобы отличить их от переменных, объявляемых в файле <curses.h>. Дополнительную информацию об этих переменных см. ниже в разделах Подпрограммы initscr(), refresh() и endwin() и Еще о строках, столбцах и подпрограмме initscr().
Рассмотрим теперь макроопределения. Многие подпрограммы curses
определены в <curses.h> как макросы, которые обращаются к другим подпрограммам и макросам из curses. Например, refresh() является макросом. Определение
#define refresh() wrefresh(stdscr)
показывает, что вызов refresh расширяется в обращение к также входящей в curses подпрограмме wrefresh(). Эта последняя, в свою очередь, вызывает две другие подпрограммы curses: wnoutrefresh() и doupdate(). Многие подпрограммы обеспечивают нужный результат, группируя два-три обращения к другим.
Предостережение
Макрорасширения curses могут создать проблемы при использовании некоторых конструкций языка C, например, ++ или --.
Одно последнее замечание о <curses.h>: он автоматически включает <stdio.h>, файл интерфейса с драйвером tty, <termio.h>. Повторное включение в программу этих файлов безвредно, но и бессмысленно.
ФАЙЛЫ ОПИСАНИЙ И ПОДСТАНОВКИ
В данном разделе описываются основные компоненты файла описаний.
Физические и виртуальные адреса
Физический адрес секции или имени есть смещение этой секции или данных, соответствующих этому имени, от начала (нулевого адреса) адресного пространства. Значение термина физический адрес, когда он используется для описания объектных файлов обычного формата, отличается от общепринятого. Физический адрес объекта не обязательно будет совпадать с адресом, по которому этот объект будет помещен во время выполнения. Так, в системах со страничной виртуальной памятью адрес берется относительно нулевого адреса виртуальной памяти, после чего операционная система выполняет дальнейшее преобразование адреса. Заголовок секции содержит два адресных поля, для физического и виртуального адресов; однако во всех версиях COFF-формата и ОС UNIX эти адреса совпадают.
Флаги
Последние два байта заголовка файла содержат флаги, характеризующие тип объектного файла. Флаги определены во включаемом файле <filehdr.h>; используемые флаги приведены в следующей таблице:
Обозначение | Значение | Смысл |
F_RELFLG | 00001 | Из файла удалена информация о настройке ссылок |
F_EXEC | 00002 | Файл является выполняемым (в нем нет неразрешенных внешних ссылок) |
F_LNNO | 00004 | Из файла удалена информация о номерах строк |
F_LSYMS | 00010 | Из файла удалена информация о локальных именах |
F_AR32W | 0001000 | 32-битное слово |
Два младших байта поля флагов определяют тип секции. Флаги описаны в следующей таблице:
Обозначение | Значение | Смысл |
STYP_REG | 0x00 | Обычная секция (размещаемая, настраиваемая, загружаемая) |
STYP_DSECT | 0x01 | Фиктивная секция (неразмещаемая, настраиваемая, незагружаемая) |
STYP_NOLOAD | 0x02 | Незагружаемая секция (размещаемая, настраиваемая, незагружаемая) |
STYP_GROUP | 0x04 | Групповая секция (формируется из входных секций) |
STYP_PAD | 0x08 | Секция-заполнитель (неразмещаемая, ненастраиваемая, загружаемая) |
STYP_COPY | 0x10 | Секция типа COPY (рабочий признак для редактора связей; неразмещаемая, ненастраиваемая, загружаемая; таблицы настройки ссылок и номеров строк обрабатываются обычным образом) |
STYP_TEXT | 0x20 | Секция содержит выполняемые команды |
STYP_DATA | 0x40 | Секция содержит инициализированные данные |
STYP_BSS | 0x80 | Секция содержит только неинициализированные данные |
STYP_INFO | 0x200 | Секция комментариев (неразмещаемая, ненастраиваемая, незагружаемая) |
STYP_OVER | 0x400 | Оверлейная секция (неразмещаемая, настраиваемая, незагружаемая) |
STYP_LIB | 0x800 | Библиотечная секция .lib (обрабатывается так же, как секция комментариев) |
Fork(2)
Системный вызов fork() создает новый процесс - точную копию вызвавшего процесса. В этом случае новый процесс называется порожденным процессом, а вызвавший процесс - родительским. Единственное существенное отличие между этими двумя процессами заключается в том, что порожденный процесс имеет свой уникальный идентификатор. В случае успешного завершения fork() возвращает порожденному процессу 0, а родительскому процессу - идентификатор порожденного процесса. Идея получения двух одинаковых процессов может показаться несколько странной, однако:
Поскольку возвращаемые значения различны для порожденного и родительского процессов, процессы могут выполняться по-разному в зависимости от возвращаемого значения.
Порожденный процесс может сказать: "Отлично, я - порожденный процесс. Скорее всего я превращусь в совершенно другой процесс с помощью вызова exec()."
Родительский процесс может сказать: "Порожденный мной процесс будет превращен в новый вызовом exec(). А я вызову wait(2) и буду ждать, пока этот процесс не завершится."
Чтобы переложить приведенные рассуждения на язык C, можно включить в программу операторы, аналогичные следующим:
#include <errno.h>
int ch_stat, ch_pid, status; char *progarg1; char *progarg2; void exit (); extern int errno;
if ((ch_pid = fork ()) < 0 ) { /* Вызов fork завершился неудачей ... проверка переменной errno */ } else if (ch_pid == 0) { /* Порожденный процесс */ (void) execl ("/bin/prog","prog",arg1,arg2,(char*)0); exit (2); /* execl() завершился неудачей */ } else { /* Родительский процесс */ while ((status = wait (&ch_stat)) != ch_pid) { if (status < 0 && errno == ECHILD) break; errno = 0; } }
Поскольку при вызове exec() идентификатор порожденного процесса наследуется новым процессом, родительский процесс знает этот идентификатор. Действие приведенного фрагмента программы сводится к остановке выполнения одной программы и запуску другой, с последующим возвращением в ту точку первой программы, где было остановлено ее выполнение. В точности то же самое происходит при вызове функции system(3S). Действительно, реализация system() такова, что при ее выполнении вызываются fork(), exec() и wait().
Следует иметь в виду, что в данный пример включено минимальное количество проверок различных ошибок. Так, необходимо проявлять осторожность при совместной работе с файлами. Помимо именованных файлов, новый процесс, порожденный вызовом fork() или exec(), наследует три открытых файла: stdin, stdout и stderr. Если вывод родительского процесса буферизован и должен появиться до того, как порожденный процесс начнет свой вывод, буфера должны быть вытолкнуты до вызова fork(). А если родительский и порожденный процессы читают из некоторого потока, то прочитанное одним процессом уже не может быть прочитано другим, поскольку при чтении продвигается указатель текущей позиции в файле.