Разработка → UNIX-подобные системы содержат кучу костылей. Крах «философии UNIX»

safinaskar 12 февраля в 06:36 78,5k
UPD от 2017-03-04: кто-то выполнил английский перевод. Обсуждение на Hacker News.

В первой части статьи перечислю кучу костылей UNIX, и вообще разных недостатков. Во второй — про «философию UNIX». Статья написана наскоро, «полировать» дальше не хочу, скажите спасибо, что написал. Поэтому многие факты привожу без ссылок.

Костыли в UNIX начали возникать ещё с момента появления UNIX, а это было ещё раньше появления не только Windows, но даже вроде бы Microsoft DOS (вроде бы, мне лень проверять, проверяйте сами). Если лень читать, хотя бы просмотрите все пункты, что-нибудь интересное найдёте. Это далеко не полный список, это просто те косяки, который я захотел упомянуть.

  • В самом начале make был программой, которую один человек написал для себя и нескольких своих знакомых. Тогда он, недолго думая, сделал так, что командами воспринимаются строки, которые начинаются с Tab. Т. е. Tab воспринимался отлично от пробела, что крайне некрасиво и нетипично ни для UNIX, ни за его пределами. Он так сделал, потому что не думал, что make будет ещё кто-то использовать кроме этой небольшой группы. Потом появилась мысль, что make — хорошая вещь и неплохо бы включить его в стандартный комплект UNIX. И тогда чтобы не сломать уже написанные мейкфайлы, т. е. написанные вот этими вот десятью людьми, он не стал ничего менять. Ну вот так и живём… Из-за тех десятерых страдаем мы все.

  • Почти в самом начале в UNIX не было папки /usr. Все бинарники размещались в /bin и /sbin. Но потом вся инфа перестала помещаться на тот диск, который был в распоряжении авторов UNIX (Томпсон, Ритчи). Поэтому они достали ещё один диск, создали папку /usr, а в ней — ещё один bin и ещё один sbin. И смонтировали новый диск в /usr. Оттуда и пошло. Так появилась «вторая иерархия» /usr, а потом в какой-то момент ещё и «третья иерархия» /usr/local, а потом ещё и /opt. Как пишет рассказчик этой истории: «Не удивлюсь, если когда-нибудь ещё появится /opt/local». UPD от 2017-02-12: я нашёл ссылку, где я почерпнул эту историю. Читайте, там более точная версия произошедшего.

  • sbin изначально означало «static bin», а не «superuser bin», как можно было бы подумать. И содержал sbin статические бинарники. Но потом sbin стал содержать динамические бинарники, его название потеряло смысл.

  • Windows часто ругают за наличие реестра и сообщают при этом, что подход UNIX-подобных систем (куча конфигов) якобы лучше. А между прочим однажды в ext4 появилась особенность (является ли это багом, вопрос спорный), из-за которой при резком выключении компа Gnome потерял все свои конфиги в рабочей папке юзера. И разработчик этой ext4 сказал в обсуждении баг репорта, что Gnome'у надо было использовать что-то вроде реестра для хранения инфы.

    UPD от 2017-02-12: источники: раз и два. Имя отписавшегося maintainer'а ext4: Theodore Ts'o. Вот его слова:

    If you really care about making sure something is on disk, you have to use fsync or fdatasync. If you are about the performance overhead of fsync(), fdatasync() is much less heavyweight, if you can arrange to make sure that the size of the file doesn't change often. You can do that via a binary database, that is grown in chunks, and rarely truncated.

    I'll note that I use the GNOME desktop (which means the gnome panel, but I'm not a very major desktop user), and «find .[a-zA-Z]* -mtime 0» doesn't show a large number of files. I'm guessing it's certain badly written applications which are creating the «hundreds of dot files» that people are reporting become zero lengh, and if they are seeing it happen a lot, it must be because the dot files are getting updated very frequently. I don't know what the bad applications are, but the people who complained about large number of state files disappearing should check into which application were involved, and try to figure out how often they are getting modified. As I said, if large number of files are getting frequently modified, it's going to be bad for SSD's as well, there are multiple reasons to fix badly written applications, even if 2.6.30 will have a fix for the most common cases. (Although some server folks may mount with a flag to disable it, since it will cost performance.)

    И это не говоря уж о том, что критичные файлы UNIX (такие как /etc/passwd), который читаются при каждом (!) вызове, скажем, ls -l, записаны в виде простого текста. И эти файлы надо заново читать и заново парсить при каждом вызове ls -l! Было бы гораздо лучше использовать бинарный формат. Или БД. Или некий аналог реестра. Как минимум, для вот таких вот критичных для производительности ОС файлов.

  • Two famous people, one from MIT and another from Berkeley (but working on Unix) once met to discuss operating system issues. The person from MIT was knowledgeable about ITS (the MIT AI Lab operating system) and had been reading the Unix sources. He was interested in how Unix solved the PC loser-ing problem. The PC loser-ing problem occurs when a user program invokes a system routine to perform a lengthy operation that might have significant state, such as IO buffers. If an interrupt occurs during the operation, the state of the user program must be saved. Because the invocation of the system routine is usually a single instruction, the PC of the user program does not adequately capture the state of the process. The system routine must either back out or press forward. The right thing is to back out and restore the user program PC to the instruction that invoked the system routine so that resumption of the user program after the interrupt, for example, re-enters the system routine. It is called «PC loser-ing» because the PC is being coerced into «loser mode,» where «loser» is the affectionate name for «user» at MIT.

    The MIT guy did not see any code that handled this case and asked the New Jersey guy how the problem was handled. The New Jersey guy said that the Unix folks were aware of the problem, but the solution was for the system routine to always finish, but sometimes an error code would be returned that signaled that the system routine had failed to complete its action. A correct user program, then, had to check the error code to determine whether to simply try the system routine again. The MIT guy did not like this solution because it was not the right thing.

    The Rise of «Worse is Better» By Richard Gabriel

    Если кратко и своими словами, то в начале разработки UNIX авторы UNIX решили попросту выдавать ошибку из ядра пользовательской программе, если пользовательская программа прервана по сигналу, и на этот сигнал повешен обработчик. Иными словами, если вы перехватили Ctrl-C (т. е. поставили на него обработчик) в своей программе, а юзер за терминалом нажал этот самый Ctrl-C, то ОС выполнит обработчик, а потом вместо простого продолжения того сисвызова, который выполнялся в момент Ctrl-C, просто прервёт его, вернув из ядра в пользовательскую программу EINTR. В результате программисту, пишущему эту программу придётся эту EINTR предусмотреть. А это усложняет этот userspace код. Ценой упрощения кода ядра. Да, нужно было сделать по-другому. Усложнить код ядра и упростить userspace код, который придётся писать всем программистам. Но тому человеку из Беркли из цитаты выше было пофигу. Он фактически сказал: «Да мне пофиг, что все будут страдать, главное, чтоб код ядра попроще был».

    Дальше — больше. Позже в UNIX-системах всё же пофиксили упомянутую особенность, добавив так называемый SA_RESTART. То есть вместо того, чтобы просто всё пофиксить, они добавили специальный флаг. Так мало того, что они это сделали, этот SA_RESTART ещё и не всегда работает! В частности, в GNU/Linux select, poll, nanosleep и др. не продолжают свою работу после перехваченного прерывания даже в случае SA_RESTART!

  • Вообще, конкретные обстоятельства, возникшие во время разработки оригинальной UNIX, сильно оказали на неё влияние. Скажем, читал где-то, что команда cp названа именно так, а не copy, потому что UNIX разрабатывали с использованием терминалов, которые очень медленно выдавали буквы. А потому набрать cp было быстрее, чем copy. UPD от 2017-02-12: найти именно ту ссылку, которую я видел когда-то давно, и в которой приводился пример с cp и copy, мне не удалось. Но есть, например, вот эта ссылка.

    Commands — Are These Real Words?

    The basic AIX commands (and all UNIX system commands) are, for the most
    part, very short, cryptic, two-letter command names. Imagine back years ago,
    when computers had only very slow teletype keyboards and paper “displays.”
    (Some of us aren’t imagining, we’re remembering!) Imagine also, people who
    didn’t like typing long commands because there was such a long delay between
    commands and the computer response. If there were any mistakes, the user had
    to retype the whole thing (especially aggravating for folks that type with only
    two fingers!).

    Also, some UNIX commands came from university students and researchers who
    weren’t bound by usability standards (no rules, merely peer pressure). They
    could write a very useful, clever command and name it anything—their own initials,
    for example (awk by Aho, Weinberger, and Kernighan), or an acronym
    (yacc, Yet Another Compiler-Compiler).

  • Вообще, названия утилит UNIX — это отдельная история. Скажем, название grep идёт от командй g/re/p в текстовом редакторе ed. (Ну а cat — от concatenation, я надеюсь, это все и так знали :) Ну и для кучи: vmlinuz — gZipped LINUx with Virtual Memory support).

  • printf внезапно является далеко не самым быстрым способом вывода информации на экран или в файл. Не знали, да? А дело в том, что printf, как и сама UNIX в целом, был придуман не для оптимизации времени, а для оптимизации памяти. printf каждый раз парсит в рантайме строку формата. Именно поэтому в веб сервере H2O был придуман специальный препроцессор, который переносит парсинг строки формата на этап компиляции.

    UPD от 2017-02-12: источник.

  • Когда Кена Томпсона, автора UNIX (вместе с Деннисом Ритчи) спросили, что бы он поменял в UNIX, он сказал, что назвал бы функцию creat (sic!) как create. UPD от 2017-02-12: источников полно, например этот. No comments. Замечу, что позже этот же Кен Томпсон вместе с другими разработчиками оригинальной UNIX создал систему Plan 9, исправляющую многие недостатки UNIX. И в ней эта функция называется create :) Он смог :)

  • Ещё одна цитата:

    A child which dies but is never waited for is not really gone in that it still consumes disk swap and system table space. This can make it impossible to create new processes. The bug can be noticed whenseveral & separators are given to the shell not followed by ancommand without an ampersand. Ordinarily things clean themselves upwhen an ordinary command is typed, but it is possible to get into asituation in which no commands are accepted, so no waits are done;the system is then hung.The fix, probably, is to have a new kind of fork which creates aprocess for which no wait is necessary (or possible); also to limit the number of active or inactive descendants allowed to a process.

    Источник

    Это цитата из очень раннего манула UNIX. Уже тогда существование зомби-процессов признавалось багом. Но потом на этот баг попросту забили. Понятное дело, что гораздо позже эта проблема всё же была решена. Т. е. в современном GNU/Linux инструменты для убивания зомби-процессов всё же существуют. Но о них мало кто знает. Обычном kill'ом зомби не убиваются. Про существование зомби-процессов все говорят: «It's for design».

  • Ещё немного про уже упомянутый язык C. Вообще язык C разрабатывался одновременно с UNIX, поэтому критикуя UNIX, нужно покритиковать и C тоже. То, что C очень плох, написано много, я не буду повторять все эти аргументы. Там, синтаксис типов плохой, препроцессор ужасен, легко выстрелить себе в ногу, всякие 4["string"], всякие sizeof ('a') != sizeof (char) (в C, не в C++!), всякие i++ + ++i, всякие while (*p++ = *q++) ; (пример из Страуструпа, второе дополненное издание) и так далее и тому подобное.

    Скажу лишь вот что. В C до сих пор не научились удобно работать со строками. Неудобство работы со строками постоянно приводит к разнообразным проблемам безопасности. И эту проблему до сих пор не решили! Вот относительно свежий документ от комитета C. В нём обсуждается весьма сомнительный способ решения проблемы со строками. И делается вывод, что этот способ плох. Год публикации: 2015. То есть даже к 2015-му году окончательного решения ещё нет!

    И это не говоря об отсутствии простой, удобной и мультиплатформенной системы сборки (а не этого монстра autotools, который ещё и не поддерживает винду, и другого монстра cmake, который поддерживает винду, но всё равно монстр), стандартного менеджера пакетов, удобного как npm (js) или cargo (rust), нормальной portability library, с помощью которой можно было кроссплатформенно хотя бы прочитать содержимое папки и хотя бы даже главного сайта C, который был бы главной точкой входа для всех новичков и содержал бы в себе не только документацию, но и краткую инструкцию по установке инструментов C на любую платформу, по созданию простого проекта на C, а также содержал бы удобный поиск по пакетам C (которые должны быть размещены в стандартном репозитории) и, главное, был бы точкой сбора user community. Я даже зарегал домен c-language.org в надежде, что когда-нибудь я создам там такой сайт. Эх, мечты, мечты. (У меня ещё cpp-language.org заныкан, бугога :)) Но всего этого нет. Хоть это и есть у всех популярных языков, кроме C и C++. И даже у Haskell всё это есть. И у Rust.

    У Rust, у этого выскочки, который, кстати говоря, метит в ту же нишу, что и C. Есть единый конфиг, который одновременно является конфигом проекта, конфигом сборки и конфигом для менеджера пакетов (собственно, cargo — это менеджер проектов и система сборки одновременно). Есть возможность указания в качестве зависимости для данного пакета другого пакета, размещённого где-то в *GIT*, в том числе указание в качестве зависимости напрямую программы на *GITHUB*. Генерация из коробки документации из сорцов, записанной в комментах на *MARKDOWN*. И пакетный менеджер, использующий для версий *SEMVER*. Итак, *GIT*, *GITHUB*, *MARKDOWN*, *SEMVER*, короче говоря *BUZZWORDS*, *BUZZWORDS* и ещё раз *HIPSTERS' BUZZWORDS*. И всё сразу из коробки. Прямо вот заходишь на их главный сайт, и вот на тебе на блюдечке с голубой каёмочкой. И работает всё одинаково на всех платформах. Несмотря на то, что Rust — это вроде как язык системного программирования, а не какой-нибудь там javascript. Несмотря на то, что в Rust можно байты гонять. И арифметика указателей там есть. Так почему же у них, у этих выскочек-растовцев, эти хипстерские баззворды есть, а у нас, сишников, их нет? Обыдно.

    Я помню, один знакомый спрашивает у меня, где посмотреть список пакетов для C/C++. Пришлось сказать ему, что такого единого места нет. Он: «Программисты на C/C++ должны страдать?» Мне нечего было ему ответить.

    Ах да, забыл ещё одну вещь. Посмотрите, пожалуйста, на прототип функции signal в том виде, в котором он дан в стандарте C: void (*signal(int sig, void (*func)(int)))(int); и попытайтесь его понять.

  • Терминал в UNIX — жуткое legacy.

  • Имена файлов в файловых системах UNIX (ext2 и пр.) есть просто поток байтов без кодировки. В какой кодировке они будут интерпретированы, зависит от локали. То есть если создать файл на ОС в одной локали, а потом пытаться посмотреть его имя в ОС в другой локали, будет плохо. В виндовом NTFS такой проблемы нет.

  • UNIX shell хуже PHP! Да, да, а вы что, не знали? Сейчас модно ругать PHP. Но ведь UNIX shell ещё хуже :) Особенно плохим он становиться, если пытаться на нём программировать, ведь полноценным языком программирования он не является. Но даже для своей ниши (скриптинг типичных задач по администрированию) он годится плохо. Виной тому примитивность shell, непродуманность, legacy, куча частных случаев, костылей, бардак с кавычками, бекслешами, специальными символами и повёрнутость shell'а (как и всего UNIX) на простом тексте.

    • Начнём с затравки. Как рекурсивно найти в папке foo все файлы с именем \? Правильный ответ таков: find foo -name '\\'. Ну или так: find foo -name \\\\. Последний вариант вызовет особенно много вопросов. Попробуйте объяснить человеку, плохо разбираемущемуся в UNIX shell, почему здесь нужно именно четыре бекслеша, а не два и не восемь (грамотеи, подскажите, как правильно написать это предложение, пишите в личку). А написать здесь нужно четыре бекслеша, потому что UNIX shell делает backslash expanding, и find тоже его делает.

    • Как touch'нуть все файлы в папке foo (и во вложенных)? На первый взгляд, один из способ таков: find foo | while read A; do touch $A; done. Ну, на первый взгляд. На самом деле здесь можно придумать аж 5 нюансов, которые могут испортить нам малину (и привести к проблемам с безопасностью):

      • Имя файла может содержать бекслеш, поэтому нужно писать не read A, а read -r A.
      • Имя файла может содержать пробел, поэтому нужно писать не touch $A, а touch "$A".
      • Имя файла может не только содержать пробел, но и начинаться с пробела, поэтому нужно писать не read -r A, а IFS="" read -r A.
      • Имя файла может содержать перевод строки, поэтому вместо find foo нужно использовать find foo -print0, а вместо IFS="" read -r A нужно использовать IFS="" read -rd "" A (тут я не совсем уверен).
      • Имя файла может начинаться с дефиса, поэтому вместо touch "$A" нужно писать touch -- "$A".

      Итоговый вариант выглядит так: find foo -print0 | while IFS="" read -rd "" A; do touch -- "$A"; done. Круто, да? И здесь мы, кстати, не учли, что POSIX не гарантирует (я не совсем в этом уверен), что touch поддерживает опцию --. Если учитывать ещё и это, то придётся для каждого файла проверять, что он начинается с дефиса (или что не начинается со слеша) и добавлять в начало ./. Теперь вы поняли, почему скрипты configure, генерируемые autoconf'ом такие большие и трудночитаемые? Потому что этому configure нужно учитывать всю эту муть, включая совместимость с разными shell'ами. (В данном примере для демонстрации я использовал решение с пайпом и циклом. Можно было использовать решение с -exec или xargs, но это было бы не так эффектно). (Ладно, хорошо, мы знаем, что имя файла начинается с foo, поэтому оно не может начинаться с пробела или дефиса).

    • В переменной A лежит имя файла, нужно удалить его на хосте a@a. Как это сделать? Может быть так: ssh a@a rm -- "$A" (как вы уже заметили, мы тут уже учли, что имя файла может содержать пробелы и начинаться с дефиса)? Ни в коем случае! ssh — это вам не chroot, не setsid, не nohup, не sudo и не какая-нибудь ещё команда, которая получает exec-команду (т. е. команду для непосредственной передачи сисвызовам семейства execve). ssh (как и su) принимает shell-команду, т. е. команду для обработки shell'ом (термины exec-команда и shell-команда — мои). ssh соединяет все аргументы в строку, передаёт строку на удалённую сторону и там выполняет shell'ом. Окей, может быть так: ssh a@a 'rm -- "$A"'? Нет, эта команда попытается найти переменную A на удалённой стороне. А её там нет, потому что переменные через ssh не передаются. Может, так: ssh a@a "rm -- '$A'"? Нет, это не сработает, если имя файла содержит одинарную кавычку. В общем, не буду вас мучать, правильный ответ таков: ssh a@a "rm -- $(printf '%q\n' "$A")". Согласитесь, удобно?

    • Как зайти на хост a@a, с него — на b@b, с него — на c@c, с него — на d@d, а с него удалить файл /foo? Ну, это легко:

      ssh a@a "ssh b@b \"ssh c@c \\\"ssh d@d \\\\\\\"rm /foo\\\\\\\"\\\"\""
      

      Слишком много бекслешей, да? Ну, не нравится так, давайте чередовать одинарные и двойные кавычки, будет не так скучно:

      ssh a@a 'ssh b@b "ssh c@c '\''ssh d@d \"rm /foo\"'\''"'
      

      А между прочим, если бы вместо shell'а был Lisp, и там функция ssh передавала бы на удалённую сторону не строку (вот она, повёрнутось UNIX на тексте!), а уже распарсенный AST (abstract syntax tree), то такого ада бекслешей не было бы:

      (ssh "a@a" '(ssh "b@b" '(ssh "c@c" '(ssh "d@d" '(rm "foo")))))
      

      «А? Что? Lisp? Что за Lisp?» Интересно, да? На, читайте. И другие статьи Грэма. На русском тоже можно найти.

    • Совместим предыдущие два пункта. Имя файла лежит в переменной A. Нужно зайти на a@a, с него — на b@b, далее на c@c, d@d и удалить файл, лежащий в переменной A. Это я оставляю вам в качестве упражнения :) (Сам я не знаю, как это сделать :) Ну, может, придумаю, если подумаю).

    • echo вроде как предназначен, чтобы печатать на экран строки. Вот только использовать его для этой цели, если строчка чуть сложнее, чем «Hello, world!», нельзя. Единственно верный способ вывести произвольную строку (скажем, из переменной A) таков: printf '%s\n' "$A".

    • Допустим, нужно направить stdout и stderr команды cmd в /dev/null. Загадка: какие из этих шести команд выполняют поставленную задачу, а какие — нет?

      cmd > /dev/null 2>&1
      cmd 2>&1 > /dev/null
      { cmd > /dev/null; } 2>&1
      { cmd 2>&1; } > /dev/null
      ( cmd > /dev/null ) 2>&1
      ( cmd 2>&1 ) > /dev/null
      

      Оказывается, правильный ответ — 1-я, 4-я и 6-я выполняют, 2-я, 3-я и 5-я — не выполняют. Опять-таки, выяснение причин этого оставляется в качестве упражения :)

  • Вообще, этот пост появился в ответ на вот этот пост. Там говорилось, мол, в винде специальная дата используется как метка драйвера от Microsoft. Вместо ввода специального аттрибута или проверки производителя. Особенностей такого рода в UNIX полно. Является ли файл скрытым, выясняется на основе наличия точки в начале файла вместо специального аттрибута. Когда я сам впервые об этом узнал (да, да, в те далёкие времена, когда я впервые поставил Ubuntu), я был шокирован. Я подумал, вот идиоты. А сейчас привык. Но если вдуматься, это жуткий костыль. Далее, shell выясняет, является ли он login shell'ом на основе дефиса, переданного первым символом в argv[0] (?!). Это abuses (ну или misuses, неправильно использует, не знаю, как по-русски сказать) argv[0]. argv[0] не для этого предназначен. Вместо какого-нибудь другого способа. Любой другой способ был бы красивее. Как угодно, любым другим аргументом, переменной окружения.

  • В BSD sockets юзер вынужден сам менять порядок байт у номера порта. А всё потому, что когда-то давно кто-то допустил в коде ядра UNIX ошибку, не предусмотрев смену порядка байт. И в качестве временного хака исправил user space код вместо кода ядра. Так и живём. Оттуда это и в Windows перешло (вместе с файлом /etc/hosts, он же C:\windows\system32\drivers\etc\hosts).

    UPD от 2017-02-12: источник.

