Руководство пользователя для GNU Awk

Arnold D. Robbins
перевод Балуева А. Н.

5. Чтение входных файлов

Оглавление

5.6 Чтение данных фиксированной ширины

В начало страницы

(В этом разделе обсуждается новая, экспериментальная возможность. Если вы еще неопытный пользователь awk, можете пропустить это при первом чтении.) В версии gawk 2.13 введено новое средство для действий с полями фиксированной ширины без распознавания разделителей полей. Данные этой природы появляются, например, на входе старых FORTRAN-программ, где числа примыкают друг к другу; или на выходе программ, которые не предусматривают их использования в качестве входа других программ. Примером последних может служить таблица, где все столбцы выпрямленным использованием переменного числа пробелов и пустые поля заполнены пробелами. Ясно, что принятое в awk нормальное разделение полей с помощью FS неудобно в этом случае. Хотя переносимые awk-программы могут использовать последовательность вызовов substr для $0 (см. раздел 12.3 [Встроенные функции для действий со строками], стр. 137), это громоздко и неэффективно при большом числе полей.

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

Следующие данные представляют выход утилиты Unix w. Они полезны для иллюстрации использования FIELDWIDTHS.


10:06pm up 21 days,
14:04, 23 users 
User tty login idle JCPU PCPU what
hzuottyV0 8:58pm 9 5 vi p24.tex hzang ttyV3 
6:37pm 50 -csh eklye ttyV5 9:53pm
7 1 em thes.tex dportein ttyV6 8:17pm 
1:47 -csh gierd ttyD3 10:00pm 1 elm
dave ttyD4 9:47pm 4 4 w brent ttyp0 
26Jun91 4:46 26:46 4:41 bash dave ttyq4
26Jun9115days 46 46 wnewmail

Следующая программа читает приведенный выше выход, переводит время простоя (idle time) в секунды и печатает первые два поля и вычисленное время простоя. (Это программа использует несколько средств awk, которые еще не рассматривались.)

BEGIN - FIELDWIDTHS = "9 6 10 6 7 7 35" "" NR ? 2 -
idle = $4 sub(/^ */, "", idle) # strip leading spaces if (idle == "")
idle = 0 if (idle ~ /:/) -
split(idle, t, ":") idle = t[1] * 60 + t[2] "" if (idle ~ /days/)
idle *= 24 * 60 * 60
print $1, $2, idle ""

Здесь приведен результат выполнения программы program на данных:

hzuo ttyV0 0 hzang ttyV3 50 eklye ttyV5 0 
dportein ttyV6 107 gierd ttyD3 1
dave ttyD4 0 brent ttyp0 286 
dave ttyq4 1296000

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

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

Присваивание некоторого значения FS побуждает gawk вернуться к использованию FS для разделения полей. Используйте `FS = FS' для этого, не зная текущего значения FS.

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

5.7 Многострочные записи

В начало страницы

В некоторых базах данных одна строка не может удобно содержать информацию одной записи. В таких случаях можно использовать многострочные записи. Первым шагом на этом пути должен быть выбор формата данных: если записи не определены как отдельные строки, как их определять? Что должно их разделять? Одна из возможностей состоит в выборе особого символа или строки для их разделения. Например, можно использовать прогон страницы (записываемый как `"f' в awk, аналогично тому как в Си), рассматривая каждую запись как страницу в файле. Чтобы сделать это, присвойте переменной RS значение ""f" (цепочка, содержащая символ конца страницы). Может также использоваться любой другой символ, не являющийся частью данных в записи.

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

Того же самого эффекта, как от `RS = ""', можно достичь, присваивая RS цепочки ""n"n+". Это regexp соответствует newline в конце записи и одной или более пустых строк после записи. Кроме того, всякое регулярное выражение всегда соответствует наиболее длинной возможной последовательности, когда имеется возможность выбора (см. раздел 4.6 [Какая порция текста соответствует?], стр. 34). Так что следующая запись не начинается, пока не будет достигнута первая непустая строка, перед которой было сколько угодно пустых строк подряд, рассматриваемых как один разделитель записей.

Имеется важное различие между `RS = ""' и `RS = ""n"n+"'. В первом случае ведущие символы newline во входном файле данных игнорируются, и если файл оканчивается без дополнительных пустых строк после последней записи, конечная newline удаляется из записи. Во втором случае такая специальная обработка не производится (d.c.).

