Главная » Программирование для UNIX » Язык сканирования и обработки шаблонов awk

0

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

$ awk  ’program’  имена5файлов …

но аргумент program имеет  другое значение:

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

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

Программа awk читает входные файлы имена5файлов построчно. Каждая  строка проверяется на соответствие с каждым шаблоном; при  на-

личии соответствия выполняются действия. Как и sed, программа awk

не изменяет входные файлы.

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

$ awk  ’/регулярное выражение/  {  print  }’  имена5файлов …

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

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

$ awk  ’/регулярное выражение /’ имена5файлов  …

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

$ awk  ’{ print }’ имена5файлов  

делает то же, что и cat, только намного медленнее.

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

$ awk  –f  cmdfile  имена5файлов

Поля

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

$ who

you

tty2

Sep 29 11:53

jim

tty4

Sep 29 11:27

$

Этим полям awk присваивает имена $1,  $2,  …,  $NF, где NF  – переменная, содержащая количество полей в строке. В приведенном примере NF=5  для обеих  строк. (Обратите  внимание на  то,  что  NF  –  это  количество полей, а $NF  – последнее поле  в строке. В отличие от оболочки, переменные awk не  имеют  префикса $,  за  исключением имен  полей.) Например, чтобы не выводить размер файла в команде du –a, напишем

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

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

$ who  | awk ’{ print $1,  $5 }’

you 11:53 jim  11:27

$

а вот те же данные, отсортированные по времени регистрации:

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

11:27  jim 11:53  you

$

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

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

$ sed  3q  /etc/passwd root:3D.fHR5KoB.3s:0:1:S.User:/: ken:y.68wd1.ijayz:6:1:K.Thompson:/usr/ken: dmr:z4u3dJWbg7wCk:7:1:D.M.Ritchie:/usr/dmr:

$

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

$ sed  3q /etc/passwd  | awk –F ’{ print  $1 }’

root  ken dmr

$

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

Печать

Кроме подсчета полей на входе  awk ведет  и другую интересную статистику. Встроенная  переменная NR  хранит порядковый номер  текущей входной строки. Для вывода номеров строк используйте команду

$ awk  ’{ print NR,  $0 }’

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

Если  возможностей форматирования оператора print недостаточно, то можно обратиться к printf. Так, например, можно напечатать номера строк в поле длиной четыре символа:

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

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

Можно переписать программу ind (рассмотренную ранее  в этой  главе) следующим образом:

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

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

Шаблоны

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

$ awk  –F: ’$2  ==  ""’ /etc/passwd

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

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

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

$2 !~  /./           Второе поле не соответствует никакому символу

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

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

Длину строки символов выдает length – встроенная функция awk. Символ ! в начале шаблона означает отрицание, например

!($2  == "")

Оператор ! подобен  оператору языка Си, но не оператору sed (в sed   !

следует за шаблоном).

Часто шаблоны применяются в awk для несложной проверки корректности  данных.  Обычно  выполняется проверка несоответствия  строк

некоторому критерию («отсутствие новостей – хорошая новость»). Например, следующий шаблон проверяет при  помощи оператора вычисления остатка %, содержит ли входная строка четное  число  полей:

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

А в следующем примере, с функцией length, печатаются строки, длина которых превышает заданную:

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

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

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

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

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

$ date

Thu Sep 29 12:17:01  EDT  1983

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

12:17

$

Упражнение 4.7.  Сколькими способами вы могли бы имитировать команду cat с помощью awk? Какой вариант самый короткий? ~

Шаблоны BEGIN и END

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

$ awk  ‘BEGIN  {  FS =  ":"  }

>           $2 ==  ""  ‘ /etc/passwd

$                            Нет вывода: у всех пользователей есть пароль

Шаблон END определяет действия, выполняемые после  того, как будет обработана последняя строка:

$ awk  ‘END  {  print  NR  }’ …

напечатает число входных строк.

Арифметические выражения и переменные

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

{  s  =  s  +  $1  } END    {  print  s  }

А поскольку в переменной NR хранится количество слагаемых, то выражение вида

END     { print  s, s/NR }

выведет сумму  и среднее значение.

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

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

{  s  +=  $1  } END    {  print  s  }

Выражение s  +=  $1 эквивалентно s  =  s  +  $1, но более компактно.

Обобщая приведенный выше пример, получим:

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

}

END     { print  NR, nw, nc }

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

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

$ cat  prpages

# prpages: вычисляет количество страниц,  которое  будет  напечатано программой  pr wc  $* |

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

END                     { print n }’

$

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

$ wc ch4.*

753

3090

18129 ch4.1

612

2421

13242 ch4.2

637

2462

13455 ch4.3

802

2986

16904 ch4.4

50

213

1117 ch4.9

2854

11172

62847 total

$ prpages  ch4.*

53

$

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

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

53

$

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

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

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

Переменная       Значение

FILENAME                     имя текущего входного файла

FS                          разделитель полей (пробел и табуляция по умолчанию)