«Философия UNIX». Есть мнение, что якобы UNIX прекрасна и идеальна. Что все её основные идеи («всё есть файл», «всё есть текст» и т. д.) прекрасны и составляют так называемую прекрасную «философию UNIX». Так вот, как вы уже начали догадываться, это не совсем так. Давайте разберём эту «философию UNIX» по пунктам. Сразу скажу: я не хочу сказать, что все пункты нужно отменить, просто я указываю на их неуниверсальность.

  • «Всё есть текст». Как мы с вами уже выяснили на примере /etc/passwd, повсеместное использование простого текста может привести к проблемам с производительностью. И вообще, авторы UNIX фактически придумали для каждого системного конфига (passwd, fstab и так далее) свой формат. Со своими правилами экранирования специальных символов. Да, а вы что думали? /etc/fstab использует пробелы и переносы строк как разделители. Но что если имена папок содержат, скажем, пробелы? На этот случай формат fstab'а предусматривает специальное экранирование имён папок. Так что любой скрипт, читающий fstab, оказывается, должен это экранирование интерпретировать. Например, с помощью специально предназначенной для этого утилиты fstab-decode (запускать от рута). Не знали, да? Идите исправляйте свои скрипты :) В результате для каждого системного конфига нужен свой парсер. И было бы гораздо проще, если бы для системных конфигов использовался вместо этого какой-нибудь JSON или XML. А может быть даже некий бинарный формат. Особенно для тех конфигов, которые постоянно читаются разными программами. И для которых, как следствие, нужна хорошая скорость чтения (а у бинарных форматов она выше).

    Я не закончил по поводу «всё есть текст». Стандартные утилиты выдают вывод в виде простого текста. Для каждой утилиты фактически нужен свой парсер. Часто приходится парсить вывод той или иной утилиты при помощи sed, grep, awk и т. д. У каждой утилиты свои опции для того, чтобы установить, какие именно столбцы нужно выдавать, по каким столбцам нужно сортировать вывод и т. д. Было бы лучше, если бы утилиты выдавали вывод в виде XML, JSON, некоего бинарного формата или ещё чего-нибудь. А для удобного вывода этой информации на экран и для дальнейшей работы с ней можно было бы пайпить результат в дополнительные утилиты, которые убирают те или иные столбцы, сортируют по тому или иному столбцу, выбирают нужные строки и т. д. И либо выводят результат в виде красивой таблички на экран, либо передают его куда-то дальше. И всё это универсальным способом, не зависящим от исходной утилиты, которая сгенерировала вывод. И без необходимости парсить что-либо регексами. Да, UNIX shell плохо работает с JSON и XML. Но ведь у UNIX shell полно других недостатков. Нужно выкинуть его вовсе и заменить на некий другой язык, который помимо всего прочего может удобно работать со всякими JSON.

    Вы только представьте! Вот допустим, нужно удалить все файлы в текущей папке с размером, большим 1 килобайта. Да, я знаю, что такое надо делать find'ом. Но давайте предположим, что это нужно сделать непременно ls'ом (и без xargs). Как это сделать? Вот так: LC_ALL=C ls -l | while read -r MODE LINKS USER GROUP SIZE M D Y FILE; do if [ "$SIZE" -gt 1024 ]; then rm -- "$FILE"; fi; done. (LC_ALL здесь нужен был, чтобы быть уверенным, что дата будет занимать именно три слова в выводе ls). Мало того, что это решение выглядит некрасиво, оно ещё страдает рядом недостатков. Во-первых, оно не будет работать, если имя файла содержит перевод строки или начинается с пробела. Далее, нам нужно явно перечислить названия всех столбцов ls, ну или как минимум помнить, на каком месте находятся интересующие нас (т. е. SIZE и FILE). Если мы ошибёмся в порядке столбцов, то ошибка выяснится лишь на этапе выполнения. Когда мы удалим не те файлы :)

    А как бы выглядело решение в идеальном мире, который я предлагаю? Как-то так: ls | grep 'size > 1kb' | rm. Кратко, а главное смысл виден из кода, и невозможно ошибиться. Смотрите. ls в моём мире всегда выдаёт всю инфу. Специальня опция -l для этого не нужна. Если нужно убрать все столбцы и оставить только имя файла, то это делается специальной утилитой, в которую нужно направить вывод ls. Итак, ls выдаёт список файлов. В некоем структуированном виде, скажем, JSON. Это представление «знает» названия столбцов и их типы, т. е. что это, строка, число или что-то ещё. Далее этот вывод направляется в grep, который в моём мире выбирает нужные строки из этого JSON. JSON «знает» названия полей, поэтому grep «понимает», что здесь означает «size». Более того, JSON содержит инфу о типе поля size. Он содержит инфу о том, что это число, и даже что это не просто число, а размер файла. Поэтому можно сравнить его с 1kb. Далее grep направляет вывод в rm. rm «видит», что он получил файлы. Да, да, JSON ещё и хранит инфу о типе этих строк, о том, что это — файлы. И rm их удаляет. А ещё JSON отвечает за правильное экранирование специальных символов. Поэтому файлы со спецсимволами «просто работают». Круто? Идею я взял отсюда (там ещё есть ссылка на более подробный английский оригинал), посмотрите. Ещё замечу, что в Windows Powershell реализовано как раз что-то похожее на эту идею.

  • UNIX shell. Ещё одна базовая идея UNIX. Причём о мелких недостатках UNIX shell я уже поговорил в первой части статьи. Сейчас будут крупные. В чём «крутость» UNIX shell? В том, что на момент своего появления (это было очень давно) UNIX shell был гораздо мощнее командных интерпретаторов, встроенных в другие ОС. И позволял писать более мощные скрипты. Да и вообще, на момент своего появления UNIX shell был, видимо, самым мощным из скриптовых языков вообще. Потому что нормальных скриптовых языков, т. е. таких, которые бы позволяли полноценное программирование, а не только скриптинг, тогда, видимо, вообще не существовало. Это потом уже в один прекрасный день один программист по имени Larry Wall заметил, что UNIX shell всё-таки недостаёт до нормального языка программирования. И он захотел соединить краткость UNIX shell'а с возможностью полноценного программирования из C. И создал Perl. Да, Perl и другие последующие скриптовые языки программирования фактически заменили UNIX shell. Это константирует даже Роб Пайк, один из авторов (как я считаю) той самой «философии UNIX» (про него мы ещё поговорим). Вот здесь на вопрос об «одной утилите для одной вещи» он сказал: «Those days are dead and gone and the eulogy was delivered by Perl». Причём я считаю, что эта его фраза относилась к типичному использованию UNIX shell, т. е. к ситуации связывания большого количества маленьких утилит в shell-скрипте. Нет, говорит Пайк, просто используйте Perl.

    Я не закончил про UNIX shell. Рассмотрим ещё раз пример кода на shell, который я уже приводил: find foo -print0 | while IFS="" read -rd "" A; do touch -- "$A"; done. Здесь в цикле вызывается touch (да, я знаю, что этот код можно переписать на xargs, причём так, чтобы touch вызывался только один раз; но давайте пока забьём на это, хорошо?). В цикле вызывается touch! То есть для каждого файла будет запущен новый процесс! Это нереально неэффективно. Код на любом другом языке программирования будет работать быстрее этого. Просто на момент появления UNIX shell он был одним из немногих языков, которые позволяют написать это действие в одну строчку.

    Короче говоря, вместо UNIX shell нужно использовать любой другой скриптовый язык программирования. Который подходит не только для скриптинга, но и для реального программирования. Который не запускает новый процесс каждый раз, когда нужно «touch'нуть» файл. Возможно, понадобится «доложить» в этот скриптовый язык средства для простого выполнения вещей, которые есть в shell, скажем, для создания пайпов.

  • Простота. Здесь я говорю не конкретно про shell и про связывание кучи простых утилит из shell'а (про это был предыдущий пункт), а про простоту вообще. Использование простых инструментов. Скажем, редактирование картинки sed'ом. Да, да. Конвертим jpg в ppm при помощи командной строки. Затем при помощи текстового редактора, grep, sed и такой-то матери редактируем картинку. А потом обратно в jpg. Да, так можно. Но часто photoshop'ом или gimp'ом всё-таки лучше. Хоть это и большие, интегрированные программы. Не в стиле UNIX.

