[Перевод] Кунг-фу стиля Linux: упрощение работы с awk

Утилита awk — это нечто вроде швейцарского ножа для обработки текстовых файлов. Но некоторые ограничения awk порой доставляют неудобства тем, кто этой утилитой пользуется. Я, для того чтобы упростить работу с awk, создал несколько функций. Но сразу хочу сказать о том, что для работы этих функций нужны возможности GNU-версии awk. Поэтому для того чтобы воспроизвести то, о чём я буду рассказывать, вам совершенно необходимо использовать gawk и ничего другого. Возможно, в вашей системе настроено сопоставление /usr/bin/awk с чем-то, и это «что-то» может представлять собой gawk. Но это может быть и mawk, и какая-то другая разновидность awk. Если вы используете дистрибутив Linux, основанный на Debian, то знайте, что команда update-alternatives — это ваш хороший друг. В данном материале я буду исходить из предположения о том, что его читатель использует gawk.

0gl_xylrhskdhkpgxejtsqnfn7i.jpeg

После того, как вы прочитаете эту статью, вы узнаете о том, как пользоваться моей библиотекой дополнительных функций для awk. А именно, речь идёт о разделении строки на поля даже в условиях, когда не существует единого символа, используемого для разделения полей. Кроме того, вы сможете обращаться к полям, используя выбранные вами имена. Например, вам не придётся помнить о том, что $2 — это поле, содержащее сведения о времени. Вместо этого можно будет просто воспользоваться конструкцией наподобие Fields_fields[«time»].

Проблема awk


Утилита awk берёт на себя решение множества стандартных задач обработки текстовых файлов. Она читает файлы, считывая по одной записи за раз. Обычно «запись» — это одна строка. Затем awk разбивает строку на поля, ориентируясь на пробелы или на другие символы, используемые в качестве разделителей полей. Можно написать код, который что-то делает со считанной строкой или с отдельными полями. Эти стандартные возможности awk хорошо подходят для решения многих задач, особенно учитывая то, что можно настраивать разделители полей и признак конца записи. Такому формату соответствует удивительно большое количество файлов.

Правда, так устроены далеко не все файлы. Если нужно работать с данными из каких-нибудь логов или из баз данных, такие данные могут быть отформатированы с использованием самых разных подходов. Например, в некоторых полях могут присутствовать структурированные данные, при оформлении которых используются различные разделители. Это, конечно, не значит, что такие данные нельзя обработать с помощью awk. Так как в нашем распоряжении оказывается вся строка, с этой строкой можно сделать всё, что нужно. Но при обработке подобных строк усложняется логика программы. А главная цель использования awk — это упрощение работы.

Например, предположим, что у нас имеется файл с данными, полученный с некоего устройства. В его строках имеется восьмизначный серийный номер, за которым следует тег длиной в шесть символов. Потом идут два числа с плавающей точкой, разделённые неким разделителем. Шаблон записи может выглядеть так:

^([0-9]{8})([a-zA-Z0-9]{6})([-+.0-9]+),([-+.0-9]+)$


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

Если в записях файла имеются единообразно оформленные поля, но при этом неизвестно их точное количество, тогда, возможно, имеет смысл воспользоваться FS или FPAT. Вот материал, в котором идёт речь об использовании FPAT при обработке HEX-файлов с помощью awk. В рассматриваемой библиотеке применяется немного другой подход. Её можно использовать для полного разбора строки на составные части. Например, часть строки может представлять собой поле фиксированной длины, после чего могут идти поля, для оформления которых используется множество различных разделителей. С помощью других методов обрабатывать подобные данные может быть довольно-таки непросто.

Регулярные выражения


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

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

Например, пусть регулярное выражение выглядит так:

"^([0-9]+)([a-z]+)$"


Обрабатываемая строка выглядит так:

123abc


Массив будет содержать следующие данные:

array[0] - 123abc
array[1] - 123
array[2] - abc
array[0start] - 1
array[0length] - 6
array[1start] - 1
array[1length] - 3
array[2start] - 4
array[2length] - 3


Можно даже использовать вложенные выражения. Так, например, регулярное выражение вида «^(([xyz])[0-9]+)([a-z]+)$» при анализе строки z1x даст array[1]=z1, array[2]=z и array[3]=x.

Теория и практика


