Text Processing в Linux

Text Processing — обработка текстовых потоков стандартными утилитами Linux. Команды можно применять к файлам или объединять через пайп |: стандартный вывод одной команды становится стандартным вводом следующей.

grep 'ERROR' app.log | sort | uniq -c | sort -nr

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


grep и egrep

grep выводит строки, соответствующие шаблону.

grep [опции] 'шаблон' файл

Примеры:

grep 'ERROR' app.log                 # найти ERROR
grep -i 'warning' app.log            # игнорировать регистр
grep -n 'timeout' app.log            # показать номера строк
grep -v '^#' config.conf             # исключить комментарии
grep -c 'ERROR' app.log               # количество строк
grep -Rni 'listen' /etc/nginx         # рекурсивный поиск
grep -w 'root' /etc/passwd            # целое слово
grep -F 'a.b[c]' file.txt             # буквальная строка
grep -C 3 'ERROR' app.log             # контекст до и после
grep -q '^enabled=true$' app.conf      # тихая проверка

Основные опции:

Опция Назначение
-i Игнорировать регистр
-v Инвертировать условие
-n Показать номера строк
-c Подсчитать подходящие строки
-l Показать только имена файлов
-r, -R Рекурсивный поиск
-w Совпадение целого слова
-x Совпадение всей строки
-E Расширенные регулярные выражения
-F Буквальный шаблон без regex
-o Только совпавшие фрагменты
-q Не выводить совпадения
-A N N строк после совпадения
-B N N строк перед совпадением
-C N Контекст с обеих сторон

grep возвращает код 0, если совпадение найдено, 1 — если совпадений нет, и значение больше 1 при ошибке:

if grep -q '^ENABLED=yes$' settings.conf; then
    echo 'Функция включена'
fi

egrep

egrep — историческое имя режима расширенных регулярных выражений. В новых командах и скриптах следует использовать grep -E:

grep -E 'ERROR|WARN|CRITICAL' app.log

Регулярные выражения

Регулярное выражение описывает шаблон текста. Обычный grep использует базовые выражения BRE, а grep -E — расширенные ERE.

Элемент Значение
. Любой один символ
^ Начало строки
$ Конец строки
[abc] Один символ из набора
[^abc] Символ вне набора
[0-9] Символ из диапазона
* Ноль или больше повторений
+ Один или больше повторений в ERE
? Ноль или одно повторение в ERE
{n,m} От n до m повторений в ERE
` `
( ) Группа в ERE
\ Экранирование
grep '^ERROR' app.log                  # начало строки
grep '\.json$' files.txt               # конец на .json
grep -E '^[0-9]+$' values.txt          # только цифры
grep -Ei 'error|warning' app.log       # один из вариантов
grep -E '^[A-Z]{2}-[0-9]{4}$' ids.txt # формат AB-1234

Классы POSIX:

[[:digit:]]  цифра
[[:alpha:]]  буква
[[:alnum:]]  буква или цифра
[[:space:]]  пробельный символ
[[:lower:]]  строчная буква
[[:upper:]]  заглавная буква

Например, комментарий с необязательными пробелами в начале:

grep -E '^[[:space:]]*#' config.conf

Glob-шаблоны оболочки и regex — разные конструкции. *.log выбирает имена файлов в shell, а регулярное выражение для окончания .log выглядит как \.log$.


sed — потоковый редактор

sed читает текст построчно, преобразует его и выводит результат. Без -i исходный файл не меняется.

sed [опции] 'команда' файл

Замена

sed 's/old/new/' file.txt        # первое совпадение в строке
sed 's/old/new/g' file.txt       # все совпадения
sed -n 's/old/new/gp' file.txt   # только изменённые строки
sed 's|/old/path|/new/path|g' paths.txt

Изменение файла с резервной копией:

sed -i.bak 's/DEBUG=true/DEBUG=false/' app.conf

Перед -i рекомендуется проверить команду без изменения файла. Синтаксис -i может немного различаться между GNU и BSD sed.

Выбор и удаление строк

