<<< оглавление >>>

руководство пользователя для gnu awk

arnold d. robbins
перевод балуева а. н.

16. практические awk-программы

эта глава представляет попурри из awk-программ для наслаждения их чтением. она состоит из двух частей. первая содержит awk-версии нескольких общих утилит posix. во второй собраны разные интересные программы. многие из них используют библиотечные функции, представленные в главе 15 [библиотека awk-функций], стр. 169.

16.1 пере изобретение колеса для забавы и пользы

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

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

программы расположены в алфавитном порядке.

16.1.1 вырезание полей и колонок

утилита cut выделяет или "вырезает" символы или поля из их стандартного ввода и посылает в стандартный вывод. cut может вырезать или список символов или список полей. по умолчанию поля разделяются символами tab, но вы можете указать в параметре командной строки другой символ как разделитель полей. в cut определение полей менее общее чем в awk. cut может только извлечь регистрационное имя зарегистрированного пользователя из входа who. например, следующий конвейер генерирует отсортированный единый список работающих пользователей:

who | cut -c1-8 | sort | uniq

параметры для cut следующие: -c list

использует list как список символов для вырезания (to cut out). элементы в списке могут разделяться запятыми, а диапазоны символов могут разделяться символами минуса. список `1-8,15,22-35' содержит символы от 1 до 8, 15 и от 22 до 35.

-f list

использует list как список полей для вырезки.

-d delim

использует delim как символ-разделитель полей вместо tab

-s

подавляет печать строк, не содержащих разделителей полей.

awk-реализация cut использует библиотечную функцию getopt (см. раздел 15.10 [обработка параметров командной строки], стр. 186), библиотечную функцию join (см. раздел 15.6 [соединение элементов массива в цепочку], стр. 176).

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


# cut.awk --- реализация cut в awk
# arnold robbins, arnold@gnu.org, public domain
# may 1993
# параметры:
# -f list cut fields
# -d c символ разделитель полей
# -c list cut characters
# # -s подавляет строки без символов-разделителей
function usage( e1, e2) 
{
e1 = "usage: cut [-f list] [-d c] [-s] [files...]"
e2 = "usage: cut [-c list] [files...]" print e1 > "/dev/stderr"
print e2 > "/dev/stderr" exit 1
}

переменные e1 и e2 используются для удобного расположения данных на странице.

долее идет правило begin, которое анализирует параметры командной строки. оно заносит в fs один символ tab, разделитель полей по умолчанию. разделитель выходных полей устанавливается тот же что и на входе. затем используется getopt для анализа параметров в командной строке. одна из переменных by_fields или by_chars устанавливается в true, для указания, что обработка будет идти по полям или по символам соответственно. при вырезке по символам разделитель выходных полей устанавливается в пустую цепочку.


     begin    \
     {
         fs = "\t"    # default
         ofs = fs
         while ((c = getopt(argc, argv, "sf:c:d:")) != -1) {
             if (c == "f") {
                 by_fields = 1
                 fieldlist = optarg
             } else if (c == "c") {
                 by_chars = 1
                 fieldlist = optarg
                 ofs = ""
             } else if (c == "d") {
                 if (length(optarg) > 1) {
                     printf("using first character of %s" \
                     " for delimiter\n", optarg) > "/dev/stderr"
                     optarg = substr(optarg, 1, 1)
                 }
                 fs = optarg
                 ofs = fs
                 if (fs == " ")    # defeat awk semantics
                     fs = "[ ]"
             } else if (c == "s")
                 suppress++
             else
                 usage()
         }
     
         for (i = 1; i < optind; i++)
             argv[i] = ""

особый случай составляет пробел как разделитель полей. использование " " (один пробел) как значение fs неправильно--awk будет разделять поля группами пробелов, tab и/или newlines, а мы хотим разделения одним пробелом. заметим также, что после конца работы getopt мы должны очистить все элементы argv от первого до optind, чтобы awk не пыталась обработать параметры командной строками как имена файлов.

после окончания действий над параметрами командной строки, программа проверяет, что параметры имеют смысл. только один из `-c' и `-f' должен быть указан, и оба требуют список полей. затем вызывается или set_fieldlist или set_charlist для выделения списка полей или символов.


         if (by_fields && by_chars)
             usage()
     
         if (by_fields == 0 && by_chars == 0)
             by_fields = 1    # default
     
         if (fieldlist == "") {
             print "cut: needs list for -c or -f" > "/dev/stderr"
             exit 1
         }
     
         if (by_fields)
             set_fieldlist()
         else
             set_charlist()
     }

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


     function set_fieldlist(        n, m, i, j, k, f, g)
     {
         n = split(fieldlist, f, ",")
         j = 1    # index in flist
         for (i = 1; i <= n; i++) {
             if (index(f[i], "-") != 0) { # a range
                 m = split(f[i], g, "-")
                 if (m != 2 || g[1] >= g[2]) {
                     printf("bad field list: %s\n",
                                       f[i]) > "/dev/stderr"
                     exit 1
                 }
                 for (k = g[1]; k <= g[2]; k++)
                     flist[j++] = k
             } else
                 flist[j++] = f[i]
         }
         nfields = j - 1
     }

