Моё хобби — открыть «Философию UNIX» Макилроя на одном мониторе, одновременно читая маны на другом.

Первый из принципов Макилроя часто перефразируют как «Делайте что-то одно, но делайте хорошо». Это сокращение от его слов «Создавайте программы, которые делают одну вещь хорошо. Для новой работы создавайте новые программы, а не усложняйте старые добавлением новых "функций"».

Макилрой приводит пример:

Для посторонних кажется удивительным тот факт, что компиляторы UNIX не выдают листинги: печать лучше осуществляется и более гибко настраивается с помощью отдельной программы.

Если вы откроете справку для ls, то она начинается с

ls [-ABCFGHLOPRSTUW@abcdefghiklmnopqrstuwx1] [file ...]

То есть однобуквенные флаги для ls включают все строчные буквы, кроме {jvyz}, 14-ти прописных букв, @ и 1. Это 22 + 14 + 2 = 38 только односимвольных вариантов.

В Ubuntu 17 справка для ls не покажет нормальное резюме, но вы увидите, что у ls есть 58 опций (включая --help и --version).

Давайте посмотрим, это уникальная ситуация или нормальное положение вещей. Составим список некоторых обычных команд, отсортированный по частоте использования.



В таблице указано количество параметров командной строки для различных команд v7 Unix (1979), slackware 3.1 (1996), ubuntu 12 (2015) и ubuntu 17 (2017). Чем больше параметров, тем темнее ячейки (в логарифмическом масштабе).

Мы видим, что с годами количество опций резко увеличивается: как правило, записи темнеют слева направо (больше опций), и нет случаев, когда записи становятся светлее (меньше опций).

Макилрой давно осуждает увеличение количества опций, размера и общей функциональности команд1:

Всё было маленьким, а сейчас моё сердце сжимается, когда я вижу в Linux размер… [неразборчиво]. Те же утилиты, которые раньше помещались в восемь килобайт, теперь стали по мегабайту. И справочная страница, которая раньше действительно была страницей, теперь представляет собой небольшой томик с тысячей опций… Раньше мы сидели в комнате Unix и рассуждали: «Что выбросить? Зачем нужна такая опция?» Это нормальные и частые вопросы, потому что в базовом дизайне обычно есть какой-то недостаток, когда вы не попали идеально в нужную точку дизайна. Вместо того, чтобы выкатывать опцию, выясните, что заставляет вас это делать. Такая логика была отчасти вызвана скромными аппаратными ресурсами… теперь она потеряна, и нам от этого не легче.

По иронии, одной из причин роста числа опций командной строки является другое изречение Макилроя: «Пишите программы для обработки текстовых потоков, потому что это универсальный интерфейс» (см. ls в качестве одного из примеров).

