Unix-философия: do one thing well
Если читать любую книгу по Unix или Linux, на третьей странице вы встретите фразу «Unix philosophy». Это не просто красивые слова — это вполне конкретный набор принципов, сформулированных Doug McIlroy в 1978 году, которые объясняют, почему ls | grep | sort | head работает так хорошо, а аналог в Windows PowerShell — куда более громоздкий.
В этом уроке разберём, что именно McIlroy сказал, почему это правило живёт уже 50 лет, и как оно отражается в каждой команде bash, которую вы будете писать.
Doug McIlroy 1978
Doug McIlroy — один из инженеров Bell Labs, где в 1969 году появился Unix. Он же изобрёл pipes (|) — может быть, главное изобретение Unix. В 1978 году в Bell System Technical Journal он сформулировал философию проекта в одном абзаце:
«Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new features.»
«Expect the output of every program to become the input to another, as yet unknown, program.»
«Don’t insist on interactive input.»
Перевод и пояснение:
- Делай одну вещь, делай её хорошо. Если программа умеет 100 вещей — ни одну не делает по-настоящему хорошо.
- Ожидай, что вывод твоей программы пойдёт в другую программу. Делай вывод machine-readable (текстом, по строкам), а не художественным форматированием.
- Не настаивай на интерактивном вводе. Программа должна работать в pipeline без участия человека.
Из этих трёх принципов выросло почти всё, что вы будете делать в bash. Давайте на конкретных примерах.
Принцип 1: одна программа — одна задача
В Unix вы не найдёте программу «утилита для работы с файлами», которая умеет копировать, переименовывать, удалять, искать и сортировать. Вместо этого есть:
Каждая утилита маленькая (ls — около 5000 строк C-кода, cat — около 600 строк). Каждая делает одну вещь, и делает её хорошо: десятилетиями оттачивались опции, обработка edge cases, производительность.
Сравните это с PowerShell. Там есть один cmdlet Get-ChildItem, который умеет всё: список файлов, фильтрация, сортировка, рекурсивный обход, форматирование вывода. Внутри — десятки параметров. Сложно запомнить, сложно отладить.
Unix-way: маленькие инструменты + способ их соединять. Это даёт композицию: из 20 базовых утилит можно собрать миллион полезных pipeline’ов.
Принцип 2: text streams как universal interface
Программы в Unix общаются через текст. Это не случайно — это сознательный выбор.
Когда ls выдаёт список файлов, она выдаёт его текстом, по одной строке:
$ ls /etc | head -5
adduser.conf
alternatives
apparmor.d
apt
bash.bashrc
Когда grep принимает вход, она читает текст построчно. Когда sort принимает вход — то же самое. Когда wc считает — построчно.
Это позволяет соединять что угодно с чем угодно:
$ ls /etc | grep '^a' | sort | head -3
adduser.conf
alternatives
apparmor.d
Что произошло:
ls /etcвыдал список текстом.grep '^a'отфильтровал строки, начинающиеся сa.sortотсортировал (lexicographically).head -3взял первые три.
Каждая программа не знает о других. Она просто читает текст из stdin и пишет текст в stdout. Pipe (|) соединяет stdout одной программы с stdin следующей.
Это — фундаментальная идея, на которой построен Linux. Когда DE парсит логи, чтобы найти ошибки за последний час:
$ grep ERROR /var/log/airflow/worker.log | grep "$(date +'%Y-%m-%d %H')" | wc -l
Это четыре независимые программы, соединённые pipes. Никто не писал «программу для подсчёта ошибок за час в логах Airflow» — она собралась из деталей.
Принцип 3: машиночитаемый вывод
«Не настаивай на интерактивном вводе» — это и про обратную сторону. Не делай вывод человеко-ориентированным, если он может быть использован программно.
Сравните два вывода ls:
# Без аргументов — компактно, для глаз
$ ls /etc | head -3
adduser.conf
alternatives
apparmor.d
# С -la — детально, всё ещё машиночитаемо
$ ls -la /etc | head -3
total 1296
drwxr-xr-x 1 root root 4096 May 13 14:00 .
drwxr-xr-x 1 root root 4096 May 13 14:00 ..
Строки идут одна за другой, разделители — пробелы и табы. Это парсится любым awk, sed, cut. Если бы ls выдавал ASCII-art таблицу с рамками и подсветкой — это бы парсилось гораздо хуже.
Современные tools иногда нарушают этот принцип ради красивого вывода (eza с цветами, bat с подсветкой синтаксиса). Но они умные: когда обнаруживают, что stdout — это pipe, а не терминал, переключаются на plain mode.
# В терминале — цветной вывод
$ eza -la
# В pipe — plain text без цветов
$ eza -la | head -5
Это называется «is stdout a tty?» детект. Старая Unix-традиция: красиво в терминале, плоско в pipe.
Когда пишете свои скрипты, придерживайтесь того же. printf "%s\t%s\n" "$name" "$size" — это машиночитаемо. echo "Файл $name размером $size байт" — нет. В будущих модулях про bash-скрипты мы будем явно следовать этому принципу.
Откуда взялось это всё
В 1969 году Ken Thompson и Dennis Ritchie в Bell Labs делали Unix. Компьютер был слабый (PDP-7, 8 KB памяти), но они захотели сделать многозадачную систему. Ограничения железа вынудили их делать маленькие программы — иначе ничего не помещалось в память.
McIlroy в 1973-м сделал концептуальный прорыв — добавил pipes как операционно-системную примитив. Идея: одна программа пишет в специальный buffer ядра, другая читает оттуда. Через эту трубу программы общаются без знания друг о друге.
Эту идею в 1978-м McIlroy и сформулировал как философию. К тому моменту Unix уже распространился по университетам и компаниям, и философия закрепилась как культурный стандарт.
Сегодня каждый Linux-сервер, каждый macOS-ноутбук, каждый Android-смартфон под капотом следуют этой философии. Когда вы пишете cat access.log | grep 404 | awk '{print $7}' | sort | uniq -c | sort -rn | head — вы делаете именно то, о чём McIlroy писал 50 лет назад.
Где философия нарушается (и почему это OK)
Любая философия имеет исключения. В Linux они тоже есть.
Это не плохо — это компромисс. systemd упрощает управление сервисами за счёт нарушения «do one thing». Docker одной командой делает то, что иначе требовало бы 10 утилит. ffmpeg покрывает все мыслимые сценарии видео.
Когда задача сложная и требует много контекста, монолитный инструмент выигрывает. Когда задача простая и хорошо ложится на «вход -> выход» — побеждают мелкие утилиты с pipe.
Для Junior DE: на 80% задач используются маленькие unix-утилиты с pipes. На 20% — большие инструменты типа Docker. Освоить надо и то, и другое.
Why this matters for DE
Вся работа DE — это data flow. Файл с данными -> читаем -> парсим -> фильтруем -> агрегируем -> пишем результат. Это буквально pipeline.
Unix-философия даёт нам готовый набор кирпичей для этого:
Такой pipeline:
- работает на любой Linux-машине без установки чего-либо лишнего;
- читает данные на лету, не загружая всё в память;
- легко модифицируется (поменяйте jq-выражение — получите другую трансформацию);
- запускается через cron в одну строку.
Это — сила Unix-философии. Сделать это же на Python заняло бы 50 строк, потребовало бы requests, pandas, и десяток зависимостей. И запускалось бы дольше.
Попробуй сам
Запустите классический pipeline для анализа логов:
$ mkdir -p ~/linux-sandbox/lesson-philosophy
$ cd ~/linux-sandbox/lesson-philosophy
# Создаём фейковый лог
$ cat > access.log <<'EOF'
192.168.1.1 GET /index.html 200
192.168.1.2 GET /about.html 200
192.168.1.1 GET /api/users 500
192.168.1.3 POST /login 200
192.168.1.2 GET /index.html 200
192.168.1.1 GET /api/users 500
192.168.1.4 GET /index.html 200
EOF
# Топ IP по числу запросов
$ cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn
3 192.168.1.1
2 192.168.1.2
1 192.168.1.4
1 192.168.1.3
# Сколько 500 ошибок
$ grep ' 500$' access.log | wc -l
2
# Какие URL получали ошибки
$ awk '$4 == 500 {print $3}' access.log | sort -u
/api/users
Каждая команда здесь — отдельная программа. Соединили — получили реальный отчёт. Это и есть Unix-философия в живом виде.