NF                                    количество полей во входной записи

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

OFMT                                  формат вывода для чисел (%g по умолчанию, см. printf(3))

1        Команда pr добавляет на каждую страницу по пять строк на шапку и подвал, поэтому делим на 56, а не на 66. – Примеч. перев.


Переменная

Значение

OFS

ORS RS

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

разделитель выходных строк (символ новой  строки по умол чанию)

разделитель входных записей (символ новой строки по умолчанию)

Таблица 4.4. Операторы awk (в порядке повышения приоритета)

Оператор

Действие

= += –= *= /=  %=

||

&&

!

> >= < <= == !=  ~ !~

ничего

+ –

* / %

++ ––

присваивание; v  op = expr эквивалентно v =  v op (expr)

логическое  ИЛИ:  expr1  || expr2  истинно, если  истинен любой из операндов

логическое И: expr1  || expr2 истинно, если истинны оба операнда

инвертирование результата выражения

операторы сравнения; ~ – совпадение, !~ – несовпадение конкатенация строк

плюс и минус

умножение, деление, вычисление остатка

инкремент и декремент (префиксная и постфиксная формы)

Упражнение 4.8.  Способ  тестирования prpages  наводит на  мысль об альтернативной реализации. Определите наиболее быструю путем эксперимента. ~

Операторы управления

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

$ cat  double

awk ‘

FILENAME  !=  prevfile {     # новый  файл

NR  = 1                            #  переустановить счетчик  строк prevfile = FILENAME

}

NF  > 0 {

if ($1  == lastword)

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

for  (i  =  2;  i  <=  NF;  i++) if  ($i  ==  $(i–1))

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

lastword = $NF

}’ $*

$

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

Условный оператор if имеет  тот же вид,  что и в языке Си.

if (условие)

оператор1

else

оператор2

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

Оператор цикла  for  аналогичен одноименному оператору Си, но отли чается от используемого в оболочке:

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

оператор

Оператор for  идентичен следующему оператору while, допустимому в

awk:

выражение1

while  (условие)  { оператор выражение2

}

Например,

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

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

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

Массивы

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

$ cat  backwards

# backwards:  построчная печать  файла в обратном порядке awk ‘        {  line[NR]  = $0 }

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

$

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

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

zymurgy zymotically zymotic zymosthenic zymosis

$

Преимущество команды tail заключается в том, что перемещение по файлу она  организует  средствами файловой системы, что  позволяет избежать чтения промежуточных данных. Функция lseek  рассмотрена в главе 7. (В системе, используемой авторами, команда tail имеет  параметр –r, инвертирующий порядок вывода строк, что позволяет отка заться от применения backwards.)

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

n  = split(s, arr, sep)

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

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

root

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

83

$

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

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

Функция

Возвращаемое значение

cos(expr) exp(expr) getline()

index(s1,s2)

int(expr) length(s) log(expr) sin(expr) split(s,a,c)

sprintf(fmt, …) substr(s,m,n)

косинус expr

экспоненциальная функция

ввод очередной строки, возвращает 0, если достигнут конец файла, иначе 1

положение строки s2 в строке s1, возвращает 0,  если  подстрока не найдена

целая часть expr, дробная часть отбрасывается длина строки s

натуральный логарифм expr

синус  expr

разбиение строки s на  элементы a[1]…a[n] по символу c, возвращает n

форматировать … согласно спецификации fmt

подстрока строки s длиной n, начиная с позиции m

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

При обработке данных часто  возникает необходимость хранения наборов пар имя–значение. Например, при вводе таких данных

Susie

400

John

100

Mary

200

Mary

300

John

100

Susie

100

Mary

100

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

John

200

Mary

600

Susie

500

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

{ sum[$1] += $2 }

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

представляет собой  законченную программу  подсчета и печати сумм  для  пар имя–значение из предыдущего примера, не требующую предварительной сортировки. Имена выполняют функцию индексов массива  sum, а в конце оператор цикла for  специального вида выполняет проход по массиву, печатая значения его элементов. Синтаксис этого варианта оператора for имеет  вид:

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

оператор

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

$ awk  ‘…’ | sort  +1nr

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

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

$ cat  wordfreq

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

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

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

the  372

.CW  345

of  220

is 185

to  175

a 167

in  109

and 100

.P1  94

.P2  94

.PP  90

$ 87

awk 87

sed  83

that  76

for  75

The 63

are  61

line  55

print 52

$

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

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

ните  реализацию и производительность wordfreq, конвейера команд из раздела 4.2 и такого решения:

sed  ‘s/[ 6][ 6]*/\

/g’ $* | sort | uniq  –c | sort  –nr

~

Строки

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

$ cat  test

A  short line.

A  somewhat longer   line.

This  line is quite a bit longer   than  the  last one.

$ fold test

A  short line.

A  somewhat  longer  li\ ne.

This  line  is  quite  a\ bit  longer  than  the\

last one.

$

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

# fold:   сворачивает длинные  строки

