Практический разбор awk на пяти задачах: суммы по группам, уникальные строки с сохранением порядка, join файлов через FNR==NR и парсинг /etc/passwd. С примерами команд.— Читать дальше «awk за пределами print $1: пять нетривиальных задач»
Пожалуй, самое частое применение awk - выдрать первую колонку из вывода, кинуть в пайплайн дальше. На этом знакомство с awk у большинства и заканчивается.
awk '{print $1}'
Но awk - это не утилита для вырезания колонок, это полноценный язык программирования с ассоциативными массивами, регулярками, форматированным выводом и управляющими конструкциями. Все то, ради чего обычно тянутся за Python-скриптом, awk умеет в одну строку прямо в терминале.
Ниже - пять задач, которые на первый взгляд выглядят элементарно, но ломают привычку "awk это про колонки". По каждой разберем три вещи: как хочется решить сходу, почему это неоптимально, и как можно при помощи awk.
Дан файл /root/spend.txt - категория трат и сумма:
food 10
toys 5
food 7
books 3
toys 8
Нужно просуммировать по каждой категории.
Первое что приходит в голову - сгруппировать. Собрать уникальные категории, по каждой отфильтровать строки и сложить. Получается что-то такое:
for cat in $(awk '{print $1}' spend.txt | sort -u); do
total=$(grep "^$cat " spend.txt | awk '{sum+=$2} END{print sum}')
echo "$cat $total"
done
Это работает. Но это три прохода по файлу на каждую категорию, внешний цикл в shell, и на большом файле оно будет работать очень медленно.
Да, awk '{sum += $2}' умеет накапливать одно число - общую сумму по всему файлу. Но как только сумм нужно несколько - по одной на категорию - рука тянется наружу, к циклу в shell, потому что "одна переменная - одна сумма". Но в awk не одна переменная. Здесь также есть ассоциативные массивы, где индексом может выступать строка.
Итого, решение через awk может выглядеть следующим образом:
awk '{s[$1]+=$2} END{for(k in s) print k, s[k]}' spend.txt
Вот как это работает:
s[$1] += $2 - берем ячейку массива s с ключом "первое поле" и прибавляем к ней второе. Для строки food 10 это s["food"] += 10, для toys 5 - s["toys"] += 5. awk сам заводит новую ячейку когда встречает новый ключ, обнулять ничего не надо. Прошли весь файл - в массиве накопились суммы по всем категориям за один проход.
Блок END выполняется после того как файл закончился. for(k in s) обходит все ключи массива, print k, s[k] печатает ключ и накопленную по нему сумму.
Дан файл /root/items.txt, строки в нем повторяются:
a
b
a
c
b
a
Нужно оставить только уникальные, сохранив порядок первого появления.
Тут даже awk сначала не вспоминаешь - есть же готовый инструмент:
sort -u items.txt
Запускаем, получаем a b c. Вроде совпало. Но совпало случайно - в этом примере уникальные значения итак идут по алфавиту. sort -u сортирует, а значит **порядок первого появления теряется
awk в свою очередь помнит, какие строки уже видел, и делает это в один проход - потому что у него, опять же, есть ассоциативный массив.
Более простое решение задачи через awk:
awk '!seen[$0]++' items.txt
По шагам:
$0 - это вся текущая строка целиком. seen[$0] - ячейка массива с ключом "эта строка". Для новой, еще не встречавшейся строки ее значение пустое, а в числовом контексте awk считает пустое за 0.
seen[$0]++ - это постфиксный инкремент: awk сначала возвращает текущее значение ячейки, а потом увеличивает его на 1. То есть при первой встрече строки выражение отдает 0 и тут же делает ячейку равной 1. При второй встрече отдает 1 (и делает равной 2), и так далее.
И ключевой момент: в awk, если условие написано без блока {...}, то то по умолчанию выполняется действие print - "напечатать строку"
В log.txt есть блок, обернутый в строки-маркеры --START-- и --END--:
noise
--START--
line1
line2
--END--
noise2
Нужно вытащить то, что между маркерами - line1 и line2
И раз есть четкие границы, напрашивается grep или sed по диапазону. У sed даже есть готовый синтаксис "от x до y":
sed -n '/--START--/,/--END--/p' log.txt
Но так мы получаем блок вместе с маркерами. Далее, чтобы выкинуть маркеры, можно начать городить что-то вроде sed -n '/--START--/,/--END--/{/--START--/d; /--END--/d; p}'. Но это уже не для слабонервных.
grep тут вообще не поможет - он смотрит на каждую строку отдельно и не знает, внутри блока она или снаружи. А вся суть задачи именно в этом - определить, находится ли текущая строка внутри интересующего нас блока. Инструменту надо помнить, где он находится, переходя от строки к строке. То есть нужна переменная-состояние - а это как раз задача для awk:
awk '/--START--/{f=1;next} /--END--/{f=0} f' log.txt
Здесь 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:
1 alice
2 bob
3 carol
ages.txt:
1 30
2 25
3 40
Нужно соединить их по ключу-id и получить имя возраст.
По сути - обычный JOIN из SQL, только над двумя текстовыми файлами.
Как вариант, решить скриптом:
while read id name; do
age=$(grep "^$id " ages.txt | awk '{print $2}')
echo "$name $age"
done < names.txt
Работает, но каждая строка первого файла запускает отдельный grep по всему второму. Для 3-х строк неважно, но для файла на сотни тысяч строк это O(n²) и будет ползти очень долго.
Решение с помощью awk:
awk 'FNR==NR {name[$1]=$2; next} {print name[$1], $2}' names.txt ages.txt
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 (возраст).
/etc/passwd устроен так: семь полей, разделенных двоеточиями (имя, пароль-заглушка, UID, GID, комментарий, домашний каталог, login shell):
root:x:0:0:root:/root:/bin/bash
yan:x:1400:1400::/home/yan:/bin/bash
zoe:x:1500:1500::/home/zoe:/bin/bash
nolog:x:1600:1600::/home/nolog:/usr/sbin/nologin
Нужно выписать имена пользователей, у которых shell - /bin/bash
Без awk эту задачу решают через grep | cut:
grep '/bin/bash' /etc/passwd | cut -d: -f1
Работает, но с нюансом: grep '/bin/bash' матчит подстроку в любом месте строки, а не только в поле shell. Если у пользователя, скажем, домашний каталог /home/bin/bash-fan - grep его ошибочно поймает. Чтобы точно, надо якорить на конец: grep '/bin/bash$'. А потом еще cut отдельно. Два инструмента, и надо помнить про якорь.
awk же фильтрует именно по седьмому полю ($7 == "/bin/bash") - точное сравнение поля целиком, ложных срабатываний нет, и фильтр с извлечением в одной команде. Но чтобы обращаться к "седьмому полю", awk сначала надо объяснить, что поля здесь разделены не пробелами.
В предыдущих задачах было достаточно стандартного для awk разбития строк по пробелам, но тут поля через двоеточия.
Для этого у awk есть флаг -F, определяющий разделитель полей.
Пример:
awk -F: '{print $1}' /etc/passwd
С -F: поле $1 - имя, $7 - shell. Двоеточие как разделитель, все семь полей сразу доступны по номерам.
Нужны только строки, где седьмое поле равно /bin/bash:
awk -F: '$7=="/bin/bash"{print $1}' /etc/passwd
$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 там еще несколько сотен.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Собери программиста в отпуск: игра-квест от Тproger | 0 | 9.84 | 21-08-2026 |
| 2 | Kubernetes на практике: гайды и малоизвестные Open Source-инструменты | 0 | 11.88 | 25-08-2026 |
| 3 | Ваши open source-контрибьюторы теперь ИИ-first. Как не утонуть в потоке агентских пулреквестов | 0 | 11.9 | 19-08-2026 |
| 4 | RAG-бот для любого сайта на Python за 60 строк кода | 0 | 14.5 | 26-08-2026 |
| 5 | Как пройти собеседование без опыта: что показать вместо стажа | 0 | 6.2 | 21-08-2026 |
| 6 | Как мы детектили фрод, а получили Джонни и Джулию: история одной браузерной разминки | 0 | 4.75 | 17-08-2026 |
| 7 | Тикет-системы: обзор 10 лучших решений для поддержки клиентов и сотрудников в 2026 году | 0 | 14.94 | 25-08-2026 |
| 8 | Задания ЕГЭ по теме «Household chores, daily routines and homes» | 0 | 2.5 | 04-05-2026 |
| 9 | Одинoкий чeлoвeк нa дeтcкoй плoщaдкe в пять утpa выглядит пoдoзpитeльнo. ... | 0 | 5.74 | 30-07-2026 |