Awk — утилита и реализованный в ней сценарный язык для построчного разбора и редактирования текстового потока. Awk развила идеи таких утилит, как Sed и Grep, поэтому многое в ней похоже на них. Если эти утилиты вам знакомы, то освоение Awk для вас будет быстрым. По сравнению с Sed, язык Awk больше похож на Си и имеет множество его возможностей, такие как:

  • объявление переменных и массивов с динамической типизацией;
  • арифметические операции;
  • ветвления и циклы;
  • объявление функций и использование библиотеки встроенных функций.

Awk по сути является продолжателем Sed, привнося в процесс поточной обработки такие улучшения, как:

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

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

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

Awk разрабатывался много лет, поэтому на практике вы можете столкнуться с разными реализациями. Данное руководство больше опирается на реализацию GNU Awk, известную как GAWK. Расширения GAWK по тексту будут выделяться. Если по тексту мы говорим Awk, то описанное применимо для любой версии Awk, а не только для GAWK.

Общие сведения править

Общий алгоритм работы править

Awk работает так:

  1. Исполняется цепочка всех блоков BEGIN, до начала обработки самого первого фрагмента.
  2. Весь текст разбивается на фрагменты по хранящемуся во встроенной переменной RS символу (обычно по символу переноса строки). Затем каждый фрагмент дополнительно разбивается на поля по символу FS (обычно это пробелы).
  3. К каждому фрагменту применяется Awk программа, состоящая из одного и более блоков.
  4. Обработка текста будет происходить, пока не кончатся все его фрагменты.
  5. Когда фрагменты данного текста закончились, исполняется цепочка блоков END.
  6. Если Awk было передано несколько файлов, то после завершения обработки одного файла начинается обработка следующего, и все предыдущие шаги повторяются.

Здесь и далее озвученные выше термины означают:

  • Текст – множество символов, завершаемых символом конца потока EOF. Источником текста может служить простой файл или поток, передаваемый через специальный файл (например pipe-файл).
  • Фрагмент – это часть текста или серия символов внутри текста, завершаемая одним или несколькими характерными, например символом переноса строки \n.
  • Поле – часть фрагмента или серия символов внутри фрагмента, завершаемая одним или несколькими характерными, например пробельный символ.

Инструкции программы, собранные в блоки, в официальной документации Awk называют правилами (rules), но мы будем использовать такие термины, как программа Awk и блок на протяжении всего руководства, чтобы быть ближе к терминам, принятым в языке программирования Си.

Вы можете видеть, что алгоритм Awk местами очень похож на Sed. Но, в отличие от Sed, в Awk есть два программных блока, называемых BEGIN и END, которые выполняются один раз соответственно в начале и в конце обработки текста. Обычно в эти блоки закладываются общие для программы обработки моменты: например в BEGIN может быть заложена общая для программы обработки инициализация, а в блок END – подсчет статистик или информирование о том, что процедура завершена.

Также разбитие обрабатываемого фрагмента на поля отличает Awk от Sed и приносит дополнительные удобства, особенно когда структура входящего фрагмента известна. К разбитым полям можно обращаться по ссылкам вида $1, $2 и так далее. Awk за один раз может разбить фрагмент только на 100 полей (если конкретная реализация не диктует иные ограничения). В отличие от большинства языков программирования, в Awk поля начинают отсчитываться с единицы, а не с нуля. В поле $0 записывается входящий фрагмент целиком. Число полей для текущего фрагмента сохраняется в переменной NF, при этом к последнему полю фрагмента всегда можно обратиться через ссылку $NF.

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

Awk подсчитывает число записей, прочитанных им в текущем файле, в переменной FNR. Это значение сбрасывается для каждого последующего файла. Другая переменная NR хранит полное число прочитанных фрагментов за все время работы Awk, и оно никогда не сбрасывается.

Далее вы узнаете больше о языке программирования Awk и о встроенных полях.

Варианты вызова править

В простом случае вызов Awk выглядит так

awk '<программа-awk>' <файл 1> <файл 2>... <файл N>

В данном вызове Awk будет выполнять свою программу над каждым переданным файлом по порядку. Имя текущего файла сохраняется в переменной FILENAME.

Обрабатываемый текст может передаваться утилите и другими средствами командной оболочки:

# По конвейеру в STDIN
echo "hello" | awk '{print $0}'

# Направление строки в STDIN (первый вариант)
awk '{print $0}' <<< "hello"

# Направление строк в STDIN через Here-docs (второй вариант)
awk '{print $0}' <<EOF
hello
EOF

# Перенаправив поток через дескриптор
awk '{print $0}' < file.txt
# аналогично
awk '{print $0}' file.txt

# Через Process Substitution в Bash
awk '{print $0}' <( printf "hello" )

Если Awk не получает в качестве ввода файл, то он читает просто STDIN, который обычно связывается с клавиатурой (интерактивный режим).

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

Допустимо передавать несколько файлов через -f: в этом случае Awk сложит из них одну большую программу. Порядок при этом важен (подробнее об этом вы узнаете позже):

$ awk -f lib.awk \
      -f lib1.awk \
      -f program.awk \
  input.txt

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

#!/bin/awk -f

# Код на языке Awk
BEGIN { print "Hello, World!" }

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

Awk в нормальной ситуации возвращает 0, если программа не прервалась по оператору exit с иным кодом возврата. В случае ошибки возвращается 1. В случае фатальной ошибки возвращается 2.

Язык программирования Awk править

Комментарии править

В программах Awk допустимы однострочные комментарии, начинающиеся с символа решетки #. Комментарий начинается с этого символа и до конца строки.

Типы данных и переменные править

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

Строго говоря, в Awk всего два типа данных: числовой и строковый. Чтобы определить переменную, достаточно инициализировать ее некоторым значением.

number = 10     # числовая переменная
string = "abc"  # строковая переменная

Первые реализации могли хранить строковые переменные только как восьмибитные ASCII-символы. На текущий момент проблем с кодировками нет в современных системах. В старых реализациях строковая переменная могла хранить не более 256 символов. В GAWK явных ограничений на длину нет.

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

1.05e+8   106e-3

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

/^hello$/

Для обращения к переменной не нужны никакие дополнительные символы, достаточно просто обратиться к ней по имени. Имя переменной должно быть цепочкой символов из цифр, букв и символа нижнего подчеркивания, и не должно начинаться с цифры. Регистр в именах имеет значение, например var и VAR это разные переменные.

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

В Awk имеется много встроенных переменных, которые имеют имена в верхнем регистре, например NF (хранит число полей текущего фрагмента), FS (хранит разделитель входящих полей). По этой причине рекомендуется объявлять переменные либо в нижнем регистре, либо в верблюжей нотации.

Кроме простых переменных, в Awk есть еще массивы, которые порождаются через их инициализацию:

array[0] = "element"

Элементы массивов можно адресовать по строковым ключам (ассоциативные массивы), например

fruits["banana"] = 60

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

Массивы править

Массивы в Awk являются одномерными и динамическими. Правила для имен массивов аналогичны правилам для переменных. Единственное имя, которое не может использоваться ни для тех, ни для других это awk.

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

В Awk единственный способ объявить массив это проинициализировать его первым значением:

array1[0]="hello"
array2[0] # Аналогично array2[0]=""
array3["apple"]=4

Чтобы проверить, что в массиве существует элемент с определенным индексом, следует использовать оператор

# Возвращает 1, если есть
<индекс> in <имя-массива>

например

print 0 in array1       # 1
print 2 in array1       # 0
print "apple" in array3 # 1

Массивы обычно перебираются в цикле for (см. ниже)

for (<ссылка-на-ключ> in <имя-массива>) { 
     <тело-цикла>
}

например

$ awk 'BEGIN{arr["a"]=1;arr["b"]=2;arr["c"]=3; for (el in arr) { print arr[el] }}'
1
2
3

$ awk 'BEGIN{arr[1]=1;arr[2]=2;arr[3]=3; for (el in arr) { print arr[el] }}'
1
2
3

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

В больших программах иногда может понадобиться удаление элементов из массивов. Для этого используется специальный оператор

delete <имя-массива>[<индекс>]

# Чтобы очистить массив целиком, следует снова использовать цикл
for (i in array)
    delete array[i]

# Массив также можно удалить целиком
delete array

В Awk нет возможности создавать многомерные массивы, однако их можно эмулировать, например так

array[0,0]=1
array[1,0]=2
array[0,1]=3
array[1,1]=4

# На самом деле массив такой же плоский, секрет в сложных ключах.

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

Преобразования данных править

Некоторые преобразования Awk умеет делать неявно:

  • Числа всегда преобразуются в строки неявно, если они конкатенируются со строками. Чтобы принудить Awk к этому преобразованию явно, можно использовать прием конкатенации с пустой строкой "".
  • Строки в контексте арифметических операций всегда конвертируются неявно в числа.
  • Строка интерпретируется всегда как число, если она состоит из цифр, точки, символа e/E и знаков + и -. Если строка не может быть конвертирована в число, то она превращается в 0.

Точные правила конвертации чисел определяются переменной CONVFMT, которая является по сути форматной строкой для системной функции sprintf. По умолчанию она инициализирована форматом %.6g, где формат g служит для сохранения значимых символов, а .6 означает сохранять 6 значимых знаков. Иногда приходится повышать точность через эту переменную, например в современных компьютерах двойная точность соответствует 16 или 17 десятичным цифрам.

До введения POSIX, awk предусматривало переменную OFMT, которая использовалась для печати чисел в операторе print и для преобразования в sprintf. Эта переменная также имеет значение по умолчанию %.6g. Такое поведение следует иметь в виду, если вы переносите старые Awk-программы на новую версию Awk.

Поля и работа с ними править

Как уже было сказано ранее, входящий фрагмент разбивается по разделителю FS (Input field separator) на поля. По умолчанию разделителем считается пробельный символ, под которым подразумеваются сразу три ASCII символа (один или несколько идущих подряд): пробел (0x20), горизонтальный табулятор (0x09) и символ новой строки (0x0A). Во многих языках программирования под пробельным символом могут пониматься и другие управляющие последовательности, например 0xC (разрыв страницы) или 0xB (вертикальная табуляция), но не в Awk.

Для текущего фрагмента вы можете:

  • Обратиться к любому полю через $<номер>, где <номер> – порядковый номер поля, начиная с 1. Соответственно в поле $0 хранится весь фрагмент.
  • Узнать на сколько полей был разбит фрагмент через переменную NF (Number of fields).
  • Обратиться к последнему полю через $NF. В данном случае здесь NF развернется в номер последнего поля автоматически.
  • Вы можете вычислить номер поля, если используйте такой синтаксис $(<выражение>), где результатом выражения должно являться число. Например $(NF-1) является обращением к предпоследнему полю, а $(1+2) – к третьему полю. Если поля с полученным номером не существует, то ссылка будет развернута в пустоту.
  • После того как поля сформировались для текущего фрагмента, их можно редактировать, например $3 = $2-1. Если вы редактируете одно из полей, входящий фрагмент будет вычислен заново так, чтобы полученное изменение применилось ко всему фрагменту. Это означает, что поле $0 в любом случае также изменится. Вы также можете изменить несуществующее поле: в этом случае оно будет добавлено ко входящему фрагменту. Любые добавления полей повлияют на счетчик NF, однако данный счетчик никогда не уменьшается, даже если вы затрете значение некоторого поля.
  • Если вы изменяете поле вне контекста присваивания, то на входящий фрагмент это не будет влиять. Например $2-1 без присваивания не повлияет на поле $2.

