Учебные материалы: механизм вместо тезисов, блоки «Факты для карточек», Ловушки (Claude Code) + diag/cards_src.tsv

This commit is contained in:
Kodlo-chan
2026-09-24 13:38:58 +07:00
parent e0ad8e0fee
commit 4259fbce75
17 changed files with 1688 additions and 115 deletions
+121 -1
View File
@@ -11,7 +11,8 @@ TOP <ip> <число запросов>
5XX <число ответов со статусом 500-599>
```
Правила:
## Правила формата
- IP — первое поле строки;
- TOP — три самых частых IP по убыванию числа запросов; при равенстве — по возрастанию
строки IP (лексикографически);
@@ -20,6 +21,125 @@ TOP <ip> <число запросов>
по времени — нужен awk/sort или один-два прохода;
- пустой файл: `TOTAL 0`, `5XX 0`, строк TOP нет.
**Факты для карточек**
- base | Сколько строк TOP выводится, если уникальных IP меньше трёх? — столько, сколько есть уникальных IP
- base | Что печатает скрипт для пустого файла? — `TOTAL 0`, `5XX 0`, без строк TOP
- core | По какому полю строки лога определяется IP? — по первому полю
Почему дальше: раз файл может быть на миллион строк, нужно понять, почему построчный
`while read` в bash не укладывается по времени и что вместо этого реально делает работу
за O(n) без интерпретации каждой строки внутри самого bash.
## Механизм: почему не построчный цикл, а awk/sort
Построчный `while read line; do ... done < file` на миллионе строк запускает bash-интерпретатор
заново на каждую итерацию цикла (разбор строки, вызов внешних утилит вроде `cut`/`grep` из
цикла добавляет ещё и `fork+exec` процесса на каждую строку) — при миллионе строк это
миллион запусков процессов, что на порядки медленнее одного прохода специализированной
утилитой. `awk` и `sort` — скомпилированные бинарники, которые сами читают файл целиком
за один проход (awk) без порождения процесса на строку, и делают всю агрегацию (подсчёт по
IP, подсчёт 5xx, TOTAL) внутри одного вызова.
Практическая схема: один проход `awk` по файлу одновременно считает `TOTAL` (число строк),
инкрементирует счётчик по IP в ассоциативном массиве и считает строки с кодом 500–599
(код ответа — обычно отдельное поле в access.log), а на выходе печатает пары `IP count`.
Дальше `sort` сортирует эти пары по счётчику по убыванию, а при равенстве — по IP по
возрастанию (составной ключ сортировки), и `head -3` берёт первые три строки. Итог — два
прохода по данным (awk-агрегация, потом sort уже по маленькому списку уникальных IP, а не
по миллиону исходных строк), а не миллион запусков процессов.
**Факты для карточек**
- core | Почему `while read line` в bash медленный на миллионе строк? — каждая итерация — это работа интерпретатора bash, а вызов внешних утилит из цикла — ещё и `fork+exec` на строку
- core | Что даёт связка awk + sort вместо построчного цикла? — один проход по файлу целиком специализированным бинарником вместо миллиона запусков процессов
- deep | По какому полю сортируется список IP при равенстве числа запросов? — по возрастанию строки IP (вторичный ключ сортировки)
Почему дальше: раз скрипт нетривиальный (несколько шагов, внешние утилиты, граничный случай
с пустым файлом), в нём легко замаскировать ошибку — отсюда `set -e` и его реальные границы.
## `set -e` и где он реально ломается
`set -e` (`errexit`) останавливает скрипт при первой команде, вернувшей ненулевой код
возврата, вместо того чтобы по умолчанию молча идти дальше — это отклонение от исторического
поведения bash, оптимизированного под интерактивную сессию, где одна неудачная команда не
должна обрывать сессию целиком. `set -o pipefail` отдельно нужен для конвейеров (`awk ... |
sort | head`): без него код возврата конвейера — это код возврата только последней команды
(`head`), и падение `awk` или `sort` посередине останется незамеченным, даже если `set -e`
включён — сам по себе `-e` конвейер как единое целое не покрывает.
`set -e` **не срабатывает**, если неудачная команда стоит частью условия — в `if`, `while`,
`until`, слева или справа от `&&`/`||`, либо после `!` — потому что в этих позициях код
возврата команды явно проверяется вызывающим кодом, а не игнорируется, и bash считает это
ожидаемым путём выполнения, а не аварией.
Отдельная и менее очевидная ловушка — **команда в присваивании переменной внутри `local`**:
```bash
local total=$(wc -l < "$file") # опасно под set -e
```
Здесь исполняются фактически две команды: `wc -l` внутри подстановки `$(...)` и сам `local`.
Если `wc -l` завершится с ошибкой, `set -e` эту ошибку не поймает, потому что итоговый код
возврата всей строки — это код возврата `local`, а `local` возвращает 0 (успех) сам по себе,
даже если команда внутри `$(...)` упала. Подстановка «теряется» внутри составной команды.
Лечится разделением на две строки:
```bash
total=$(wc -l < "$file") # код возврата — это код возврата wc -l, set -e сработает
local total
```
**Ловушки**
- Полагаться на голый `set -e` для пайплайна `awk | sort | head` → падение `awk` посередине
незаметно, `head` вернёт 0 → нужен `set -o pipefail`, видно только по неверному выводу,
не по коду возврата.
- `local var=$(cmd)` в одну строку → ошибка `cmd` маскируется кодом возврата `local` (всегда 0
для валидного объявления) → `set -e` не остановит скрипт, видно по неверному/пустому `var`
без явной ошибки.
- Не обработать пустой файл отдельно → `sort`/`head` на пустом входе не печатают строк TOP,
но если код по ошибке считает `TOTAL` через конвейер с `wc -l | ...`, легко перепутать
0 строк с ошибкой чтения файла.
- Не заквотить `"$file"` → путь с пробелом разбивается на несколько слов → скрипт падает
на несуществующем файле или обрабатывает не тот аргумент.
**Факты для карточек**
- base | Что делает `set -e`? — останавливает скрипт при первой команде с ненулевым кодом возврата
- core | В каких позициях `set -e` не останавливает скрипт при ошибке команды? — в условиях `if`/`while`/`until`, слева/справа от `&&`/`||`, после `!`
- core | Почему `local var=$(cmd)` не ловится `set -e`, если `cmd` упал? — итоговый код возврата строки — это код возврата `local`, а он 0 даже при упавшей подстановке внутри
- core | Зачем `set -o pipefail` отдельно от `set -e`? — без него код возврата конвейера — это код возврата только последней команды, падение команды посередине конвейера остаётся незамеченным
## Проверка
Запуск: `bash solution.sh access.log`
Проверка: `python3 grade.py 08` (тест гоняет скрипт на фикстуре и на большом логе).
Критерий: точное совпадение вывода, код возврата 0.
**Факты для карточек**
- base | Каким кодом возврата должен завершаться `solution.sh` при успехе? — 0
- core | Что именно сверяет `grade.py 08` с эталоном? — точное совпадение вывода (все четыре строки) на фикстуре и на большом логе
<details>
<summary>Проверь себя</summary>
1. Почему пайплайн `grep 500 access.log | wc -l` под одним `set -e` (без `pipefail`) может
молча пропустить ошибку `grep` (например, если файл не существует)?
<details><summary>Ответ</summary>Код возврата конвейера по умолчанию — это код возврата
последней команды (`wc -l`), которая отработает и вернёт 0, даже если `grep` перед ней
упал с ошибкой чтения файла; нужен `pipefail`, чтобы код возврата конвейера отражал первую
упавшую команду.</details>
2. Почему `local total=$(false)` не остановит скрипт при `set -e`, а `total=$(false)` (без
`local` на той же строке) — остановит?
<details><summary>Ответ</summary>В первом случае итоговый код возврата строки — это код
возврата встроенной команды `local`, которая возвращает 0 при корректном синтаксисе
объявления независимо от результата подстановки внутри; во втором случае код возврата
присваивания — это код возврата самой подстановки `$(false)`, то есть 1, и `set -e`
сработает.</details>
3. Почему построчный `while read ip status rest; do ...; done < access.log` с миллионом строк
не проходит по времени, даже если внутри цикла нет вызовов внешних утилит?
<details><summary>Ответ</summary>Каждая итерация цикла — это работа самого интерпретатора
bash (разбор строки, обновление переменных, проверка условий), и миллион таких итераций на
порядки медленнее одного прохода скомпилированным awk, который читает и агрегирует файл
целиком за один запуск процесса.</details>
</details>