AWK - это язык программирования, созданный в семидесятых годах Альфредом Ахо, Питером Вайнбергером и Брайаном Керниганом (отсюда и название AWK).

Несмотря на то, что он завершен по Тьюрингу, он был разработан для решения одной конкретной задачи - обработки текста -. То есть какой-то текст входит, происходят преобразования, а другой текст гаснет. Вот почему большинство программ AWK являются однострочными, которые анализируют вывод других команд UNIX.

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

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

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

Обработка текста с помощью AWK.

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

Company    Email
Foo Inc.   [email protected]
Bar Corp.  [email protected]
Baz        [email protected]

Программа AWK для этого будет выглядеть примерно так:

NR > 1 { print $NF }

Да, вы правильно прочитали. Всего одна строчка кода. Не нужно открывать файлы, читать строки, закрывать дескрипторы или что-то в этом роде. AWK сделает это за вас. Вам просто нужно указать, как обрабатывать каждую запись (в данном случае - строку).

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

# Skip headers and prints the last field from each record.
File.readlines("companies.txt").each_with_index do |line, idx|
	next if (idx == 0) # Skip headers.
	puts line.split.last
end

Хотя синтаксис Ruby лаконичен и точен, он даже близко не подходит к тому, что вы можете делать с AWK.

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

Сохраните приведенный выше код в файл с именем extract_emails.awk и выполните следующие команды:

$ cat ./companies.txt | awk -f ./extract_emails.awk

Поскольку программы AWK работают «поверх» входных потоков (некоторой формы текста), вы должны предоставить один. Именно это и делает оболочка в приведенной выше последовательности, когда она «перенаправляет» вывод команды cat в awk. По сути, он говорит: «awk, пожалуйста, используйте все, что возвращает cat, в качестве входного потока».

Конечный результат должен быть примерно таким:

[email protected]
[email protected]
[email protected]

Вы также можете запускать «эфемерные» программы AWK со своего терминала. Давай попробуем.

(Важное замечание: заключите вашу программу в одинарные кавычки. В противном случае это не сработает.)

$ cat ./companies.txt | awk 'NR > 1 { print $NF }'
[email protected]
[email protected]
[email protected]

Как видите, результат точно такой же.

Структура программ AWK

Программа AWK - это последовательность шаблонов и действий, выполняемая против входного потока. Когда текущая запись соответствует шаблону, выполняется соответствующее действие. Вы можете иметь столько шаблонов, сколько захотите; AWK выполнит каждое подходящее действие. (Не только первый подходящий, как это бывает с другими инструментами, например, с библиотеками веб-маршрутизации.)

В псевдокоде это будет:

sepatator = ' '
while ((record = read_record()) != EOF) {
	fields = record.split(separator)
	// test rule 1
	if (match(pattern1, fields)) { /* run this code */ }
	// test rule 2
	if (match(pattern2, fields)) { /* run this code */ }
	// test rule 3
	if (match(pattern3, fields)) { /* run this code */ }
	// rule N...
	// ...
}

В терминах AWK каждое условие в приведенном выше коде будет шаблоном, а блок кода, который находится внутри оператора if, будет действием.

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

Чтобы получить доступ к полям из текущей записи, вы должны использовать переменные «индекса доллара», которые AWK определяет для вас. 1 доллар указывает на первое поле, 2 доллара - на второе и т. Д. (Индексы AWK основаны на 1).

В псевдокоде это будет:

separator = ' '
record    = "foo bar baz"
fields    = record.split(separator)
$1        = fields[0]
$2        = fields[1]
$3        = fields[2]

Например, если вы запустите следующую программу, она напечатает «полосу».

echo "foo bar baz" | awk '$2 { print $2 }'

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

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

NR > 1 { print $NF }

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

Но зачем использовать $ NF вместо $ 2?