На этом я закончу эти пункты. Да, хватит. Есть идеи в UNIX, которые мне реально нравятся. Скажем, «программа должна делать одну вещь и делать её хорошо». Но не в контексте shell. Вы уже поняли, что я не люблю shell. (Ещё раз повторю, я считаю, что в приведённом выше интервью Пайка он воспринял принцип «программа должна делать одну вещь и делать её хорошо» именно в контексте shell и потому отверг его). Нет, я говорю про этот принцип в своей сути. Скажем, консольный почтовый клиент не должен иметь встроенный текстовый редактор, он должен просто запустить некий внешний редактор. Или вот принцип, по которому нужно писать консольное ядро для программы и потом графическую оболочку для этого ядра.

Теперь общая картина. Однажды появился UNIX. На момент появления он был прорывом. И он был во многом лучше своих конкурентов. UNIX имел много идей. И, как и любая ОС, UNIX требовал от программистов соблюдения некоторых принципов при написании прикладных программ. Идеи, лежащие в основе UNIX, стали называться «философией UNIX». Одним из тех людей, которые сформулировали философию UNIX, был уже упомянутый Роб Пайк. Он это сделал в своей презентации «UNIX Style, or cat -v Considered Harmful». После презентации он вместе с Керниганом опубликовал статью по мотивам презентации. В ней авторы рассказали о том, что, скажем, предназначение cat — это только конкатенация и ничего больше (ну то есть «склеивание» файлов, мы с вами помним, как расшифровывается cat, так ведь?). Возможно, что это Пайк как раз и придумал «философию UNIX». В честь этой презентации был назван сайт cat-v.org, почитайте его, очень интересный сайт.