Разделителем FS может быть строка, состоящая из нескольких символов: в этом случае вся строка является разделителем. Переменная FS встроена в Awk и она не наследует каких-либо значений от командной оболочки неявно. Так, переменная POSIX IFS для разделения полей в Awk никак не используется.

Разделителем FS может быть регулярное выражение, когда разделитель динамический, либо его так проще объявить. Никаких особых правил записи такого разделителя в Awk нет: вы должны просто записать регулярное выражение в грамматике ERE, например

$ echo "a+b c:d:e+f" | awk 'BEGIN {FS="[+:]+"} {print NF}'
5 # Разделителями являются один или несколько символов '+' или ':'.

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

$ echo "a+b c:d:e+f" | awk 'BEGIN {FS=""} {print NF}'
11

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

Разделитель полей может быть установлен из командной оболочки через опцию -F (не путать с -f, через который передается файл с программой Awk), например

$ echo "a:b:c:d" | awk -F: '{print NF}'  # разделитель ':'
4

Следует помнить о тонкостях разбивки на поля, если FS меняется по ходу программы:

  • В POSIX предполагается, что фрагмент разбивается на поля сразу после чтения очередного фрагмента, однако на деле многие реализации в качестве оптимизации откладывают эту процедуру до первого обращения к полю. Этот момент следует выяснить сразу для конкретной реализации, чтобы не получить неожиданный результат в будущем. В GAWK фрагмент разбивается на поля в начале обработки без каких-либо оптимизаций.
  • Предполагается, что Awk пользуется актуальным значением FS на момент разбивки. На практике это означает, что когда вы меняете FS во время обработки некоторого фрагмента, измененное значение FS будет использоваться уже на следующей итерации. Например сравните
    $ echo -e "a:b:c:d\ne+f+h" | awk -F: '{FS="+"; print NF;}'
    4 # В первом фрагменте разделитель ':' и там 4 буквы.
    3 # Мы поменяли FS во время обработки первого фрагмента. Во второй строке по разделителю '+' 3 буквы.
    

У переменной FS есть парная ей OFS (Output field separator), которая позволяет транслировать входящий разделитель в другой для исходящей печати (когда это удобно) через оператор конкатенации , оператора print. На разбиение полей OFS никак не влияет.

$ echo "a:b" | awk -F: 'BEGIN{OFS="="}{print $1,$2}'
a=b

Общая структура программы Awk править

Программа Awk строится из одного и более блоков, границы которых обозначают фигурные скобки. Общая структура программы в более-менее полном виде выглядит так

# Необязательный блок BEGIN
BEGIN {
	<инструкции блока BEGIN>
}
# Основной блок
{
	<инструкции основного блока>
}

# Необязательный блок END
END {
	<инструкции блока END>
}

Любая Awk программа строится по меньшей мере из одного основного блока, в который помещаются инструкции, применяемые к каждому фрагменту, если блок не имеет шаблона (см. ниже).

Помимо основного блока, есть еще два необязательных: BEGIN и END. Блок BEGIN исполняется один раз в начале обработки каждого текста. Часто туда закладывается начальная инициализация, которая актуальна на время обработки всего текста. Например, если бы задачей Awk было разбиение полей фрагмента по колонкам, то в блок BEGIN логично поместить процедуру вывода шапки для этих колонок.

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

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

Если программа имеет всего один блок BEGIN, то программа завершается сразу после его исполнения, независимо от числа переданных фрагментов. Ранние реализации Awk в этой ситуации не игнорировали фрагменты, а просто, видя что нет основной программы, их по порядку пропускали, при этом все внутренние переменные актуализировались. С целью оптимизации, последние версии Awk полностью игнорируют ввод в такой ситуации.

Если в программе есть один блок BEGIN и один END, но нет основного блока, то Awk всегда пройдется по всем фрагментам. Это нужно потому, что потенциально в блоке END может быть заложен анализ переменных по типу NR (полное число прочитанных фрагментов).

В общем случае основной блок может быть пустым, но блоки BEGIN и END пустыми быть не могут – поведение для пустых BEGIN и END неопределенное в общем случае.

Существует несколько тонкостей, о которых следует помнить:

  • В блоке BEGIN нет смысла обращаться к полям фрагмента, так как он исполняется до начала чтения самого первого фрагмента. Любые обращения к полям или переменным, завязанным на них, будут возвращать пустоту или ноль.
  • В блоке END по логике также нет смысла обращаться к полям и переменным, завязанным на них, потому что к этому моменту последний фрагмент уже обработан. В старых реализациях обращения к полям и переменным, завязанных на них, могут возвращать пустоту или ноль в блоке END, однако, например, в GAWK обращение к $0 вернет последний фрагмент, а NF – число полей в нем. Так как эта ситуация неоднозначна, следует избегать обращений к полям в блоке END.
  • По причине первых двух пунктов, вызов print без аргументов может приводить в разных реализациях либо к печати фрагмента (print $0), либо пустой строки. Вызывать print без аргументов в BEGIN и END является порочной практикой, поэтому в этих блоках настоятельно рекомендуется вызывать только print "" для печати пустой строки.

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

Основные блоки программы править

Программа Awk строится из одного и более обычных блоков. На самом деле блочное устройство Awk было заимствовано у Sed, разве что фигурные скобки в Awk нужно ставить всегда. Общий синтаксис блока выглядит так

# Первый вариант
<условное выражение> { <команды блока> }

# Второй вариант
/<шаблон>/{ <команды блока> }

Программа Awk пытается применить шаблон блока к фрагменту или проверить условие, и если шаблон совпадает или условие ИСТИНА, то исполняются команды этого блока. Если блок не имеет явного шаблона или условия, то он будет применяться ко всем фрагментам. Обычные блоки можно мешать с блоками BEGIN и END и, если пожелаете, записывать в одну строку. Между блоками никакие разделители не используются, так как фигурные скобки не создают неоднозначности.

Обычный блок может быть пустым, хотя это и лишено смысла. Также блок можно опустить, оставив только шаблон или условие. В этом случае подразумевается блок с командой print $0, т.е.

# следующая запись
/<шаблон>/
# эквивалентна
/<шаблон>/{ print }

# или
awk 'NR==1'    # Напечатать только первый фрагмент
# аналогична
awk 'NR==1 {print}'

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

$ echo -n "a 2 b" | awk 'BEGIN{RS=" "} { print $0 ": Block 1" } /[[:digit:]]+/{ print $0 ": Block 2"} { print $0 ": Block 3" }'
a: Block 1
a: Block 3
2: Block 1
2: Block 2
2: Block 3
b: Block 1
b: Block 3

В этом примере у нас три блока. Первый и третий блок не имеют шаблона, а второй имеет шаблон, который проверяет состоит ли входящий фрагмент только из цифровых знаков. На вход этой программе мы передаем три символа: букву a, цифру 2 и букву b. Вы можете видеть, что для букв применились только 1 и 3 блок, а для цифры все три блока.

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

Каждый обычный блок создает свою изолированную область видимости для переменных, другими словами, переменная, объявленная в некотором блоке, видна только в его пределах. Единственный блок, чьи переменные видят все остальные, это блок BEGIN. Давайте рассмотрим такой пример

$ echo -n "a" | awk 'BEGIN{RS=" "; global_b=10; global_1=11 } { var=5; global_1=12; print var,global_b,global_1 }{ var=6; print var,global_b,global_1 }'
5 10 12
6 10 12

В этом примере два обычных блока имеют по своему экземпляру переменной с именем var. В этом примере мы из простых блоков обратились к глобальным переменным global_b и global_1, объявленным в блоке BEGIN. Обратите внимание, что изменение глобальной переменной в некотором блоке (global_1) приведет к ее изменению в оставшихся блоках, которые как то к ней обращаются.

Блоки можно вкладывать. Вложенные блоки главным образом используются для объединения инструкций, относящихся к конструкциям проверки условий if..else или циклам. В отличие от языка Си, вложенные блоки не создают еще одну область видимости. Например, сравните

$ echo -n "a" | awk '{ var=1; { var=2; print var } print var }'
2
2

Диапазоны в блоках править

Как и в Sed к блоку вместо простого шаблона можно применить диапазон. Напомним, что диапазон позволяет описать не одну строку, а сразу несколько идущих подряд. Общий синтаксис в этом случае такой

/шаблон-начала/,/шаблон-конца/{ <команды блока> }

/шаблон-начала/,/шаблон-конца/ # В этом случае подразумевается блок { print }

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

Рассмотрим такой пример

$ echo -n "1 2 3 4 5" | awk 'BEGIN{RS=" "} /2/,/4/{ print }'
2
3
4

В Awk нет варианта объявления диапазона через порядковые номера фрагментов, как это можно делать в Sed. Вероятно так сделано из-за другого подхода к чтению входящего потока, либо потому, что все потребности можно покрыть регулярными выражениями.

Операции править

  • Общие
    • Сложение +
    • Унарный плюс +
    • Конкатенация строк ("строка" "строка")
    • Вычитание -
    • Унарное отрицание -
    • Умножение *
    • Возведение в степень ^ или ** (GAWK) (по POSIX только ^)
    • Деление /
    • Остаток от деления %
    • Префиксный/постфиксный инкремент ++
    • Префиксный/постфиксный декремент --
    • Группировка для изменения приоритетов операторов ()
  • Присваивание
    • Простое присваивание =
    • Присваивание с операцией: += -= *= /= %= ^= **= (GAWK)
  • Логические (используются в условных конструкциях)
    • Больше > и больше либо равно >=
    • Меньше < и меньше либо равно <=
    • Равно ==
    • Не равно !=
    • Логическое ИЛИ ||
    • Логическое И &&
    • Логическое НЕ !
  • Операции сравнения с шаблоном (используются в регулярных выражениях)
    • Совпадает ~
    • Не совпадает !~
  • Операции управления обработкой фрагментов и файлов
    • next
      Оператор принуждает Awk прекратить обработку текущего фрагмента и перейти к следующему. Это означает, что все команды, следующие за опреатором, будут пропущены для текущего фрагмента. По POSIX использование этого оператора в блоках BEGIN и END приводит к неопределенному поведению. Многие реализации запрещают использование этого оператора в пользовательских функциях. В GAWK такой проблемы нет. Если вызов оператора приводит к концу ввода, то происходит исполнение блока END.
    • nextfile
      Если Awk передается на обработку несколько файлов, то данный оператор принуждает прекратить обработку текущего и перейти к следующему. Если вызов оператора приводит к концу ввода, то происходит исполнение блока END. Этот оператор является расширением GAWK и скорее всего будет отсутствовать в других реализациях. Обратите внимание, что до GAWK 3.0 этот оператор писался в два слова (next file), а после он стал писаться в одно слово. Такой вызов еще поддерживается, однако GAWK будет выводить предупреждение, и возможно в будущем такой вызов будет вообще запрещен.
    • exit
      Оператор требует немедленно прекратить выполнение и выйти из программы, при этом будет исполнен блок END, если он есть. Если он вызывается в блоке BEGIN, то весь последующий код будет пропущен, однако, если есть блок END, то он выполнится. Вызов в блоке END требует немедленное прекращение программы. У данного оператора есть аргумент в виде возвращаемого кода возврата, в противном случае будет возвращаться нулевой код. Ненулевой код иногда используется, чтобы пропускать блок END, где он может быть проверен.

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