функция set_charlist сложнее чем set_fieldlist. здесь идея состоит в использовании gawk-переменной fieldwidths (см. раздел 5.6 [чтение данных фиксированной ширины], стр. 49), где описывается ввод с постоянной шириной. при использовании списка символов мы точно это и имеем.

установка fieldwidths более сложна, чем просто перечисление полей, которые должны быть напечатаны. мы должны следить за полями для печати и также за промежуточными символами, которые должны быть пропущены. например, предположим, вы ищете символы от 1 до 9, 15, и от 222 до 35. вы должны использовать `-c 1-8,15,22-35'. необходимое значение для fieldwidths будет "8 6 1 6 14". это дает нам пять полей, и должны быть напечатаны $1, $3 и $5. промежуточные поля есть "заполнители", мусор между нужными данными.

flist перечисляет поля для печати, а t следит за полным списком полей, включая поля-заполнители.


     function set_charlist(    field, i, j, f, g, t,
                               filler, last, len)
     {
         field = 1   # count total fields
         n = split(fieldlist, f, ",")
         j = 1       # index in flist
         for (i = 1; i <= n; i++) {
             if (index(f[i], "-") != 0) { # range
                 m = split(f[i], g, "-")
                 if (m != 2 || g[1] >= g[2]) {
                     printf("bad character list: %s\n",
                                    f[i]) > "/dev/stderr"
                     exit 1
                 }
                 len = g[2] - g[1] + 1
                 if (g[1] > 1)  # compute length of filler
                     filler = g[1] - last - 1
                 else
                     filler = 0
                 if (filler)
                     t[field++] = filler
                 t[field++] = len  # length of field
                 last = g[2]
                 flist[j++] = field - 1
             } else {
                 if (f[i] > 1)
                     filler = f[i] - last - 1
                 else
                     filler = 0
                 if (filler)
                     t[field++] = filler
                 t[field++] = 1
                 last = f[i]
                 flist[j++] = field - 1
             }
         }
         fieldwidths = join(t, 1, field - 1)
         nfields = j - 1
     }

вот правило, по которому фактически обрабатываются данные. если параметр `-s' был дан, то suppress будет иметь значение true. первый оператор if подтверждает, что входная запись должна иметь разделитель полей. если cut обрабатывает поля, то suppress есть true, и символ разделителя полей не входит в запись, когда запись пропускается. если запись правильна, то в этой точке gawk разделяет данные на поля, либо используя символ в fs либо используя поля фиксированной ширины и fieldwidths. цикл распространяется на список полей, которые должны быть напечатаны. если соответствующее поле имеет в себе данные, оно печатается. если следующее поле также имеет данные, то символ-разделитель вставляется между полями.


     {
         if (by_fields && suppress && index($0, fs) != 0)
             next
     
         for (i = 1; i <= nfields; i++) {
             if ($flist[i] != "") {
                 printf "%s", $flist[i]
                 if (i < nfields && $flist[i+1] != "")
                     printf "%s", ofs
             }
         }
         print ""
     }