Но потом, через много лет, этот же Пайк сделал ещё две презентации, в которых, как я считаю, отменил свою философию обратно. Поняли, фанатики, да? Ваш кумир отказался от своей же философии. Можете расходиться по домам. В первой презентации «Systems Software Research is Irrelevant» Пайк сетует на то, что никто больше не пишет новых ОС. А даже если и пишут, то просто ещё один UNIX (который подразумевается в этой презентации уже чем-то неинтересным): «New operating systems today tend to be just ways of reimplementing Unix. If they have a novel architecture — and some do — the first thing to build is the Unix emulation layer. How can operating systems research be relevant when the resulting operating systems are all indistinguishable?»

Вторую презентацию Пайк прямо называет: «The Good, the Bad, and the Ugly: The Unix Legacy». Пайк говорит, что простой текст не универсален, он хорош, но работает не всегда: «What makes the system good at what it's good at is also what makes it bad at what it's bad at. Its strengths are also its weaknesses. A simple example: flat text files. Amazing expressive power, huge convenience, but serious problems in pushing past a prototype level of performance or packaging. Compare the famous spell pipeline with an interactive spell-checker». Далее: «C hasn't changed much since the 1970s… And — let's face it — it's ugly». Дальше Пайк признаёт ограниченность пайпов, соединяющих простые утилиты, ограниченность регексов.