Теперь, когда ввод разделен на записи, следующий шаг состоит в разделении полей в записи. Один путь состоит в разделении каждой строки на поля обычным образом. Это происходит по умолчанию в результате следующей особенности: когда RS присвоена пустая цепочка string, символ newline всегда действует как разделитель полей. Это происходит дополнительно к всякому разделителю полей, проистекающему от FS. Исходным мотивом для этого специального исключения было, по-видимому, намерение добиться полезного поведения в случае действия по умолчанию (т.е. когда FS равно " "). Но это может привести к проблемам, если вы не хотите, чтобы символ newline разделял поля, поскольку нет способа предотвратить такое действие. Но можно обойти ситуацию, используя функцию split для расчленения записи вручную (см. раздел 12.3 [Встроенные функции для действий со строками], стр. 137).

Другой путь для отделения полей есть расположение полей на разных строках: чтобы сделать это, достаточно присвоить переменной FS цепочку ""n". (Это простое регулярное выражение соответствует одиночной newline.) Практическим примером файла данных, организованного таким образом, может быть список адресов, где члены списка разделены пустыми строками. Если список адресов находится в файле с именем `addresses', он выглядит примерно так:

Jane Doe 123 Main Street Anywhere, SE 12345-6789
John Smith 456 Tree-lined Avenue Smallville, MW 98765-4321
...

Простая программа обработки такого 
файла будет подобна следующей:

# addrs.awk --- простая программа для список адресов
# Записи разделены пустыми строками.
# Каждая запись состоит из одного поля.
BEGIN - RS = "" ; FS = ""n" ""
-
print "Name is:", $1 print "Address is:", 
$2 print "City and State are:",
$3 print "" ""

Исполнение программы породит следующий выход:


$ awk -f addrs.awk addresses
a Name is: Jane Doe
a Address is: 123 Main Street
a City and State are: Anywhere, SE 12345-6789
a
a Name is: John Smith
a Address is: 456 Tree-lined Avenue
a City and State are: Smallville, MW 98765-4321
a...

