Журнал nginx на восемь гигабайт не открывается ни в одном графическом редакторе, а vim задумывается на минуты. awk и sed идут другим путём: читают файл как поток, обрабатывают строку и тут же забывают её, поэтому расход памяти измеряется килобайтами при любом размере входа. sed появился в Bell Labs в 1974 году, awk тремя годами позже, обе утилиты входят в POSIX и ставятся в любом дистрибутиве по умолчанию, включая крошечные образы alpine с busybox. Возраст здесь оборачивается предсказуемостью: однострочник, отлаженный десять лет назад, сегодня выдаст ровно тот же результат. Дальше собраны команды под три частые задачи: разбор журналов веб-сервера, массовые правки конфигурационных файлов и фильтрация вывода других программ.
Разбор строки на поля в awk с флагом -F и переменными NR и NF
awk прогоняет каждую строку входа через схему "шаблон { действие }": строка подошла под шаблон, выполняется действие. Разделителем полей по умолчанию служит цепочка пробелов и табуляций, первое поле адресуется как $1, последнее как $NF, число полей лежит в NF, номер строки в NR. Ссылка $(NF-1) достаёт предпоследнее поле и выручает, когда число столбцов заранее неизвестно. Флаг -F назначает другой разделитель, скажем двоеточие для passwd:
awk -F: '{print $1, $6}' /etc/passwd
Команда печатает имя учётной записи и домашний каталог каждого пользователя системы. Блоки BEGIN и END выполняются до чтения входа и после его окончания, что удобно для накопления итогов. Число запросов в журнале без wc:
awk 'END {print NR}' /var/log/nginx/access.log
Часы пик в журнале выделяются разбором четвёртого поля, которое содержит дату и время запроса:
awk '{split($4, t, ":"); c[t[2]":00"]++} END {for (h in c) print h, c[h]}' /var/log/nginx/access.log
Функция split() рубит поле по двоеточию, второй кусок это час, и счётчик с ключом вида 12:00 накапливает запросы за этот час. На выходе получается готовая таблица нагрузки по часам суток, и для неё не потребовалось ни одной временной переменной.
На Debian и Ubuntu ссылка awk по умолчанию указывает на реализацию mawk 1.3.4. Она ощутимо быстрее GNU awk на коротких скриптах, но не знает функции gensub() и правки файла на месте. Полная версия ставится пакетом gawk, актуальная ветка носит номер 5.4.1. Все приёмы в статье опираются на стандарт POSIX, поэтому работают и в mawk, и в busybox awk на маршрутизаторах.
Извлечение столбцов из журнала веб-сервера связкой awk, sort и uniq
Типичная строка журнала nginx в формате combined выглядит так:
203.0.113.45 - - [10/Nov/2026:12:03:44 +0300] "GET /catalog/item?id=42 HTTP/1.1" 200 5123 "-" "Mozilla/5.0 (X11; Linux x86_64)"
Поля нумеруются слева направо: $1 это адрес клиента, $7 путь запроса, $9 код ответа, $10 размер в байтах. Двадцать самых активных адресов собираются конвейером из трёх утилит:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
Флаг -c у uniq приписывает каждому адресу число повторов, sort -rn сортирует численно по убыванию. Распределение кодов ответа уместится в одну строку даже без uniq, потому что awk хранит счётчики в ассоциативных массивах:
awk '{c[$9]++} END {for (s in c) print s, c[s]}' /var/log/nginx/access.log | sort -n
Адреса, на которых чаще всего сыплются серверные ошибки:
awk '$9 >= 500 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
Условие перед фигурными скобками работает как фильтр, печатаются только строки с кодом от 500. Объём трафика по каждому клиенту в мегабайтах:
awk '{t[$1]+=$10} END {for (a in t) printf "%.1f MB %s\n", t[a]/1048576, a}' /var/log/nginx/access.log | sort -rn | head -10
Средний размер ответа считается там же, в блоке END:
awk '{s+=$10; n++} END {print int(s/n), "байт на запрос"}' /var/log/nginx/access.log
Пути часто содержат мусор вроде идентификаторов сессии в query-строке, и для честной статистики параметр отсекают той же функцией split():
awk '{split($7, u, "?"); print u[1]}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
Так запросы с разными значениями после знака вопроса падают в одну и ту же строку пути, и топ адресов перестаёт дробиться на десятки почти одинаковых позиций.
Квотированные значения вроде строки агента проще доставать сменой разделителя: при разбивке по двойным кавычкам агент занимает шестое поле.
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
Гигабайтный журнал такой конвейер переваривает за секунды: awk отдаёт sort уже сокращённый поток из тысяч строк вместо миллионов.
Фильтрация вывода df, free, ps и ip на лету без промежуточных файлов
Главная сила awk не в чтении файлов, а в роли последнего звена любого конвейера. Разделы, занятые сильнее чем на 80 процентов:
df -h | awk 'NR>1 && $5+0 >= 80 {print $6, $5}'
Выражение $5+0 превращает строку "83%" в число 83, отбрасывая знак процента, а NR>1 убирает заголовок таблицы. Доля занятой памяти из free:
free -m | awk '/^Mem/ {printf "Занято %d из %d МБ, это %.0f процентов\n", $3, $2, $3/$2*100}'
Шаблон /^Mem/ цепляет строку памяти и отсекает swap. Память по пользователям из ps:
ps aux | awk '{m[$1]+=$6} END {for (u in m) printf "%6.0f МБ %s\n", m[u]/1024, u}' | sort -rn | head
Шестой столбец вывода ps aux хранит резидентную память в килобайтах, деление на 1024 переводит её в мегабайты. Адреса интерфейсов вместе с именами:
ip -4 addr show | awk '/inet/ {print $NF, $2}'
Здесь $2 выдаёт адрес с маской вида 10.0.0.5/24, а $NF подхватывает имя интерфейса из конца строки. sed дополняет набор приёмом "замени и напечатай": с флагом -n и командой p на выходе остаются только строки, к которым применилась замена. Классика жанра, каждая запись переменной PATH на своей строке:
echo "$PATH" | sed 's/:/\n/g'
Тот же приём работает и на списках процессов: номера задач службы nginx достаются удалением всего лишнего.
pgrep -a nginx | sed -E 's/^([0-9]+).*/\1/'
Шаблон цепляет число в начале строки, запоминает его скобками и заменяет этой группой всю строку целиком.
Массовая замена параметров в конфигурациях через sed -i с резервной копией
Ключ к правке файлов это флаг -i, записывающий изменения прямо в оригинал. С суффиксом -i.bak рядом остаётся копия, и неудачная замена откатывается одним mv. Подъём лимита памяти в php.ini от пакета php8.3-fpm, где одна и та же строка может достаться и закомментированной, и действующей:
sed -i.bak -E 's/^;?memory_limit = .*/memory_limit = 512M/' /etc/php/8.3/fpm/php.ini
Знак вопроса после точки с запятой делает её необязательной, поэтому шаблон срабатывает в обоих случаях и вписывает 512M вместо стандартных 128M. Изменение одного порта только внутри блока VirtualHost, а не по всему файлу, делается диапазоном адресов:
sed -i '/<VirtualHost/,/<\/VirtualHost>/ s/:8080/:9090/g' /etc/apache2/sites-enabled/000-default.conf
sed ограничит замену строками между открывающим и закрывающим тегами, остальной файл не тронет. Одновременное обновление домена во всех включённых хостах nginx:
grep -rl 'old.example.com' /etc/nginx/sites-enabled/ | xargs sed -i.bak 's/old\.example\.com/new.example.com/g'
grep -rl печатает только имена файлов с совпадением, xargs передаёт их sed по одному. Точки в домене экранированы: без обратных слэшей точка совпала бы с любым символом, и шаблон зацепил бы лишнее. Идемпотентная правка sysctl.conf на случай, когда параметра в файле может не быть:
grep -q '^net.ipv4.ip_forward' /etc/sysctl.conf || echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
sed -i 's/^net.ipv4.ip_forward.*/net.ipv4.ip_forward = 1/' /etc/sysctl.conf
Первая строка дописывает параметр в конец файла, только если его там нет, вторая приводит существующую строку к нужному виду. Скрипт можно гонять повторно без риска получить дубликаты. Удаление строк выполняет команда d, и сброс конфигурации до сути выглядит так:
sed '/^#/d; /^$/d' /etc/nginx/nginx.conf
Флаг -i здесь намеренно опущен: результат сначала смотрят на экране и лишь потом переносят в файл. Обратная операция, вставка строки после совпавшей, выполняется командой a:
sed -i '/^http {/ a\ access_log /var/log/nginx/access.log;' /etc/nginx/nginx.conf
Сразу после открывающей скобки блока http появляется строка с настройкой журнала, отступ сохранён пробелами.
Расширенные регулярные выражения sed -E и обратные ссылки в заменах
Без флагов sed понимает базовые регулярные выражения, где фигурные скобки и плюс требуют экранирования. Флаг -E включает расширенный синтаксис с ? и | без обратных слэшей; в POSIX его стандартизовали в 2024 году, а в GNU sed 4.10 он работает наравне со старым синонимом -r. Скобочные группы запоминаются и подставляются в замену как \1 и \2, амперсанд разворачивается во весь совпавший фрагмент. Перестановка октетов адреса, пригождающаяся при обезличивании журналов перед передачей аналитику:
sed -E 's/([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/\4.\3.\2.\1/' access.log
Замена адреса сайта с автосохранением протокола, где решётка вместо слэша снимает экранирование:
sed -E 's#(https?://)old\.example\.com#\1new.example.com#g' /var/www/site/index.html
Группа (https?) захватывает http или https вместе с двоеточием и двумя слэшами и возвращается в замену как \1, так что протокол переключать вручную не требуется. Амперсанд экономит скобки, когда совпадение нужно лишь обрамить:
echo server1 | sed 's/server[0-9]/&-backup/'
Вывод будет server1-backup. Точечное извлечение из строки журнала тоже решается sed с квантификаторами:
sed -nE 's/^\[([0-9]{2}\/[A-Za-z]{3}\/[0-9]{4}):.*/\1/p' access.log
На выходе остаются одни даты вида 10/Nov/2026, потому что флаг -n глушит обычный вывод, а p печатает лишь строки, где замена состоялась. Флаг i у команды s отменяет чувствительность к регистру, и для латиницы он срабатывает везде, а на кириллицу влияет активная локаль системы. Флаг g нужен всегда, когда в строке ожидается больше одного совпадения, иначе sed меняет лишь первое вхождение.
Портативность и типичные ошибки при написании однострочников sed и awk
Почти каждая потерянная правка конфигурации повторяет одни и те же промахи, поэтому короткий чек-лист экономит и время, и нервы:
- Сначала выполнить команду без -i и оценить результат на экране, затем дописывать флаг;
- Оставлять суффикс у -i, например -i.bak, чтобы оригинал лежал рядом;
- Экранировать точки в шаблонах, потому что точка совпадает с любым символом;
- Передавать переменные оболочки в awk через флаг -v, а не внутрь текста программы;
- На macOS после -i ставить пустой аргумент sed -i '', BSD-версия требует аргумент даже при пустом суффиксе.
Пункт про -v заслуживает примера, поскольку одинарные кавычки не раскрывают переменные оболочки, а замена их на двойные ломает знаки доллара внутри программы:
awk -v limit=1048576 '$10 > limit {print $7, $10}' /var/log/nginx/access.log
Флаг -v заводит переменную limit прямо в программе awk, и текст шаблона остаётся нетронутым при любом значении из оболочки. Вторая ловушка связана с различиями реализаций: mawk не умеет gensub() и -i inplace, появившиеся в gawk 4.1.0, поэтому скрипты с этими возможностями требуют установленного gawk. Третья ловушка это многострочный текст: sed по умолчанию мыслит строками, и для шаблонов, пересекающих переводы строк, нужна команда N или флаг -z, разбивающий вход не по переводам строк, а по нулевым байтам. Четвёртая ловушка тоньше: флаг -i собирает новый файл и подменяет им оригинал, из-за чего жёсткие ссылки на правленый файл перестают указывать на свежее содержимое. А правило выбора инструмента простое: замена текста по шаблону это работа sed, подсчёты, суммы и группировки это работа awk, и смешивать их в одной команде незачем.
awk и sed не заменяют языки общего назначения там, где нужна сложная логика или несколько проходов по этим. Зато в нише быстрой правки и извлечения им почти нет равных по усилию на результат: команда пишется в одну строку, работает в любом дистрибутиве и не требует установки. Через несколько недель практики связки из этой статьи переходят в мышечную память, и гигабайтный журнал перестаёт пугать: ответ приходит раньше, чем редактор успел бы отрисовать первую сотню мегабайт.