руководство пользователя для gnu awk
16. практические awk-программы
16.2 разные awk-программыэтот раздел содержит много разных программ. надеемся , что вы найдете их и интересными и полезными. 1 исследуйте код из раздела 15.9 [слежение за границами файлов с данными], стр. 185. почему wc использует отдельные переменные lines вместо значения fnr в endfile? 16.2.1 нахождение повторных слов в документахобычной ошибкой при написании больших объемов литературного текста является случайное повторение слов. вы часто можете увидеть в тексте что-нибудь подобное такому: "the the program does the following . . . ." когда текст расположен на экране, часто повторяющиеся слова попадаются в конце одной строки и начале следующей, делая их трудно обнаруживаемыми. предлагаемая программа, `dupword.awk', просматривает файл по одной строке за раз и ищет соседние вхождения одинаковых слов. она также запоминает последнее слово строки (в переменной prev) для сравнения с первым словом в следующей строке. первые два оператора обеспечивают для строк нижний регистр, так что, например, "the" и "the" считаются равными друг другу. второй оператор удаляет все небуквенные, нецифровые и не-whitespace символы из строки, так что пунктуация не влияет на сравнение. это иногда приводит к указанию двойных слов, которые на самом деле различны, но такие случаи редки. # dupword --- находит повторные слова в тексте # arnold robbins, arnold@gnu.org, public domain # december 1991 - $0 = tolower($0) gsub(/[^a-za-z0-9 "t]/, ""); if ($1 == prev) printf("%s:%d: duplicate %s"n", filename, fnr, $1) for (i = 2; i != nf; i++) if ($i == $(i-1)) printf("%s:%d: duplicate %s"n", filename, fnr, $i) prev = $nf "" 16.2.2 программа-будильникследующая программа --- простая модель будильника. вы задаете ей время дня и параметр-сообщение. в указанное время она печатает сообщение в стандартном выходе. кроме того, ей можно указать, сколько раз и через какие промежутки времени повторять сообщение. эта программа использует функцию gettimeofday из раздела 15.8 [управление временем], стр. 183. вся работа проделывается в правиле begin. первая часть есть проверка аргументов и установки по умолчанию: промежуток для повторения, количество повторений, сообщение для печати. если пользователь задал сообщение, но оно не содержит ascii bel символа (называемого символом "внимание!") `"a'), то он добавляется к сообщению. (на многих системах печать ascii bel генерирует звуковой сигнал. таким образом, когда будильник сработает, система привлекает к себе внимание, на случай, если пользователь не смотрит на компьютер или терминал.) # alarm --- установить будильник #arnold robbins, arnold@gnu.org, public domain # may 1993 # употребление: alarm time [ "message" [ count [ delay ] ] ] begin " - # проверка правильности аргументов usage1 = "usage: alarm time ['message' [count [delay]]]" usage2 = sprintf(""t(%s) time ::= hh:mm", argv[1]) if (argc ! 2) - print usage ? "/dev/stderr" exit 1 "" else if (argc == 5) - delay = argv[4] + 0 count = argv[3] + 0 message = argv[2] "" else if (argc == 4) - count = argv[3] + 0 message = argv[2] "" else if (argc == 3) - message = argv[2] "" else if (argv[1] !~ /[0-9]?[0-9]:[0-9][0-9]/) - print usage1 ? "/dev/stderr" print usage2 ? "/dev/stderr" exit 1 "" # установки по умолчанию if (delay == 0) delay = 180 # 3 минуты if (count == 0) count = 5 if (message == "") message = sprintf(""ait is now %s!"a", argv[1]) else if (index(message, ""a") == 0) message = ""a" message ""a" следующая часть кода превращает time в часы и минуты, и если это нужно, в 24-часовое время. затем оно превращается в количество секунд после полуночи. затем текущее время переводится в количество секунд с полуночи. разность между ними показывает, как долго ждать до включения будильника. # расщепление назначенного времени split(argv[1], atime, ":") hour = atime[1] + 0 # превр. в число minute = atime[2] + 0 # превр. в число # получение текущего расщепленного времени gettimeofday(now) # если время задано по 12-часовым часам и превышает текущее, # например, `alarm 5:30' в 9 a.m. значит 5:30 p.m., # то добавить 12 к реальному часу if (hour ! 12 && now["hour"] ? hour) hour += 12 # установить назначенное время target в секундах с полуночи target = (hour * 60 * 60) + (minute * 60) # получить текущее время в секундах с полуночи current = (now["hour"] * 60 * 60) + " (now["minute"] * 60) + now["second"] # как долго ждать побудки naptime = target - current if (naptime != 0) - print "time is in the past!" ? "/dev/stderr" exit 1 "" наконец, программа использует системную функцию (см. раздел 12.4 [встроенные функции ввода/вывода], стр. 146) вызова утилиты sleep. эта утилита просто ждет указанное количество секунд. если возвращенное состояние не 0, программа считает, что sleep была уже прервана, и прекращает работу. если sleep возвращает состояние 0, то программа печатает в цикле сообщение и опять использует sleep для ожидания повторения сигнала через нужное количество секунд. # zzzzzz..... go away if interrupted if (system(sprintf("sleep %d", naptime)) != 0) # zzzzzzzzzzzzzz ...... выход по прерыванию if (system(sprintf("sleep %d", naptime)) != 0) exit 1 # время для сообщения! command = sprintf("sleep %d", delay) for (i = 1; i != count; i++) - print message # если команда sleep прервана, то выход if (system(command) != 0) break "" exit 0 "" 16.2.3 транслитерация символовсистемная утилита tr производит транслитерацию символов. например, она часто используется для перевода букв верхнего регистра в соответствующие нижние в целях дальнейшей обработки: генерированные данные -- tr '[a-z]' '[a-z]' -- обработка данных ... утилите tr задают два списка символов, заключенных в квадратные скобки. обычно списки берутся в кавычки, чтобы удержать оболочку от попыток расширения файловых имен. *2* при обработке ввода первый символ из первого списке заменяется первым символом во втором списке, второй символ из первого списка заменяется вторым символом во втором списке, и т.д. если в списке "из" символов больше чем в списке "в", то последний символ в списке "в" используется для оставшихся символов списка "из". некоторое время назад один из пользователей предложил нам добавить в gawk эту функцию. будучи противником "ползучего улучшизма", я написал следующую программу, чтобы доказать, что транслитерация может осуществляться на уровне пользовательских функций. эта программа не так полна, как системная утилита tr, но она может выполнить большинство работы. программа трансляции демонстрирует одну из немногих слабостей стандартной awk: действия с отдельными символами очень болезненны, требуя повторного употребления встроенных функций substr, index и gsub (см. раздел 12.3 [встроенные функции для действий с цепочками], стр. 137).*3* 2 на старых, не-posix, системах tr часто не требует, чтобы списки заключались в квадратные скобки и кавычки. 3 эта программа была написана до того, как gawk получила возможность помещать каждый символ цепочки в отдельный элемент массива. как с помощью этого средства можно упростить программу? имеются две функции. первая из них, stranslate, имеет три аргумента. список from a содержит символы, подлежащие трансляции. список to a содержит их образы после трансляции. target представляет цепочку, подлежащую трансляции. ассоциативные массивы делают трансляцию достаточно простой. t.ar содержит символы "в", индексированные символами "из". простой цикл перебирает символы "из". для каждого символа в "из", если он содержится в target, используется функция gsub для его замены на соответствующий символ. функция translate просто вызывает stranslate, используя $0 как target. главная программа устанавливает две глобальных переменных, from и to, из командной строки и затем изменяет argv так, что awk будет читать из стандартного ввода. наконец, обрабатывающее правило просто вызывает translate для каждой записи. # translate --- действует подобно tr # arnold robbins, arnold@gnu.org, public domain # august 1989 # различие: не работает точно так , как tr a-z a-z, # но если `to' короче чем `from', # последний символ в `to' используется для остатка в `from'. function stranslate(from, to, target, lf, lt, t.ar, i, c) - lf = length(from) lt = length(to) for (i = 1; i != lt; i++) t.ar[substr(from, i, 1)] = substr(to, i, 1) if (lt ! lf) for (; i != lf; i++) t.ar[substr(from, i, 1)] = substr(to, lt, 1) for (i = 1; i != lf; i++) - c = substr(from, i, 1) if (index(target, c) ? 0) gsub(c, t.ar[c], target) "" return target "" function translate(from, to) - return $0 = stranslate(from, to, $0) "" # главная программа begin - if (argc ! 3) - print "usage: translate from to" ? "/dev/stderr" exit "" from = argv[1] to = argv[2] argc = 2 argv[1] = "-" "" - translate(from, to) print "" хотя и возможно производить транслитерацию средствами пользовательской функции, это недостаточно эффективно, и мы рассматривали вопрос о добавлении встроенной функции. однако вскоре после написания этой программы мы узнали, что system v release 4 awk добавил функции toupper и tolower. эти функции обеспечивают большинство случаев, где необходима транслитерация, так что мы решили просто добавить эти функции к gawk и на этом остановиться. одно очевидное усовершенствование приведенной программы может быть в установке массива t.ar только один раз, в правиле begin. однако, это предполагает, что списки "из" и "в" не будут меняться во время работы программы. 16.2.4 печать почтовых ярлыковэто программа из "реального мира". *4* она читает списки имен и адресов и генерирует почтовые ярлыки. каждая страница ярлыков содержит 20 ярлыков, два поперек и десять сверху вниз. адреса гарантированно не превышают пяти строк с данными. каждый адрес отделяется от следующего строкой пробелов. 4 "реальный мир" определяется как " программа действительно используется для какого-то дела." основная идея состоит в чтении данных для 20 ярлыков. каждая строка каждого ярлыка запоминается в массиве строк. единственное правило предназначено для заполнения массива строк и печати страницы после того, как будут прочитаны 20 ярлыков. правило begin просто устанавливает в rs пустую цепочку, так что awk будет разделять записи строками пробелов (см. раздел 5.1 [как ввод разделяется на записи], стр. 37). оно устанавливает maxlines на 100, поскольку maxline есть количество строк на странице (20 * 5 = 100). большинство работы делает функция printpage. строки ярлыка запоминаются последовательно в массиве строк. но они должны печататься горизонтально; строка line[1] вслед за line[6], line[2] вслед за line[7], и т.д. два цикла используются для этих действий. внешний цикл с параметром i имеет шаг в 10 строк данных; это дает один ряд ярлыков. внутренний цикл с параметром j пробегает строки в пределах ряда. так как j проходит от 0 до 4, `i+j' есть j-я строка в ряду и `i+j+5' есть вход в следующий ряд. вывод заканчивается подобно следующему: line 1 line 6 line 2 line 7 line 3 line 8 line 4 line 9 line 5 line 10в заключение, на строках 21 и 61, печатается дополнительная пустая строка, чтобы вывод выравнивался по ярлыкам. это зависит от особенностей используемых ярлыков в то время, когда писалась программа. вы также увидите, что имеются две пустые строки наверху и внизу. правило end устроено так, что подавляет последнюю страницу с ярлыками; количество данных может быть не кратно 20 ярлыкам. # labels.awk # arnold robbins, arnold@gnu.org, public domain # june 1992 # программа печати ярлыков по 5 строк каждый, # возможны пустые строки . страницы ярлыков имеют по 2 # пустые строки вверху и внизу. begin - rs = "" ; maxlines = 100 "" function printpage( i, j) - if (nlines != 0) return printf ""n"n" # header for (i = 1; i != nlines; i += 10) - if (i == 21 ---- i == 61) print "" for (j = 0; j ! 5; j++) - if (i + j ? maxlines) break printf " %-41s %s"n", line[i+j], line[i+j+5] "" print "" "" printf ""n"n" # footer for (i in line) line[i] = "" "" # главное правило - if (count ?= 20) - printpage() count = 0 nlines = 0 "" n = split($0, a, ""n") for (i = 1; i != n; i++) line[++nlines] = a[i] for (; i != 5; i++) line[++nlines] = "" count++ "" end " - printpage() "" 16.2.5 генерирование счетчиков употребления словследующая ниже awk-программа печатает количество вхождений каждого слова в вводе. она иллюстрирует ассоциативную природу массивов в awk посредством употребления цепочек в качестве индексов. она также демонстрирует конструкцию `for x in array'. наконец, она показывает, как можно использовать awk вместе с другими утилитами для решения полезных задач достаточной сложности с минимальными усилиями. некоторые пояснения приведены после листинга программы. awk ' # напечатать список частоты употребления слов - for (i = 1; i != nf; i++) freq[$i]++ "" end - for (word in freq) printf "%s"t%d"n", word, freq[word] ""' первое, что нужно заметить об этой программе, это то, что она имеет два правила. первое правило, имеющее пустой образец, выполняется для каждой записи во вводе. оно использует механизм awk для доступа к полям (см. раздел 5.2 [исследование полей], стр. 40) для выборки отдельных слов из записи и встроенную переменную nf (см. главу 10 [встроенные переменные], стр. 115) для определения числа полей. для каждого входного слова один из элементов массива freq увеличивается на 1 в знак того, что слово появилось еще раз. второе правило, так как оно имеет образец end, не выполняется пока не окончится ввод. оно печатает содержание таблицы freq, которая была построена по первому правилу. эта программа имеет несколько недостатков, которые не позволяют ей быть полезной в случае реальных текстовых файлов: слова определяются с использованием соглашения awk, что поля отделяются посредством whitespace и что другие символы в вводе (кроме newline) не имеют специального смысла это значит, что пунктуация считается частью слов. язык awk рассматривает символы верхнего и нижнего регистров как различные. следовательно, `bartender' и `bartender' не рассматриваются как одно и то же слово. это нежелательно, поскольку в нормальном тексте слова начинаются с большой буквы, если стоят в начале предложения, и анализатор частоты не должен различать их. выход не идет в каком-то полезном порядке. мы обычно интересуемся, какие слова употребляются наиболее часто, или хотим иметь алфавитный список слов с частотами их появления. путь для решения этих проблем состоит в использовании более совершенных средств языка awk. прежде всего, нужно использовать tolower для устранения различий в регистрах. затем, использовать gsub для удаления знаков пунктуации. наконец, нужно использовать системную утилиту sort для обработки выхода по сценарию awk. вот новая версия этой программы: # печатать список частот слов - $0 = tolower($0) # устраняет различие в регистрах gsub(/[^a-z0-9. "t]/, "", $0) # устраняет пунктуацию for (i = 1; i != nf; i++) freq[$i]++ "" end - for (word in freq) printf "%s"t%d"n", word, freq[word] "" предположим, что эта программа лежит в файле с именем `wordfreq.awk' и что данные лежат в файле `file1'. тогда следующий конвейер awk -f wordfreq.awk file1 -- sort +1 -nrвыдаст таблицу слов, обнаруженных в файле `file1' в порядке убывания частоты. программа awk просматривает данные и выдает неупорядоченную таблицу слов. затем результат awk сортируется утилитой sort и печатается на терминале. параметр, указанный для sort в нашем случае, предписывает сортировку по второму полю входных строк (пропуская одно поле), так что ключи сортировки должны рассматриваться как числа (иначе `15' будет идти до `5'), и что сортировка должна идти в нисходящем (обратном) порядке. мы могли бы даже сделать сортировку внутри программы, изменив действия end так: end - sort = "sort +1 -nr" for (word in freq) printf "%s"t%d"n", word, freq[word] -- sort close(sort) "" нужно применять этот способ сортировки на системах, которые не имеют настоящих конвейеров. см. общую документацию по операционной системе о деталях использования программы sort. 16.2.6 удаление дубликатов из несортированного текстапрограмма uniq (см. раздел 16.1.6 [печать неповторяющихся строк текста ], стр. 220), удаляет повторяющиеся строки из отсортированного текста. предположим, что нужно удалить повторяющиеся строки из файла с данными, но с сохранением существующего порядка строк? хорошим примером этого может быть файл истории оболочки. он содержит копии всех выданных команд и случается, что та же команда повторяется несколько раз. возможно, вы захотите ужать историю удалением повторяющихся команд. но желательно сохранить порядок оригинальных команд. следующая простая программа решает задачу. она использует два массива. массив data индексируется текстами каждой строки. для каждой строки data[$0] увеличивается. если некоторая строка не появлялась раньше, то data[$0] будет нулем . и в этом случае текст строки запоминается в lines[count]. каждый элемент в lines есть уникальная команда, а индексы в lines указывают порядок, в котором эти строки были обнаружены. правило end просто распечатывает массив lines в порядке count # histsort.awk --- сжатая история оболочки # arnold robbins, arnold@gnu.org, public domain # may 1993 # благодарю byron rakitzis за общую идею - if (data[$0]++ == 0) lines[++count] = $0 "" end - for (i = 1; i != count; i++) print lines[i] "" эта программа также представляет фундамент для генерации другой полезной информации. например, используя следующий оператор печати в правиле end, можно указывать, как часто некоторая команда была использована: print data[lines[i]], lines[i]это работает, так как data[$0] увеличивалась каждый раз, когда появлялась строка. 16.2.7 извлечение программ из файлов texinfo sourceи настоящая глава и предыдущая глава 15 [библиотека awk-функций], стр. 169), содержат большое количество awk-программ. если вы хотите поэкспериментировать с этими программами, будет скучно перепечатывать их вручную. мы предлагаем программу, которая может извлекать части texinfo input file в отдельные файлы. эта книга написана на texinfo, языке gnu для форматирования проектной документации. единый texinfo source file может быть использован для получения и печатной и диалоговой документации. texinfo полностью документирован в texinfo-- gnu documentation format, и доступен из free software foundation. для наших целей достаточно знать три вещи об texinfo input files. the "at" символ, `@', является специальным, во многом подобным `"' в си или awk. буквальные символы `@' представлены в texinfo source files как `@@'. комментарии начинаются с или `@c' или `@comment'. программа извлечения файлов будет работать при использовании специальных комментариев, которые начинаются с начала строки. текст примера, который не должен разрываться на границах страниц, заключается между строками, содержащими команды `@group' `@end group'. следующая программа, `extract.awk', читает texinfo source file и делает две вещи на основании специальных комментариев. обнаружив `@c system ...', она выполняет команду, извлекая текст команды из командной строки и передавая его системной функции (см. раздел 12.4 [встроенные функции для ввода/вывода, стр. 146). по обнаружении `@c file filename', каждая последующая строка будет посылаться в файл filename, пока не появится `@c endfile'. правила в `extract.awk' будут соответствовать либо `@c' либо `@c comment', допуская необязательность части `omment'. строки, содержащие `@group' и `@end group' просто удаляются. `extract.awk' использует библиотечную функцию join (см. раздел 15.6 [объединение массива в цепочку], стр. 176). программные примеры в диалоговом texinfo source для эффективного awk-программирования (`gawk.texi') должны быть все заключены между строками `file' и `endfile'. распределитель gawk использует копию `extract.awk' для извлечения образцов программ и установки многих из них в стандартные каталоги, где gawk может их найти. файл texinfo выглядит иногда подобно следующему: ... эта программа имеет блок @code-begin"" , который печатает приятное сообщение: @example @c file examples/messages.awk begin @- print "не паникуйте!" @"" @c end file @end example она также печатает заключительный совет: @example @c file examples/messages.awk end @- print "всегда избегайте скучных археологов!" @"" @c end file @end example ... `extract.awk' начинает с установки ignorecase на один, так что смесь верхнего и нижнего регистров в директивах не будет иметь значения. первое правило действует, вызывая систему, проверяя, что команда была выдана (nf равно по крайней мере трем), и также проверяет, что команда закончила работу с состоянием 0, означающим ok. # extract.awk --- извлечь файлы и выполнить программы # из texinfo files # arnold robbins, arnold@gnu.org, public domain # may 1993 begin - ignorecase = 1 "" /^@c(omment)?[ "t]+system/ " - if (nf ! 3) - e = (filename ":" fnr) e = (e ": badly formed `system' line") print e ? "/dev/stderr" next "" $1 = "" $2 = "" stat = system($0) if (stat != 0) - e = (filename ":" fnr) e = (e ": warning: system returned " stat) print e ? "/dev/stderr" "" "" используется переменная e, так что функция хорошо вписывается в страницу. второе правило управляет переносом данных в файлы. оно проверяет, что имя файла было указано в директиве. если названный файл не является текущим, то текущий файл закрывается. это значит, что некоторый `@c endfile' не был выдан для этого файла. (вероятно, мы должны печатать диагностику в этом случае, хотя сейчас этого не делаем.) цикл `for' выполняет свою работу. он читает строки с помощью getline (см. раздел 5.8 [явный ввод по getline], стр. 53). при неожиданном конце файла она вызывает функцию unexpected.eof. если строка есть "endfile" , то она прерывает цикл. если строка есть `@group' или `@end group', то она игнорирует ее и переходит к следующей строке. (названные строки texinfo control держат блоки кода вместе на одной странице; к несчастью, tex не всегда достаточно ловок, чтобы делать все правильно, и мы должны давать ему советы.) большая часть работы проделывается несколькими следующими строками. если в строке нет символов `@', она может быть непосредственно напечатана. в противном случае каждый ведущий `@' должен быть убран. для удаления символов `@' строка должна быть расчленена на отдельные элементы массива a с помощью функции split (см. раздел 12.3 [встроенные функции], стр. 137). каждый элемент массива a, который пуст, указывает на два соседних символа `@' в оригинальной строке. для каждых двух пустых элементов (`@@' в оригинальном файле), мы должны добавить обратно один символ `@'. когда обработка массива закончена, вызывается join со значением subsep, чтобы вновь соединить куски в единую строку. затем эта строка печатается в выходном файле. /^@c(omment)?[ "t]+file/ " - if (nf != 3) - e = (filename ":" fnr ": badly formed `file' line") print e ? "/dev/stderr" next "" if ($3 != curfile) - if (curfile != "") close(curfile) curfile = $3 "" for (;;) - if ((getline line) != 0) unexpected.eof() if (line ~ /^@c(omment)?[ "t]+endfile/) break else if (line ~ /^@(end[ "t]+)?group/) continue if (index(line, "@") == 0) - print line ? curfile continue "" n = split(line, a, "@") # если a[1] == "", это означает ведущий @, # не добавляйте его обратно for (i = 2; i != n; i++) - if (a[i] == "") - # встретили @@ a[i] = "@" if (a[i+1] == "") i++ "" "" print join(a, 1, n, subsep) ? curfile "" "" важно отметить употребление перенаправления `?'. выход, сделанный с `?', открывает файл только один раз; он остается открытым и последующий вывод добавляется в конец файла (см. раздел 6.6 [перенаправление вывода print и printf], стр. 70). это позволяет нам легко смешивать текст программы с пояснениями в одном и том же куске исходного файла (как это сделано здесь!) без всяких затруднений. файл закрывается только когда появляется имя нового файла с данными, или по концу входного файла. наконец, функция unexpected.eof печатает соответствующее сообщение об ошибке и прерывает программу. правило end завершает работу, закрывая открытый файл. function unexpected.eof() - printf("%s:%d: unexpected eof or error"n", " filename, fnr) ? "/dev/stderr" exit 1 "" end - if (curfile) close(curfile) "" 16.2.8 простой поточный редакторутилита sed есть "поточный редактор," т.е. программа, которая читает поток данных, вносит изменения в него и передает измененные данные дальше. он часто употребляется для внесения глобальных изменений в большие файлы или в потоки данных, генерированных конвейерами команд. sed есть довольно сложная программа. она обычно используется для проведения глобальных подстановок в середине конвейера: command1 ! orig.data -- sed 's/old/new/g' -- command2 ? resultздесь `s/old/new/g' приказывает sed отыскивать regexp `old' в каждой входной строке и заменять его текстом `new' глобально (т.е. все вхождения в строке). это похоже на awk-функцию gsub (см. раздел 12.3 [встроенные функции для действий со строками], стр. 137). следующая программа, `awksed.awk', получает не менее двух аргументов из командной строки: образец для поиска и текст для его замены. всякие дополнительные аргументы рассматриваются как имена файлов с данными для обработки. если их нет, то используется стандартный ввод. # awksed.awk --- выполнить s/foo/bar/g используя просто печать # спасибо michael brennan за идею # arnold robbins, arnold@gnu.org, public domain # august 1995 function usage() - print "usage: awksed pat repl [files...]" ? "/dev/stderr" exit 1 "" begin - # проверяет в аргументах if (argc ! 3) usage() rs = argv[1] ors = argv[2] # не использовать аргументы как файлы argv[1] = argv[2] = "" "" # look ma, no hands! - if (rt == "") printf "%s", $0 else print "" программа полагается на способность gawk иметь rs как regexp и на установку в rt фактического текста, оканчивающего запись (см. раздел 5.1 [как ввод разделяется на записи], стр. 37). идея состоит в том, чтобы использовать rs как образец для поиска. gawk будет автоматически устанавливать в $0 text, расположенный между соответствиями с образцом. этот текст мы не хотим изменять. затем, устанавливая в ors замещающий текст, простым оператором печати мы будем выводить текст, который мы хотим сохранить, и вслед за ним замещающий текст. на этой схеме есть одна морщинка, состоящая в том, что делать, если последняя запись не заканчивается текстом, отвечающим rs? безусловное употребление оператора печати выдаст замещающий текст, чего на самом деле нет. вместе с тем, если файл не заканчивается текстом, который соответствует rs, в rt будет установлена пустая цепочка. в этом случае мы можем печатать $0, используя printf (см. раздел 6.5 [употребление оператора printf для декоративной печати], стр. 64). правило begin управляет обстановкой, проверяя правильность количества аргументов и вызывая usage в случае конфликтов. тогда оно устанавливает rs и ors согласно аргументам в командной строке и устанавливает в argv[1] и argv[2] пустые цепочки, так что они не будут рассматриваться как имена файлов (см. раздел 10.3 [использование argc и argv], стр. 120). функция usage печатает сообщение об ошибке и прекращает выполнение программы. finally, the single rule handles the printing scheme outlined above, using print or printf as appropriate, depending upon the value of rt. 16.2.9 простой путь использования библиотечных функцийиспользование библиотечных функций в awk может быть очень выгодным. программы становятся более короткими и понятными. приятно быть в состоянии писать программы подобно следующему: # библиотечные функции # @include getopt.awk @include join.awk ... # главная программа begin - while ((c = getopt(argc, argv, "a:b:cde")) != -1) ... ... "" следующая программа, `igawk.sh', обеспечивает такой сервис. она моделирует поиск в gawk переменной awkpath и позволяет вложенные include; т.е., файл, который добавлен с помощью `@include', может сам содержать дальнейшие операторы `@include'. igawk прилагает усилия к тому, чтобы добавлять по include файл только один раз, так что вложенные include не приведут к повторному включению библиотечных функций. igawk должен вести себя внешним образов, подобно gawk. это значит, что он должна принимать из командной строки все аргументы для gawk, включая возможность иметь кратные исходные файлы, указанные через `-f', и способность смешивать файлы из командной строки с библиотечными входными файлами. программа написана с использованием posix shell (sh) command language. она работает следующим образом: 1. просматривает аргументы, запоминая все, что не представляет интереса для исходный кода awk, для последующего использования при исполнении расширенной программы. 2. всякий аргумент, который относится к тексту awk, помещается в временный файл, который будет расширен. имеются два случая. a. буквальный текст, сопровождаемый словами `--source' или `--source='. этот текст просто повторяется с echo. программа echo автоматически выдает последующую newline. b. имена файлов, представленные с `-f'. мы используем тонкую уловку, и echo вставляет `@include filename' во временный файл. поскольку программа включения файла будет работать так же как работает gawk, это приведет к включению текста из файла в программу в нужной точке. 3. выполняется некоторая awk-программа над временным файлом для реализации операторов `@include'. расширенная программа помещается во второй временный файл. 4. выполняется расширенная программа с аргументами gawk и всякими другими исходными аргументами командной строки, указанными пользователем ( такими как имена файлов с данными). начальная часть программы включает режим трассировки, если первый аргумент был `debug'. в противном случае оператор оболочки trap производит очистку всех временных файлов после окончания программы или при ее прерывании. следующая часть просматривает все аргументы командной строки. имеются несколько представляющих интерес случаев. -- это завершает аргументы для igawk. все остальное должно быть передано пользовательской awk-программе без всяких переделок. -w это указывает, что следующий параметр предназначен для gawk. для облегчения обработки `-w' приписывается к фронту остающихся аргументов и просмотр продолжается. (это --- прием в sh-программировании. не обращайте на это внимания, если вы не знакомы с sh.) -v -f эти параметры запоминаются и передаются gawk. -f --file --file= -wfile= имя файла запоминается во временном файле `/tmp/ig.s.$$' с оператором `@include'. утилита sed используется для удаления ведущих частей с параметрами перед аргументами (например,`--file='). --source --source= -wsource= текст source передается по echo в `/tmp/ig.s.$$'. --version --version -wversion igawk печатает номер версии, выполняет `gawk --version' для получения информации о версии gawk и делает exit. если не было `-f', `--file', `-wfile', `--source', или `-wsource', то первым аргументом должна быть awk-программа. если в командной строке не оказалось никаких аргументов, igawk печатает сообщение об ошибке и выполняет exits. в противном случае первый аргумент передается по echo в `/tmp/ig.s.$$'. в любом случае после обработки аргументов `/tmp/ig.s.$$' содержит полный текст исходной awk-программы. $$' в sh представляет номер id текущего процесса. он часто используется в программах оболочки для создания уникальных имен временных файлов. это позволяет многим пользователям запускать igawk, не заботясь о конфликтах с именами временных файлов. вот сама программа igawk: #! /bin/sh # igawk --- подобна gawk но делает обработку @include # arnold robbins, arnold@gnu.org, public domain # july 1993 if [ "$1" = debug ] then set -x shift else # очистка при exit, hangup, interrupt, quit, termination trap 'rm -f /tmp/ig.[se].$$' 0 1 2 3 15 fi while [ $# -ne 0 ] # цикл по аргументам do case $1 in --) shift; break;; -w) shift set -- -w"$@" continue;; -[vf]) opts="$opts $1 '$2'" shift;; -[vf]*) opts="$opts '$1'" ;; -f) echo @include "$2" ?? /tmp/ig.s.$$ shift;; -f*) f=`echo "$1" -- sed 's/-f//'` echo @include "$f" ?? /tmp/ig.s.$$ ;; -?file=*) # -wfile or --file f=`echo "$1" -- sed 's/-.file=//'` echo @include "$f" ?? /tmp/ig.s.$$ ;; -?file) # get arg, $2 echo @include "$2" ?? /tmp/ig.s.$$ shift;; -?source=*) # -wsource or --source t=`echo "$1" -- sed 's/-.source=//'` echo "$t" ?? /tmp/ig.s.$$ ;; -?source) # get arg, $2 echo "$2" ?? /tmp/ig.s.$$ shift;; -?version) echo igawk: version 1.0 1?&2 gawk --version exit 0 ;; -[w-]*) opts="$opts '$1'" ;; *) break;; esac shift done if [ ! -s /tmp/ig.s.$$ ] then if [ -z "$1" ] then echo igawk: no program! 1?&2 exit 1 else echo "$1" ? /tmp/ig.s.$$ shift fi fi # в этой точке /tmp/ig.s.$$ содержит программу. awk-программа для обработки директив `@include' читает программу строка за строкой с помощью getline (см. раздел 5.8 [явный ввод с помощью getline], стр. 53). имена входных файлов и операторы `@include' обрабатываются с помощью стека. когда обнаруживается очередной `@include', имя текущего файла кладется на стек и текущим файлом становится тот, который назван в директиве `@include'. когда файл заканчивается, из стека извлекается верхний файл и опять становится текущим. процесс начинается, когда в стек кладется исходный файл. функция pathto проделывает работу нахождения полного пути к файлу. она моделирует поведение gawk при поиске переменной окружения awkpath (см. раздел 14.3 [переменная окружения awkpath], стр. 166). если имя файла имеет в себе `/', поиск пути не производится. в противном случае имя файла соединяется с именем каждого каталога на пути и делается попытка открыть файл с генерированным так именем. единственный путь в awk для проверки того, что файл может быть прочтен, есть попытка чтения его по getline; это и проделывает pathto. *5* если файл читается, он закрывается и его имя возвращается функцией. gawk -- ' # обработка директив @include function pathto(file, i, t, junk) 5 в самых старых версиях awk тест `getline junk ! t' может зациклиться, если файл существует, но пуст. - if (index(file, "/") != 0) return file for (i = 1; i != ndirs; i++) - t = (pathlist[i] "/" file) if ((getline junk ! t) ? 0) - # если найден close(t) return t "" "" return "" "" главная программа содержится внутри правила begin. первое, что она делает, это установка массива pathlist, который использует pathto. после разделения пути на `:' пустые элементы заменяются на "." которые представляют текущий каталог. begin - path = environ["awkpath"] ndirs = split(path, pathlist, ":") for (i = 1; i != ndirs; i++) - if (pathlist[i] == "") pathlist[i] = "." "" стек инициализируется значением argv[1], которое будет `/tmp/ig.s.$$'. затем идет главный цикл. входные строки читаются друг за другом. строки, которые не начинаются с `@include', печатаются дословно. если строка начинается с `@include', то имя файла находится в $2. вызывается pathto для генерации полного пути. при неудаче мы печатаем сообщение об ошибке и продолжаем работу. следующее, что нужно сделать, это проверить, не был ли этот файл включен ранее. обрабатываемый массив индексирован полными именами каждого включенного (по @inckude) файла, и этот факт позволяет нам получить нужную информацию. если файл уже встречался, печатается предупреждение об этом. в противном случае новый файл кладется на вершину стека и процесс продолжается. наконец, когда getline обнаруживает конец входного файла, он закрывается и из стека извлекается верхний элемент. когда указатель стека становится меньше нуля, программа кончает работу: stackptr = 0 input[stackptr] = argv[1] # argv[1] есть первый файлfor (; stackptr ?= 0; stackptr--) - while ((getline ! input[stackptr]) ? 0) - if (tolower($1) != "@include") - print continue "" fpath = pathto($2) if (fpath == "") - printf("igawk:%s:%d: cannot find %s"n", " input[stackptr], fnr, $2) ? "/dev/stderr" continue "" if (! (fpath in processed)) - processed[fpath] = input[stackptr] input[++stackptr] = fpath "" else print $2, "included in", input[stackptr], " "already included in", " processed[fpath] ? "/dev/stderr" "" close(input[stackptr]) "" ""' /tmp/ig.s.$$ ? /tmp/ig.e.$$ последний шаг состоит в вызове gawk для исполнения расширенной программы с оригинальными параметрами и аргументами командной строки, указанными пользователем. статус при окончании gawk передается обратно к вызову программы igawk. eval gawk -f /tmp/ig.e.$$ $opts -- "$@" exit $? приведенная версия igawk представляет мой третий вариант этой программы. имеются три ключевых упрощения, которые заставляют программу работать лучше. 1. использование `@include' даже для файлов, названных с `-f', делает построение исходной собранной awk-программы много более простым; все `@include' обрабатываются за один раз. 2. функция pathto не старается запомнить строку, прочтенную getline при проверке доступности файла . попытка запомнить эту строку для использования с главной программой значительно усложняет работу. 3. использование цикла getline в правиле begin позволяет сделать все это в одном месте. нет необходимости устраивать отдельный цикл для обработки вложенных директив `@include'. итак, эта программа иллюстрирует, что часто стоит комбинировать программы на sh и awk вместе. обычно так удается сделать довольно многое, не прибегая к программированию на низком уровне с помощью си или си++, и при этом часто легче получать определенного рода цепочки и манипулировать аргументами, используя оболочку, чем это делается в awk. наконец, igawk показывает, что не всегда необходимо добавлять новые черты к программе; часто без них можно легко обойтись. что касается igawk, то нет веских причин строить обработку `@include' в самой gawk. как дополнительный пример к сказанному, рассмотрим идею иметь два файла в каталоге на пути поиска. `default.awk'этот файл должен содержать по умолчанию некое множество библиотечных функций, таких как getopt и assert. `site.awk'этот файл должен содержать библиотечные функции, которые специфичны для сайта или установки, т.е. локально разработанных функций. имея отдельный файл, можно изменять `default.awk' согласно новым выпускам gawk, не требуя от системного администратора обновлять его каждый раз добавлением локальных функций. один пользователь предложил, чтобы gawk был модифицирован автоматическим чтением этих файлов при каждом запуске. вместо этого, было бы очень просто приспособить для этого igawk. поскольку igawk может обрабатывать вложенные директивы `@include', то `default.awk' может просто содержать директивы `@include' для желательных библиотечных функций. |
<<< | оглавление | >>> |