sed  ‘s/\(6/        /g’ $* |    #  преобразовать табуляции в пробелы awk ‘

BEGIN  {

N  = 80                  # сворачивать на 80–й позиции

for  (i = 1;  i <= N;  i++)          # заполнить строку  пробелами

blanks  = blanks  "  "

}

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

else {

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

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

}

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

}

} ‘

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

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

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

Упражнение 4.10.  Измените программу fold так, чтобы она сворачивала строки по пробелам и табуляциям, не разбивая слова. Предусмотрите обработку длинных слов. ~

Взаимодействие с оболочкой

Предположим, требуется написать программу field n, которая должна  выводить n-е  поле каждой входной строки так, чтобы, например, команда

$ who  | field 1

выводила только регистрационное имя. Выбор поля в awk осуществляется  элементарно, проблема в том,  как передать ей номер  поля n. Вот пример:

awk ‘{ print  $’$1′  }’

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

awk "{  print  \$$1  }"

В этом случае аргумент интерпретируется оболочкой, которая заменяет \$ на $, а $1 – на значение n. Авторы отдают  предпочтение первому

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

Другой пример – программа addup n, суммирующая числа из n-го поля:

awk  ‘{ s  +=  $’$1′  } END  {  print  s  }’

В следующем примере подсчитываются отдельные суммы для n столбцов и общий итог:

awk ‘

BEGIN  { n = ‘$1′ }

{         for  (i  =  1;  i  <=  n;  i++) sum[i]  +=  $i

}

END  {for  (i  =  1;  i  <=  n;  i++)  { printf  "%6g  ",  sum[i] total  +=  sum[i]

}

printf "; total  = %6g\n",  total

} ‘

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

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

Календарь на основе awk

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

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

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

$ cat  calendar

Sep  30    Мамин  день  рождения Oct  1      Днем  обедаем  с  Джо Oct  1      Совещание  в  16.00

$

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

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

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

$ date

Thu Sep 29 15:23:12  EDT  1983

$ cat  bin/calendar

# calendar:   версия 1 –  только для сегодняшнего  дня awk <$HOME/calendar  ‘

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

$1 ==  date[2] &&  $2 ==  date[3] ‘ |  mail  $NAME

$

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

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

$ cat  bin/calendar

# calendar:   версия 2 –  только сегодня,  без  кавычек (date; cat  $HOME/calendar)  |

awk ‘

NR  == 1       { mon  = $2;  day = $3 } # установить дату

NR  > 1 &&   $1 == mon   && $2 ==  day   # вывести построчно ‘ |  mail  $NAME

$

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

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

Здесь-то и пригодятся ассоциативные массивы. В двух массивах – days и  nextmon,  –  индексами в  которых служат названия месяцев, будем хранить число  дней  в месяце и  название  следующего месяца. Тогда  значение days["Jan"] равно  31,  а значение nextmon["Jan"] – Feb. Вместо того чтобы писать последовательность операторов вида

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

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

$ cat  calendar

# calendar:    версия 3 –  сегодня и  завтра awk  <$HOME/calendar  ‘

BEGIN  {

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

"Jul  31 Aug  31 Sep 30 Oct  31 Nov  30 Dec 31 Jan  31" split(x,  data)

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

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

}

split("’"`date`"’",  date)

mon1  =  date[2];  day1  =  date[3] mon2  =  mon1;  day2  =  day1  +  1

if  (day1  >=  days[mon1])  { day2  =  1

mon2  = nextmon[mon1]

}

}

$1 == mon1    && $2 == day1 ||  $1 == mon2  &&   $2 == day2 ‘ |  mail  $NAME

$

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

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

$ at  5am calendar ctld

$

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

$ cat  early.morning

calendar

echo early.morning | at 5am

$

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

Упражнение 4.11.  Измените программу calendar так, чтобы  она  «знала»  о выходных днях: в  пятницу «завтра» включает в себя субботу, воскресенье и  понедельник. Добавьте  поддержку високосных годов.  Нужна ли поддержка праздников? Как ее реализовать? ~

Упражнение 4.12.  Надо  ли обрабатывать даты  в середине строки, а не только в начале? И как насчет дат,  записанных в других форматах, например 10/1/83? ~

Упражнение 4.13.  Почему программа calendar использует вызов getna– me, а не переменную $NAME? ~

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

Нерассмотренные возможности

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

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

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

•         Конструкция «шаблон, шаблон» является селектором: как и в программах ed и sed группа строк может быть определена парой шаблонов.  Селектор позволяет выбрать строки в диапазоне от вхождения

первого  шаблона  до   следующего   вхождения   второго  шаблона.

Простой пример

NR  == 10,  NR  == 20

показывает селектор, соответствующий строкам с 10 по 20 включительно.

Источник: Керниган Б., Пайк Р., UNIX. Программное окружение. – Пер. с англ. – СПб: Символ-Плюс, 2003. – 416 с., ил.

По теме:

  • Комментарии