awk за пределами print $1: пять нетривиальных задач
Разбираем пять задач, где awk обходит громоздкие связки из grep, sed и циклов в шелле: группировка, дедупликация, блоки между маркерами, join двух файлов и разбор /etc/passwd.

Пожалуй, самое частое применение awk - выдрать первую колонку из вывода, кинуть в пайплайн дальше. На этом знакомство с awk у большинства и заканчивается.
Но awk - это не утилита для вырезания колонок, это полноценный язык программирования с ассоциативными массивами, регулярками, форматированным выводом и управляющими конструкциями. Все то, ради чего обычно тянутся за Python-скриптом, awk умеет в одну строку прямо в терминале.
Ниже - пять задач, которые на первый взгляд выглядят элементарно, но ломают привычку "awk это про колонки". По каждой разберем три вещи: как хочется решить сходу, почему это неоптимально, и как можно при помощи awk.
Задача 1: сумма по группам.
Дан файл /root/spend.txt - категория трат и сумма:
Нужно просуммировать по каждой категории.
Первое что приходит в голову - сгруппировать. Собрать уникальные категории, по каждой отфильтровать строки и сложить. Получается что-то такое:
Это работает. Но это три прохода по файлу на каждую категорию, внешний цикл в shell, и на большом файле оно будет работать очень медленно.
Да, awk '{sum += $2}' умеет накапливать одно число - общую сумму по всему файлу. Но как только сумм нужно несколько - по одной на категорию - рука тянется наружу, к циклу в shell, потому что "одна переменная - одна сумма". Но в awk не одна переменная. Здесь также есть ассоциативные массивы, где индексом может выступать строка.
Итого, решение через awk может выглядеть следующим образом:
Вот как это работает:
s[$1] += $2 - берем ячейку массива s с ключом "первое поле" и прибавляем к ней второе. Для строки food 10 это s["food"] += 10, для toys 5 - s["toys"] += 5. awk сам заводит новую ячейку когда встречает новый ключ, обнулять ничего не надо. Прошли весь файл - в массиве накопились суммы по всем категориям за один проход.
Блок END выполняется после того как файл закончился. for(k in s) обходит все ключи массива, print k, s[k] печатает ключ и накопленную по нему сумму.
Задача 2: уникальные строки с сохранением порядка
Дан файл /root/items.txt, строки в нем повторяются:
Нужно оставить только уникальные, сохранив порядок первого появления.
Тут даже awk сначала не вспоминаешь - есть же готовый инструмент:
Запускаем, получаем a b c. Вроде совпало. Но совпало случайно - в этом примере уникальные значения итак идут по алфавиту. sort -u сортирует, а значит **порядок первого появления теряется
awk в свою очередь помнит, какие строки уже видел, и делает это в один проход - потому что у него, опять же, есть ассоциативный массив.
Более простое решение задачи через awk:
По шагам:
$0 - это вся текущая строка целиком. seen[$0] - ячейка массива с ключом "эта строка". Для новой, еще не встречавшейся строки ее значение пустое, а в числовом контексте awk считает пустое за 0.
seen[$0]++ - это постфиксный инкремент: awk сначала возвращает текущее значение ячейки, а потом увеличивает его на 1. То есть при первой встрече строки выражение отдает 0 и тут же делает ячейку равной 1. При второй встрече отдает 1 (и делает равной 2), и так далее.
И ключевой момент: в awk, если условие написано без блока {...}, то то по умолчанию выполняется действие print - "напечатать строку"
Задача 3: строки между маркерами
В log.txt есть блок, обернутый в строки-маркеры --START-- и --END--:
Нужно вытащить то, что между маркерами - line1 и line2
И раз есть четкие границы, напрашивается grep или sed по диапазону. У sed даже есть готовый синтаксис "от x до y":
Но так мы получаем блок вместе с маркерами. Далее, чтобы выкинуть маркеры, можно начать городить что-то вроде sed -n '/--START--/,/--END--/{/--START--/d; /--END--/d; p}'. Но это уже не для слабонервных.
grep тут вообще не поможет - он смотрит на каждую строку отдельно и не знает, внутри блока она или снаружи. А вся суть задачи именно в этом - определить, находится ли текущая строка внутри интересующего нас блока. Инструменту надо помнить, где он находится, переходя от строки к строке. То есть нужна переменная-состояние - а это как раз задача для awk:
Здесь f - флаг. По умолчанию у неинициализированной переменной значение 0 = "не печатать".
Вся "программа" - это 3 правила подряд:
/--START--/{f=1; next} - строка совпала со стартовым маркером: ставим флаг в 1 и через next сразу прыгаем к следующей строке, не доходя до печати. Сам маркер не выводится.
/--END--/{f=0} - встретили конечный маркер, обнуляем флаг.
f - это третье правило, и оно ключевое. Голое условие без блока {...} в awk значит "напечатать строку, если условие истинно" (мы это уже видели в !seen[$0]++). Здесь условие - просто значение f. Когда f равен 1, строка печатается. Когда f равен 0, строка молча пропускается.
На выходе получаем ровно line1 и line2.
Задача 4: соединить два файла по ключу
Даны два файла с общим ключом в первом поле.
names.txt:
ages.txt:
Нужно соединить их по ключу-id и получить имя возраст.
По сути - обычный JOIN из SQL, только над двумя текстовыми файлами.
Как вариант, решить скриптом:
Работает, но каждая строка первого файла запускает отдельный grep по всему второму. Для 3-х строк неважно, но для файла на сотни тысяч строк это O(n²) и будет ползти очень долго.
Решение с помощью awk:
awk читает несколько файлов подряд как один поток и ведет два счетчика строк:
NR - общий номер строки с начала, сквозной по всем файлам.
FNR - номер строки внутри текущего файла, сбрасывается в 1 на каждом новом файле.
Пока awk идет по первому файлу, счетчики идут одинаково: FNR == NR. Как только awk переходит ко второму файлу, FNR обнуляется и стартует заново, а NR продолжает расти - равенство ломается. То есть FNR == NR - это способ понять, какой файл читается в данный момент.
FNR == NR {name[$1]=$2; next} - пока идем по names.txt, запоминаем: в ячейку name с ключом-id кладем имя.
{print name[$1], $2} - для каждой строки id возраст печатаем name[id] (имя, которое достаем из хэша по ключу мгновенно) и $2 (возраст).
Задача 5: awk на реальном файле - разбор /etc/passwd
/etc/passwd устроен так: семь полей, разделенных двоеточиями (имя, пароль-заглушка, UID, GID, комментарий, домашний каталог, login shell):
Нужно выписать имена пользователей, у которых shell - /bin/bash
Без awk эту задачу решают через grep | cut:
Работает, но с нюансом: grep '/bin/bash' матчит подстроку в любом месте строки, а не только в поле shell. Если у пользователя, скажем, домашний каталог /home/bin/bash-fan - grep его ошибочно поймает. Чтобы точно, надо якорить на конец: grep '/bin/bash$'. А потом еще cut отдельно. Два инструмента, и надо помнить про якорь.
awk же фильтрует именно по седьмому полю ($7 == "/bin/bash") - точное сравнение поля целиком, ложных срабатываний нет, и фильтр с извлечением в одной команде. Но чтобы обращаться к "седьмому полю", awk сначала надо объяснить, что поля здесь разделены не пробелами.
В предыдущих задачах было достаточно стандартного для awk разбития строк по пробелам, но тут поля через двоеточия.
Для этого у awk есть флаг -F, определяющий разделитель полей.
Пример:
С -F: поле $1 - имя, $7 - shell. Двоеточие как разделитель, все семь полей сразу доступны по номерам.
Нужны только строки, где седьмое поле равно /bin/bash:
$7 == "/bin/bash" - правило-условие: блок выполняется только для строк, где shell именно /bin/bash. Системные аккаунты с nologin отсеиваются сами. И печатаем только $1 - имя.
Ни в одной из пяти задач не пришлось тянуться за Python или склеивать пайп из четырех утилит - awk справился сам. Все потому, что это не "вырезалка колонок", а язык с ассоциативными массивами, состоянием между строками и полями.
И учить его целиком не надо. Достаточно нескольких шаблонов:
массив[ключ] += значение - суммы по группам
!seen[$0]++ - убрать дубли, сохранив порядок
f=1 … f=0 … f - вырезать блок между маркерами
FNR == NR{…; next} {…} - склеить два файла по ключу
-F: - сменить разделитель
Задачи я взял из своего сервиса для практики в изучении Linux и DevOps - IzzyLab.ru. Там команды набираешь в живом терминале в браузере, а решение проверяется автоматически по состоянию системы, а не по тому, что ты ввел. Задач по Linux и DevOps там еще несколько сотен.