x = y = z = 5

В этом примере по цепочке справа налево будет присвоено значение 5 всем трем переменным. Присваивание допустимо в любой части r-value выражения, например так

x != (y = 1)     # сначала y будет присвоена 1, а затем выполнится логическая операция

Однако такие записи впоследствии очень сложно читать, поэтому такой прием не нужно брать за правило или использовать в больших программах.

В Awk нет специальных булевых типов и здесь все заимствуется у Си. Так, условно ИСТИНОЙ считается ненулевое значение числа и не пустая строка, иначе значение считается ЛОЖЬЮ.

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

  1. (), $<поле>
  2. ++, --
  3. ^, (**)
  4. унарный +, унарный -, логическое НЕ
  5. *, /, %
  6. +, -

Условные выражения править

В Awk нет специального условного типа данных, т.е. условные операторы работают с имеющимися типами данных напрямую. ИСТИНОЙ считается любое ненулевое число и любая не пустая строка, соответственно все остальное это ЛОЖЬ.

Логические выражения, построенные на логических операторах, возвращают 0, если результатом является ЛОЖЬ, и 1 — если ИСТИНА. Обычно логические выражения используются в конструкциях if..else, циклах и в условиях перед обычными блоками.

Ниже представлены примеры некоторых логических выражений:

BEGIN {
        print "1:" (5 > 2)                     # 1:1    ИСТИНА
        print "2:" (2 == 3)                    # 2:0    ЛОЖЬ
        print "3:" (1 < 2 && 2 < 5)            # 3:1    ИСТИНА
        print "4:" (2 + 2 == 4)                # 4:1    ИСТИНА
        print "5:" (1 || 0)                    # 5:1    ИСТИНА  (дизъюнкция)
        print "6:" (123 ~ /[[:digit:]]+/)      # 6:1    ИСТИНА  (потому что строка соответствует 
                                               #                  регулярному выражению)
        print "7:" ("abc" !~ /[[:digit:]]+/)   # 7:1    ИСТИНА  (потому что строка действительно
                                               #                     не записана только цифрами)
        print "8:" (!"abc")                    # 8:0    ЛОЖЬ    (отрицание истины)
        print "9:" (!4)                        # 9:0    ЛОЖЬ    (отрицание истины)
        print "10:" (!-4)                      # 10:0   ЛОЖЬ    (отрицание истины)
        print "11:" ("apple" in array)         # 11:0   ЛОЖЬ    (такого индекса нет в массиве)
}

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

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

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

Awk использует диалект регулярных выражений, похожий на ERE. Однако в GAWK этот диалект немного расширен за счет классов литералов, очень похожих на те что есть в диалекте Perl.

Ниже перечислены все метасимволы традиционных регулярных выражений

\ ^ $ . [] [^...] | (...)

\x## (запись символа в шестнадцатеричном коде)

# Квантификаторы
* + ?
{n}
{n,}
{n,m}

# Классы
[:alnum:]    Алфавитно-цифровой
[:alpha:]    Буква
[:blank:]    Пробел и табуляция
[:cntrl:]    Управляющая последовательность
[:digit:]    Цифра
[:graph:]    Символы, которые могут быть выведены и видимы
[:lower:]    Буква в нижнем регистре
[:print:]    Печатаемые символы (не управляющие)
[:punct:]    Символы пунктуации
[:space:]    Пробельный символ (пробел, табулятор, вертикальный табулятор,
             новая строка, разрыв страницы, возврат каретки)
[:upper:]    Буква в верхнем регистре
[:xdigit:]   Цифра в шестнадцатеричной системе

Следующие метасимволы являются расширением GAWK.