эта версия cut опирается на gawk-переменную fieldwidths, чтобы осуществить основанное на символе вырезание. хотя это возможно и в других реализациях awk с помощью substr (см. раздел 12.3 [встроенные функции для действий с цепочками], стр. 137), это делать очень болезненно. переменная fieldwidths позволяет элегантно решить проблему отделения входной строки от символов.

16.1.2 поиск регулярных выражений в файлах

утилита egrep ищет в файлах образцы. она использует регулярные выражения, которые почти идентичны тем, которые используются в awk (см. раздел 7.1.2 [константы регулярных выражений], стр. 77). она используется следующим образом:

egrep [ options ] 'pattern' files ... образец ('pattern') есть regexp. обычно regexp заключается в кавычки, чтобы удержать оболочку от обработки различных специальных символов. обычно egrep печатает строки, которые соответствуют образцу. если в командной строке указано много имен файлов, каждая выходная строка предваряется именем файла и двоеточием.

имеются следующие параметры:

-c   печатать количества строк, отвечающих образцу,
	 вместо самих строк.

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

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

-i   игнорировать различия в регистре в образце
	 и входных данных.

-l   при соответствии печатать только имена файлов,
	 в которых обнаружены
	 соответствия, а не сами строки.

-e pattern     использовать  pattern как  regexp
	 для соответствия. целью
	 параметра `-e' является разрешение
	 для pattern начинаться с `-'.

эта версия использует библиотечную функцию getopt (см. раздел 15.10 [обработка параметров командной строки], стр. 186), и библиотечную программу смены файлов (см. раздел 15.9 [обнаружение границ файлов с данными], стр.185). программа начинается с описательных комментариев, затем идет правило begin, которое обрабатывает аргументы командной строки с помощью getopt. параметр `-i' (игнорировать регистр) удовлетворить особенно просто в gawk; мы просто используем встроенную переменную ignorecase (см. главу 10 [встроенные переменные], стр. 115).

# egrep.awk --- моделирование  egrep в awk
# arnold robbins, arnold@gnu.org, public domain # may 1993
# options:
# -c считать строки
# -s молчание - использовать код возврата
# -v обратный тест; успех, если нет соответствий
# -i игнорировать регистр
# -l печатать только имена файлов
# -e аргумент есть образец

     begin {
         while ((c = getopt(argc, argv, "ce:svil")) != -1) {
             if (c == "c")
                 count_only++
             else if (c == "s")
                 no_print++
             else if (c == "v")
                 invert++
             else if (c == "i")
                 ignorecase = 1
             else if (c == "l")
                 filenames_only++
             else if (c == "e")
                 pattern = optarg
             else
                 usage()
         }

далее следует код, который обслуживает специфические свойства egrep. если с `-e' не указаны никакие образцы, то используется первый не параметр в командной строке. аргументы командной строки вплоть до argv[optind] очищаются, так что awk не будет обрабатывать их как файлы. если файлы не указаны, используется стандартный ввод, а если было указано несколько файлов, мы обнаруживаем это и делаем так, чтобы имена файлов при выводе предшествовали строкам с соответствиями.

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


         if (pattern == "")
             pattern = argv[optind++]
     
         for (i = 1; i < optind; i++)
             argv[i] = ""
         if (optind >= argc) {
             argv[1] = "-"
             argc = 2
         } else if (argc - optind > 1)
             do_filenames++
     
     #    if (ignorecase)
     #        pattern = tolower(pattern)
     }