Если бы передавались структурированные данные или объекты, форматирование можно оставить на заключительный этап. Но в случае с обычным текстом форматирование и содержимое смешиваются; поскольку форматирование можно выполнить только путём разбора содержимого, команды обычно для удобства добавляют параметры форматирования. Кроме того, форматирование может быть выполнено, когда пользователь применяет свои знания о структуре данных и «кодирует» эти знания в аргументы для cut, awk, sed и т. д. (пользователь также использует свои знания о том, как эти программы работают с форматированием, потому что оно отличается для разных программ, так что и пользователь должен знать, например, чем cut -f4 отличается от awk '{ print $4 }2). Это намного больше хлопот, чем передача одного или двух аргументов следующей команде в последовательности, и это переносит сложность инструмента на пользователя.

Люди иногда говорят, что не хотят поддерживать структурированные данные, потому что тогда в универсальном инструменте пришлось бы поддерживать несколько форматов. Но им уже приходится поддерживать несколько форматов, чтобы сделать универсальный инструмент. Некоторые стандартные команды не могут считывать выходные данные из других команд, потому что используют разные форматы. Например, wc -w неправильно обрабатывает Юникод и т. д. Сказать, что «текст» — это универсальный формат, всё равно что сказать, что «двоичный» — это универсальный формат.

Говорят, что для такого усложнения инструментов командной строки нет никакой альтернативы. Но люди, которые так говорят, никогда не пробовали альтернативу, что-то вроде PowerShell. У меня много претензий к PowerShell, но передача структурированных данных и возможность легко работать со структурированными данными без необходимости держать в голове метаданные, чтобы я мог передать их в нужные инструменты командной строки в нужных местах конвейера, не входит в число моих жалоб3.

Когда вам говорят, что программы должны быть простыми и совместимыми, обрабатывая текст, — эти люди притворяются, что у текстовых данных нет структуры, которую нужно парсить4. В некоторых случаях мы можем представить всё как одну строку, разделённую пробелами, или как таблицу с разделителями строк и столбцов (с поведением, которое, конечно, не согласуется с другими инструментами). Это добавляет немного проблем. Есть ещё случаи, когда сериализация данных в обычный текстовый формат добавляет значительную сложность, так как из-за структуры данных простая сериализация в текст впоследствии требует значительных усилий по парсингу для повторного усвоения данных осмысленным образом.

Другая причина, почему у команд теперь больше опций, заключается в том, что люди добавили удобные флаги для функциональности, которую можно было реализовать конвейером из нескольких команд. Такая практика повелась со времён Unix v7, где в ls появилась опция для изменения порядка сортировки (хотя это можно было сделать путём передачи выходных данных в tac).

Со временем чисто для удобства добавили дополнительные параметры. Например, бывшая изначально без параметров команда mv теперь может переместить файл и одновременно создать его резервную копию (три опции; два разных способа указать резервную копию, один из которых принимает аргумент, а другой не принимает аргументов, считывает неявный аргумент из переменной среды VERSION_CONTROL; ещё одна опция позволяет переопределить суффикс резервной копии по умолчанию). Теперь у mv есть ещё опции никогда не перезаписывать файлы или перезаписывать только более новые файлы.

mkdir ещё одна программа, у которой раньше не было опций. Сегодня все её флаги, за исключением опций безопасности для SELinux или SMACK, а также справки и номера версии, добавлены исключительно для удобства: установка разрешений для нового каталога и создание родительских каталогов, если они не существуют.

У tail изначально была только одна опция -number, указывающая на стартовую точку для работы. Потом добавили как форматирование, так и опции для удобства форматирования. Флаг -z заменяет разделитель строк на null. Вот другие примеры опций, добавленных для удобства: -f для печати при появлении новых изменений, -s для установки интервала ожидания между проверкой изменений /code>-f, а также -retry для повторных попыток доступа к файлу, если он недоступен.

Макилрой критикует разработчиков за то, что они добавили все эти опции, но лично мне так лучше. Хотя я никогда не использовал некоторые из них, а другие использовал редко, но в этом и заключается красота параметров командной строки — в отличие от графического интерфейса, добавление этих параметров не загромождает интерфейс. Да, маны и справка разбухают, но в эпоху Google и Stackoverflow в любом случае многие просто гуглят любой вопрос, а не читают маны.

Конечно, добавление опций увеличивает нагрузку на мейнтейнеров. Но это справедливая плата за пользу, которую они приносят. Учитывая соотношение количества мейнтейнеров и пользователей, логично возложить дополнительную нагрузку на первых, а не на вторых. Это аналогично замечанию Гэри Бернхардта, что разумно репетировать выступление 50 раз. Если аудитория составляет 300 человек, то отношение времени, потраченного на просмотр выступления, к времени, потраченному на репетиции, всё равно составит 6:1. У популярных инструментов командной строки это соотношение ещё экстремальнее.

Кто-то может возразить, что все эти дополнительные опции накладывают лишнее бремя на пользователей. Это не совсем неправильно, но это бремя сложности всегда будет существовать. Вопрос только в том, где именно. Если представить, что набор инструментов командной строки вместе с оболочкой образуют язык, на котором каждый может написать новый метод, и в случае популярности метод эффективно добавляется в стандартную библиотеку, а стандарты определяются аксиомами типа «Писать программы для обработки текстовых потоков, потому что это универсальный интерфейс», то язык превратится в бессвязный хаос write-only, если взять его целиком. По крайней мере, благодаря инструментам с большим набором опций и функциональности пользователи Unix могут заменить гигантский набор дико несогласованных инструментов просто большим набором инструментов, которые, хотя и несогласованы друг с другом снаружи, но обладают некоторой внутренней согласованностью.

Макилрой подразумевает недостаточную продуманность набора инструментов. Мол, отцам-основателям Unix следовало сесть в одной комнате и хорошенько подумать, пока они не придумали бы набор последовательных инструментов «необычайной простоты». Но он не будет масштабироваться, сама философия Unix неизбежно привела к беспорядку, в котором мы находимся. Дело не в том, что кто-то не думал долго или усердно. Дело в философии, которая не масштабирутся за пределы относительно небольшой команды с общим культурным пониманием, способной поместиться в одной комнате.

Если кто-то хочет написать инструмент на основе «философии Unix», то у разных людей будут разные мнения о том, что означает «простота» или принцип «делать одну вещь»5, как правильно должен работать инструмент — и пышным цветом расцветёт непоследовательность, в результате чего вы получите огромную сложность, подобную дико непоследовательным языкам вроде PHP. Люди высмеивают PHP и JavaScript за различные странности и несоответствия, но как язык и стандартная библиотека, любая популярная оболочка с коллекцией популярных инструментов *nix, если взять вместе, намного хуже и содержит гораздо больше случайной сложности из-за несоответствия даже в пределах одного дистрибутива Linux. А иначе и быть не может. Если сравнить дистрибутивы Linux, BSD, Solaris, AIX и т. д., то количество случайной сложности, которую пользователи должны держать в голове при переключении систем, затмевает несогласованность PHP или JavaScript. Наиболее широко осмеянные языки программирования — настоящие образцы великолепного дизайна по сравнению с ними.

Для ясности, я не утверждаю, что я сам или кто-то другой мог бы лучше справиться с разработкой в 70-е годы, учитывая доступные тогда знания, и создать систему, которая была бы одновременно и полезной в то время, и элегантной сегодня. Конечно, легко оглянуться и найти проблемы в ретроспективе. Я просто не согласен с комментариями некоторых знатоков Unix, как Макилрой, которые намекают, что мы забыли или не понимаем ценность простоты. Или Кена Томпсона, который говорит, что C — такой же безопасный язык, как и любой другой, и если мы не хотим появления ошибок, нужно просто писать код без ошибок. Такого рода комментарии подразумевают, что мало что изменилось за эти годы. Якобы в 70-е мы строили системы так же, как и сегодня, а пять десятилетий коллективного опыта, десятки миллионов человеко-лет ничему нас не научили. И если мы обратимся к истокам, к создателям Unix, то всё будет хорошо. Со всем уважением, я не согласен.

Приложение: память


Хотя жалобы Макилроя на раздувание бинарников немного выходят за рамки этой статьи, я отмечу, что в 2017 году я купил Chromebook с 16 ГБ оперативной памяти за 300 долларов. Бинарник на 1 мегабайт мог бы стать серьёзной проблемой в 1979 году, когда стандартный Apple II оснащался 4 килобайтами памяти. Apple II стоил $1298 в 1979 году или $4612 в 2020 году. Сегодня вы можете купить недорогой Chromebook, который стоит меньше 1/15 от этой цены, при этом имеет в четыре миллиона раз больше памяти. Жалобы, что использование памяти выросло в тысячу раз, кажутся немного смешными, когда (портативная!) машина стоит на порядок дешевле и имеет в четыре миллиона раз больше памяти.

Мне нравится оптимизация, поэтому я ужал свою домашнюю страницу до двух пакетов (был бы один, если бы CDN поддерживал высокоуровневый brotli), но это чисто эстетическое требование, я делаю это для удовольствия. Узким местом инструментов командной строки является не использование памяти, и время на оптимизацию памяти инструмента размером один мегабайт, — это всё равно что сводить домашнюю страницу к одному пакету. Возможно, забавное хобби, но не более того.

Методология составления таблицы


Частота использования команд получена из общедоступных файлов истории команд на github, она не обязательно соответствует вашему личному опыту. Подсчитывались только «простые» команды, без учёта экземпляров вроде curl, git, gcc (у последней более 1000 опций) и wget. Понятие простоты относительно. Встроенные команды оболочки, такие как cd, тоже не учитывались.

Повторение флагов не считалось отдельным вариантом. Например, у git blame -C, git blame -C -C и git blame -C -C -C разное поведение, но все они будут считаться одним аргументом, хотя -C -C и -C — это фактически разные аргументы.

Подопции в таблице подсчитываются как один вариант. Например, ls поддерживает следующие:

--format=WORD across -x, commas -m, horizontal -x, long -l, single-column -1, verbose -l, vertical -C

Несмотря на семь опций format, это считается как одна опция.

Опции, которые явно указаны как бесполезные, по-прежнему считаются опциями, например, ls -g, которая игнорируется, тоже считается.

Несколько версий одной и той же опции считаются одной опцией. Например, -A и --almost-all для ls.

Если справка говорит, что опция существует, но в реальности её нет, то она не учитывается. Например, в справке по v7 mv написано:

БАГИ

Если file1 и file2 в разных файловых системах, то mv должен скопировать файл и удалить оригинал. В этом случае имя владельца становится именем процесса копирования, а любая связь с другими файлами теряется.

mv должен принимать флаг -f, как rm, чтобы подавить сообщение о существовании целевого файла, недоступного для записи.

Но -f не считается флагом в таблице, потому что опция на самом деле не существует.

Таблица заканчивается в 2017 году, потому что тогда был написан первый черновик этой статьи. Только сейчас дошли руки вычитать его.

По теме



Благодарю Ли Хэнсона, Хиллела Уэйна, Уэсли Аптекар-Касселса, Трэвиса Даунса и Юрия Вишневского за комментарии/исправления/обсуждение.



1. Эта цитата немного отличается от общеупотребительной версии, потому что я смотрел исходное видео. Насколько я могу судить, все копии этой цитаты в интернете (индексы Bing, DuckDuckGo и Google) взяты из одной транскрипции одного человека. Там есть определённая двусмысленность, потому что звук низкого качества, и я слышу слова, которые немного отличаются от того, что услышал тот человек. [вернуться]

2. Другой пример, как сложность перекладывается на пользователя, потому что разные команды обрабатывают форматирование по-разному, — это форматирование времени. Встроенное в оболочку время time, конечно, несовместимо с /usr/bin/time. Пользователь должен быть осведомлён об этом факте и знать, как это обрабатывать. [вернуться]

3. Например, для любого объекта можно использовать ConvertTo-Json или ConvertTo-CSV. Или «командлеты» для изменения отображения свойств объектов. Вы можете написать файлы конфигурации форматирования, которые определяют предпочтительные способы форматирования.

Другой способ взглянуть на это — через призму закона Конвея. Если у нас есть набор инструментов командной строки, созданных разными людьми, часто из разных организаций, эти инструменты будут дико непоследовательны, если кто-то не сможет определить стандарт и заставить людей принять его. Это на самом деле работает относительно хорошо в Windows, а не только в PowerShell.

Распространённая жалоба на Microsoft заключается в массивном обороте API, часто по нетехническим организационным причинам (например, см. действия Стивена Синофски, как описанные в ответах на удалённый твит). Это правда. Тем не менее, с точки зрения наивного пользователя стандартное программное обеспечение Windows, как правило, намного лучше передаёт нетекстовые данные, чем *nix. Охват нетекстовых данных в Windows восходит, по крайней мере, к COM в 1999 году (и, возможно, к OLE и DDE, выпущенным в 1990 и 1987 годах соответственно).

Например, если вы копируете из Foo, который поддерживает двоичные форматы A и B, в Bar, который поддерживает форматы B и C, а затем копируете из Bar в Baz, который поддерживает C и D, всё будет нормально работать, даже если у Foo и Baz нет общих поддерживаемых форматов.

Когда вы что-то вырезаете или копируете, приложение в основном «сообщает» буферу обмена, в каких форматах оно может предоставить данные. При вставке в приложение конечное приложение может запросить данные в любом из доступных форматов. Если данные уже находятся в буфере обмена, «Windows» предоставляет их. Если это не так, Windows получает данные из исходного приложения, а затем передаёт целевому приложению, а копия сохраняется в течение некоторого времени. Если вы «вырезаете» из Excel, он скажет «вам», что у него есть данные, доступные во многих десятках форматов. Такая система довольно хороша для совместимости, хотя её определённо не назовёшь простой или минималистичной.

В дополнение к хорошей поддержке многих форматов достаточно долгое время, чтобы многие программы начали хорошо разбираться с этими возможностями, в Windows из коробки обычно хорошая поддержка буфера обмена.

Предположим, вы копируете, а затем вставляете небольшое количество текста. В большинстве случаев никаких неожиданностей не произойдёт ни на Windows, ни на Linux. Но теперь предположим, что вы скопировали какой-то текст, закрыли программу, из которой вы скопировали, и затем вставили его. Многие пользователи склонны думать, что при копировании данные хранятся в буфере обмена, а не в программе, из которой они копируются. В Windows программное обеспечение обычно пишется в соответствии с этим ожиданием (хотя технически пользователи API буфера обмена не должны этого делать). Это менее распространено в Linux с X, где правильная ментальная модель для большинства программ заключается в том, что копирование сохраняет указатель на данные, которые всё ещё принадлежат программе, из которой они копируются. То есть вставка не сработает, если программа закрыта. Когда я (неофициально) опрашивал программистов, они обычно удивлялись этому, если только в реальности не работали с функцией копирования+вставки для своего приложения. Когда я опрашивал непрограммистов, они, как правило, находили такое поведение не только удивительным, но и сбивающим с толку.

Недостаток передачи буфера обмена в ОС в том, что копирование больших объёмов данных обходится дорого. Предположим, вы копируете действительно большой объём текста, много гигабайт или какой-то сложный объект, а затем не вставляете его. На самом деле вы не хотите копировать эти данные из вашей программы в ОС, чтобы они хранились там и были доступны. Windows справляется с этим разумно: приложения могут предоставлять данные только по запросу, если это считается выгодным. В нашем случае, когда пользователь закрывает программу, она может определить, поместить данные в буфер обмена или удалить их. В этом случае многие программы (например, Excel) будут предлагать «сохранить» данные в буфере обмена или удалить их, что довольно разумно.

Некоторые из этих функций можно реализовать в Linux. Например, спецификация ClipboardManager описывает механизм сохранения, и приложения GNOME обычно поддерживают его (хотя и с некоторыми ошибками), но ситуация на *nix действительно отличается в худшую сторону от повсеместно распространённой поддержки приложениями Windows, где обычно реализован грамотный буфер обмена. [вернуться]

4. Другой пример — инструменты поверх современных компиляторов. Вернёмся назад и посмотрим на канонический пример Макилроя, где правильные компиляторы Unix настолько специализированы, что листинг выполняется отдельным инструментом. Но сегодня это изменилось, хотя и остался отдельный инструмент для листинга. У некоторых популярных компиляторов Linux буквально тысячи опций — и они чрезвычайно многофункциональны. Например, одна из многих функций современного clang — это статический анализ. На момент написания этой статьи существует 79 обычных проверок статического анализа и 44 экспериментальных проверок. Если бы это были отдельные команды, они всё равно полагались бы на ту же базовую инфраструктуру компилятора и накладывали бы ту же нагрузку на обслуживание — на самом деле неразумно, чтобы эти инструменты статического анализа работали с обычным текстом и переопределяли всю цепочку инструментов компилятора, необходимую для получения точки, где они могут выполнять статический анализ. Они могут быть отдельными командами вместо того, чтобы объединяться в clang, но они всё равно будут зависеть от того же механизма и либо наложат на компилятор бремя обслуживания и сложности (который должен поддерживать стабильные интерфейсы для инструментов, которые работают поверх него), либо будут постоянно ломаться.

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

Тот же clang просто более функционален, чем любой компилятор, который существовал в 1979 году, или даже все компиляторы, которые существовали в 1979 году вместе взятые, независимо от того, выполнен он монолитной командой или тысячами меньших команд. Легко сказать, что в 1979 году всё было проще и что мы, современные программисты, сбились с пути. Но в реальности трудно предложить дизайн, который намного проще и будет действительно всеми принят. Невозможно, чтобы такой дизайн мог сохранить всю существующую функциональность и конфигурируемость и быть таким же простым, как что-то из 1979 года. [вернуться]

5. С момента своего создания утилита curl перешла от поддержки трёх протоколов к 40. Означает ли это, что она «делает 40 вещей», и философия Unix требует разделить её на 40 отдельных команд? Зависит от того, кого спросить. Если бы каждый протокол был собственной командой, созданной и поддерживаемой другим человеком, у нас возник бы такой же бардак, как с командами. Несогласованные параметры командной строки, несогласованные форматы вывода, несмотря на то, что всё это текстовые потоки и т. д. Это приблизит нас к простоте, за которую ратует Макилрой? Зависит от того, кого спросить. [вернуться]