Это потому, что в этом случае количество полей в записи является переменным. Да, вы правильно прочитали. Хотя входной файл может выглядеть как пара записей, извлеченных из базы данных, разделителем полей, который программа использует по умолчанию, является пробел. Поэтому, когда у компании есть составное имя, например «foo co.», AWK разделит это имя, как если бы это были два разных поля, поэтому для доступа к адресу электронной почты для этой конкретной компании вы должны использовать 3 доллара вместо ожидаемого средства доступа. 2 доллара.

Давайте посмотрим на эти записи:

$1  $2   $3 | $NF
Foo Inc. [email protected]
$1  $2 | $NF
Baz [email protected]

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

Еще одна интересная особенность awk заключается в том, что она довольно снисходительна к неопределенным полям: если вы запрашиваете поле, которого нет, оно просто возвращает «». Ни ошибок, ни сбоев, ничего подобного. (Что приятно, потому что вам не нужно добавлять нулевые проверки повсюду.)

Использование регулярного выражения в шаблонах AWK

Допустим, теперь вы хотите распечатать электронные письма только от компаний, названия которых начинаются с буквы «Б». Это немного сложнее, но поскольку шаблоны AWK могут быть регулярными выражениями, они все же однострочны:

/^B/ { print $NF }

Запустите указанную выше программу, у вас должно получиться:

[email protected]
[email protected]

О разделителях полей (ФС)

По умолчанию AWK будет использовать пробелы в качестве разделителей полей. Я предполагаю, что это связано с тем, что большинство программ UNIX используют пробелы для форматирования своего вывода. (Вы можете запустить: ps aux или ls -s, чтобы понять, что я имею в виду.)

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

Сохраните следующий листинг в файл с именем «companies.csv» и снова запустите программу.

Company Name;Email
Foo Inc.;[email protected]
Bar Corp.;[email protected]
Baz;[email protected]

Извлечь электронные письма:

cat ./companies.csv | awk -f extract_emails.awk
# --------------^ (remember, csv.)

Как видите, программа не работает… Но не беспокойтесь, это легко исправить, вам просто нужно указать AWK использовать «полуфабрикаты» в качестве разделителя полей, и ваша программа снова заработает.

Для этого вы воспользуетесь разделом НАЧАТЬ. BEGIN - это специальный раздел, который позволяет вам написать код, который необходимо запустить до того, как AWK начнет обрабатывать записи.

Единственное, что вам нужно сделать на этот раз, это изменить значение FS на «;» (Остальная часть программы остается прежней.)

BEGIN { FS = ";" }
NR > 1 { print $NF }

Запустите программу еще раз, и на этот раз вы должны получить правильные результаты.

Добавляем немного структуры

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

Для этого вы собираетесь использовать новый файл под названием «fake_companies.txt», который содержит этот листинг.

Company Name;Email
Acme Corporation;[email protected]
Globex Corporation;[email protected]
Soylent Corp;[email protected]
Initech;[email protected]
Bluth Company;[email protected]
Umbrella Corporation;[email protected]
Hooli;[email protected]
Vehement Capital Partners;[email protected]
Massive Dynamic;[email protected]
Wonka Industries;[email protected]
Stark Industries;[email protected]
Gekko & Co;[email protected]
Wayne Enterprises;[email protected]
Bubba Gump;[email protected]
Cyberdyne Systems;[email protected]
Genco Pura Olive Oil Company;[email protected]
The New York Inquirer;[email protected]
Duff Beer;[email protected]
Olivia Pope & Associates;[email protected]
Sterling Cooper;[email protected]
Ollivander's Wand Shop;[email protected]
Cheers;[email protected]
Krusty Krab;[email protected]
Good Burger;[email protected]

Программа, которую вы собираетесь увидеть, использует раздел END. END - это специальный раздел как BEGIN, но он запускается, когда AWK завершает обработку записей;

В следующей программе я также представляю «определяемые пользователем функции». Это функции, которые вы можете создать для расширения встроенной функциональности AWK.

Создайте файл с именем «extract_and_sort.txt» и вставьте в него этот код.

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