со следующих строк комментарий нужно снять, если используется не gawk. это правило переводит все символы входной строки в нижний регистр, если был указан параметр `-i'. оно не является необходимым в gawk.


     #{
     #    if (ignorecase)
     #        $0 = tolower($0)
     #}

функция beginfile вызывается правилом `ftrans.awk' перед началом обработки каждого нового файла. в нашем случае она очень проста; нужно только инициализировать нулем переменную fcount. она следит за тем, сколько строк в текущем файле соответствуют образцу.


     function beginfile(junk)
     {
         fcount = 0
     }

функция endfile вызывается после завершения обработки каждого файла. она используется только тогда, когда пользователь хочет подсчитывать количество строк с соответствиями. no_print получит значение true только если нужен выходной статус. count_only будет true, если подсчеты строк желательны. поэтому egrep будет только печатать счетчик строк, если задействованы печать и счет. выходной формат должен быть выбран в зависимости от того, сколько файлов будут обрабатываться. в конце fcount добавляется к total, и мы узнаем, сколько всего строк соответствуют образцу.


     function endfile(file)
     {
         if (! no_print && count_only)
             if (do_filenames)
                 print file ":" fcount
             else
                 print fcount
     
         total += fcount
     }

это правило производит большую часть работы по определения соответствия строк.


of matching lines. the variable matches will be true if the line matched the pattern. if the user wants lines that did not match, the sense of the matches is inverted using the `!' operator. fcount is incremented with the value of matches, which will be either one or zero, depending upon a successful or unsuccessful match. if the line did not match, the next statement just moves on to the next record.


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

наконец, если необходимо, можно печатать каждую строку с предшествующими именем файла и двоеточием.


     {
         matches = ($0 ~ pattern)
         if (invert)
             matches = ! matches
     
         fcount += matches    # 1 or 0
     
         if (! matches)
             next
     
         if (! count_only) {
             if (no_print)
                 nextfile
     
             if (filenames_only) {
                 print filename
                 nextfile
             }
     
             if (do_filenames)
                 print filename ":" $0
             else
                 print
         }
     }

правило end заботится о выдаче правильного выходного статуса. если соответствия не были обнаружены, он равен 1, в противном случае 0.


     end    \
     {
         if (total == 0)
             exit 1
         exit 0
     }

функция usage печатает свое сообщение в случаи ошибок в параметрах и прерывает программу.


     function usage(    e)
     {
         e = "usage: egrep [-csvil] [-e pat] [files ...]"
         e = e "\n\tegrep [-csvil] pat [files ...]"
         print e > "/dev/stderr"
         exit 1
     }

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

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

16.1.3 печать информации о пользователях

утилита id перечисляет пользовательские реальные и фактические идентификационные номера, реальные и фактические групповые идентификационные номера, и, если они имеются, множества пользовательских групп. id печатает эффективные пользовательские и групповые идентификаторы только тогда, когда они отличаются от реальных. если возможно, id также выдает пользовательские и групповые имена. вывод может выглядеть так:


     $ id
     -| uid=2076(arnold) gid=10(staff) groups=10(staff),4(tty)

это та же самая информация, которая выдается специальным файлов gawk `/dev/user' (см. раздел 6.7 [специальные имена файлов в gawk], стр. 72). однако, утилита id обеспечивает более удобный вывод, чем просто цепочка чисел.

приведем простую версию id, написанную на awk. она использует библиотечные функции пользовательской базы данных (см. раздел 15.11 [чтение пользовательской базы данных], стр. 192), и библиотечные функции групповой базы данных (см. раздел 15.12 [чтение групповой базы данных], стр. 197).

эта программа достаточно незамысловата. вся работа делается в правиле begin. пользовательские и групповые идентификационные номера получаются от `/dev/user'. если файла `/dev/user' нет, программа не работает.