В теории это — всё, что нужно. Можно писать регулярные выражения для разбора строк, а потом обращаться к отдельным элементам строки, пользуясь массивом. На практике же гораздо удобнее будет, если все подготовительные процедуры будут проведены заранее, что позволит работать с данными, используя обычные имена.

Вот пример строки, которую может понадобиться обработать:

11/10/2020 07:00 The Best of Bradbury, 14.95 *****


Здесь имеется дата в формате, принятом в США, время в 24-часовом формате, название товара, цена и рейтинг товара, который может выглядеть как последовательность из 1–5 звёздочек и, кроме того, может отсутствовать. Написание регулярного выражения, способного выделить из этой строки каждое интересующее нас поле, будет достаточно сложной, но решаемой задачей. Вот один из вариантов такого регулярного выражения:

"^(([01][0-9])/([0-3][0-9])/(2[01][0-9][0-9]))[[:space:]]*(([0-2][0-9]):([0-5][0-9]))[[:space:]]+([^,]+),[[:space:]]*([0-9.]+)[[:space:]]*([*]{1,5})?[[:space:]]*$"


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

Библиотека, облегчающая работу с gawk


После того, как вы скачали файлы с GitHub, вы можете поместить функции с именами вида fields_* в свой код. В блоке BEGIN нужно выполнить некоторые настройки. Затем строки обрабатывают с помощью функции fields_process. Вот небольшой пример использования моей библиотеки (код самих функций из библиотеки опущен):

BEGIN {
fields_setup("^(([01][0-9])/([0-3][0-9])/(2[01][0-9][0-9]))[[:space:]]*(([0-2][0-9]):([0-5][0-9]))[[:space:]]+([^,]+),     [[:space:]]*([0-9.]+)[[:space:]]*([*]{1,5})?[[:space:]]*$")
fields_setupN(1,"date")
fields_setupN(2,"month")
fields_setupN(3,"day")
fields_setupN(4,"year")
fields_setupN(5,"time")
fields_setupN(6,"hours")
fields_setupN(7,"minutes")
fields_setupN(8,"item")
fields_setupN(9,"price")
fields_setupN(10,"star")
 
}
 
{
v=fields_process()
 
... тут будет ваш код...
 
}


В своём коде для обработки вышеприведённой строки вы можете воспользоваться, например, такой конструкцией:

cost=Fields_fields["price"] * 3


Как по мне, так это сильно упрощает работу. Функция fields_process возвращает false в том случае, если ей ничего не удалось найти. При этом, если нужно, можно работать с обычными awk-полями вроде $0 или $2.

Об устройстве предлагаемых функций


Мои функции основаны на двух механизмах. Во-первых — это gawk-расширение функции match. Во-вторых — это механизм ассоциативных массивов awk. Ранее я добавил именованные ключи к существующему массиву найденных совпадений. Поэтому к данным можно обращаться любым способом. Но я модифицировал массив, поэтому он является локальным, так как мне почти никогда не нужна эта возможность, и, таким образом, если понадобятся все данные из массива, нужно будет отфильтровать из него все дополнительные поля.

Часто имеет смысл начинать регулярное выражение с символа ^ и заканчивать символом $ для того чтобы ориентироваться на обработку всей строки. Главное — не забывать о том, что регулярное выражение должно, как в примере, учитывать использование пробелов. Это часто бывает нужно в том случае, когда имеются поля, которые могут содержать пробелы. Но если надо, чтобы пробелы, в любом случае, играли роль символов-разделителей полей, то вам, возможно, лучше использовать стандартную схему разбора строк.

Ещё один интересный приём заключается в получении «остатка строки» — всего того, что осталось после обработки первых полей. Сделать это можно, добавив в конец регулярного выражения конструкцию «(.*)$». Главное — не забудьте задать тег для этой конструкции, используя fields_setupN, что позже позволит обратиться к этим данным.

К моей библиотеке можно добавить простое расширение, которое превращает шаблон в массив шаблонов. Функция обработки строк может перебирать и испытывать элементы массива до тех пор, пока не найдёт совпадение с одним из них. Затем функция возвращает индекс сработавшего шаблона или false — в том случае, если не найдено совпадения ни с одним шаблоном. Вероятно, при таком подходе нужно будет предусмотреть использование различных наборов тегов полей для каждого из шаблонов.

Пользуетесь ли вы gawk? Планируете ли применять функции, предложенные автором этого материала?

oug5kh6sjydt9llengsiebnp40w.png

3piw1j3wd_cgmzq9sefgferaumu.png

© Habrahabr.ru