\s \S \w \W 
\< \> \y \B
\` \'

Общие сведения:

  • Пустое регулярное выражение // соответствует пустой строке, в которой начало есть конец.
  • Константные регулярные выражения следует использовать, когда используются операторы ~ и !~.
  • В GAWK есть встроенная переменная IGNORECASE, установив которую в не ноль, можно игнорировать регистр в проверяемых по регулярным выражениям строках. В старых Awk этого не было: обычно в этом случае строку прогоняли сначала через функцию tolower() или toupper(), чтобы привести регистр.

Управление печатью Awk править

Для печати в Awk предусмотрено два оператора: print и printf. Оператор print является упрощенной версией printf и производит печать в стандартном формате. Для печати с помощью print вам нужно указать только что печатать. В простом случае его вызов выглядит так

print <элемент 1>, <элемент 2> ..., <элемент N>

где элементами могут быть строковые литералы, переменные, поля, в общем все, что может быть сконвертировано в строку. В общем случае print не требует аргументов: тогда он просто напечатает поле $0.

Ниже приведены примеры вызова оператора print.

print "Hello World!" # Печать литерала
# Hello World!

# Литералы можно передать через пробелы или через запятую:
# тогда будет напечатана их конкатенация
print "Hello" "World!"
# HelloWorld!

# Обратите внимание, что пробел между литералами не является частью строки.
# Более корректнее было бы вызвать так
print "Hello", "World!"
# Hello World!

# Печать конкатенации литерала и значения переменной
print "NF=" NF
# NF=1

# Если переменные разделены пробелом, то они будут сконкатенированы после преобразования
print NF NF
# 11

# При использовании запятой между печатаемыми элементами будет 
# вставляться разделитель, записанный в OFS. По умолчанию это пробел.
print NF,NF
# 1 1

# Если внутри печатаемого литерала встречаются управляющие последовательности,
# то они будут корректно переданы в поток вывода
print "\tHello\n\t\tWorld!"
#        Hello
#                World!

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

Мы уже упомянули о разделителе OFS, который вставляется между элементами печати. Во время вывода используется еще один разделитель ORS (Output record separator), который вставляется в конец всего, что напечаталось в одном вызове print. По умолчанию там хранится символ новой строки, именно поэтому каждая печать начинается с новой строки.

$ echo "Hello World" | awk -F: 'BEGIN{ORS="!\n"}{print}'  # Печатаем в конце строки '!'
# Hello World!

Хотя в print не указывается форматная строка, все же форматом можно управлять через встроенную переменную OFMT. Она используется во время конвертации числа в строковую переменную и по умолчанию имеет значение %.6g. Используя правила объявления формата для стандартной функции библиотеки Си sprintf, вы можете менять точность выводимых вещественных чисел, например

$ echo "2.456865484123147" | awk 'BEGIN{OFMT="%.2f"}{print $1+0}'
2.46

Обратите внимание, что в предыдущем примере мы складываем входящее поле с нулем, чтобы принудить Awk сконвертировать входящую строку в число, а затем снова в строку во время печати. Без этого к сожалению ничего не получится.

Форматированная печать с помощью printf править

В сложных ситуациях может понадобится форматированный вывод, в котором все аспекты контролируются так называемой форматной строкой. Для тех, кто когда-либо писал программы на Си и использовал стандартную библиотеку ввода/вывода, правила написания форматной строки будут знакомы.

Общий вызов printf выглядит так

printf "<форматная строка>", <элемент 1> ..., <элемент N>

Форматная строка объясняет printf структуру выводимой строки с помощью специальных символов, называемых форматами, которые впоследствии заменяются на передаваемые элементы. Каждому формату сопоставляется ровно один элемент слева направо со второго аргумента.

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

  • %c (char) – единичный символ.
  • %s (string) – используется для строк.
  • %d или %i (decimal) – знаковое десятичное целое число.
  • %u (unsigned decimal) – беззнаковое целое десятичное число. Беззнаковое означает, что в двоичном представлении числа старший бит не тратится на хранение знака, благодаря чему удваивается диапазон представления положительных чисел.
  • %o (octal) – число в восьмеричной системе счисления. Для этого формата, кроме конвертации, будет подписываться ведущий ноль.
  • %x или %X (hexdecimal) – число в щестнадцатеричной системе счисления, причем для %X символы A-F выводятся в верхнем регистре. Для этого формата, кроме конвертации, будет подписываться префикс 0x.
  • %e или %E (exponential) – вещественное число будет представлено в экспоненциальной форме, причем для %E символ экспоненты будет записан в верхнем регистре.
  • %f или %g или %G (float) – число с плавающей запятой. Формат %g позволяет Awk выбрать для числа наиболее компактную запись между %f или %e. Вариант %G соответственно делает выбор между %f и %E.

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

  • Модификатор +. Используется в числовых форматах. Требует всегда печатать знак + для положительного числа.
  • Модификатор -. Используется в строковых форматах, чтобы выравнивать текст в выводимом поле по левому краю. По умолчанию текст выводится по правому краю.
  • Пробел (например % d). Используется в числовых форматах, чтобы выделить знакоместо под знак числа. Если число положительное или равняется нулю, то вместо знака будет вставлен пробел, а если отрицательное – знак минус. Используется, когда требуется тонкое выравнивание в колонке.
  • Ведущий ноль (например %0d). Используется в числовых форматах, чтобы заполнить выводимое поле ведущими нулями.
  • Ширина поля (например %8d). На самом деле каждый конкретный формат выводится в невидимом поле, размер которого по умолчанию подстраивается под его содержимое. Если вам нужно чтобы формат тратил фиксированное число знакомест, используйте ширину формата. Отметим, что ширина формата задает минимальный размер поля, т.е. если содержимое не будет влезать, то в любом случае будут задействованы дополнительные знакоместа.
  • Точность (например %.6f). Используется для форматов вещественных и целых чисел. Для форматов %e, %E и %f точность означает сколько десятичных разрядов нужно выводить после десятичной точки. Для форматов %g и %G точность означает максимальное число значащих цифр. Для форматов целых чисел точность означает минимальное количество печатаемых символов. Точность может использоваться для строкового формата: в этом случае она работает как жесткий ограничитель на количество выводимых символов, задавая максимальный размер поля.

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

# Пример вывода данных таблицей
$ seq 3 | awk 'BEGIN{printf "%4.4s %10.10s%6.6s %-8.8s\n","ID","Name"," ","Phone"; print "----","----------------","--------"}{printf "%.4u %16.16s %8.8s\n",12,"John Doe","555-3201235"}'

  ID       Name       Phone
---- ---------------- --------
0012         John Doe 555-3201
0012         John Doe 555-3201
0012         John Doe 555-3201

Обратите внимание, что в заголовке мы выделили 3 колонки, где имя колонки выравнено по правому краю, средняя – по центу и правая – по левому краю. Среднюю колонку мы выравнивали за счет двух форматов, в первый из которых помещается заголовок, а во второй помещается пробел. Далее мы выводим символы подчеркивания без явного форматирования. Далее мы 3 раза выводим одни и те же данные, где поле первой колонки выводится как беззнаковое целое на 4 знакоместа максимум, второе поле на 16 знакомест и третье поле на 8 знакомест. Обратите внимание, что телефон на самом деле имеет больше 8 символов, однако поле жестко ограничено по знакоместам, поэтому три последних символа обрезаются. Также обратите внимание, что мы специально указываем минимальное число знакомест (число до точки), чтобы поле заполнилось пробелами или, в случае беззнакового целого, нулями.

# Выводим числа в разных форматах
$ awk '{printf "%+04d|% 3.3g|%.4o|%.4X\n",$0+0,$0+0,$0+0,$0+0}' <<EOF
26
-4
3.1415
-63.1257
EOF
+026| 26|0032|001A
-004| -4|1777777777777777777774|FFFFFFFFFFFFFFFC
+003| 3.14|0003|0003
-063|-63.1|1777777777777777777701|FFFFFFFFFFFFFFC1

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

Динамическая ширина поля править

Модификатор ширины поля может быть вынесен за форматную строку, и в таком случае ширину поля можно определять через числовую переменную. Этот прием может использоваться со всеми типами данных, но чаще всего его используют со строками.

Чтобы вынести ширину поля за форматную строку, нужно в позиции модификатора просто поставить символ звездочки *, а ширину поля передавать дополнительным аргументом (перед данными). Все остальное остается неизменным. Ниже приведено несколько примеров.

$ awk 'BEGIN{printf "|%*s|\n",15,"right"; printf "|%-*s|\n",15,"left"}'
|          right|
|left           |

# Поле может быть сколь угодно широким и при этом строка может быть жестко ограничена
$ awk 'BEGIN{printf "|%*.5s|\n",30,"very long string"}'
|                         very |

# Ширина может использоваться с любыми типами данных
$ awk 'BEGIN{printf "|%*.6G|\n",10,3.1415926535897932384626433832795}'
|   3.14159|

# Форматная строка может быть сложной. Если при этом ширина поля выносится, то
# перед каждыми выводимыми данными должна задаваться ширина поля.
# Ниже мы выводим три поля с вынесенной шириной.
$ awk 'BEGIN{printf "|%*.4u|%-*s|%*s|\n",10,1,15,"John",30,"Comment"}'
|      0001|John           |                       Comment|

# Подходы к формированию ширины поля можно смешивать в рамках одного вызова.
$ awk 'BEGIN{printf "|%.4u|%-s|%*s|\n",1,"John",30,"Comment"}'
|0001|John|                       Comment|

Перенаправление печати править

По умолчанию print и printf выводят данные в STDOUT. Тем не менее, вы можете перенаправлять их вывод в отдельные файлы, используя синтаксис близкий к перенаправлению потоков в командной оболочке:

  • > "<путь-к-файлу>"
    Перенаправляет поток вывода в файл, при этом если файл не пустой, то он будет перезаписан.
  • >> "<путь-к-файлу>"
    Перенаправляет поток вывода в файл с режимом на дозапись, если файл не пустой.
# Запишет 3 цифры в файл test.txt. Если файла нет, то он будет создан.
$ seq 3 | awk '{print >> "test.txt"}'

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

Кроме простого перенаправления, данные можно посылать другой команде через конвейер прямо из Awk. При этом конвейерный pipe-файл будет открыт из Awk.

Следующий пример демонстрирует использование конвейера

$ echo {Z..A} | awk '{cmd="rev"; print | cmd; close(cmd);}'
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Мы передаем Awk латинский алфавит, записанный в обратном порядке. Далее мы инициализируем переменную cmd командой rev, которая отзеркаливает переданную ей строку, в результате чего мы получаем алфавит, записанный в прямом порядке. Обратите внимание, что мы вынуждены передавать команду через переменную, так как внутренне Awk открывает дескриптор на pipe-файл, используя команду как идентификатор. Этот pipe остается открытым на протяжении всей работы Awk, поэтому хорошим тонном является его закрытие сразу после его использования. Обратите внимание, что команда на правом конце конвейера будет работать до закрытия pipe-файла (т.е. до закрытия Awk или до явного закрытия pipe-файла). Также следует помнить, что если бы у нас было несколько строк, то pipe-файл открывался бы каждый раз заново для каждой новой строки из-за close(). Если мы опустим close(), то каждая поступившая строка будет передана rev через один и тот же pipe-файл. Многое может зависеть от программы на правом конце конвейера, поэтому используя эту технику, вы должны представлять, что вы делаете.

Перенаправления тоже открывают дескрипторы на файлы и их необходимо освобождать, если Awk программа исполняется сравнительно долго. Для закрытия дескрипторов необходимо использовать опять же close().

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

$ seq 3 | awk '{print >> "test.txt"; close("test.txt")}'

Функция close() возвращает 0, если дескриптор успешно закрыт, иначе она возвращает не ноль и записывает в переменную ERRNO строку с описанием ошибки.

Функция sprintf() править

Операторы print и printf могут выводить строки только в файл и не могут являться частью r-value выражения. Было бы здорово, если бы формируемые строки с помощью формата можно было бы присваивать переменным. Именно для этого используется функция sprintf(), синтаксис которой имеет вид

sprintf(<форматная-строка>, <элемент 1>, ...)

Вызов этой функции возвращает строку в соответствии с форматом, при этом абсолютно все правила, описанные нами ранее для printf, справедливы и для этой функции. Ниже приведено несколько примеров.

$ awk 'BEGIN{pi = 3.14159; s = sprintf("%010.3f", pi); print s}'
000003.142

$ awk 'BEGIN{message="Hello, World!"; out = sprintf("[%s]: %s", "INFO", message); print out}'
[INFO]: Hello, World!

Ветвления править

if (<условное выражение>)
	<ветвь 1>
else
	<ветвь 2>

Если условное выражение ИСТИНА, то исполняются инструкция в ветви 1, иначе исполняется инструкция ветви 2. В общем случае ветвь else не обязательна. Чтобы поместить в какую-либо ветвь больше одной инструкции, их следует поместить в фигурные скобки (т.е. в блок).

Ветвей может быть больше двух, если ветвления вкладывать по аналогии с языком Си

if (<условное выражение 1>) {
	<ветвь 1>
} else if (<условное выражение 2>) {
	<ветвь 2>
} 
...
else {
	<ветвь N>
}

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

if (<условное выражение 1>) <инструкция>; else <инструкция>

Упомянем также наличие тернарной конструкции

<условие> ? <ветвь 1> : <ветвь 2>

Тернарная конструкция обычно используется для компактного присваивания значения переменной, когда это значение зависит от условия

num = $0 >= 0 ? $0 : 0

Циклы править

В Awk есть три типа циклов:

  • for
  • while
  • do-while

а также два управляющих для циклов слова:

  • break
  • continue

Цикл for править

Цикл for строится так

for (<блок инициализации>; <блок условия повторения>; <блок инкремента>)
	<тело цикла>

Цикл for повторяет инструкции тела до тех пор, пока условие блока повторения остается истинным. В блоке инициализации обычно определяются счетчики цикла, которые участвуют в условии. Блок инициализации выполняется один раз перед входом в цикл.

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

Вообще в этом цикле все блоки являются необязательными: в этом случае цикл становится бесконечным.

for (;;) ...

Вы также можете указать только условие

for (;i != 100;) ...

однако такие циклы лучше записывать через while.

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

BEGIN{
    for (i = 2; i <= 9; i++) {
       for (j = 2; j <= 9; j++) {
            printf "%2d x %1d = %2d ", i, j, (i * j)
       }
       print ""
    }
}

Вывод будет следующий

$ awk -f for.awk
 2 x 2 =  4  2 x 3 =  6  2 x 4 =  8  2 x 5 = 10  2 x 6 = 12  2 x 7 = 14  2 x 8 = 16  2 x 9 = 18
 3 x 2 =  6  3 x 3 =  9  3 x 4 = 12  3 x 5 = 15  3 x 6 = 18  3 x 7 = 21  3 x 8 = 24  3 x 9 = 27
 4 x 2 =  8  4 x 3 = 12  4 x 4 = 16  4 x 5 = 20  4 x 6 = 24  4 x 7 = 28  4 x 8 = 32  4 x 9 = 36
 5 x 2 = 10  5 x 3 = 15  5 x 4 = 20  5 x 5 = 25  5 x 6 = 30  5 x 7 = 35  5 x 8 = 40  5 x 9 = 45
 6 x 2 = 12  6 x 3 = 18  6 x 4 = 24  6 x 5 = 30  6 x 6 = 36  6 x 7 = 42  6 x 8 = 48  6 x 9 = 54
 7 x 2 = 14  7 x 3 = 21  7 x 4 = 28  7 x 5 = 35  7 x 6 = 42  7 x 7 = 49  7 x 8 = 56  7 x 9 = 63
 8 x 2 = 16  8 x 3 = 24  8 x 4 = 32  8 x 5 = 40  8 x 6 = 48  8 x 7 = 56  8 x 8 = 64  8 x 9 = 72
 9 x 2 = 18  9 x 3 = 27  9 x 4 = 36  9 x 5 = 45  9 x 6 = 54  9 x 7 = 63  9 x 8 = 72  9 x 9 = 81

Существует форма цикла for, удобная для перебора массивов

for (i in arr)
	print arr[i]

Такие циклы сами контролируют выход индекса за пределы массива.

Цикл while править

Цикл while строится так

while (<условие>)
	<тело цикла>

Цикл while исполняется до тех пор, пока условие ИСТИННО. Условие проверяется на входе каждой новой итерации.

Цикл do-while работает похоже, но условие проверяется не в начале входа в цикл, а в конце. Таким образом, цикл всегда выполняется минимум один раз.

do 
	<тело цикла>
while (<условие>)

Управляющие операторы циклов править

Управляющий оператор break позволяет прервать цикл на текущем шаге итерации и переместить точку следования на следующую команду за циклом. Оператор continue прерывает исполнение итерации цикла в текущей точке и перемещает точку следования в начало новой итерации, тем самым пропуская оставшиеся команды.

Хотя использование этих операторов вне циклов не имеет смысла, исторически такой вызов трактовался как оператор next. Стандарт POSIX требует, чтобы эти операторы использовались только в циклах, поэтому последние версии Awk будут генерировать синтаксическую ошибку. В GAWK можно включить историческое поведение, если вызывать команду с опцией --traditional.

Пользовательские функции править

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

Синтаксис определения выглядит так

function <имя функции>(<список аргументов>)
	<тело функции>

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

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

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

Внутри функции можно вызывать другие функции. Также можно вызывать текущую функцию рекурсивно. Во многих реализациях Awk (в том числе и в GAWK) ключевое слово function может быть сокращено до func. Однако в POSIX определена только function. Использование func в POSIX-совместимом режиме может приводить к неожиданным ошибкам, поэтому использование func в целом не рекомендуется.

Некоторые реализации Awk разрешают вызов неопределенной функции, если фактического вызова не происходит. В GAWK можно разрешить вывод предупреждений о вызовах неопределенных функций, если вызывать команду с опцией --lint.

В некоторых реализациях Awk запрещено использовать оператор next внутри функций. В GAWK такой проблемы нет.

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

function func1(arg1, arg2) {
        print arg1,arg2
}
function func2() {
        return 0
}
{
        # Аргументы для любой функции не обязательны
        func1()
        func1("Hello")
        func1("Hello ", "World!")
        
        # Возвращаемые результаты обычно как то анализируются
        if (func2()) {
                print "Do something"
        } else {
                print "Do nothing"
        }
        
        print func3()
}
# Функция может объявляться после первого обращения к ней
function func3() {
        # Функция без проблем может возвращать строки, как результат
        return "Hello!"
}

Встроенные функции править

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

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

Функции для работы с числами править

int(x)
Возвращает ближайшее целое к x число между x и нулем, округленное в сторону нуля.
exp(x)
Возвращает экспоненту в степени x, либо ошибку, если результат выходит за допустимые пределы.
log(x)
Возвращает натуральный логарифм x, либо ошибку, если x отрицательная.
sin(x)
Возвращает синус x в радианах.
cos(x)
Возвращает косинус x в радианах.
atan2(x, y)
Возвращает арктангенс от y/x в радианах.
rand()
Возвращает случайное число равномерно распределенное между 0 и 1. Если нужно генерировать случайные целые числа, можно использовать такую пользовательскую функцию
function randint(n)
return int(n * rand())
srand([x])
Инициализирует генератор псевдослучайных чисел начальным числом x. Если опустить аргумент, то функция будет использовать текущие дату и время.

Функции для работы со строками править

index(in, find)
Ищет в строке in первое вхождение find и возвращает порядковый номер первого символа этого вхождения в строке in. Если ничего найти не удается, то возвращается 0. Заметим, что символы в Awk в строке отсчитываются с единицы.
length([string])
Возвращает число символов в строке string. Если вместо string будет передано число, то оно будет преобразовано в строку и далее будет возвращаться число символов в этой строке. Если аргумент опустить, то в качестве аргумента по умолчанию используется поле $0. В некоторых реализациях эта функция может быть вызвана вообще без скобок, однако такой возможностью никогда не нужно пользоваться.
match(string, regexp)
Функция ищет в строке string самую первую и самую длинную подстроку, удовлетворяющую регулярному выражению regexp. Функция возвращает ноль, если ничего найти не удалось, либо номер символа начала вхождения, начиная с единицы. К номеру символа начала вхождения также можно обратиться через переменную RSTART. Во встроенную переменную RLENGTH будет записана длина вхождения.
split(string, array [, fieldsep])
Разбивает строку string на части по разделителю fieldsep и помещает эти части в массив array. Если разделитель fieldsep опущен, то будет использоваться значение переменной FS. Функция возвращает число записанных частей, причем части размещаются в массиве, начиная с индекса 1. В GAWK в качестве fieldsep можно передавать пустую строку: в этом случае вся строка будет разбита на отдельные символы. Если split не может разбить исходную строку по разделителю, то в первый элемент массива будет записана вся входящая строка.
sub(regexp, replacement [, target])
Функция заменяет в строке target первое самое длинное вхождение по регулярному выражению regexp на значение replacement. Полученный результат заменит исходное значение target. Если target опущен, то по умолчанию используется поле $0. Функция возвращает число подстановок. Если вместо replacement использовать &, то в качестве замены будет использоваться подстрока, раскрываемая регулярным выражением regexp. Аргумент target может быть переменной, полем или ссылкой на элемент массива. Некоторые реализации допускают использовать выражение, которое не является l-value, но результат подстановки в таком случае будет отбрасываться. Такой возможностью лучше не пользоваться, так как такой вызов может приводить к фатальным ошибкам. Если regexp не является константой регулярного выражения, а является строкой, то это строка будет приведена к константе регулярного выражения.
gsub(regexp, regexp [, target])
Работает как sub, но заменяет все вхождения, а не только самое первое.
gensub(regexp, replacement, how [, target])
Функция является расширением GAWK, и ее нельзя использовать в режиме совместимости. Работает как sub и gsub, но оригинальная строка target не меняется, а результирующая строка возвращается самой функцией. Кроме того, вы можете указать через how сколько замен нужно сделать в виде числа, либо можете передать строку, первый символ которой должен быть g или G, который указывает, что нужно заменить всё. Также именно в этой версии функции можно использовать ссылки на группы в регулярном выражении в качестве replacement через простые номера. Если ни одной замены не удалось сделать, то возвращается оригинальная строка. Если в how передается отрицательное число, то функция будет интерпретировать его как 1.
substr(string, start [, length])
Возвращает подстроку строки string начиная с символа start и длиной length. Начальный символ следует отсчитывать с единицы. Если length опущена, то функция будет это трактовать как до конца строки. Следует помнить, что возвращаемый функцией результат нельзя использовать как l-value или использовать как третий аргумента функций семейства sub.
tolower(string)
Переводит все символы string в нижний регистр и возвращает результат как копию.
toupper(string)
Переводит все символы string в верхний регистр и возвращает результат как копию.

Функции для работы с вводом/выводом править

close(filename)
Закрывает файл filename.
fflush([filename])
Отключить буферизацию для вывода в файл filename. Если аргумент опущен или передана пустая строка, то требует сбросить все буферы Awk на диск. Данная функция не объявлена в POSIX и не доступна в режиме --posix. Функция возвращает 0, если сброс удался, или не ноль в противном случае.
system(command)
Позволяет вызывать команду command операционной системы из Awk-программы. Возвращает код, который был возвращен вызванной командой. Если операционная система не способна реализовать системный вызов, то данная функция вернет фатальную ошибку. Можно использовать system() другим интересным способом, а именно, если ей передать пустую строку, то побочный эффект будет аналогичен вызову fflush(). Это полезно, когда последняя не поддерживается.

Функции для работы с датой и временем править

systime()
Возвращает текущее системное время в виде секунд от полуночи 1 января 1970 (UTC).
strftime([format [, timestamp]])
Возвращает строку с датой и временем в формате format. Синтаксис формата аналогичен правилам, описанным для одноименной функции ANSI Си. Если в timestamp передается время, то оно будет сконвертировано в формат format. Если аргументы опущены, то возвращается текущее время в формате "%a %b %d %H:%M:%S %Z %Y". Вообще говоря, аргументы для этой функции в GAWK необязательными стали с версии 3.0.

Команда getline править

Рядовой пользователь обычно не вмешивается в процедуру чтения фрагментов. Тем не менее, в Awk заложена встроенная команда getline, позволяющая явно управлять чтением входящего текста. Эта команда не должна использоваться новичками, плохо понимающими процедуру чтения текста, а также вы всегда должны представлять, что вы делаете.

Команда getline обычно используется, когда текст имеет логическую структуру и простого разбиения его на фрагменты недостаточно.

Команде нужно передать одну или несколько переменных, в которые она попытается записать следующий по отношению к текущему фрагмент. Если ей это удается, то она возвращает не ноль; если ничего не прочитано — 0; и возвращает -1 в случае ошибки и инициализирует ERRNO сообщением об ошибке. Команда может вызываться и без аргументов, тогда новый фрагмент следует брать из поля $0.

Понять, когда getline может использоваться, легче всего на конкретных примерах.

Следующая Awk программа позволяет удалять многострочные Си комментарии из текста.

# Файл: rem_comms.awk
{
    while ((start = index($0, "/*")) != 0) {  # Ищем начало многострочного комментария
        out = substr($0, 1, start - 1)        # Вырезаем подстроку до начала комментария
        rest = substr($0, start + 2)          # Вырезаем подстроку после начала комментария
        # Пытаемся найти конец комментария в оставшемся тексте в цикле
        while ((end = index(rest, "*/")) == 0) { 
            # Здесь getline используется для проверки ошибки, когда комментарий забыли закрыть
            if (getline <= 0) {
                print("unexpected EOF or error:", ERRNO) > "/dev/stderr"
                exit
            }
            rest = rest $0 # Собираем текст внутри комментария
        }
        rest = substr(rest, end + 2)  # Вычисляем позицию за символами */
        $0 = out rest                 # Наконец собираем исходный текст, но без комментария
    }
    print $0                          # Печатаем результат
}

Пусть у нас есть такой файл с исходным кодом на языке Си:

/* File: main.c
 * Sample of C-program
 *
 */

int main(/* Argument counter */ int argc, /* Pointer to an array with input arguments */ char** argv) {
        return 2+2;  /* Return result  */
}

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

$ awk -f rem_comms.awk main.c


int main( int argc,  char** argv) {
        return 2+2;
}

Обратите внимание, что вызов getline неявно обновляет $0, NF, NR, FNR и RT, т.е. в этом блоке оригинальное значение $0 будет потеряно.

Следующая программа не имеет конкретной цели, а просто демонстрирует что будет, если передать getline аргумент.

# Файл: program.awk
{
	if ((getline nline) > 0) { # Читаем пока текст не кончится
		print nline           # Печатаем то, что попало в переменную
		print $0              # Обратите внимание, что оригинальную строку мы не теряем
	} else
		print $0
}

Результат работы

$ awk -f program.awk <<EOF
1
2
3
4
EOF
2
1
4
3

Еще раз обратим внимание, что при передаче getline аргумента, неявно обновятся только NR, FNR и RT, но не $0.

Команде getline можно передавать файл через поток ввода. Это позволяет обойти главный поток ввода Awk, чтобы, например, читать два источника за один раз.

getline < "<путь-к-файлу>"

При таком чтении переменные NR и FNR не обновляются, так как они привязаны к главному потоку ввода Awk, но $0, NF и RT будут обновляться в обычной манере.

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

getline < (wdir "/" filepath)

Соответственно комбинировать вариант перенаправления с передачей переменной также можно

getline line < "file.txt"

Команда getline можно принимать данные также по конвейеру, например

while ((cmd | getline) > 0) { ...; } close(cmd)

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

{
	while ( (("echo " "hello") | getline ) > 0 ) {
		print
	}
	close("echo hello")
}

Используя предыдущий подход, можно запрашивать из Awk-программы данные у различных системных утилит и записывать результаты в переменные, например

BEGIN {
	"date" | getline cur_time
	close("date")
	print "Today is " cur_time
}

Документация по реализации getline в GAWK рекомендует помнить о следующих моментах, связанных с этой командой:

  • Когда getline правит $0 и все, что с ней связано, не происходит автоматического перехода в начало Awk-программы с чтением нового фрагмента. Вы это должны контролировать самостоятельно с помощью оператора next.
  • Старые реализации Awk не разрешают за один раз использовать больше одного pipe-файла. В GAWK такой проблемы нет, и все зависит от ограничений самой системы.
  • Вызов getline без перенаправления не рекомендуется использовать в блоках BEGIN, так как в этом случае входящий поток формально еще не готов для чтения.
  • Не следует вызывать getline как getline < FILENAME, так как в этом случае возникает гонка за один и тот же файл из разных потоков.
  • Следует осторожно использовать getline, если ей передается аргумент выражением, в котором есть побочные эффекты. Положим такую ситуацию
    while ((getline a[++c] < "file") > 0) { }
    
    В этом выражении побочный эффект связан с наращиванием индекса массива. В данном случае выполнится ли инкремент, если getline нечего читать? Например если входящий файл будет состоять из одной строчки, оканчивающейся символом новой строки, то в GAWK результатом будет 2, т.е. инкремент до проверки на EOF выполнится. Это происходит потому, что GAWK вызывает getline как функцию. Тем не менее, старые реализации могут скрывать то, что стоит за getline, и не делать вызов, если читать нечего, т.е. результат 1 возможен.

Расширения GAWK править

Реализация GAWK очень продвинута по сравнению с первыми реализациями Awk. В этой главе мы попытаемся раскрыть некоторые крайне полезные расширения реализации GAWK.

Включение других файлов в программу править

Эта особенность используется, чтобы выносить повторяющиеся и редко изменяющиеся типовые части программы в отдельные файлы. Такие файлы можно собирать в библиотеки и повторно использовать в новых Awk программах. На практике обычно имеет смысл выносить только блоки BEGIN и END, однако это только рекомендация.

Чтобы включить один файл Awk в другой файл нужно использовать встроенную директиву

@include "<путь-к-файлу>"

Директива похожа на макрос

#include

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

Кроме этой директивы, можно воспользоваться опцией -i в самом вызове GAWK. Имя файла всегда должно быть литералом, т.е. переменные с директивой @include использовать нельзя.

Если путь к файлу не является абсолютным, то GAWK использует переменную окружения AWKPATH для поиска файлов, которая по умолчанию пустая. В простом случае Awk просматривает текущую рабочую директорию.

Пусть у нас есть два файла

# Файл: lib.awk
# Библиотечный файл Awk
BEGIN {
	print "Library"
}

Во второй файл мы включим библиотечный

# Файл: program.awk
@include "lib.awk"

BEGIN {
	print "Main"
}

Теперь вызовем основную программу

$ gawk -f program.awk
Library
Main

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

Пусть у нас есть такие файлы:

# Файл: lib_1.awk
# Библиотечный файл Awk
BEGIN {
	print "Library 1"
}
# Файл: program_1.awk
BEGIN {
	print "Main"
}

Тогда мы могли бы вызвать их комбинацию так

$ gawk -f program_1.awk -f lib_1.awk
Main
Library 1

Обратите внимание, что в обоих методах четко прослеживается принцип порядка включения. Если при использовании @include порядок определяется статически, то с опцией -f порядок может зависеть от вызова.

Пространства имен править

По умолчанию все объекты Awk-программы сосуществуют в одном единственном пространстве имен, или другими словами, имена этих объектов должны быть уникальными для их совместного сосуществования. Хотя такое встречается редко в Awk, на практике вам может понадобиться использовать разные реализации функции с одним и тем же именем, либо отделить одноименные глобальные переменные, приходящие из разных библиотечных файлов. Вы можете переименовать совпадающие по именам объекты, либо воспользоваться пространствами имен, чем-то похожими на пространства имен C++. Пространства имен появились в GAWK, начиная с 5-й версии.

Для объявления пространства имен служит директива

@namespace "<имя-пространства>"

Чтобы обратиться к объекту некоторого пространства имен, используется С++-подобный синтаксис:

<имя-пространства>::<имя-объекта>

В данном случае объектами могут быть глобальные переменные и функции. В GAWK по умолчанию уже создано одно пространство имен с именем awk, которое всегда подразумевается, если в вызове пространство имен не указано.

Пространства имен в GAWK плоские, т.е. их нельзя вкладывать, как это можно делать в C++, да и это в GAWK по большому счету и не нужно.

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

Пространства имен не влияют на порядок выполнения блоков.

Давайте рассмотрим принцип размещения объектов в пространствах имен на следующем примере.

# Файл: lib.awk

# Когда директива не написана явно, то подразумевается пространство "awk"
function say_something() {
	print "Hello"
}

@namespace "awk"
# Все что объявлено после директивы пространства помещается в него
BEGIN {
	phrase = "HELLO!"
}

@namespace "another"
BEGIN {
	phrase = "Hello my friend"
}

@namespace "my"
function say_something() {
	print awk::phrase
}

Теперь запишем такой код в основной программе

# Файл: program.awk
BEGIN {
	# Вызов одной и той же функции
	say_something()
	awk::say_something()
	
	# Обращаемся к переменной из другого пространства
	print another::phrase
	
	# Обращаемся к функции из другого пространства
	my::say_something()
}
$ gawk -f lib.awk -f program.awk
Hello
Hello
Hello my friend
HELLO!

Конструкция switch..case править

Конструкция языка Си switch..case не поддерживается в оригинальном Awk, однако, вероятно для полноты, ее поддержку включили в GAWK. Напомним, что данная конструкция позволяет облегчить написание цепочки проверки условий, если используется условие типа проверка по образцу. Синтаксис в GAWK абсолютно идентичен синтаксису в Си:

switch (<входящее-значение>) {
	case "<значение-образец-1>":
		<инструкции>
		break
	case "<значение-образец-2>":
		<инструкции>
		break
	...
	default:
		<инструкции>
		break
}

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

В отличие от Си, образцами в GAWK могут выступать как простые литералы и числа, так и регулярные выражения.

Блоки BEGINFILE и ENDFILE править

В GAWK помимо прочих блоков, есть еще два специальных: BEGINFILE и ENDFILE.

Блок BEGINFILE исполняется один раз прямо перед обработкой самого первого фрагмента текста. Соответственно ENDFILE вызывается после обработки самого последнего фрагмента файла.

Следующая программа демонстрирует порядок вызова специальных блоков.

$ echo "1" | gawk 'BEGIN{ print "pre-BEGIN" } BEGINFILE{ print "BEGINFILE" } BEGIN { print "post-BEGIN" } END{ print "pre-END" } ENDFILE{ print "ENDFILE" } END{ print "post-END" }'
pre-BEGIN
post-BEGIN
BEGINFILE
ENDFILE
pre-END
post-END

Эти блоки были придуманы для сугубо технологических целей. Например в BEGINFILE может быть заложена проверка возможности продолжать обработку, так как файл вообще говоря может хранить бинарные данные. Вы можете заложить оператор nextfile, если это бинарный файл, чтобы не прерывать исполнение по фатальной ошибке. В ENDFILE можно закладывать процедуры освобождения ресурсов системы.

Сопроцессы править

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

В общем случае синтаксис выглядит так

print "<запрос-по-протоколу-сервера>" |& "<процесс-сервера>"
"<процесс-сервера>" |& getline

Собственно оператор |& здесь ключевой. Один канал (в сторону сервера) пробрасывается между командой Awk (например print) и серверным процессом, а второй – между серверным процессом и командой getline, на которую будут приходить ответы сервера. Как и в случае с pipe-файлами, переменные NR и FNR не будут изменяться.

Покажем эту технику на примере обращения к программе sort.

# Файл: sort-server.awk
BEGIN {
    command = "LC_ALL=C sort" # Мы указываем локаль C, чтобы sort работал только с ASCII символами
    n = split("abcdefghijklmnopqrstuvwxyz", a, "")

    for (i = n; i > 0; i--)
        print a[i] |& command
    close(command, "to")

    while ((command |& getline line) > 0)
        print "got", line
    close(command)
}

Результат работы

$ gawk -f sort-server.awk
got a
got b
got c
got d
got e
got f
got g
got h
got i
got j
got k
got l
got m
got n
got o
got p
got q
got r
got s
got t
got u
got v
got w
got x
got y
got z

Здесь Awk процесс открывает два канала и запускает процесс sort. Внутренне канал в сторону сервера именуется "to", а канал в сторону GAWK – "from". Используя эти идентификаторы, мы можем явно закрывать каналы, когда это нужно. Без их указания, оба канала будут закрыты.

Процесс sort работает, пока есть что читать из канала в его сторону. Когда мы закрываем канал "to", то посылаем sort символ EOF, который и завершит сопроцесс. Пока EOF не посылается, sort накапливает данные для сортировки. Затем после EOF отправляет их же, но отсортированными и завершается. Awk просто печатает ответы и закрывает канал "from". Закрывать каналы явно является правилом хорошего тона, которым в больших программах пренебрегать нельзя.

Практические примеры править

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

Программы-однострочники править

Удаление повторов править

Следующий вызов похож на утилиту uniq и фильтрует повторы во входящем потоке.

awk '!seen[$0]++'

# Если вам не понятна короткая форма, то следующая запись аналогична предыдущей
awk '!seen[$0]++ { print }'

В этом примере используется блок с условием. Когда программа обращается к массиву первый раз и пытается применить на результате отрицание (!seen[$0]), то получает ИСТИНУ, потому что элемента еще не существует (пустая строка это ЛОЖЬ, а ее инверсия это ИСТИНА). Побочным эффектом этой операции является создания в массиве seen элемента с ключом $0, а его значение будет увеличено на единицу (счетчик повторений данного фрагмента).

Так как условие ИСТИНА, то входящий фрагмент будет напечатан. В следующий раз, когда встретиться тот же фрагмент, обращение к массиву вернет его счетчик. Так как ненулевое число это ИСТИНА, а инверсия это ЛОЖЬ, то фрагмент не будет напечатан.

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

# Опцией -v мы изменяем разделитель фрагментов RS
$ echo -n "1 2 1 1 3 3 5 7 8 5 6 2 2 6 6 8 5" | awk -v RS=" " '!seen[$0]++'
1
2
3
5
7
8
6

Работа с файлами, как с множествами править

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

cat SET1 SET2 | awk '!seen[$0]++'                                  # Объединение
awk 'NR == FNR {lut[$0] = 1; next} $0 in lut {print}' SET1 SET2    # Пересечение
awk 'NR == FNR {lut[$0] = 1; next} !($0 in lut) {print}' SET1 SET2 # Разница

Реализация объединения вам должна быть понятна по предыдущему примеру.

Пересечение и разница работают похоже:

  1. Сначала работает первый блок с условием NR == FNR, который помещает в массив lut фрагменты до тех пор, пока не кончится текст первого множества.
  2. Когда текст первого множества обработан полностью, то счетчик NR не обнуляется и продолжает считать дальше, а счетчик FNR начинает считать для нового файла. Так как условие первого блока теперь всегда будет ЛОЖНЫМ, то он отключается и включается в работу второй блок.
  3. Второй блок получает фрагменты второго множества и пытается определить были ли они в первом множестве через массив. Соответственно для пересечения печать происходит, если они есть в массиве, а для разницы — если нет.
$ cat > SET1 <<EOF
1
3
5
2
6
8
EOF

$ cat > SET2 <<EOF
1
2
3
6
7
8
9
10
EOF

$ cat SET1 SET2 | awk '!seen[$0]++' | sort -n # объединение
1
2
3
5
6
7
8
9
10

$ awk 'NR == FNR {lut[$0] = 1; next} $0 in lut {print}' SET1 SET2 | sort -n # пересечение
1
2
3
6
8

$ awk 'NR == FNR {lut[$0] = 1; next} !($0 in lut) {print}' SET1 SET2 | sort -n # разница
7
9
10

Используя похожий подход, можно сравнить два файла на одинаковость. Следующая команда возвращает 0, если файлы имеют одинаковое содержимое, и 1 — если разное.

awk '!($0 in a) {c++;a[$0]} END {exit(c==NR/2?0:1)}' file1 file2

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

Использование регулярных выражений с условиями править

Awk можно использовать вместо grep и sed, со всеми преимуществами Awk-языка. Так можно комбинировать регулярные выражения с условиями. Вот лишь несколько примеров коротких, но очень полезных программ.

awk 'NR % 6'  # Выводить фрагменты кратные 6
awk 'NR > 5'  # Начать печать с шестого фрагмента. Аналогично в sed '6,$p'
awk '$2 == "foo"'    # Выводить фрагменты, где во втором поле есть "foo"
awk '/foo/ && /bar/' # Печатать только строки, которые удовлетворяют двум регулярным выражениям
awk '/foo/ || /bar/' # Печатать только строки, которые удовлетворяют любому из двух выражений
awk 'NF'             # Печатать только непустые фрагменты
awk 'NF--'           # Печатать непустые фрагменты без последнего поля
awk '$0 = NR" "$0'   # Печатает фрагмент сопроводив его порядковым номером

Программы посложнее править

Подключение к серверам по протоколам TCP/IP править

В GAWK поддерживается возможность открытия двустороннего TCP/IP соединения, используя специальный синтаксис:

/net-type/protocol/local-port/remote-host/remote-port

Здесь:

  • net-type — тип сети: inet, inet4 или inet6.
  • protocol — протокол tcp или udp.
  • local-port — номер локального порта. Если использовать 0, то система сама выберет среди свободных портов.
  • remote-host — адрес удаленного хоста.
  • remote-port — порт на удаленном хосте, с которым открывается соединение. Можно использовать 0, если вас не заботит с каким портом связываться.

Давайте попробуем соединиться с публичным сервером по протоколу HTTP и узнать наш внешний IP-адрес. Следующая программа реализует это.

#!/usr/bin/awk -f
# Файл: get-ip.awk
BEGIN {
    # Адрес сервера
    site = "httpbin.org"
    
    # Подготавливаем параметры для подключения
    server = "/inet/tcp/0/" site "/80"
    # Передаем запрос на сервер
    print "GET /ip HTTP/1.0" |& server
    # Передаем поле с адресом хоста
    print "Host: " site |& server
    # Завершаем формирование запроса
    print "\r\n\r\n" |& server

    # Ждем, когда сервер ответит и сохраняем его ответ
    while ((server |& getline line) > 0 ) {

        content = content line "\n"
    }
    # Закрываем сокеты
    close(server)
    # Печатаем результат
    print content
}

Ниже показан пример работы программы.

$ ./get-ip.awk
HTTP/1.1 200 OK
Date: Fri, 28 Oct 2022 21:59:56 GMT
Content-Type: application/json
Content-Length: 34
Connection: close
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "origin": "188.130.168.200"
}

CSV-парсер на базе Awk править

CSV довольно популярный формат для хранения таблиц в текстовом файле из-за его простоты. Напомним как он реализуется.

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

CSV форматы понимают некоторые крупные программы, например можно импортировать CSV-таблицу в Microsoft Excel. На практике может понадобиться реализовывать преобразователи CSV-формата в другой. Данная задача прекрасно решается в Awk. К счастью эта задача уже давно была решена Лорансом Стинсоном, который любезно поделился с миром своими наработками[1].

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

#!/usr/bin/awk -f
#**************************************************************************
#
# Этот файл находится в открытом доступе.
#
# Чтобы получить больше информации, email LoranceStinson+csv@gmail.com.
# или пройдите по ссылке http://lorance.freeshell.org/csv/
#
# Парсит CSV строку в массив.
# Число найденных полей возвращается как результат.
# В случае ошибки, возвращает отрицательное значение и устанавливает
# переменную csverr. Ниже перечислены возможные варианты ошибок.
#
# Параметры:
# string  = Входящая строка.
# csv     = Массив, в который складываются найденные поля.
# sep     = Разделитель полей. По умолчанию ','
# quote   = Открывающая кавычка. По умолчанию "
# escape  = Закрывающая кавычка. По умолчанию "
# newline = Признак новой строки. Укажите символ, который будет интерпретироваться
#           как начало новой строки. Если оставить пустым,
#           произойдет ошибка.
# trim    = Флаг для удаления пробелов вокруг значений полей.
#           Влияет на парсинг. Если не установлен, то пробел между
#           разделителем и кавычкой приводит к игнорированию кавычки.
#
# Эти переменные только для внутреннего использования:
# fields  = Количество полей, найденных на данный момент.
# pos     = Позиция, из которой нужно извлечь поле в исходной строке.
# strtrim = Если строка найдена, то истина, а значит можно удалить кавычки.
#
# Коды ошибок:
# -1  = Не могу прочитать очередную строку.
# -2  = Пропущена кавычка.
# -3  = Пропущен разделитель.
#
# Примечания:
# Данный код написан с расчетом, что каждому полю предшествует разделитель,
# даже первому. Это значительно упрощает логику программы, но также требует
# искусственно добавить недостающие разделяющие символы.
#**************************************************************************
function parse_csv(string,csv,sep,quote,escape,newline,trim, fields,pos,strtrim) {
    # Проверяем, что есть что парсить.
    if (length(string) == 0) return 0;
    string = sep string; # Добавляем искусственно разделитель в начало строки.
    fields = 0; # Количество полей, найденных на данный момент.
    while (length(string) > 0) {
        # Удалить пробелы за разделителем, если требуется.
        if (trim && substr(string, 2, 1) == " ") {
            if (length(string) == 1) return fields;
            string = substr(string, 2);
            continue;
        }
        strtrim = 0; # Используется для обрезки незакавыченных строк
        # Обработка закавыченных полей.
        if (substr(string, 2, 1) == quote) {
            pos = 2;
            do {
                pos++
                if (pos != length(string) &&
                    substr(string, pos, 1) == escape &&
                    (substr(string, pos + 1, 1) == quote ||
                     substr(string, pos + 1, 1) == escape)) {
                    # Удаляет экранированные кавычки.
                    string = substr(string, 1, pos - 1) substr(string, pos + 1);
                } else if (substr(string, pos, 1) == quote) {
                    # Конец строки найден.
                    strtrim = 1;
                } else if (newline && pos >= length(string)) {
                    # Захват следующей строки.
                    if (getline == -1) {
                        csverr = "Unable to read the next line.";
                        return -1;
                    }
                    string = string newline $0;
                }
            } while (pos < length(string) && strtrim == 0)
            if (strtrim == 0) {
                csverr = "Missing end quote.";
                return -2;
            }
        } else {
            # Захват пустого поля.
            if (length(string) == 1 || substr(string, 2, 1) == sep) {
                csv[fields] = "";
                fields++;
                if (length(string) == 1)
                    return fields;
                string = substr(string, 2);
                continue;
            }
            # Поиск разделителя.
            pos = index(substr(string, 2), sep);
            # Если разделителя нет, оставшаяся часть строки считается полем.
            if (pos == 0) {
                csv[fields] = substr(string, 2);
                fields++;
                return fields;
            }
        }
        # Удалить пробелы после разделителя, если требуется.
        if (trim && pos != length(string) && substr(string, pos + strtrim, 1) == " ") {
            trim = strtrim
            # Подсчет найденных пробелов.
            while (pos < length(string) && substr(string, pos + trim, 1) == " ") {
                trim++
            }
            # Удалить их из строки.
            string = substr(string, 1, pos + strtrim - 1) substr(string,  pos + trim);
            # Подогнать позицию pos под обрезанные пробелы, если кавычки не были найдены.
            if (!strtrim) {
                pos -= trim;
            }
        }
        # Убеждаемся, что мы в конце строки или есть разделитель.
        if ((pos != length(string) && substr(string, pos + 1, 1) != sep)) {
            csverr = "Missing separator.";
            return -3;
        }
        # Собираем поле вместе.
        csv[fields] = substr(string, 2 + strtrim, pos - (1 + strtrim * 2));
        fields++;
        # Удаляем поле из строке перед следующим проходом.
        string = substr(string, pos + 1);
    }
    return fields;
}

{
    num_fields = parse_csv($0, csv, ",", "\"", "\"", "\\n", 1);
    if (num_fields < 0) {
        printf "ERROR: %s (%d) -> %s\n", csverr, num_fields, $0;
    } else {
        printf "%s -> ", $0;
        printf "%s", num_fields;
        for (i = 0;i < num_fields;i++) {
            printf "|%s", csv[i];
        }
        printf "|\n";
    }
}

Пусть у нас есть такой CSV-файл:

ID,Name,Phone
1,John Doe,555-3441
2,Alice Brown,555-0012
3,   "    Bill Cowl   "   ,555-0013

Теперь попробуем передать этот файл программе.

$ ./csv.awk test.csv
ID,Name,Phone -> 3|ID|Name|Phone|
1,John Doe,555-3441 -> 3|1|John Doe|555-3441|
2,Alice Brown,555-0012 -> 3|2|Alice Brown|555-0012|
3,   "    Bill Cowl   "   ,555-0013 -> 3|3|    Bill Cowl   |555-0013|

Поясним немного назначение параметра trim. Дело в том, что в данной реализации можно указать с какого символа следует значение поля: область внутри кавычек или область между разделителями. В предыдущем примере флаг был поднят, поэтому для Bill Cowl были захвачены только символы между кавычками. Если флаг опустить, то кавычки будут проигнорированы, и будут захвачены все символы между разделителями.

$ ./csv.awk test.csv
ID,Name,Phone -> 3|ID|Name|Phone|
1,John Doe,555-3441 -> 3|1|John Doe|555-3441|
2,Alice Brown,555-0012 -> 3|2|Alice Brown|555-0012|
3,   "    Bill Cowl   "   ,555-0013 -> 3|3|   "    Bill Cowl   "   |555-0013|

Камень-ножницы-бумага править

Следующий пример демонстрирует, что на базе Awk может быть создано полноценное интерактивное приложение. Следующий код реализует простенькую игру Камень-ножницы-бумага. Программа просит пользователя выбрать между камнем, ножницами и бумагой, а затем показывает, что выбрала она, используя генератор псевдослучайных чисел. Далее все зависит от того, кто более удачливый.

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

#!/usr/bin/awk -f
BEGIN {

    srand() # Инициализируем генератор случайных чисел

    opts[1] = "rock"      # Камень
    opts[2] = "paper"     # Бумага
    opts[3] = "scissors"  # Ножницы

    # Основной цикл
    do {
        print "1 - rock"
        print "2 - paper"
        print "3 - scissors"
        print "9 - end game"

        ret = getline < "-"    # Перенаправляем ввод с консоли на STDIN

        if (ret == 0 || ret == -1) {
            exit
        }

        val = $0
        # Проверяем, что ввел пользователь
        if (val == 9) {
            exit
        } else if (val != 1 && val != 2 && val != 3) {
            print "Invalid option"
            continue
        } else {
            play_game(val)   # Вызов основной процедуры
        }
    } while (1)   # Бесконечный цикл
}

function play_game(val) {
    r = int(rand()*3) + 1   # Генерируем случайное число
    print "I have " opts[r] " you have "  opts[val]
    # Проверяем что получилось
    if (val == r) {  # Ничья
        print "Tie, next throw"
        return
    }
    # Определяем кто выиграл
    if (val == 1 && r == 2) {
        print "Paper covers rock, you loose"
    } else if (val == 2 && r == 1) {
        print "Paper covers rock, you win"
    } else if (val == 2 && r == 3) {
        print "Scissors cut paper, you loose"
    } else if (val == 3 && r == 2) {
        print "Scissors cut paper, you win"
    } else if (val == 3 && r == 1) {
        print "Rock blunts scissors, you loose"
    } else if (val == 1 && r == 3) {
        print "Rock blunts scissors, you win"
    }
}

Ниже показано несколько партий.

$ ./rps.awk
1 - rock
2 - paper
3 - scissors
9 - end game
1
I have paper you have rock
Paper covers rock, you loose
1 - rock
2 - paper
3 - scissors
9 - end game
2
I have paper you have paper
Tie, next throw
1 - rock
2 - paper
3 - scissors
9 - end game
2
I have rock you have paper
Paper covers rock, you win
1 - rock
2 - paper
3 - scissors
9 - end game
9

Приложения править

Некоторые опции Awk править

-F <разделитель>
--field-separator <разделитель>
Данная опция переопределяет значение переменной FS.
-f <файл-с-программой-awk>
--file <файл-с-программой-awk>
Позволяет передать программу Awk отдельным файлом. Данная опция может быть использована несколько раз в одном вызове: в таком случае конечная программа получается конкатенацией всех переданных файлов. Если в файле с программой не указано пространство имен, то по умолчанию он будет интерпретироваться как будто в нем в начале записана строка @namespace "awk".
-v <имя-переменной>=<значение>
--assign <имя-переменной>=<значение>
Позволяет определить переменную из вызова команды. Данная опция может использоваться несколько раз в одном вызове. Данным методом не рекомендуется устанавливать некоторые встроенные переменные.
-c
--traditional
Запускает Awk в режиме совместимости, отключая все расширения GAWK.
-e <текст-с-программой-Awk>
--source <текст-с-программой-Awk>
Позволяет передать несколько фрагментов текста с программой. Это используется, например, когда вызов формируется динамически другой программой.
-E <файл-с-программой-awk>
--exec <файл-с-программой-awk>
Работает как -f, но отключает встроенный парсинг опций. Также запрещается инициализация переменных в вызове.
-i <файл-с-программой-awk>
--include <файл-с-программой-awk>
Эта опция эквивалентна директиве @include и похожа на опцию -f со следующими отличиями: если исходный код уже был загружен, то он не будет загружаться повторно (-f загружает код, даже если он повторяется); файлы указанные этой опцией не считаются основной программой, т.е. GAWK не начнет интерпретацию, пока не найдет код, переданный непосредственно, или по -f. Если в файлах не указано пространство имен, то они интерпретируются как будто в них записано @namespace "awk".
-P
--posix
Запускает Awk в POSIX-совместимом режиме. Его поведение аналогично --traditional, но также отключаются расширения, которых нет в POSIX.

Переменные окружения, которыми пользуется Awk править

Примечание
Все переменные, которыми пользуется только GAWK, имеют соответствующий префикс.
AWKPATH
Пути к одной или нескольким директориям системы, которые просматриваются для поиска файлов, передаваемых опциями -f, -i, если имя файла не передается как абсолютный путь. Пути в этой директории отделяются друг от друга двоеточием :. По умолчанию в переменной используется два пути: .:/usr/local/share/awk.
Механизм поиска очень похож на тот, что реализует командная оболочка с переменной PATH. Если переменная не пустая, то Awk по очереди проверит каждую директорию. Если файл не был найден в первом проходе, то Awk подставит к имени файла расширение .awk. Текущая директория при не пустой AWKPATH проверяется последней, если конечно она явно не прописана.
В GAWK к этой переменной можно получить доступ из встроенного массива ENVIRON["AWKPATH"].
AWKLIBPATH
Используется для загрузки расширяющих модулей GAWK. По умолчанию имеет значение /usr/local/lib/gawk. Механизм поиска аналогичен AWKPATH, но в качестве расширения используется .so. К переменной можно обратиться ENVIRON["AWKLIBPATH"].
GAWK_MSEC_SLEEP
Определяет интервал времени между попытками подключения в миллисекундах.
GAWK_PERSIST_FILE
Определяет имя файла, который является отображением на память системы, в которой GAWK разместит свою кучу (heap). Используется в GAWK, начиная с 5.2.
GAWK_READ_TIMEOUT
Таймаут в миллисекундах перед обработкой входящей информации. Когда он выходит, GAWK сгенерирует ошибку. Используется при работе с медленным вводом.
GAWK_SOCK_RETRIES
Количество попыток открыть TCP/IP соединение. Используется, когда GAWK открывает соединения через сокеты.
PMA_VERBOSITY
Устанавливает подробность возвращаемых сообщений во время выделения памяти для GAWK.
POSIXLY_CORRECT
Используется чтобы отметить, что все неподдерживаемые POSIX расширения отключены.
AWKBUFSIZE
Используется в POSIX-совместимом режиме для выделения памяти под буфер ввода-вывода. Значение указывает на размер в байтах. Если он не указан, то Awk ориентируется на размер входного файла.
AWK_HASH
Определяет способ вычисления Hash-значений для элементов массивов. Используется в основном, когда требуется это оптимизировать в конкретной системе.
AWKREADFUNC
Используется для переключения способа чтения входных данными: текстовыми строками или блоками.
GAWK_MSG_SRC
В этой переменной можно указать файл, в который следует генерировать ошибки и предупреждения с указанием на строку и функцию из исходных файлов GAWK. Используется в отладочных целях.
GAWK_LOCALE_DIR
Используется для интернационализации выводимых сообщений GAWK.
GAWK_NO_DFA
Если эта переменная есть, то отключает DFA модуль для вычисления регулярных выражений в конструкциях типа соответствует. Используется только когда есть проблемы с регулярными выражениями.
GAWK_STACKSIZE
Указывает размер, с которым должен расти стек.
INT_CHAIN_MAX
Предполагаемый размер хеш-суммы для массивов, которые индексируются числами.
STR_CHAIN_MAX
Предполагаемый размер хеш-суммы для массивов, которые индексируются по ключам.
TIDYMEM
Используется, чтобы включить mtarce() для поиска утечек памяти. Она не используется, когда включена GAWK_PERSIST_FILE.

Список встроенных переменных править

BINMODE (GAWK)
Используется, когда GAWK ведет обработку файлов не в POSIX системе, в бинарном режиме. В качестве значения используется число 1, 2 или 3, что означает соответственно для входных, выходных или всех файлов использовать двоичный режим обработки ввода-вывода. Числа меньше 0 и больше трех будут рассматриваться соответственно как 0 (отключено) и 3. Вместо цифр можно использовать строку: "r", "w" или "rw"/"wr" в тех же смыслах.
CONVFMT
Управляет преобразованием чисел в строки. По умолчанию имеет значение "%.6g".
FIELDWIDTHS (GAWK)
Строка, состоящая из разделенных пробелами чисел. Указывает GAWK как разбить входящие данные с фиксированными границами столбцов, которые числа и обозначают. Использование этой переменной перекрывает FPAT и FS.
FS (Field separator)
Символ(ы) или регулярное выражение, используемое как разделитель полей.
IGNORECASE (GAWK)
Если переменная не пустая или не ноль, то для всех регулярных выражений включается режим игнорирования регистра.
LINT (GAWK)
Активизируется, когда не пустая или не ноль. Также может быть инициализирована через опцию --lint. Если имеет значение "fatal", то GAWK начинает генерировать ошибки и предупреждения в любой ненормальной ситуации; "invalid" — только предупреждения.
OFMT (Output formatter)
Переменная контролирует преобразование чисел в строки для оператора print. По умолчанию имеет значение "%.6g".
OFS (Output field separator)
Используется в операторе print, когда выводимые части отделяются в вызове запятой ,. По умолчанию имеет значение " ".
ORS (Output record separator)
Строка, которой инициализирована данной переменной, будет вставляться в конце каждого вызова print. По умолчанию имеет значение "\n".
PREC (GAWK)
Рабочая точность для чисел с плавающей точкой. По умолчанию 53.
ROUNDMODE (GAWK)
Режим округления. По умолчанию "N" (IEEE 754), т.е. до ближайшего четного.
RS (Record separator)
Разделитель входящих фрагментов. По умолчанию "\034".
SUBSEP
Разделитель для ключей массивов. По умолчанию "\034".
TEXTDOMAIN (GAWK)
Используется для интернационализации на уровне GAWK. Устанавливает текстовый домен. По умолчанию "messages".

Управляющие переменные править

ARGC ARGV
ARGV – это массив с переданными команде аргументами. В элементе ARGV[0] сохраняется имя исполняющей программы, а в оставшихся элементах, переданные аргументы, не являющиеся опциями. Размер этого массива определяется значением ARGC, причем последний элемент имеет индекс ARGC-1. В ARGV обычно хранится список из обрабатываемых файлов.
ARGIND (GAWK)
Счетчик, который указывает на элемент в ARGV, который в данный момент обрабатывается. Интересно отметить, что условие FILENAME == ARGV[ARGIND] всегда ИСТИНА.
ENVIRON
Массив, который хранит все переменные окружения, переданные в данном вызове.
ERRNO (GAWK)
Если во время чтения по getline, перенаправления на getline или во время close() возникает системная ошибка, то данная переменная будет хранить конкретное сообщение об этой ошибке, в противном случае она будет хранить предыдущее значение. Перед чтением очередного файла эта переменная очищается. Если ошибка не системная, то ее можно проверить в PROCINFO["errno"].
FILENAME
Хранит имя текущего обрабатываемого файла. Если текст приходит из STDIN, то значение это переменной "-".
FNR
Номер фрагмента в текущем обрабатываемом файле.
NF
Число полей, на которое был разбит входящий фрагмент.
FUNCTAB (GAWK)
Данный массив хранит имена всех встроенных, определенных пользователем и расширенных функций. Данный массив нельзя редактировать вручную.
NR
Количество фрагментов, которое Awk обработал с начала запуска программы.
RLENGTH
Подстрока, вычисленная функцией match(), помещается в эту переменную.
RSTART
Позиция первого символа найденной match() подстроки в исходной строке. Если ничего не найдено, то значение переменной равно нулю.
RT (GAWK)
Данная переменная хранит RS, с которым был получен текущий фрагмент.
SYMTAB (GAWK)
Данный массив хранит имена всех объявленных глобальных переменных и массивов.

Примечания править

  1. Официальная страница AWK CSV Parser

Ссылки править