в коде программы много повторений. вход пользовательской базы с реальным числовым идентификатором расщепляется на части посредством `:'. имя есть первое поле. подобный же код используется для эффективного идентификационного номера пользователя и для групповых номеров.

# id.awk --- реализация  id в awk
# arnold robbins, arnold@gnu.org, public domain # may 1993
# выход таков:
# uid=12(foo) euid=34(bar) gid=3(baz) \
# egid=5(blat) groups=9(nine),2(two),1(one)

     begin    \
     {
         uid = procinfo["uid"]
         euid = procinfo["euid"]
         gid = procinfo["gid"]
         egid = procinfo["egid"]
     
         printf("uid=%d", uid)
         pw = getpwuid(uid)
         if (pw != "") {
             split(pw, a, ":")
             printf("(%s)", a[1])
         }
     
         if (euid != uid) {
             printf(" euid=%d", euid)
             pw = getpwuid(euid)
             if (pw != "") {
                 split(pw, a, ":")
                 printf("(%s)", a[1])
             }
         }
     
         printf(" gid=%d", gid)
         pw = getgrgid(gid)
         if (pw != "") {
             split(pw, a, ":")
             printf("(%s)", a[1])
         }
     
         if (egid != gid) {
             printf(" egid=%d", egid)
             pw = getgrgid(egid)
             if (pw != "") {
                 split(pw, a, ":")
                 printf("(%s)", a[1])
             }
         }
     
         for (i = 1; ("group" i) in procinfo; i++) {
             if (i == 1)
                 printf(" groups=")
             group = procinfo["group" i]
             printf("%d", group)
             pw = getgrgid(group)
             if (pw != "") {
                 split(pw, a, ":")
                 printf("(%s)", a[1])
             }
             if (("group" (i+1)) in procinfo)
                 printf(",")
         }
     
         print ""
     }

16.1.4 разделение большого файла на части

программа split разделяет большие текстовые файлы на меньшие части. по умолчанию, выходные файлы именуются `xaa', `xab', и т. д. каждый файл содержит 1000 строк, за исключением последнего. для изменения количества строк в каждом файле нужно указывать в командной строке число с предшествующим минусом, например `-500' для файлов с 500 строк вместо 1000. чтобы изменить имена выходных файлов на что-нибудь вроде `myfileaa', `myfileab', и т.д., нужно указать дополнительный аргумент с именем файла.

приведем версию split в awk. она использует функции ord и chr, представленные в разделе 15.5 [перевод символов в числа и обратно], стр. 174. программа сначала устанавливает параметры по умолчанию, а затем проверяет, не слишком ли много аргументов задано. затем она смотрит на каждый заданный аргумент. первый аргумент может быть минусом со следующим за ним числом. если так, то он выглядит как отрицательное число. это число превращается в положительное и становится счетчиком строк. имя файла с данными обходится и последний аргумент используется как префикс для имен выходных файлов.


     begin {
         outfile = "x"    # default
         count = 1000
         if (argc > 4)
             usage()
     
         i = 1
         if (argv[i] ~ /^-[0-9]+$/) {
             count = -argv[i]
             argv[i] = ""
             i++
         }
         # test argv in case reading from stdin instead of file
         if (i in argv)
             i++    # skip data file name
         if (i in argv) {
             outfile = argv[i]
             argv[i] = ""
         }
     
         s1 = s2 = "a"
         out = (outfile s1 s2)
     }

следующее правило делает большую часть работы. tcount (временный счетчик) следит, сколько строк было напечатано до сих пор в выходном файле. если это больше чем count, нужно закрыть текущий файл и начать новый. s1 and s2 следят за текущими суффиксами имени файла. если они обе `z', файл слишком велик. в противном случае s1 передвигается на следующую букву алфавита, а s2 начинается опять с `a'.


     {
         if (++tcount > count) {
             close(out)
             if (s2 == "z") {
                 if (s1 == "z") {
                     printf("split: %s is too large to split\n",
                            filename) > "/dev/stderr"
                     exit 1
                 }
                 s1 = chr(ord(s1) + 1)
                 s2 = "a"
             }
             else
                 s2 = chr(ord(s2) + 1)
             out = (outfile s1 s2)
             tcount = 1
         }
         print > out
     }

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

16.1.5 размножение вывода в несколько файлов

