355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Брайан Керниган » UNIX — универсальная среда программирования » Текст книги (страница 11)
UNIX — универсальная среда программирования
  • Текст добавлен: 6 октября 2016, 20:23

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


Автор книги: Брайан Керниган


Соавторы: Роб Пайк

Жанры:

   

ОС и Сети

,

сообщить о нарушении

Текущая страница: 11 (всего у книги 31 страниц) [доступный отрывок для чтения: 12 страниц]

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


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

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



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

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


Упражнение 4.8

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

Управление

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

$ cat double

awk '

FILENAME != prevfile { # new file

 NR = 1                # reset line number

 prevfile = FILENAME

}

NF > 0 {

 if ($1 == lastword)

  printf "double %s, file %s, line %dn" ,$1, FILENAME, NR

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

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

   printf "double %s, file %s, line %dn", $i, FILENAME, NR

 if (NF > 0)

  lastword = $NF

}' $*

*

$

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

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

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

if (условие)

 оператор1

else

 оператор2

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

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

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

 оператор

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

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

 оператор

 выражение2

}

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

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

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

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

Массивы

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

$ cat backwards

# backwards: print input in backward line order

awk ' { line[NR] = $0 }

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

$

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

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

zymurgy

zymotically

zymotic

zymosthenic

zymosis

$

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

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

n = split(s, arr, sep)

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

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

root

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

83

$

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


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

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


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

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

Susie 400

John  100

Mary  200

Mary  300

John  100

Susie 100

Mary  100

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

John  200

Mary  600

Susie 500

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

    {sum[$1] += $2}

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

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

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

 оператор

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

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

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

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

$ cat wordfreq

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

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

' $*

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

the 372 .CW 345 of  220 is   185

to  175 a   167 in  109 and  100

.PI  94 .P2  94 .PP  90 $     87

awk  87 sed  83 that 76 for   75

The  63 are  61 line 55 print 52

$

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

Упражнение 4.9

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

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

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

Строки

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

$ cat тест

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

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

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

$ fold тест

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

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

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

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

$

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

# fold: fold long lines

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

awk '

 BEGIN {

  N = 80                   # folds at column 80

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

   blanks = blanks " "

 }

 {

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

   print

  else {

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

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

    i += N;

   }

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

  }

 } '

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

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

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

Упражнение 4.10

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

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

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

$ who | field 1

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

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

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

awk "{print $$1}"

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

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

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

END {print s}'

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

awk '

BEGIN { n = '$1' }

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

   sum[i] += $1

}

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

      {

       printf "%6g ", sum[i]

       total += sum[i]

      }

      printf "; total = %6g ", total

    }'

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

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

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

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

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

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

$ cat calendar

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

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

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

$

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

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

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

$ date

Thu Sep 29 15:23:12 EDT 1983

$ cat bin/calendar

# calendar: version 1 – today only

awk <$HOME/calendar '

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

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

' | mail $NAME

$

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

$ cat /bin/calendar

# calendar: version 2 – today only, no quotes

(date; cat $HOME/calendar) |

awk '

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

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

' | mail $NAME

$

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

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

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

...

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

$ cat calendar

# calendar: version 3 – today and tomorrow

awk <$HOME/calendar '

BEGIN {

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

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

 split(x, data)

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

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

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

 }

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

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

 mon2 = mon1; day2 = day1 + 1

 if (day1 >= days[mon1]) {

  day2 = 1

  mon2 = nextmon[mon1]

 }

}

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

' | mail $NAME

$

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

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

$ at 5 am

calendar

ctl-d

$

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

$ cat early.morning

calendar

echo early morning | at 5am

$

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

Упражнение 4.11

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

Упражнение 4.12

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

Упражнение 4.13

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

Упражнение 4.14

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

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

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

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

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

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

NR == 10, NR == 20

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


    Ваша оценка произведения:

Популярные книги за неделю