BEGIN  { FS = ";" }
NR > 1 { 
    # This time, instead of printing the email address to 
    # the console, the program stores it into a hash alike 
    # data structure to print them latter.
    # (Note that there is no need to define *emails*. AWK will do 
    # that for us the first time we use that variable.)

    # $1 == company name.
    # $2 == emails address.
    emails[$1] = $2;
}
END {
    # This section is responsible for sorting and printing fields.
    # 1. Get unsorted company's names (a.k.a. keys)
    for (name in emails)
        companies[++i] = name;

    # 2. Sort companies using the user defined function 
    #    *isort*. (See at the bottom of the program.)
    isort(companies, NR);

    # 3. Print company's information sorted by name.
    for (i = 1; i < length(companies); ++i) {
        name = companies[i]
        printf("%s %s\n", name, emails[name])
    }
}

# (Since AWK doesn't have this function, we have to roll our own.)
# Insertion sort.
function isort(arr, n) {
    for (i = 2; i < n; ++i) {
        for (j = i; j > 1 && arr[j-1] > arr[j]; --j) {
            # swap
            tmp      = arr[j-1];
            arr[j-1] = arr[j];
            arr[j]   = tmp;
        }
    }
}

Теперь направьте fake_companies.txt в awk и используйте новую версию программы для получения отсортированного списка компаний и их адресов электронной почты.

$ cat fake_companies.csv | awk -f extract_and_sort.awk

Еще один, и вы будете готовы к работе.

Чтобы завершить это введение в AWK, мы собираемся добавить в программу еще одну функцию: «группировку по буквам».

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

# Sort companies by name and print their name and email.
# The input file format is
# Company Name;Email
BEGIN  { FS = ";" }
NR > 1 { 
        emails[$1] = $2;
}
END {
    # Get unsorted company's names (a.k.a. keys)
    for (name in emails)
        companies[++i] = name;

    # Sort keys.
    isort(companies, NR);

    # Print companies sorted by name.
    last_seen = ""
    for (i = 1; i < length(companies); ++i) {
        name = companies[i]
        if (begin_group()) {
            print_line(80)
            last_seen = name
        }
        printf("%s %s\n", name, emails[name])
    }
    print_line(80)
}

function begin_group() {
    # Note that since variables are global, you don't have to pass
    # *last_seen* and *name* as agruments to this funcion. 
    # (Unless that you need function scoped variables, you 
    # don't need to pass arguments.)
    return (substr(last_seen, 1, 1) != substr(name, 1, 1));
}

# What's going on with the *i* paramter?
# In AWK all variables are global, and since this function 
# is called from a loop that also uses the *i* variable, you 
# need to add *i* to the parameter list to create a local 
#(function scoped) version of it.
function print_line(width, i) {
    for (i = 1; i <= width; ++i) {
        printf("%s", "-");
    }
    print ""
}

# Insertion sort.
function isort(arr, n) {
	for (i = 2; i < n; ++i) {
		for (j = i; j > 1 && arr[j-1] > arr[j]; --j) {
			# swap
			tmp      = arr[j-1];
			arr[j-1] = arr[j];
			arr[j]   = tmp;
		}
	}
}

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

$ cat fake_companies.csv | awk -f extract_and_sort.awk

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

Встроенные переменные

Вот список часто используемых встроенных переменных.

ARGC      Number of arguments.
ARGV      Array of arguments.
FILENAME  Current file name.
$0        Current input record.
FS        Input field separator  (default ' ').
RS        Input record separator (default '\n').
NR        Current input records count since beginning.
NF        Number of fields in current record.
OFS       Output field separator  (default ' ').
ORS       Output record separator (default '\n').
OFMT      Output format for numbers.

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

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

Пара однострочных

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

Выведите общее количество строк, которые ссылаются на «foo»:

/foo/ { count = count + 1 }
END   { print count       }

Печатать строки, длина которых превышает 80 символов.

length($0) > 80

Выведите общее количество строк:

END { print NR }

Резюме / Резюме

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

Хотите узнать больше об awk?

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

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

Спасибо за чтение! Надеюсь увидеть тебя в следующий раз!

PS: Не забывайте хлопать, если вам понравился этот пост :)