UNIX был гениальным на момент своего появления. Особенно, если учесть, какие инструменты были в распоряжении у авторов UNIX. У них не было уже готового UNIX, чтобы на нём можно было разрабатывать UNIX. У них не было IDE. И программировали они вообще на ассемблере изначально. У них, видимо, был только ассемблер и текстовый редактор.

Люди, стоящие у истоков UNIX, в определённый момент начали писать новую ОС: Plan 9. В том числе упомянутые Томпсон, Ритчи и Пайк. Учитывая многие ошибки UNIX. Но и Plan 9 никто не возводит в абсолют. В «Systems Software Research is Irrelevant» Пайк упоминает Plan 9, но несмотря на это всё равно призывает писать новые ОС.

James Hague, ветеран программирования (занимается программированием с восьмидесятых) пишет: «What I was trying to get across is that if you romanticize Unix, if you view it as a thing of perfection, then you lose your ability to imagine better alternatives and become blind to potentially dramatic shifts in thinking» (ссылка). Прочитайте эту статью и его же статью «Free Your Technical Aesthetic from the 1970s», на которую он ссылается. (Вообще, если вам понравилась моя статья, то и его блог тоже, наверное, понравится, погуляйте там по ссылкам).

Итак, я не хочу сказать, что UNIX — плохая система. Просто обращаю ваше внимание на то, что у неё есть полно недостатков, как и у других систем. И «философию UNIX» я не отменяю, просто обращаю внимание, что она не абсолют. Мой текст обращён скорее к фанатикам UNIX и GNU/Linux. Провокационный тон просто чтобы привлечь ваше внимание.