sed -n '5p' file.txt                       # пятая строка
sed -n '10,20p' file.txt                  # строки 10–20
sed -n '/BEGIN/,/END/p' file.txt           # диапазон шаблонов
sed '/^[[:space:]]*$/d' file.txt           # убрать пустые строки
sed '/^[[:space:]]*#/d' config.conf        # убрать комментарии
sed -E 's/[[:space:]]+/ /g' file.txt       # сжать пробелы

Основные команды:

Команда Назначение
s/a/b/ Замена
p Печать строки
d Удаление строки из результата
a\text Добавление после строки
i\text Вставка перед строкой
c\text Замена выбранной строки
q Завершение обработки

Группы и обратные ссылки:

printf '%s\n' 'Ivan Petrov' | sed -E 's/^([^ ]+) ([^ ]+)$/\2, \1/'

Результат: Petrov, Ivan. Ссылки \1, \2 соответствуют найденным группам.


awk — обработка структурированного текста

awk рассматривает вход как записи и поля. По умолчанию запись — строка, а поля разделены пробелами или табуляцией.

$0   вся строка
$1   первое поле
$2   второе поле
$NF  последнее поле
NF   число полей
NR   номер строки во всём потоке
FNR  номер строки текущего файла

Общий синтаксис:

awk 'условие { действие }' файл

Поля и разделители

awk '{ print $1 }' file.txt
awk '{ print NR ":", $0 }' file.txt
awk -F: '{ print $1, $7 }' /etc/passwd
awk -F: 'BEGIN { OFS="\t" } { print $1, $3, $7 }' /etc/passwd

-F: задаёт входной разделитель, а OFS — разделитель вывода.

Условия и вычисления

awk '$3 > 100 { print $1, $3 }' data.txt
awk '/ERROR/ { print NR, $0 }' app.log
awk '$2 == "ERROR" && $3 >= 500 { print }' events.txt
awk '{ sum += $2 } END { print sum }' values.txt
awk '{ sum += $2; n++ } END { if (n) print sum / n }' values.txt

BEGIN выполняется до чтения данных, END — после:

awk 'BEGIN { print "START" } { print NR, $0 } END { print "Строк:", NR }' file

Форматированный вывод

awk -F, 'NR > 1 { printf "%-20s %10.2f\n", $1, $3 }' report.csv

Полезные функции:

awk '{ print length($0) }' file.txt
awk '{ print toupper($0) }' file.txt
awk '{ gsub(/old/, "new"); print }' file.txt

Подсчёт значений через ассоциативный массив:

awk '{ count[$1]++ }
     END { for (value in count) print count[value], value }' data.txt

Передача переменной из shell:

limit=100
awk -v min="$limit" '$2 >= min { print }' values.txt

Простой awk -F, не является полноценным CSV-парсером: кавычки, запятые внутри полей и многострочные значения требуют специализированного инструмента.


cut, sort, uniq, wc, tr

cut

cut выбирает поля или позиции символов:

cut -d: -f1 /etc/passwd       # первое поле
cut -d: -f1,7 /etc/passwd     # поля 1 и 7
cut -d, -f2-4 data.csv        # поля 2–4
cut -c1-10 file.txt           # символы 1–10

-d принимает простой разделитель. Для переменного количества пробелов и более сложной логики удобнее awk.

sort

sort names.txt
sort -r names.txt                   # обратный порядок
sort -n numbers.txt                 # числа
sort -k2,2nr report.txt             # второе поле, число, убывание
sort -t: -k3,3n /etc/passwd         # UID как число
sort -h sizes.txt                   # 10K, 20M, 1G
sort -V versions.txt                # версии

Результат зависит от локали. Для воспроизводимой побайтовой сортировки:

LC_ALL=C sort file.txt

Не записывайте результат через sort file > file: оболочка очистит файл до запуска команды. Используйте:

sort file.txt -o file.txt

uniq

uniq объединяет только соседние одинаковые строки, поэтому обычно используется после sort:

sort values.txt | uniq
sort values.txt | uniq -c          # количество повторений
sort values.txt | uniq -d          # только повторяющиеся
sort values.txt | uniq -u          # только неповторяющиеся
sort values.txt | uniq -c | sort -nr

Удаление дублей с сохранением порядка первых появлений:

awk '!seen[$0]++' file.txt

wc