программа tee известна как "подгонка к конвейеру". tee копирует свой стандартный ввод в свой стандартный выход и также дуплицирует его в файлы, названные в командной строке. она используется так:

tee [-a] file ... параметр `-a' побуждает писать в конец названных файлов, вместо того, чтобы закрывать их запускаться вновь.

правило begin сначала делает копию всех аргументов командной строки в массив с именем copy. argv[0] не копируется, так как не нужен. tee не может использовать argv непосредственно, поскольку awk будет пытаться обрабатывать каждый файл, названный в argv, как входные данные.

если первый аргумент есть `-a', то флажок append ставится в true и обе argv[1] и copy[1] вычеркиваются. если argc меньше двух, то никакие файлы не были названы, и tee печатает сообщение usage и кончает работу. в конце awk побуждается к чтению стандартного ввода установкой в argv[1] значения "-", и двойки в argc.

# tee.awk --- tee в awk
# arnold robbins, arnold@gnu.org, public domain
# may 1993
# revised december 1995

     begin    \
     {
         for (i = 1; i < argc; i++)
             copy[i] = argv[i]
     
         if (argv[1] == "-a") {
             append = 1
             delete argv[1]
             delete copy[1]
             argc--
         }
         if (argc < 2) {
             print "usage: tee [-a] file ..." > "/dev/stderr"
             exit 1
         }
         argv[1] = "-"
         argc = 2
     }

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


     {
         # вынесение if из цикла ускоряет его работу
         if (append)
             for (i in copy)
                 print >> copy[i]
         else
             for (i in copy)
                 print > copy[i]
         print
     }

можно было бы кодировать цикл так:

     for (i in copy)
         if (append)
             print >> copy[i]
         else
             print > copy[i]

это короче, но менее эффективно. `if' проверяется для каждой записи и для каждого выходного файла. при повторении тела цикла `if' проверяется только раз для каждой входной записи. если имеются n входных записей и m входных файлов, первый метод выполняет только n операторов `if', в то время как второй будет выполнять n *m операторов `if'

в конце работы правило end закрывает все выходные файлы.

     end    \
     {
         for (i in copy)
             close(copy[i])
     }

16.1.6 печать, не дуплицированных строк текста

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

uniq [-udc [-n]] [+n] [ input file [ output file ]]
параметры имеют следующий смысл:

-d   печать только повторенных строк.
-u   печать только неповторяющихся строк.
-c   считать строки. этот параметр подавляет `-d' и `-u'.
	 считаются как повторяющиеся, так
	 и неповторяющиеся строки.

-n   пропустить  n полей перед сравнением строк.
	определение полей  по
	правилу умолчания в awk: не-whitespace символы,
	отделенные группами
	пробелов и/или tab.

+n   пропустить n символов перед сравнением строк.
	предварительно пропускаются
	поля, указанные параметром `-n'.

input file  входные данные читаются из входного файла,
	названного в командной
	строке, а не из стандартного ввода.


output file  генерированный вывод направляется
	в названный выходной файл,
	а не в стандартный вывод.

нормально uniq работает так, как будто присутствуют оба параметра `-d' и `-u'.