UPD от 2017-02-14: комментаторы указывают, что сравнивать UNIX shell с PHP некорректно. Конечно, некорректно! Потому что UNIX shell не претендует на то, чтобы быть полноценным языком программирования, он предназначен для скриптинга системы. Вот только я в одно время этого не знал. И вдобавок считал UNIX shell прекрасным. Вот для людей в таком же положении я всё это и говорю. Ещё как минимум один комментатор говорит, что сравнивать UNIX shell нужно с cmd. Я бы сказал, что сравнивать надо с Windows Powershell. Последний, как я уже говорил, в чём-то превосходит UNIX shell.

UPD от 2017-02-14: мне понравился вот этот коммент от sshikov:

Но я скажу за автора — к сожалению, прямо сегодня можно найти сколько угодно восторженных статей типа «А вот есть такая замечательная фигня, как bash, щас я вам про нее расскажу...» — где unix way откровенно перехваливается неофитами. Это не помешает иногда компенсировать долей скепсиса.

Да, в этом-то и всё дело! Достало, что хвалят UNIX way. Что считают UNIX красивым и ещё и других учат. А использовать-то UNIX можно.

UPD от 2017-02-14: как минимум один комментатор сказал, что пересел с Windows на UNIX-подобные ОС и счастилив. Что поначалу он плевался от UNIX, но потом решил, что программировать под UNIX гораздо проще, чем под Windows. Так вот, я тоже сперва использовал и программировал на Windows. Потом пересел на UNIX. И сперва, конечно, было очень непривычно. Потом прочувствовал «философию UNIX», ощутил всю её мощь. Программировать под UNIX стало легко. Но позже пришло ещё одно озарение. Что UNIX неидеальна, а «философия UNIX» неабсолютна. Что программирование на «голом UNIX», с использованием C и Shell сильно уступает, скажем, Web-программированию. И далеко не только потому, что в Web-программировании используются языки, в которых трудно выстрелить себе в ногу, в отличие от C (тут языку C предъявить нечего, он намеренно является низкоуровневым). Но ещё и из-за всех этих quirks мейкфайлов, шела, языка C. Отсутствия удобных инструментов, систем сборки, менеджеров пакетов. Всё это, в принципе, можно было бы исправить. Вот я написал эту статью, чтобы открыть на это глаза тем, кто об этом не знает. У Windows тоже полно недостатков (я разве где-то говорил, что Windows лучше UNIX?). Но в чём-то Windows лучше UNIX (как минимум в некоторых особенностях Powershell). Сейчас я продолжаю использовать и программировать под UNIX. UNIX меня устраивает, мне достаточно удобно, хотя теперь уже я вижу многие его недостатки. Я не призываю бросать UNIX. Используйте UNIX дальше, просто не считайте его идеалом.