wc -l file.txt     # строки
wc -w file.txt     # слова
wc -c file.txt     # байты
wc -m file.txt     # символы
wc -L file.txt     # длина самой длинной строки

Подсчёт ошибок:

grep -c 'ERROR' app.log
# или
grep 'ERROR' app.log | wc -l

wc -l фактически считает переводы строк, поэтому последняя строка без \n может не попасть в результат.

tr

tr заменяет, удаляет или сжимает символы и обычно читает stdin:

tr '[:lower:]' '[:upper:]' < file.txt
printf '%s\n' 'one,two,three' | tr ',' '\n'
tr -d '\r' < windows.txt > unix.txt
tr -s ' ' < file.txt
tr -s '[:space:]' ' ' < file.txt

tr работает с символами, а не с регулярными выражениями. Для шаблонной замены используйте sed или awk.


head, tail и tail -f

head показывает начало файла, tail — конец. По умолчанию выводится десять строк.

head file.txt
head -n 20 file.txt
head -c 100 file.bin

tail file.txt
tail -n 50 app.log
tail -n +100 file.txt        # начиная со строки 100

Наблюдение за дописываемым логом:

tail -f app.log

Последние 100 строк и дальнейшее наблюдение:

tail -n 100 -f app.log

Фильтрация новых строк в GNU-системах:

tail -f app.log | grep --line-buffered 'ERROR'

При ротации лога удобнее следить за именем файла:

tail -F app.log

-F повторно открывает файл, если он был заменён или временно исчез. Остановка наблюдения — Ctrl+C.

Для журналов systemd часто применяют:

journalctl -f
journalctl -u nginx -f

Пайплайны обработки текста

Пайп | связывает команды:

команда1 | команда2 | команда3

Частые оболочки пользователей

awk -F: '{ print $7 }' /etc/passwd | sort | uniq -c | sort -nr

Частота слов

tr '[:upper:]' '[:lower:]' < document.txt \
  | tr -cs '[:alnum:]' '\n' \
  | sed '/^$/d' \
  | sort \
  | uniq -c \
  | sort -nr \
  | head -n 20

Сохранение промежуточного результата через tee

grep 'ERROR' app.log | tee errors.txt | wc -l

tee -a добавляет данные в файл, а не перезаписывает его.

Запись в защищённый файл:

printf '%s\n' 'key=value' | sudo tee /etc/example.conf > /dev/null

Безопасная обработка имён файлов

Имена файлов могут содержать пробелы и переводы строк. Используйте нулевой разделитель:

find . -type f -name '*.tmp' -print0 | xargs -0 rm --

Или передавайте аргументы напрямую:

find . -type f -name '*.tmp' -exec rm -- {} +

Перед удалением сначала проверьте результат с -print.

Ошибки в пайплайнах

Обычно код пайплайна определяется последней командой. В Bash включите pipefail, если ошибка любого этапа должна считаться ошибкой:

set -o pipefail

В скриптах часто встречается:

set -euo pipefail

Эти режимы имеют нюансы, поэтому ожидаемые ненулевые коды нужно обрабатывать явно.


Работа с логами

Логи могут храниться в /var/log, каталоге приложения, контейнере или журнале systemd. Конкретные файлы зависят от дистрибутива и настроек:

/var/log/syslog
/var/log/messages
/var/log/auth.log
/var/log/secure
/var/log/nginx/access.log
/var/log/nginx/error.log

Просмотр и поиск

tail -n 100 /var/log/nginx/error.log
tail -F /var/log/nginx/error.log
grep -Ei 'error|fail|critical|panic' app.log
grep -Ei -C 3 'error|critical' app.log
grep 'ERROR' app.log | grep -v 'known harmless message'

Анализ HTTP access log

Для распространённого combined log format IP обычно находится в поле 1, путь — в поле 7, код — в поле 9. Формат нужно сначала проверить:

head -n 5 access.log

Частые IP:

awk '{ print $1 }' access.log | sort | uniq -c | sort -nr | head

Частые пути:

awk '{ print $7 }' access.log | sort | uniq -c | sort -nr | head

Коды ответа:

awk '{ print $9 }' access.log \
  | grep -E '^[1-5][0-9]{2}$' \
  | sort | uniq -c | sort -nr

