# D5, часть 3. Bash, awk/sed, /proc (урок) Это не проверка, а урок: сначала разбираем механизм, потом решаешь задачи. Код в разборах можно копировать и запускать — это образец, а не готовый ответ на задачу. ## 1. Пайплайны и код возврата `cmd1 | cmd2 | cmd3` запускает три отдельных процесса одновременно, соединённых анонимными каналами (pipe) — `cmd2` начинает читать данные, ещё пока `cmd1` их пишет, ничего не буферизуется целиком в памяти между шагами. `$?` после пайплайна — это код возврата **последней** команды (`cmd3`), а не всего конвейера: если `cmd1` упал с ошибкой, а `cmd3` завершилась успешно, `$?` покажет 0 — ошибка `cmd1` потеряется молча. Чтобы увидеть код возврата каждой команды пайплайна отдельно, есть массив `PIPESTATUS`: `echo "${PIPESTATUS[@]}"` после `cmd1 | cmd2` печатает, например, `1 0` — `cmd1` упал, `cmd2` отработал нормально, хотя `$?` был бы 0. **Факты для карточек** - base | Чей код возврата хранит `$?` после `cmd1 | cmd2 | cmd3`? — только последней команды (`cmd3`) - core | Как узнать код возврата каждой команды пайплайна отдельно? — массив `PIPESTATUS` (`${PIPESTATUS[@]}`) Почему дальше: раз `$?` по умолчанию скрывает ошибки середины пайплайна, логично разобрать режимы `set`, которые меняют это поведение по умолчанию. ## 2. `set -euo pipefail`: что каждая буква включает и где ломается - **`-e`** — скрипт немедленно завершается, если любая команда вернула ненулевой код. - **`-u`** — обращение к неопределённой переменной — ошибка, а не пустая строка. - **`-o pipefail`** — код возврата пайплайна становится кодом **первой** упавшей команды в нём, а не только последней (закрывает дыру из раздела 1). `-e` не так надёжен, как кажется, — есть места, где он **не срабатывает**: - Команда внутри условия (`if cmd; then`, `while cmd; do`, после `&&`/`||`) — код возврата там ожидаем и проверяется явно, `-e` эту команду не трогает даже при ошибке. - Команда в подстановке `$(cmd)`, присвоенной переменной без немедленного использования результата в условии — ошибка внутри подстановки сама по себе не всегда останавливает внешний скрипт, поведение зависит от контекста использования результата. - Любая команда как часть пайплайна, кроме последней, — без `pipefail` `-e` реагирует только на код последней команды пайплайна (та же дыра, что и в разделе 1). **Ловушки** - Полагаться на `-e` внутри `if some_check; then ... fi` для остановки скрипта при ошибке `some_check` → скрипт не остановится, потому что команда — часть условия → ошибка проглатывается и обнаруживается заметно позже, в неожиданном месте. - Забыть `pipefail` → `grep pattern file | sort` при отсутствующем `file` вернёт 0 (потому что `sort` пустого потока отрабатывает успешно) → скрипт продолжит работу, как будто файл найден. **Факты для карточек** - base | Что делает `-e` в `set -euo pipefail`? — скрипт завершается сразу при ненулевом коде возврата любой команды - base | Что делает `-u`? — обращение к неопределённой переменной — ошибка вместо пустой строки - core | Что делает `pipefail` и какую проблему из раздела 1 это закрывает? — код возврата пайплайна = код первой упавшей команды, а не только последней; закрывает потерю ошибок середины пайплайна - core | В каком месте `-e` не остановит скрипт при ошибке команды? — если команда — часть условия (`if`, `while`, после `&&`/`||`) или не последняя команда пайплайна без `pipefail` Почему дальше: `set -u` ловит неопределённые переменные, но не спасает от другой частой причины сюрпризов — как bash разбивает строку на слова при подстановке без кавычек. ## 3. Кавычки, word splitting, IFS, массивы Без двойных кавычек bash после подстановки переменной делает **word splitting** — разбивает результат на отдельные слова по символам из `IFS` (по умолчанию пробел, таб, перевод строки) и затем **globbing** — раскрывает символы `*`, `?`, `[...]` как маски файлов. `"$var"` в двойных кавычках отключает оба шага — переменная передаётся как одна строка целиком, что почти всегда и нужно. ```bash file="my file.txt" rm $file # word splitting: rm воспринимает это как rm "my" "file.txt" — два аргумента rm "$file" # правильно: один аргумент "my file.txt" ``` `IFS` можно временно переопределить, чтобы разбить строку по нужному разделителю: ```bash IFS=',' read -ra parts <<< "a,b,c" # parts=(a b c) ``` Массивы: `arr=(a b c)`, обращение по индексу `${arr[1]}`, все элементы — `${arr[@]}` (каждый элемент — отдельное слово при развёртывании в кавычках, `"${arr[@]}"`), длина — `${#arr[@]}`. **Ловушки** - Забыть кавычки вокруг переменной с путём, содержащим пробел, → word splitting режет путь на несколько аргументов → «No such file or directory» на файле, который на самом деле есть. - `${arr[@]}` без кавычек при переборе `for x in ${arr[@]}` → элементы с пробелами внутри сами разбиваются на несколько слов → цикл обрабатывает не те элементы, что были в массиве. **Факты для карточек** - base | Что по умолчанию входит в IFS? — пробел, таб, перевод строки - core | Что отключают двойные кавычки вокруг `"$var"`? — word splitting и globbing (`*`/`?`/`[...]` не раскрываются) - core | Как правильно перебрать массив с элементами, содержащими пробелы? — `for x in "${arr[@]}"` (с кавычками) Почему дальше: помимо кода возврата команды и переменных окружения, у самого запущенного скрипта и его процессов есть ещё несколько специальных переменных и способ отреагировать на сигнал — `trap`. ## 4. `$?`, `$!`, `$$`, `trap` - **`$?`** — код возврата последней выполненной команды (см. раздел 1 про пайплайны). - **`$!`** — PID последнего запущенного в фоне процесса (`cmd &` затем `pid=$!`), нужен, чтобы потом сделать `wait $pid` или `kill $pid`. - **`$$`** — PID самого текущего скрипта/шелла, часто используется для уникальных временных файлов: `tmpfile="/tmp/out.$$"`. - **`trap`** — регистрирует обработчик на сигнал или на выход из скрипта: `trap 'rm -f "$tmpfile"' EXIT` гарантированно удалит временный файл при любом завершении скрипта — обычном, по ошибке (`-e`) или по сигналу (если сигнал перечислен), без ручного дублирования `rm` в каждой точке выхода. **Факты для карточек** - base | Что хранит `$!`? — PID последнего фонового процесса - base | Что хранит `$$`? — PID текущего скрипта/шелла - core | Зачем `trap ... EXIT` используют для временных файлов? — гарантирует очистку при любом пути завершения скрипта (успех, ошибка, сигнал), без дублирования кода в каждой точке выхода Почему дальше: `$$` в имени временного файла — это про один процесс; когда данные для команды нужно передать через аргументы, а не через имя файла, и данных много (список файлов, список PID), на сцену выходит `xargs`. ## 5. `xargs -0` `xargs` берёт поток строк со стандартного ввода и подставляет их как аргументы в конец указанной команды, разбивая по частям, если список слишком длинный для одного вызова (ограничение ОС на длину аргументов). По умолчанию `xargs`, как и bash, разбивает вход по пробельным символам — имена файлов с пробелами или переводами строк внутри (редко, но бывает) ломают такой разбор. `-0` — вход разделён не пробелами/переводами строк, а нулевыми байтами (`\0`), которые не могут встретиться внутри легального имени файла в Unix. Источник таких нулевых разделителей — `find ... -print0`: ```bash find . -name '*.tmp' -print0 | xargs -0 rm -f ``` Это единственная безопасная пара для случая, когда имена файлов заранее не гарантированы «простыми» (без пробелов, кавычек, переводов строк). **Ловушки** - `find ... | xargs rm` без `-print0`/`-0` на именах файлов с пробелами → имя разбивается на несколько «аргументов» → `rm` пытается удалить несуществующие файлы или удаляет не то. **Факты для карточек** - base | Чем `-0` в `xargs -0` отличается от поведения по умолчанию? — вход разделяется нулевыми байтами `\0` вместо пробелов/переводов строк - core | С какой опцией `find` обычно комбинируют `xargs -0`? — `-print0` Почему дальше: `xargs` передаёт список аргументов другой команде; сами команды для поиска и агрегации данных в потоке — отдельный набор утилит с конкретными флагами. ## 6. Поиск и агрегация: grep, sort, uniq - **`grep -c PATTERN`** — печатает не совпавшие строки, а их количество. - **`grep -o PATTERN`** — печатает только совпавшую часть строки, а не строку целиком (удобно вместе с `wc -l`, чтобы посчитать число вхождений, а не строк с хотя бы одним вхождением). - **`grep -v PATTERN`** — инвертирует фильтр, печатает строки, которые **не** совпали. - **`sort -k N`** — сортирует по N-му полю (по умолчанию разделитель — пробел), а не по всей строке целиком. - **`sort -n`** — числовая сортировка (`10` идёт после `9`, а не перед `2`, как при лексикографической сортировке строк по умолчанию). - **`sort -u`** — сортировка с одновременным удалением дубликатов (эквивалент `sort | uniq` в один проход). - **`uniq -c`** — схлопывает соседние повторяющиеся строки, добавляя счётчик повторов перед каждой строкой; работает только на **уже отсортированном** входе — несмежные повторы `uniq` не видит. ```bash awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -3 # топ-3 IP по числу запросов ``` **Ловушки** - Вызвать `uniq -c` без предварительного `sort` → повторы, не идущие подряд в исходном файле, не схлопнутся → счётчики занижены относительно реального числа повторов. - `sort` без `-n` на числовых полях (порты, счётчики) → `10 < 9` лексикографически → строки идут не в том порядке, который ожидался. **Факты для карточек** - base | Что печатает `grep -c`? — количество совпавших строк, а не сами строки - base | Почему `uniq -c` нужно применять после `sort`? — `uniq` схлопывает только соседние одинаковые строки, несмежные повторы не видит - core | Чем `sort -u` эквивалентен по результату? — `sort | uniq`, но за один проход Почему дальше: `grep`/`sort`/`uniq` работают со строками целиком или по одному полю через внешний ключ; когда нужна произвольная логика по колонкам с накоплением состояния (суммы, счётчики по ключу), нужен `awk`. ## 7. awk: поля, NR/NF, ассоциативные массивы `awk` читает вход построчно и автоматически разбивает каждую строку на поля по разделителю (`FS`, по умолчанию — пробел/таб): `$1`, `$2`, … — поля, `$0` — строка целиком, `NR` — номер текущей строки от начала потока, `NF` — число полей в текущей строке. ```bash awk '{sum[$1] += $2} END {for (k in sum) print k, sum[k]}' data.txt ``` `sum` здесь — ассоциативный массив: ключ `$1` (первое поле), значение — накапливаемая сумма второго поля. `awk` — полноценный язык с переменными, циклами и такими массивами, а не просто фильтр, поэтому в нём можно агрегировать данные по ключу за один проход, без промежуточных файлов. Почему `awk` на файле в 10⁶ строк быстрее bash-цикла построчного чтения с внешними командами внутри: `awk` — один процесс, который читает поток и делает линейный проход по строкам целиком внутри себя. Bash-цикл вида `while read -r line; do grep ... <<< "$line"; done < file` порождает (форкает) отдельный новый процесс **на каждую строку** — на миллионе строк это миллион `fork`+`exec`, каждый из которых на порядки дороже одной итерации внутреннего цикла `awk`. Разница — не в разы, а на порядки по времени выполнения. **Ловушки** - Обрабатывать большой лог циклом `for`/`while` с построчным чтением и вызовом `grep`/`awk` внутри цикла → минуты вместо секунд на файле в миллионы строк → видно по `time` на запуске скрипта; правильный подход — один проход `awk`/`sort` на весь поток целиком. **Факты для карточек** - base | Что означает `$1` в awk? — первое поле текущей строки - base | Что содержит `NR`? — номер текущей строки от начала потока - core | Почему awk на 1e6 строк быстрее bash-цикла с grep внутри? — awk — один процесс с линейным проходом по строкам, bash-цикл форкает новый процесс на каждую итерацию (миллион fork+exec против одного процесса) Почему дальше: `awk` агрегирует и печатает; когда нужно не посчитать, а точечно отредактировать текст по шаблону прямо в потоке, для этого — `sed`. ## 8. sed `sed` применяет команды редактирования к каждой строке потока и печатает результат — потоковый редактор, не интерактивный. Самая частая команда — замена по шаблону: `sed 's/OLD/NEW/'` (заменяет первое вхождение в строке), `sed 's/OLD/NEW/g'` (все вхождения в строке, флаг `g` = global). Без флага `-i` `sed` не меняет исходный файл, а печатает результат в stdout — `sed -i 's/OLD/NEW/g' file` редактирует файл на месте. ```bash sed -n '5,10p' file # напечатать только строки с 5 по 10 (-n подавляет вывод по умолчанию) sed '/^#/d' file # удалить строки, начинающиеся с # ``` **Факты для карточек** - base | Что делает флаг `g` в `s/OLD/NEW/g`? — заменяет все вхождения в строке, а не только первое - core | Чем `sed -i` отличается от `sed` без флага? — редактирует файл на месте вместо печати результата в stdout Почему дальше: `grep`/`awk`/`sed` работают с текстовыми потоками из файлов и команд; отдельный источник текстовых данных в Linux — это `/proc`, где ядро публикует состояние процессов и ресурсов в виде текстовых файлов. ## 9. `/proc`: что там реально лежит `/proc` — виртуальная файловая система: файлы в ней не лежат на диске, ядро генерирует их содержимое в реальном времени по запросу чтения. Ключевые пути: - **`/proc//status`** — читаемая человеком сводка о процессе: состояние, использование памяти (VmRSS и другие), UID/GID, число потоков. - **`/proc//cmdline`** — команда запуска процесса с аргументами, разделёнными нулевыми байтами `\0` (не пробелами — тот же принцип, что у `find -print0`, аргумент с пробелом внутри не спутать с границей между аргументами). - **`/proc//fd/`** — каталог, где каждый файл — символическая ссылка на открытый этим процессом файловый дескриптор (обычный файл, сокет, pipe); число ссылок в этом каталоге — реальное число открытых дескрипторов процесса прямо сейчас. - **`/proc//maps`** — карта отображений памяти процесса: диапазоны адресов, права доступа (r/w/x), какому файлу или сегменту (куча, стек, конкретная библиотека) принадлежит каждый диапазон. - **`/proc/cpuinfo`** — параметры процессора (модель, число ядер, флаги поддерживаемых инструкций). - **`/proc/meminfo`** — распределение оперативной памяти: занято, свободно, в кэше, в буферах (источник данных для `free -h`). - **`/proc//stat`** — машинно-читаемая (не для человека) строка с числовыми полями состояния процесса, включая, например, время в user/kernel-режиме; источник данных для `ps`/`top`. Утилиты вроде `ps`, `top`, `free`, `iostat` — это не отдельный механизм сбора данных, а тонкая обёртка над чтением этих же файлов `/proc`; ядро уже держит эти данные готовыми, специально «собирать» их не нужно. **Ловушки** - Читать `/proc//cmdline` как обычную строку с пробелами в качестве разделителей аргументов → аргумент вида `"two words"` неотличим от двух отдельных аргументов → нужен именно разбор по `\0`. **Факты для карточек** - base | Что такое /proc с точки зрения хранения данных? — виртуальная файловая система, содержимое генерируется ядром на лету, не хранится на диске - base | Чем разделены аргументы в /proc//cmdline? — нулевыми байтами `\0` - core | Что можно узнать из /proc//maps? — диапазоны адресов памяти процесса, права доступа (r/w/x) и к какому файлу/сегменту относится каждый диапазон - core | Откуда `free -h` берёт данные о памяти? — из /proc/meminfo Почему дальше: `/proc//fd/` показывает текущее число открытых дескрипторов процесса; верхнюю границу на это число задаёт `ulimit`. ## 10. `ulimit`: дескрипторы, core, стек `ulimit` задаёт лимиты ресурсов для текущего шелла и процессов, запущенных из него: - **`ulimit -n`** — максимальное число одновременно открытых файловых дескрипторов на процесс; упереться в этот лимит на нагруженном сервере — типичная причина ошибки «Too many open files». - **`ulimit -c`** — максимальный размер core dump файла; по умолчанию часто `0` (core dump отключён), поэтому перед тем как ловить редкий краш, выставляют `ulimit -c unlimited` — иначе программа упадёт, а файла с состоянием памяти на момент падения просто не будет. - **`ulimit -s`** — размер стека потока; именно этот лимит определяет, на какой глубине рекурсии процесс упадёт с переполнением стека (см. риск глубокой рекурсии при мемоизации в теме ДП). **Ловушки** - Ждать редкий краш под нагрузкой без `ulimit -c unlimited` заранее → программа падает, core dump не создаётся (лимит 0 по умолчанию) → единственный шанс поймать причину падения упущен, воспроизвести заново может быть нечем. **Факты для карточек** - base | Что ограничивает `ulimit -n`? — максимальное число открытых файловых дескрипторов на процесс - core | Почему перед отладкой редкого краша выставляют `ulimit -c unlimited`? — по умолчанию размер core dump часто ограничен нулём, без этого файл с состоянием памяти на момент падения не создастся Почему дальше: лимиты `ulimit` — это про ресурсы процесса; отдельная система ограничений — про то, кому вообще разрешено читать, писать и исполнять конкретный файл. ## 11. Права: `chmod`/`chown`, `umask` Права файла — три триады бит (владелец/группа/остальные) × (чтение r / запись w / выполнение x), каждая триада кодируется одной восьмеричной цифрой: r=4, w=2, x=1, сумма даёт цифру для триады. `755` = `rwxr-xr-x`: владелец — 7 (4+2+1, полный доступ), группа — 5 (4+1, чтение и выполнение без записи), остальные — 5 (то же самое). `chmod 755 file` выставляет эти права явно; `chown user:group file` меняет владельца и группу файла. `umask` — маска, которая **вычитается** из прав по умолчанию при создании нового файла/каталога (обычно каталоги создаются с базовым 777, файлы — с 666, дальше действует `umask`): при `umask 022` новый файл получает `666 & ~022 = 644` (`rw-r--r--`), новый каталог — `777 & ~022 = 755`. `umask` не меняет права существующих файлов — только права по умолчанию для новых. **Факты для карточек** - base | Расшифровка прав 755? — rwxr-xr-x (владелец: полный доступ, группа и остальные: чтение+выполнение) - base | Какие числа кодируют r, w, x? — r=4, w=2, x=1 - core | Что делает `umask 022` с правами нового файла по умолчанию (666)? — вычитает 022, получается 644 (rw-r--r--) Почему дальше: права управляют доступом к файлам статически; когда нужно понять, что процесс делает с файлами/сетью прямо сейчас, в динамике, нужны отдельные диагностические утилиты. ## 12. `strace`, `lsof`, `ss` как инструменты - **`strace -p PID`** (или `strace command`) — трассирует системные вызовы процесса построчно: какой syscall, с какими аргументами, что вернул. Способ увидеть, например, что процесс зависает именно на `read()` конкретного файлового дескриптора, а не где-то в логике приложения. - **`lsof -p PID`** — список файлов (в широком unix-смысле — включая сокеты, pipe), открытых процессом; тот же смысл, что и `/proc//fd/`, но в удобном для чтения формате с дополнительной информацией. - **`ss -tan`** — состояние TCP-сокетов (замена устаревшему `netstat`): адреса, порты, состояние соединения (`ESTABLISHED`, `TIME_WAIT` и так далее — см. состояния TCP). **Факты для карточек** - base | Что показывает `strace`? — системные вызовы процесса с аргументами и результатом, построчно - core | Чем `lsof -p PID` и `/proc//fd/` пересекаются по смыслу? — оба показывают список файловых дескрипторов, открытых процессом Ссылки на задачи этого дня: `tasks/08_bash` — разбор access-лога (`TOTAL`/`TOP`/`5XX`) на файле до миллиона строк; построчный bash-цикл не проходит по времени (раздел 7), нужен `awk`/`sort` за один-два прохода.
Проверь себя 1. Скрипт с `set -e` содержит `if grep -q pattern file; then echo found; fi`, и `grep` не находит `pattern` (возвращает 1). Остановится ли скрипт на этой строке?
ОтветНет — команда внутри условия `if` не подчиняется `-e`, даже если она вернула ненулевой код; это одно из исключений `-e`, скрипт продолжит выполнение дальше.
2. `grep pattern missing_file.txt | wc -l` при отсутствующем файле напечатает `0`, а `$?` пайплайна без `pipefail` будет `0`. Как обнаружить, что реальная проблема — отсутствующий файл, а не отсутствие совпадений?
ОтветВключить `set -o pipefail` — тогда код возврата пайплайна станет кодом первой упавшей команды (`grep` на несуществующем файле), а не только `wc -l`; либо проверить `${PIPESTATUS[0]}` явно после пайплайна.
3. На файле в миллион строк нужно посчитать число запросов на каждый IP-адрес. Почему решение через `while read -r line; do ...; done < access.log` с вызовом `awk`/`grep` внутри цикла будет на порядки медленнее, чем `awk '{print $1}' access.log | sort | uniq -c`?
ОтветЦикл на bash с внешней командой внутри порождает (fork+exec) новый процесс на каждую из миллиона строк; связка `awk | sort | uniq -c` — это фиксированное малое число процессов (по одному на каждый элемент конвейера), каждый из которых делает один линейный проход по всему потоку целиком.
4. Процесс с `ulimit -c 0` неожиданно падает по SIGSEGV под нагрузкой, воспроизвести падение по требованию не получается. Что нужно было сделать заранее, чтобы разобрать причину постфактум?
ОтветВыставить `ulimit -c unlimited` до запуска процесса — тогда при падении ядро сохранит core dump со снимком памяти на момент сбоя, и его можно будет открыть позже (`gdb prog core`), не дожидаясь повторного воспроизведения краша.
## Материалы - man7.org, `proc(5)` — https://man7.org/linux/man-pages/man5/proc.5.html - man7.org, `getrlimit(2)` — https://man7.org/linux/man-pages/man2/getrlimit.2.html - man7.org, `ptrace(2)` — https://man7.org/linux/man-pages/man2/ptrace.2.html - man7.org, `chmod(2)` — https://man7.org/linux/man-pages/man2/chmod.2.html - man7.org, `umask(2)` — https://man7.org/linux/man-pages/man2/umask.2.html - man7.org, `signal(7)` — https://man7.org/linux/man-pages/man7/signal.7.html