Найти, заменить, посчитать — в журналах и конфигурациях
Регулярное выражение — шаблон для поиска в тексте. Его понимают grep, sed, awk, [[ =~ ]] в bash, редакторы и языки программирования. Это не шаблоны имён файлов: там * — «что угодно», здесь — «повтор предыдущего».
| Элемент | Значение | Пример |
|---|---|---|
. | Любой один символ | h.t — hat, hot |
[abc], [0-9], [^#] | Один символ из набора / не из набора | [0-9] — цифра |
*, +, ? | Повтор предыдущего: 0 и больше, 1 и больше, 0 или 1 | [0-9]+ — число |
{n,m} | От n до m повторов | [0-9]{1,3} |
^, $ | Начало, конец строки | ^# — комментарий |
(a|b) | Группа, «или» | (GET|POST) |
\. | Буквальная точка (любой спецсимвол — через \) | \.php |
Есть два диалекта. В базовом (BRE, по умолчанию в grep и sed) + ? { } ( ) | — обычные символы, а их особый смысл включается обратной косой чертой. В расширенном (ERE) — наоборот. Пишите расширенные: grep -E, sed -E; awk понимает их всегда.
grep -E '^[0-9]+$'. В двойных bash подставит $ и \ раньше grep, и шаблон станет другим.Журнал nginx для примеров — строка на запрос: адрес, время, запрос, код ответа, размер, клиент.
192.168.1.10 - - [14/Oct/2026:10:01:12 +0300] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0"
10.0.0.7 - - [14/Oct/2026:10:02:03 +0300] "GET /admin HTTP/1.1" 404 162 "-" "curl/8.5.0"
192.168.1.22 - - [14/Oct/2026:11:07:10 +0300] "GET /api/orders HTTP/1.1" 500 98 "-" "Mozilla/5.0"
$ grep -E '" (4|5)[0-9]{2} ' access.log | wc -l # ответы с ошибкой 4xx и 5xx
5
$ grep -oE '^[0-9]{1,3}(\.[0-9]{1,3}){3}' access.log | sort -u # -o: только совпадение
10.0.0.7
192.168.1.10
192.168.1.15
192.168.1.22
$ grep -E 'POST|500' access.log
192.168.1.10 - - [14/Oct/2026:10:02:30 +0300] "POST /login HTTP/1.1" 302 0 "-" "Mozilla/5.0"
192.168.1.22 - - [14/Oct/2026:11:07:10 +0300] "GET /api/orders HTTP/1.1" 500 98 "-" "Mozilla/5.0"
Кавычка и пробелы вокруг кода в первом шаблоне — не украшение: без них [45][0-9]{2} совпадёт и с размером ответа 5120. Хороший шаблон опирается на то, что вокруг искомого.
| Опция | Действие |
|---|---|
-q | Ничего не выводить, только код возврата — для if |
-o | Выводить только совпавшую часть |
-w | Совпадение целым словом |
-F | Шаблон — обычная строка, без регулярных выражений (точки, скобки буквально) |
-P | Регулярные выражения Perl: \d, \K, просмотр вперёд |
$ echo "id=42 user=anna" | grep -oP 'user=\K\w+' # \K — «выводить с этого места»
anna
sed — потоковый редактор: читает строки, применяет команды и выводит результат. Главная команда — замена s/шаблон/замена/флаги.
$ cat app.conf
# Настройки приложения
port = 8080
#debug = true
host = 127.0.0.1
log_level = info
$ sed 's/8080/9090/' app.conf | grep port
port = 9090
$ sed -E 's/^#?debug = .*/debug = false/' app.conf | grep debug # закомментированная или нет
debug = false
$ sed -e '/^#/d' -e '/^$/d' app.conf # без комментариев и пустых строк
port = 8080
host = 127.0.0.1
log_level = info
| Запись | Действие |
|---|---|
s/a/b/, s/a/b/g | Заменить первое / все вхождения в строке |
s|/var/www|/srv|g | Другой разделитель — когда в шаблоне есть / |
-E 's/(..)-(..)/\2-\1/' | Группы и обратные ссылки \1, \2 |
/шаблон/d | Удалить строки, где есть шаблон |
-n '2,3p', -n '/шаблон/p' | Вывести только строки 2–3 / только совпавшие |
/шаблон/a текст | Добавить строку после совпавшей (i — перед) |
$ sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/\4.\3.\2.\1/' <<< 'host 192.168.1.10'
host 10.1.168.192
$ sed 's|/var/www/html|/srv/site|g' <<< 'root /var/www/html; alias /var/www/html/img;'
root /srv/site; alias /srv/site/img;
С опцией -i sed меняет файл на месте. Суффикс после -i оставляет копию оригинала — используйте его, пока скрипт не отлажен:
$ sed -i.bak 's/^log_level = .*/log_level = warn/' app.conf
$ diff app.conf.bak app.conf
6c6
< log_level = info
---
> log_level = warn
Скрипт настройки должен быть идемпотентным: второй запуск не меняет то, что уже сделано первым. Замена через s/^параметр = .*/…/ идемпотентна. А дописывание строки — нет: два запуска дадут две строки. Поэтому сначала проверка:
grep -q '^timeout' app.conf || echo 'timeout = 30' >> app.conf
Запущенная дважды, эта строка оставит в файле ровно одну запись timeout. Для системных файлов полный порядок: копия → правка → проверка синтаксиса встроенной командой службы (sshd -t, nginx -t) → перезагрузка настроек; при неудаче — вернуть копию.
jq для JSON) или средства управления конфигурацией (курс «Основы Ansible»).awk делит каждую строку на поля: $1, $2, …, $NF — последнее, $0 — вся строка. Разделитель по умолчанию — любые пробелы, другой задаётся -F. Программа awk — пары условие {действие}: действие выполняется для строк, где условие истинно.
$ awk '{print $1, $9}' access.log | head -3 # адрес и код ответа
192.168.1.10 200
192.168.1.15 200
10.0.0.7 404
$ awk '$9 == 404 {print $7}' access.log # какие пути не найдены
/admin
/wp-login.php
/.env
/phpmyadmin/
$ awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $6}' /etc/passwd # люди, не системные
student /home/student
$ awk '/Mozilla/ && $9 >= 500' access.log # без действия — вывести строку
192.168.1.22 - - [14/Oct/2026:11:07:10 +0300] "GET /api/orders HTTP/1.1" 500 98 "-" "Mozilla/5.0"
Число полей NF, номер строки NR. Значение из bash передают через -v, а не вставкой в текст программы:
$ awk -v limit=1000 '$10 > limit {printf "%-20s %8d\n", $7, $10}' access.log
/ 5120
/about.html 2048
/ 5120
/contacts.html 1830
/img/logo.png 15400
$ df -P | awk 'NR > 1 && $5+0 >= 80 {print $6, $5}' # разделы, заполненные на 80% и больше
$5+0 превращает «18%» в число 18: awk берёт числовое начало строки.
Блок END выполняется после последней строки — там выводят итоги; BEGIN — до первой. Переменные создаются при первом использовании со значением 0.
$ awk '{bytes += $10} END {print bytes, NR, bytes/NR}' access.log
30264 11 2751,27
$ awk '{n[$1]++} END {for (ip in n) print n[ip], ip}' access.log | sort -rn
4 10.0.0.7
3 192.168.1.10
2 192.168.1.22
2 192.168.1.15
n[$1]++ — массив awk с ключом-адресом: так в одну строку делается то, на что в bash ушёл цикл с ассоциативным массивом. Сложнее — запросы по часам. Разделитель [][] — «квадратная скобка», второе поле — время, split режет его по двоеточию:
$ awk -F'[][]' '{split($2, t, ":"); h[t[2]]++} END {for (k in h) print k":00", h[k]}' access.log | sort
10:00 7
11:00 4
2751,27: в русской локали awk выводит дробные числа с запятой, и следующая программа может их не понять. В скриптах, которые считают, задавайте LC_ALL=C: LC_ALL=C awk 'BEGIN {printf "%.2f\n", 10/3}' → 3.33.awk разбирает простой текст, но не CSV с кавычками: в строке a,b,"c,d",e он насчитает 5 полей вместо 4. Для настоящего CSV и JSON берите Python или jq.
| Задача | Инструмент | Пример |
|---|---|---|
| Есть ли строка, отобрать строки | grep | grep -q '^PermitRootLogin no' sshd_config |
| Вытащить кусок по шаблону | grep -o | grep -oE '[0-9.]+' |
| Заменить в тексте или файле | sed | sed -i 's/^port = .*/port = 9090/' |
| Поле по номеру, разделитель — пробелы | awk | awk '{print $9}' |
| Посчитать, сгруппировать, сравнить числа | awk | awk '{s += $10} END {print s}' |
| Поле по номеру, разделитель — один символ | cut | cut -d: -f1 |
| Заменить или удалить символы | tr | tr -dc 0-9, tr -s ' ' |
Цепочка grep | awk часто сворачивается в один awk: grep 404 | awk '{print $7}' = awk '$9 == 404 {print $7}' — и второе точнее, потому что проверяет именно поле кода, а не «404» где угодно в строке.
-E), в одинарных кавычках, с опорой на окружение искомого-q, -o, -w, -F)s/шаблон/замена/g; правка файла — -i.bak, затем проверка и перезагрузка службыgrep -q-v для переменных, LC_ALL=C для чисел