приведем awk-реализацию uniq. она использует библиотечную функцию getopt (см. раздел 15.10 [обработка параметров командной строки, стр. 186), и библиотечную функцию join (см. раздел 15.6 [соединение элементов массива в цепочку], стр. 176).

программа начинается с функции usage, затем следует краткий обзор параметров и комментарий об их назначении.

правило begin работает с аргументами командной строки и параметрами. оно использует уловку, чтобы заставить getopt действовать с параметрами формы `-25', рассматривая такой параметр как параметр-букву `2' с аргументом `5'. если действительно были указаны две или более цифр (optarg выглядит как число), optarg сцепляется с цифрой параметра и затем результат складывается с нулем для превращения его в число. если имеется только одна цифра в параметре, то optarg не нужен и optind должен быть уменьшен так, что getopt будет обрабатывать его в следующий раз. так что этот код в некоторой степени необычный.

если параметров нет, по умолчанию печатаются и повторенные и не повторенные строки. выходной файл, если указан, присваивается в outputfile. ранее outputfile инициализировался как стандартный вывод , `/dev/stdout'.

# uniq.awk --- модель uniq в awk
# arnold robbins, arnold@gnu.org, public domain
# may 1993

     function usage(    e)
     {
         e = "usage: uniq [-udc [-n]] [+n] [ in [ out ]]"
         print e > "/dev/stderr"
         exit 1
     }

# -c счет строк. подавляет -d и -u
# -d только повторные строки
# -u только не повторяющиеся строки
# -n пропустить n полей
# +n пропустить  n символов, но сначала пропускать поля

     begin   \
     {
         count = 1
         outputfile = "/dev/stdout"
         opts = "udc0:1:2:3:4:5:6:7:8:9:"
         while ((c = getopt(argc, argv, opts)) != -1) {
             if (c == "u")
                 non_repeated_only++
             else if (c == "d")
                 repeated_only++
             else if (c == "c")
                 do_count++
             else if (index("0123456789", c) != 0) {
                 # getopt requires args to options
                 # this messes us up for things like -5
                 if (optarg ~ /^[0-9]+$/)
                     fcount = (c optarg) + 0
                 else {
                     fcount = c + 0
                     optind--
                 }
             } else
                 usage()
         }
     
         if (argv[optind] ~ /^\+[0-9]+$/) {
             charcount = substr(argv[optind], 2) + 0
             optind++
         }
     
         for (i = 1; i < optind; i++)
             argv[i] = ""
     
         if (repeated_only == 0 && non_repeated_only == 0)
             repeated_only = non_repeated_only = 1
     
         if (argc - optind == 2) {
             outputfile = argv[argc - 1]
             argv[argc - 1] = ""
         }
     }

следующая функция, are_equal, сравнивает текущую строку, $0, с предыдущей строкой, last. она реализует пропуск полей и символов. если не были заказаны ни счет полей ни счет символов, то are.equal просто возвращает 1 или 0 в зависимости от результата простого сравнения last и $0. в противном случае действия усложняются. если должны быть пропущены поля, каждая строка превращается в массив с помощью split (см. раздел 12.3 [встроенные функции для действий с цепочками], стр. 137), и затем нужные поля опять соединяются в строку с помощью join. полученные строки поступают в clast и cline. если никакие поля не пропускаются, в clast и cline устанавливаются last и $0 соответственно. наконец, если пропускаются символы, используется substr для удаления ведущих charcount символов в clast и cline. затем две цепочки сравниваются и are_equal возвращает результат сравнения.


     function are_equal(    n, m, clast, cline, alast, aline)
     {
         if (fcount == 0 && charcount == 0)
             return (last == $0)
     
         if (fcount > 0) {
             n = split(last, alast)
             m = split($0, aline)
             clast = join(alast, fcount+1, n)
             cline = join(aline, fcount+1, m)
         } else {
             clast = last
             cline = $0
         }
         if (charcount) {
             clast = substr(clast, charcount + 1)
             cline = substr(cline, charcount + 1)
         }
     
         return (clast == cline)
     }

два следующие правила составляют тело программы. первое выполняется только для самой первой строки данных. оно устанавливает last равным $0, для сравнения с последующими строками текста.

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

если uniq не подсчитывает, count увеличивается, если строки равны. иначе, если uniq подсчитывает повторные строки и более чем одна строка была замечена, или если uniq считает неповторяющиеся строки и только одна строка имеется, то строка печатается и count сбрасывается.

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


     nr == 1 {
         last = $0
         next
     }
     
     {
         equal = are_equal()
     
         if (do_count) {    # overrides -d and -u
             if (equal)
                 count++
             else {
                 printf("%4d %s\n", count, last) > outputfile
                 last = $0
                 count = 1    # reset
             }
             next
         }
     
         if (equal)
             count++
         else {
             if ((repeated_only && count > 1) ||
                 (non_repeated_only && count == 1))
                     print last > outputfile
             last = $0
             count = 1
         }
     }
     
     end {
         if (do_count)
             printf("%4d %s\n", count, last) > outputfile
         else if ((repeated_only && count > 1) ||
                 (non_repeated_only && count == 1))
             print last > outputfile
     }

16.1.7 подсчет объектов

утилита wc (word count) подсчитывает количества строк, слов и символов в одном или более входных файлов. она используется так:

wc [-lwc] [ files ... ]

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

-l    только подсчет строк

-w    только подсчет слов.  "слова" есть
	  связные последовательности символов,
	  не содержащая символов whitespace, разделенные
	  пробелами и/или символами tab.
	  к счастью, это нормальный способ для awk разделения
	  полей в входных данных.

-c    только подсчет символов.

реализация wc средствами awk особенно элегантна, поскольку awk сама делает большую часть работы; она разделяет строки на слова (т.е. поля) и считает их, подсчитывает строки( т.е. записи) и может легко сообщать нам длину строк. эта версия использует библиотечную функцию getopt (см. раздел 15.10 [обработка параметров командной строки], стр. 186), и функции переключения файлов (см. раздел 15.9 [слежение за границами файлов с данными], стр. 185). эта версия имеет одно важное отличие от традиционных версий wc. наша версия постоянно печатает счетчики в порядке: строки, слова, символы. традиционные версии запоминают порядок параметров `-l', `-w' и `-c' в командной строке и печатают счетчики в этом порядке.

правило begin обрабатывает аргументы. переменная print_total будет true, если в командной строке указано более одного файла.

# wc.awk --- счет строк, слов, символов
# arnold robbins, arnold@gnu.org, public domain # may 1993
# параметры:
# -l счет только строк
# -w счет только слов
# -c счет только символов
# #по умолчанию подсчет строк, слов и символов
begin {
        # побуждаем  getopt печатать сообщение
        # о неверных параметров. мы игнорируем их если
         while ((c = getopt(argc, argv, "lwc")) != -1) {
             if (c == "l")
                 do_lines = 1
             else if (c == "w")
                 do_words = 1
             else if (c == "c")
                 do_chars = 1
         }
         for (i = 1; i < optind; i++)
             argv[i] = ""
     
         # если параметров нет, делать все

         if (! do_lines && ! do_words && ! do_chars)
             do_lines = do_words = do_chars = 1
     
         print_total = (argc - i > 2)
     }

функция beginfile проста; она просто сбрасывает счетчики строк, слов и символов на 0 и запоминает имя текущего файла в fname. функция endfile добавляет значения текущего файла к соответствующим суммам для строк, слов и символов. затем печатает эти данные для файла, который был только что прочтен. она оставляет для beginfile сброс счетчиков для следующего файла с данными.


     function beginfile(file)
     {
         chars = lines = words = 0
         fname = filename
     }

     function endfile(file)
     {
         tchars += chars
         tlines += lines
         twords += words
         if (do_lines)
             printf "\t%d", lines
         if (do_words)
             printf "\t%d", words
         if (do_chars)
             printf "\t%d", chars
         printf "\t%s\n", fname
     }

имеется одно правило, которое выполняется для каждой строки. оно добавляет длину строки к переменной chars. оно должно добавлять 1, поскольку символ newline, разделяющий записи (значение rs), не входит в состав строки. переменная lines увеличивается для каждой прочтенной записи, а words увеличивается на значение nf, количество "слов" в этой строке.*1*


# делать для каждой строки

     # do per line
     {
         chars += length($0) + 1    # get newline
         lines++
         words += nf
     }
     
наконец, правило  end просто печатает суммы по всем файлам.
     
     end {
         if (print_total) {
             if (do_lines)
                 printf "\t%d", tlines
             if (do_words)
                 printf "\t%d", twords
             if (do_chars)
                 printf "\t%d", tchars
             print "\ttotal"
         }
     }


<<< оглавление >>>