UPD от 2017-02-15: habrahabr.ru/post/321652/#comment_10070776.

UPD от 2017-02-15: habrahabr.ru/post/321652/#comment_10071096.

UPD от 2017-02-15: habrahabr.ru/post/321652/#comment_10071714.

UPD от 2017-02-16: понравился этот коммент: habrahabr.ru/post/321652/#comment_10066240.

UPD от 2017-02-16: многие комментаторы рассказывают, как же полезны и удобны UNIX системы. Что они есть уже десятки лет, на них работает весь интернет. Что они стабильны и прекрасно справляются с возложенными на них задачами. И даже удалённо переустановить GNU/Linux можно :) А я и не спорю. Я не призываю отказываться от UNIX. Я просто хочу, чтобы вы видели недостатки UNIX. UNIX работает, используйте его. Процитирую James Hague, на которого я уже ссылался:

Enough time has passed since the silly days of crazed Linux advocacy that I'm comfortable pointing out the three reasons Unix makes sense:

1. It works.
2. It's reliable.
3. It stays constant.

But don't--do not--ever, make the mistake of those benefits being a reason to use Unix as a basis for your technical or design aesthetic. Yes, there are some textbook cases where pipelining commands together is impressive, but that's a minor point. Yes, having a small tool for a specific job sometimes works, but it just as often doesn't.

Одно время я тоже, как и многие из вас, повёлся на эту «философию UNIX». Думал, что она прекрасна. А потом понял, что это не так. И вот этим своим открытием я хочу с вами поделиться. Мои мысли не новы. Они уже есть в приведённых мною ссылках. Я просто хочу сообщить эти мысли аудитории Хабра. Мой пост написан наскоро, ночью. Читайте скорее не его, а ссылки, которые я привожу. В первую очередь две презентации Пайка, в которых он «отменяет философию UNIX» и два поста от James Hague. Мой пост фактически написан, чтобы привлечь внимание к этим ссылкам.

Как минимум один из комментаторов сказал, что многие из названных мной «недостатков» UNIX недостатками не являются. Например, слишком короткие имена команд. Ну да. Это не недостаток. Но это пример необдуманного решения. Сиюминутного решения, принятого под влиянием обстоятельств, имевших важность тогда. Как и с тем примером с /usr или make. Я показываю, что UNIX была непродумана. Да и вообще, вглядитесь в историю UNIX! Сотрудникам Bell Labs не понравилась сложность проекта Multics. Они сказали: «Да ну этот Multics, давайте по-быстрому напишем свою ОС, запростецкую». И написали. Понимаете? ОС получилась довольно хорошей. Но не идеальной. UNIX — это хак. Успешный хак, который выполнил свою миссию и продолжает её выполнять.

В комментариях была мысль, что заголовок поста не соответствует содержанию, и что я критикую не самую суть, философию UNIX, а просто привожу некий список недостатков. Возможно даже не всего класса UNIX-подобных систем, а конкретных реализаций. Так вот, это не так. Да, статья начинается с перечисления мелких недостатков. Этим я обращаю внимание на то, что в UNIX полно костылей, как и в других системах. В том числе очень старых, оставшихся во всех UNIX системах и попавших во все стандарты. Но я критикую и саму философию UNIX. Основные принципы (но не все!). Язык C, UNIX shell, идею конвееров, «всё есть текст». Замечу, что компилятор C и make, хоть и являются по идее отдельными программами, всегда рассматриваются как неотъемлемая часть экосистемы UNIX. И входят в POSIX. Некоторые комментаторы пишут: «А я сижу в IDE и не использую этот ваш make». Ну окей, хорошо, мой пост предназначен скорее как раз для тех фанатиков, которые считают, что всякие IDE — это не труъ и что программировать нужно непременно используя голый C, make и shell.

И я не говорю, что философия UNIX (даже в тех местах, которые мне не нравятся) всегда не верна. Часто конвееры и shell-скрипты — это именно то, что нужно. Но не всегда.

Некоторые комментаторы указывают, что голый shell, make и прочее часто скрыты от глаз юзера всякими обёртками, всякими IDE, сложными системами сборки, GUI-интерфейсами и пр. Ну да. Так ведь это и есть признак кривости системы :) Когда что-то уродское покрывают слоем красоты. А ещё абстракции протекают. А потому использовать, скажем, autotools ещё сложнее, чем голый make. Потому что чтобы использовать autotools, нужно знать ещё и m4, make и shell. Да, да, всю эту цепочку языков, используемых при генерации окончательного мейкфайла.

Один комментатор приводит следующие принципы UNIX:

Write programs that do one thing and do it well.
Write programs to work together.
Write programs to handle text streams, because that is a universal interface.

С первыми двумя я согласен при условии, что они понимаются в отрыве от UNIX shell и конвееров. Их можно перенести даже на новомодные микросервисы, общающиеся с помощью REST. С третьим я не согласен (как я понимаю, подразумевается именно придумываение простого кастомного текстового формата для каждого случая вместо единого формата наподобие JSON). Часто текст — это именно то, что нужно. Но пихать его везде как universal interface глупо. На эту роль скорее претендует JSON или XML. Или, может, какой-нибудь формат для структуированных данных, который ещё не изобрели.

Многие указали на искусственность некоторых примеров на shell. Ну да, я знаю, что их можно было бы переписать на find -exec или xargs. Ну что вы хотите, наскоро написанная статья. Можно было привести примеры получше, просто мне не хотелось. Это не отменяет того, что в shell'е постоянно возникают проблемы со специальными символами. Которые нужно по-особому обходить. И вообще у shell'а полно quirks, которые нужно постоянно держать в голове. И он запускает новые программы на каждый чих.

Я вам ещё покушать принёс. Вот вам цитата от безусловно ещё одного вашего кумира Линуса Торвальдса:

