руководство пользователя для gnu awk
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 } |
<<< | оглавление | >>> |