См. раздел 16.2.4 [Печать почтовых меток], стр. 233, где имеются более интересные программы с адресными списками. Следующая таблица содержит краткие сведения о разделении записей с помощью значения RS. (`==' означает "имеет значение")

RS == ""n" Записи разделены символами newline (`"n'). В действительности, каждая строка в файле данных есть отдельная запись, включающая пустые строки. Это --- действия по умолчанию.

RS == любой одиночный символ Записи разделяются вхождениями этого символа. Кратные последовательные вхождения ограничивают пустые записи.

RS == "" Записи разделяются группами пустых строк. Символ newline всегда означает разделитель полей, в дополнение к любому значению в FS. Ведущие и заключительные newline в файле игнорируются.

RS == regexp Записи разделяются вхождениями символов, соответствующих regexp. Ведущие и заключительные соответствия regexp ограничивают пустые записи.

Во всех случаях gawk присваивает RT входной текст, соответствующий значению, указанному в RS.

5.8 Явный ввод по команде getline

В начало страницы

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

5.8.1 Введение для getline

В начало страницы

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

getline возвращает код 1, если нашла запись, и код 0 если обнаружила конец файла. Если при поиске записи обнаружено, что файл нельзя открыть, то getline выдает \Gamma 1. В таком случае gawk устанавливает в переменной ERRNO цепочку с описанием случившейся ошибки. В последующих примерах слово command заменяет значение цепочки, представляющей команду оболочки.

5.8.2 Использование getline без аргументов

В начало страницы

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

Рассмотрим пример:

awk '-
if ((t = index($0, "/*")) != 0) -
# value will be "" if t is 1 tmp = substr($0, 1, t - 1) u = index
(substr($0, t + 2), "*/") while (u == 0) -
if (getline != 0) -
m = "unexpected EOF or error" m = (m ": " ERRNO) print m ? "/dev/stderr"
 exit "" t = -1 u = index($0, "*/") "" # substr expression will be "" if */
# occurred at end of line $0 = tmp substr($0, t + u + 3) "" print $0 ""'

Эта awk-программа удаляет все комментарии Cи-типа, `/* ... */', из ввода. Посредством замены `print $0' другими операторами вы можете осуществить более сложную обработку ввода с удаленными комментариями, такую как поиск соответствий регулярному выражению. Эта программа имеет один недостаток --- она не работает, если один комментарий кончается, а другой начинается на той же строке.

Эта форма команды getline устанавливает NF (количество полей; см. раздел 5.2 [Обследование полей], стр. 40), NR (количество прочитанных пока записей; см. раздел 5.1 [Как ввод разделяется на записи, стр. 37), FNR (количество записей, прочитанных и текущего файла) и значение $0.

Замечание: новое значение $0 используется для проверки образцов любых последующих правил. Исходное значение $0, которое вызвало правило с выполнением getline, пропадает (d.c.). В отличие от этого, следующий оператор читает новую запись и немедленно начинает ее обработку нормальным образом, начиная с первого правила в программе. См. раздел 9.7 [Следующий оператор], стр.111.

5.8.3 Использование getline в переменную

В начало страницы

Можно воспользоваться `getline var' для чтения следующей записи из ввода awk в переменную. Никакой другой обработки при этом не происходит. Например, предположим, что следующая строка есть комментарий или некоторая специальная цепочка и вы хотите прочесть ее без употребления каких-нибудь правил. Эта форма getline позволяет вам прочесть эту строку и запомнить ее в переменной так что главный цикл читать-строку-и-проверять-каждое-правило ее никогда не обнаружит.

Следующий пример переставляет каждую пару строк в вводе. 

Пусть заданы:

wan tew free phore

Тогда он выдаст:

tew wan phore free

Вот эта программа:

awk '-
if ((getline tmp) ? 0) -
print tmp print $0 "" else
print $0 ""'

Такая форма getline устанавливает только переменные NR and FNR (и, конечно, var). Запись не разделяется на поля, так что значения полей (включая $0) и значение NF не меняются.

5.8.4 Употребление getline из файла

В начало страницы

`getline ! file' используется для чтения следующей записи из файла file. Здесь file есть выражение со значением цепочки, указывающей имя файла. `! file' называется перенаправлением, поскольку пере направляет ввод на другой источник. Например, следующая программа читает свои входные записи из файла `secondary.input' после обнаружения первого поля со значением, равным 10 в текущем входном файле.

awk '-
if ($1 == 10) -
getline ! "secondary.input" print "" else
print ""'

Поскольку главный входной поток не используется, значения NR и FNR не меняются. Но прочтенная запись разделяется на поля нормальным образом, так что значения $0 и других полей изменяются. А также значение NF.

В соответствии с POSIX, `getline ! expression' не определено, если expression содержит операторы без скобок, отличные от `$'; например , `getline ! dir "/" file' не определено, поскольку оператор конкатенации не заключен в скобки. Нужно писать `getline ! (dir "/" file)', если вы хотите, чтобы ваша программа была переносимой на другие реализации awk.

5.8.5 Использование getline в переменную из файла

В начало страницы

Команда `getline var ! file' используется для чтения ввода из файла file и помещения его в переменную var. Как и выше, file означает выражение со значением цепочки, которое определяет файл для чтения. В этом варианте getline ни одна из встроенных переменных не меняется и запись не разделяется на поля. Изменяется только переменная var.

Например, следующая программа копирует все входные файлы на выход, за исключением записей вида: `@include filename'. Каждая такая запись заменяется содержимым файла filename.

awk '-
if (NF == 2 && $1 == "@include") -
while ((getline line ! $2) ? 0)
print line close($2) "" else
print ""'

Заметим, что здесь имя дополнительного входного файла не встроено в программу; оно берется из данных, a из второго поля в строке `@include'. Функция close вызывается для обеспечения того, что если две одинаковых строки `@include' появятся во вводе, то весь указанный файл будет включен дважды. См. раздел 6.8 [Закрытие входных и выходных файлов и конвейеры], стр. 74.

Недостаток этой программы в том, что она не обрабатывает вложенные `@include' (операторы `@include' в включаемых файлах) так, как это делает макро-препроцессор. См. раздел 16.2.9 [Простой способ использования библиотечных функций], стр. 244, о программах, которые обрабатывают вложенные операторы `@include'.

5.8.6 Использование getline из конвейера

В начало страницы

Можно передавать конвейером (pipe) выход от command к getline, используя `command -- getline'. В таком случае слово command исполняется как команда оболочки и ее выход передается в awk для использования в качестве ввода. Эта форма getline читает за раз одну запись из конвейера.

Например, следующая программа копирует ее ввод на вывод, за исключением строк, которые начинаются с `@execute'; такие строки заменяются выходом, полученным при выполнении остатка строки в качестве команды оболочки.

awk '-
if ($1 == "@execute") -
tmp = substr($0, 10) while ((tmp -- getline) ? 0)
print close(tmp) "" else
print ""'

Функция close вызывается для того, чтобы в случае двух идентичных строк `@execute' во входе команда command выполнялась для каждой из них. См. раздел 6.8 [Закрытие входных и выходных файлов и конвейеры], стр. 74.

Пусть задан ввод:

foo bar baz @execute who bletch

программа может выдать:

foo bar baz arnold ttyv0 
Jul 13 14:22 miriam ttyp0 
Jul 13 14:23 (murphy:0)
bill ttyp1 
Jul 13 14:23 (murphy:0) bletch

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

Этот вариант getline разделяет записи на поля, устанавливает значение NF и пере вычисляет значение $0. Значения NR и FNR не меняются. В соответствии с POSIX, `expression -- getline' не определена, если expression содержит не заключенные в скобки операторы, отличные от `$'; например, `"echo " "date" -- getline' не определена, так как конкатенация не заключена в скобки, нужно писать `("echo " "date") -- getline', чтобы программа была переносимой на другие реализации awks. (Бывает, что gawk ее выполнит правильно, но на это не надо полагаться. Во всяком случае, скобки делают программу легче читаемой.)

5.8.7 Использование getline в переменную из конвейера

В начало страницы

Если вы употребите `command -- getline var', выход команды command будет послан по конвейеру в getline и в переменную var. Например, следующая программа читает текущие дату и время в переменную current.time, используя утилиту date, а затем печатает значение переменной.

awk 'BEGIN -
"date" -- getline current.time close("date") 
print "Report printed on "
current.time ""'

В этой версии getline не меняется ни одна из встроенных переменных и запись не разделяется на поля.

5.8.8 Сводка вариантов getline

В начало страницы

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

Многие реализации awk ограничивают количество конвейеров, которые может открыть awk-программа, только одним! В gawk такого ограничения нет. Можн открыть столько конвейеров, сколько позволяет операционная система.

Интересный побочный эффект может случиться, если вы используете getline (без перенаправлений) внутри правила BEGIN. Поскольку не перенаправленная getline читает из файлов данных командной строки, первая команда getline заставляет awk установить значение в FILENAME. Нормально FILENAME не имеет значения внутри правил BEGIN, поскольку еще не началась обработка файлов с данными, указанных в командной строке (d.c.). (См. раздел 8.1.5 [Специальные образцы BEGIN и END], стр. 100, и раздел 10.2 [Встроенные переменные, которые передают информацию], стр. 117.)

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

getline устанавливает  $0, NF, FNR and NR.
getline var устанавливает  var, FNR and NR.
getline ! file устанавливает  $0 and NF.
getline var ! file устанавливает var.
command -- getline устанавивает  $0 и and NF.
command -- getline var устанавливает  var.
В начало страницы

<<< Оглавление Страницы: 5  6 >>>