Позиции полей могут быть другими при пользовательском формате логирования.

Сжатые логи

zgrep 'ERROR' app.log.gz
zcat app.log.gz | awk '/ERROR/ { count++ } END { print count }'

При анализе больших файлов сначала ограничивайте объём точным фильтром, tail или временным диапазоном. sort может потребовать значительный объём памяти и временного места.


jq — обработка JSON

jq разбирает JSON, фильтрует значения и создаёт новые структуры.

Пример users.json:

{
  "users": [
    {"id": 1, "name": "Anna", "active": true, "roles": ["admin"]},
    {"id": 2, "name": "Boris", "active": false, "roles": ["viewer"]}
  ]
}

Форматирование и выбор данных

jq '.' users.json
jq '.users' users.json
jq '.users[0]' users.json
jq '.users[0].name' users.json
jq '.users[]' users.json
jq -r '.users[].name' users.json

-r выводит строки без JSON-кавычек.

Фильтрация и преобразование

jq '.users[] | select(.active)' users.json
jq -r '.users[] | select(.active) | .name' users.json
jq '.users[] | {id, name}' users.json
jq '[.users[] | select(.active) | {id, name}]' users.json
jq '.users | sort_by(.name)' users.json
jq '.users | length' users.json

Значение по умолчанию:

jq '.user.email // "email не указан"' profile.json

Переменные shell

Строка:

name='Anna'
jq --arg name "$name" '.users[] | select(.name == $name)' users.json

Число или готовое JSON-значение:

minimum=2
jq --argjson minimum "$minimum" '.users[] | select(.id >= $minimum)' users.json

Не вставляйте значение shell-переменной непосредственно в выражение jq.

CSV и TSV

jq -r '.users[] | [.id, .name, .active] | @tsv' users.json
jq -r '.users[] | [.id, .name, .active] | @csv' users.json

JSON Lines и логи

Для файла с одним JSON-объектом в строке:

jq -r 'select(.level == "ERROR") | .message' app.jsonl
jq -r '.level' app.jsonl | sort | uniq -c | sort -nr
tail -F app.jsonl | jq --unbuffered -r 'select(.level == "ERROR") | .message'

Проверка JSON

jq -e empty response.json > /dev/null

Проверка условия в скрипте:

if jq -e '.status == "ok"' response.json > /dev/null; then
    echo 'Операция выполнена'
fi

jq не изменяет исходный файл автоматически. Безопасная запись:

jq '.settings.debug = false' config.json > config.json.tmp \
  && mv config.json.tmp config.json

Типичные ошибки

Лишний cat

cat file.txt | grep 'ERROR'

Проще:

grep 'ERROR' file.txt

Отсутствие кавычек

grep -- "$pattern" file.txt

Если текст должен восприниматься буквально:

grep -F -- "$text" file.txt

Ожидание глобальной уникальности от uniq

sort file.txt | uniq

Без сортировки удаляются только соседние повторы.

Разбор сложного формата простым разделителем

Регулярные выражения не заменяют парсер JSON, а awk -F, — полноценный CSV-парсер. Выбирайте инструмент, понимающий формат данных.

Немедленное изменение оригинала

Сначала проверьте результат sed или jq, затем создайте резервную копию и только после этого заменяйте файл.


Краткая памятка

Задача Команда
Найти текст grep 'text' file
Найти буквальную строку grep -F 'a.b' file
Использовать ERE `grep -E 'a
Рекурсивный поиск grep -Rni 'text' dir
Заменить текст sed 's/old/new/g' file
Вывести строки 10–20 sed -n '10,20p' file
Вывести первое поле awk '{ print $1 }' file
Разделитель : awk -F: '{ print $1 }' file
Выбрать поля cut -d: -f1,7 /etc/passwd
Числовая сортировка sort -n file
Подсчитать повторы `sort file
Подсчитать строки wc -l file
Изменить регистр tr '[:lower:]' '[:upper:]'
Начало файла head -n 20 file
Конец файла tail -n 20 file
Следить за логом tail -F app.log
Форматировать JSON jq '.' file.json
Извлечь строку JSON jq -r '.name' file.json

Рекомендации