iTWire: Systemd seems to depart to a large extent from the original idea of simplicity that was a hallmark of UNIX systems. Would you agree? And is this a good or a bad thing?

Linus Torvalds: So I think many of the «original ideals» of UNIX are these days more of a mindset issue than necessarily reflecting reality of the situation.

There's still value in understanding the traditional UNIX «do one thing and do it well» model where many workflows can be done as a pipeline of simple tools each adding their own value, but let's face it, it's not how complex systems really work, and it's not how major applications have been working or been designed for a long time. It's a useful simplification, and it's still true at *some* level, but I think it's also clear that it doesn't really describe most of reality.

It might describe some particular case, though, and I do think it's a useful teaching tool. People obviously still do those traditional pipelines of processes and file descriptors that UNIX is perhaps associated with, but there's a *lot* of cases where you have big complex unified systems.

And systemd is in no way the piece that breaks with old UNIX legacy. Graphical applications seldom worked that way (there are certainly _echoes_ of it in things like «LyX», but I think it's the exception rather than the rule), and then there's obviously the traditional counter-example of GNU emacs, where it really was not about the «simple UNIX model», but a whole new big infrastructure thing. Like systemd.

Now, I'm still old-fashioned enough that I like my log-files in text, not binary, so I think sometimes systemd hasn't necessarily had the best of taste, but hey, details…


UPD от 2017-02-18: ещё по поводу надуманных примеров на shell. Вы говорите, примеры надуманные, что можно сделать find -exec или xargs. Да, можно. Но как минимум сам факт того, что нужно постоянно держать в голове, что, мол, цикл нельзя и нужен -exec и xargs — это уже костыль. Проистекающий из принципа «всё есть текст», ну или из слишком тупой реализации этого принципа в UNIX shell.

Итак, сейчас я приведу такую задачу, в которой любое решение будет уродским, даже с использованием find -exec и xargs.

Вернёмся к моему примеру с touch'ем. «Как touch'нуть все файлы в папке foo (и во вложенных)?» Допустим, что нужно не touch'нуть их, а grep'нуть из них все строки со словом bar и положить результат туда же. Т. е. для каждого файла file сделать grep bar file > tmp; mv tmp file. Как быть? Если делать решение с циклом, то мы упираемся в те пять хаков, которые нужно сделать, чтобы не выстрелить себе в ногу. Результат будет таким, со всеми пятью хаками:

find foo -print0 | while IFS="" read -rd "" A; do
	grep -- bar "$A" > tmp
	mv -- tmp "$A"
done

Ладно, хорошо, мы знаем, что имя файла начинается на foo, а потому не может начинаться с дефиса и пробела. Но оно может заканчиваться на пробел, а потому тот трюк с IFS всё равно нужен. Так что единственный хак, от которого можно избавиться, зная, что имя начинается с foo — это написание --. Но даже от этого хака я бы не советовал избавляться, т. к. постоянное использование -- даёт понять читающему: «Да, я подумал об этом». Это как условия Йоды.

Окей, можно ли этот пример написать проще с использованием xargs или find -exec? Если бы каждый файл нужно было всего лишь touch'нуть, то да, можно было бы написать существенно проще. Но если нужно выполнить два действия: grep и переименование, то существенного упрощения мы уже не получим. Два действия означают, что нам уже нужно запихивать эти два действия в вызов shell'а, в sh -c. Как будет выглядеть результат? Может быть, так?

find foo -exec sh -c "grep -- bar '{}' > tmp; mv -- tmp '{}'" ';'

Нет, неправильно! Это не будет работать, если имя содержит одинарную кавычку. Правильный вариант таков:

find foo -exec sh -c 'grep -- bar "$1" > tmp; mv -- tmp "$1"' dummy '{}' ';'

Видите? Опять хак. Нам пришлось передать имя файла через $1. И по-прежнему нужно помнить, что нам нужны двойные кавычки вокруг $1. То же самое было бы с xargs. Опять нужен sh -c и опять нужно передавать аргументы через $1.

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

Теперь по поводу другого примера. Где нужно удалить все файлы определённого размера. Да, всё это можно сделать одним вызовом find. Там есть опции и для проверки размера, и для удаления. Да. Вот только я вижу здесь хак. Хак в том, что find имеет фактически в себе целый sublanguage, подъязык. Язык вот этих вот опций. Почитайте хорошенько ман find'а. Вы узнаете, что, оказывается, порядок опций find'а имеет значение. Что каждая опция имеет truth value, т. е. булевское значение. Что можно по-хитрому комбинировать эти опции. Что в зависимости от порядка опции, от их truth value find принимает решение, в какой момент нужно остановить обработку опций для данного файла и нужно ли descend в данный каталог (т. е. нужно ли искать внутри этого этого каталога).

Я помню, как однажды жутко оплошался, не зная этих тонкостей. Я набрал find -delete -name '*~' вместо find -name '*~' -delete или что-то такое. Ну подумаешь, думал я, опции не в том порядке. Смысл же тот же. И find удалил всё. Снёс мои важные файлы. Потом я восстановил из бекапа, так что всё ок. Это потом уже я понял, что -name имеет truth value true в случае, если файл соответствует маске. И если -name вернул true, то обработка опций продолжается.

Что тут плохого? Плохо то, что find имеет свой sublanguage. Что это ещё один язык в дополнение к shell. (А sed, кстати говоря — это ещё один язык, а awk — это ещё один язык и так далее, авторы UNIX'а любили создавать по языку на каждый чих.) Нужно было вместо этого сделать так, чтобы find только умел искать файлы. А всю остальную функциональность нужно вынести из него. Проверки на размер файла должны быть снаружи. А если find'у нужно принять решение, нужно ли descend в данный каталог, то он должен вызывать внешний callback. Да, в UNIX shell так вряд ли получится. На то он и UNIX shell.

UPD от 2017-08-01.

echo вроде как предназначен, чтобы печатать на экран строки. Вот только использовать его для этой цели, если строчка чуть сложнее, чем «Hello, world!», нельзя.

И даже для этой цели echo использовать можно не всегда. Недавно прочитал, что в интерактивном старом bash нельзя писать echo "Hello, world!". Вот несколько абзацев текста, объясняющих суть проблемы и пути обхода. В новых bash такого нет, на моей системе не воспроизводится.
Проголосовать:
+139
Сохранить: