Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Командная оболочка Bourne-Again Shell

Оригинал: "The Bourne-Again Shell", глава из книги "The Architecture of Open Source Applications"
Автор: Chet Ramey
Дата публикации: 2012 г.
Перевод: Н.Ромоданов
Дата перевода: август 2012 г.

3.1. Ведение

Командная оболочка Unix предоставляет интерфейс, который позволяет пользователю при помощи запуска команд взаимодействовать с операционной системой. Но в командной оболочке также представлен довольно богатый язык программирования: есть конструкции управления порядком выполнения команд, изменения этого порядка, организации циклов, условного исполнения; есть также основные математические операции, именованные функции, строковые переменные, а также двунаправленная связь между командной оболочкой и командами, которые в ней вызываются.

Командные оболочки можно использовать в интерактивном режиме, обращаясь к ним из терминала или эмулятора терминала, например, из xterm, или неинтерактивно, читая команды из файла. В большинстве современных командных оболочек, в том числе и в bash, есть средства редактирования командной строки, в которых с помощью команд, похожих на команды emacs или vi, можно манипулировать с командной строкой, пока она еще не введена, а также есть различные варианты сохранения команд в списке уже выполненных команд.

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

В данной главе мы рассмотрим основные компоненты bash - обработку входных данных, синтаксический анализ, различные раскрытия слов и прочую обработку команд, а также исполнение команд, если их рассматривать с точки зрения конвейерной обработки. Эти компоненты выступают в роли конвейера, который считывает данные с клавиатуры или из файла, превращая их в выполненные команды.

Компонентная архитектура bash

Рис. 3.1: Компонентная архитектура bash

3.1.1. Bash

Bash является командной оболочкой, которая появилась в операционной системе GNU, обычно реализуемой поверх ядра Linux, а также в некоторых других операционных системах и, в первую очередь, в Mac OS X. По сравнению с ушедшими в историю версиями оболочки sh, в bash улучшены функциональные возможности, касающиеся как интерактивного режима, так и возможностей программирования.

Название является сокращением от Bourne-Again SHell, каламбура объединения имени Стивена Борна (Stephen Bourne - автор прямого предшественника используемой в текущих версиях Unix командной оболочки /bin/sh, который появился в версии Bell Labs Seventh Edition Research системы Unix) с понятием перерождения через новую реализацию. Подлинным автором командной оболочки bash был Брайан Фокс (Brian Fox), сотрудник фонда Free Software Foundation. Я работаю в качестве волонтера в университете Case Western Reserve University в Кливленде, штат Огайо, и в настоящее время являюсь разработчиком bash, а также осуществляю его поддержку.

Точно также как и другое программное обеспечение GNU, bash является полностью переносимым. В настоящее время он работает практически на любой версии Unix, а также на нескольких других операционных системах — есть независимые порты bash , например, для сред Cygwin и MinGW, поддерживаемых в Windows, также порты bash входят в состав дистрибутивов для Unix-подобных операционных систем, например, QNX и Minix. Чтобы собрать и запустить bash, необходима только среда Posix, например, такая, что предоставлена фирмой Microsoft в составе сервисов Services for Unix (SFU).

3.2. Синтаксические единицы и примитивы

3.2.1. Примитивы

Что касается bash, то в ней есть три основных вида лексем: зарезервированные слова, слова и операторы. Зарезервированными словами являются такие, которые в командной оболочке и в ее языке программирования имеют особое значение; как правило, эти слова применяются для написания конструкций, определяющих последовательность выполнения действий, например, if и while. Операторы состоят из одного или нескольких метасимволов - символов, которые, сами по себе, имеют в командной оболочке особое значение, например, | и >. Остальные данные, вводимые в командной оболочке, представляют собой обычные слова, некоторые из которых в зависимости от того, где в командной строке они находятся, имеют особое значение — например, инструкции присваивания или числа.

3.2.2. Переменные и параметры

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

Значениями переменных являются строки. Некоторые значения, в зависимости от контекста, трактуются специальным образом; об этом будет рассказано позже. Переменные назначаются с помощью инструкций вида name=value. Значение value является необязательным; если оно не указано, то переменной по имени name присваивается пустая строка. Если значение указано, оболочка раскрывает значение и присваивает его переменной по имени name. В зависимости от того, задано ли значение переменной или нет, оболочка может выполнять разные операции, однако единственным способом задать переменной значение является присваивание. Переменные даже в случае, если они были объявлены и для них были заданы атрибуты, но которым значение еще не присвоено, считаются неопределенными (имеют значение unset).

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

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

Типизация переменных минимальна: в дополнение к обычным строковым переменным есть целые числа (тип integer) и массивы (тип integer). Переменные целочисленного типа трактуются как числа: любая присваиваемая им строка раскрывается как арифметическое выражение и полученный результат присваивается переменной в качестве значения. Массивы могут быть индексные или ассоциативные; в индексных массивах в качестве индексов используются числа, а в ассоциативных массивах - произвольные строки. Элементы массива являются строками, которые, если это необходимо, можно рассматривать как целые числа. Элементами массива не могут быть другие массивы.

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

3.2.3. Язык программирования командной оболочки

Простая команда командной оболочки, которая более всего известна большинству читателей, состоит из имени команды, например, echo или cd, и списка, состоящего из нуля или большего числа аргументов и перенаправлений. Перенаправления позволяют пользователю командной оболочки управлять в вызываемых командах вводом и выводом данных. Как отмечалось выше, пользователи могут для простых команд определять локальные переменные.

С помощью зарезервированных слов вводятся более сложные команды командной оболочки. Есть конструкции, обычные для любого высокоуровневого языка программирования, например, if-then-else, while, цикл for для итерации по списку значений, а также арифметический цикл for, похожий на используемый в языке C. С помощью этих более сложных команд командная оболочка может выполнить команду или другую проверку условия и, в зависимости от полученного результата, выполнять различные операции, либо может повторять выполнение команды много раз.

Одним из подарков, который Unix принес в компьютерный мир, является конвейер: линейный список команд, в котором выход одной команды в списке становится входом следующей команды. В конвейере можно использовать любую конструкцию командной оболочки; не редкость видеть конвейеры, в которых в цикле обрабатываются данные, выдаваемые командами.

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

Bash позволяет запоминать программы, работающие в командной оболочке, и использовать их более одного раза. Есть два способа именования групп команд - создать функцию командной оболочки или создать скрипт командной оболочки — и выполнить их точно также, как и любую другую команду. Функции командной оболочки объявляются с использованием специальных синтаксических правил, а затем запоминаются и выполняются в контексте той же самой командной оболочки; скрипт командной оболочки создается при помощи записи команд в файл и выполнение этого файла представляет собой запуск нового экземпляра командной оболочки, которая интерпретирует этот файл. Функции обычно выполняются в командной оболочке в том же самом контексте исполнения, из которой они были вызваны; однако скрипты, поскольку они интерпретируются в новом экземпляре командной оболочки, могут совместно использовать только те данные, которые в среде командных оболочек могут передаваться между процессами.

3.2.4. Дополнительное замечание

Когда вы будете читать дальше, имейте в виду, что все возможности командной оболочка реализованы с использованием всего лишь нескольких структур данных: массивов, деревьев, односвязных и двусвязных списков и хэш-таблиц. Почти все конструкции, имеющиеся в командной оболочке, реализованы с использованием этих примитивов.

Основной структурой данных, используемой командной оболочкой для передачи информации от одного этапа к другому, а также для работы с элементами данных на каждом этапе обработки, является структура WORD_DESC:

typedef struct word_desc {
  char *word;           /* Zero terminated string. */
  int flags;            /* Flags associated with this word. */
} WORD_DESC;

Слова, например, объединяются в списки аргументов с помощью простых связанных списков:

typedef struct word_list {
  struct word_list *next;
  WORD_DESC *word;
} WORD_LIST;

Списки слов вида WORD_LIST используются в командной оболочке повсюду. Простая команда является списком слов, результат раскрытия выражения является списком слов, а в каждой встроенной команде список слов используется в качестве аргументов.

3.3. Обработка входных данных

Первой стадией конвейерной обработки в bash является обработка входных данных: из терминала или из файла берутся символы, из них формируются строки, а затем строки передаются в синтаксический анализатор командной оболочки для их преобразования в команды. Как вы и ожидали, строки представляют собой последовательности символов, заканчивающиеся символами новой строки.

3.3.1. Readline и редактирование командной строки

Bash, когда он находится в интерактивном режиме, читает входные данные с терминала, либо, в противном случае, из файла-скрипта, указываемого в качестве аргумента. В интерактивном режиме bash позволяет пользователю с помощью известных последовательностей нажатий клавиш и команд редактирования, похожих на те, что есть в системе Unix в редакторах emacs и vi, редактировать командные строки, которые он набрал.

В bash для редактирования командных строк используется библиотека readline. В ней есть функции, позволяющие пользователям редактировать строки команд, сохраняющие строки команд по мере их ввода, повторно обращающиеся к ранее набранным командам, а также раскрывающие команды по списку истории команд наподобие того, как это сделано в csh. Bash является основным клиентским приложением readline и они разрабатываются вместе, но в readline нет кода, зависящего от bash. Библиотека readline используется во многих других проектах для реализации интерфейса редактирования строк, вводимых с терминала.

Readline также позволяет пользователям связывать последовательности нажатий клавиш неограниченной длины с любой из команд из большого количества команд, имеющихся в readline. В readline есть команды для перемещения курсора вдоль строки, вставки и удаления текста, поиска предыдущих строк и автозавершения частично набранных слов. Помимо этого, пользователи могут определять макросы, которые являются строками символов, вставляемых в командную строку в ответ на нажатие последовательности клавиш; в макросах используется тот же самый синтаксис, что и при связывании последовательностей клавиш с командами. Макросы предоставляют пользователям библиотеки readline средства простой подстановки строк и сокращения усилий при вводе данных.

Структура readline

С точки зрения структуры readline представляет собой цикл "чтения / диспетчеризации / исполнения / повторного отображения". Библиотека читает символы с клавиатуры с помощью операции read или другой эквивалентной, либо получает их из макроса. Каждый символ рассматривается как индекс в таблице раскладки клавиатуры или таблицы диспетчеризации. Хотя в качестве индексов используются одиночные восьмибитовые символы, содержимое каждого элемента таблицы раскладки может использоваться для различных целей. Символы могут использоваться для доступа к дополнительным таблицам клавиатурных раскладок, в которых могут быть заданы многосимвольные последовательности нажатий клавиш. Если выясняется, что символ является некоторой командой readline, например, командой beginning-of-line (начало строки), то будет выполнена эта конкретная команда. Символ, связанный с командой self-insert (самоподставляемый), запоминается в буфере редактирования. Также можно связать последовательность нажатия клавиш с некоторой командой, причем в качестве этой команды использовать последовательность нескольких объединенных вместе различных команд (возможность, добавленная сравнительно недавно); в таблице раскладки есть специальный индекс, указывающий, что была сделана такая привязка. Привязка последовательности нажатия клавиш к макросам позволяет достичь еще большой гибкости: от вставки в командную строку произвольных строк и до создания горячих клавиш для сложных последовательностей операций редактирования. Библиотека readline запоминает каждый символ, связанный с командой self-insert (самоподставляемый), в буфере редактирования, который, когда он отображается, может занимать на экране одну или несколько строк.

В библиотеке readline используются символьные буфера и строки, содержащие только тип данных chars языка C и, если необходимо, из них создаются многобайтовые символы. Тип данных wchar_t внутри библиотеки не используется по причинам, связанным со скоростью работы и способами хранения данных, а также из-за того, что код, с помощью которого выполняется редактирование, был создан раньше, чем поддержка многобайтовых символов получила широкое распространение. Когда в локали поддерживается использование многобайтовых символов, то readline автоматически считывает целиком весь многобайтовый символ и помещает его в буфер редактирования. Можно связать многобайтовые символы с командами редактирования, но нужно связывать такой символ как последовательность нажатия клавиш; это возможно, но сложно и, как правило, не требуется. Например, в существующих наборах команд emacs и vi многобайтовые символы не используются.

Как только в команде редактирования будет окончательно определена последовательность нажатий клавиш, readline обновит изображение, выдаваемое на дисплей, для того, чтобы отобразить эти результаты. Это происходит независимо от того, будут ли результаты работы команды в виде символов вставлены в буфер, измениться ли позиция, в которой выполняется редактирование, или будет ли частично или полностью заменена строка. Некоторые связываемые команды редактирования, например, те, что изменяют файл истории команд, не будут вызывать никаких изменений в содержимом буфере редактирования.

Хотя процесс обновления изображения, выдаваемого на терминал, и кажется простым, он состоит из ряда действий. Библиотека readline должна следить за тремя вещами: за текущим содержимым буфера символов, отображаемых на экране, за обновлениями содержимого этого буфера изображений и за фактически отображаемыми символами. Когда имеются многобайтовые символы, отображаемые символы не соответствуют точно содержимому буфера и средства обновления изображения должны это учитывать. Когда происходит обновление изображения, readline должна сравнить содержимое буфера текущего изображения с обновленным буфером, выявить различия, и решить, как с учетом обновлений, имеющихся в буфере, наиболее эффективно обновить изображение. Эта проблема была предметом серьезного исследования на протяжении многих лет (проблема корректировки вида "строка в строку"). Подход, используемый в readline, состоит в выявлении начала и конца той части буфера, которая отличается, вычисления затрат на обновление только этой части, в том числе на перемещение курсора назад или вперед (например, потребуется ли больше затрат для того, чтобы выдать на терминал команды, которые удалят символы, а затем вставят новые, вместо простой перезаписи текущего содержимого экрана?), выполнении самого меньшего по затратам варианта обновления, а затем, если это необходимо, очистки — удаления всех символов, оставшихся в конце строки, и установке курсора в нужном месте.

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

Readline возвращает содержимое буфера редактирования в приложение, из которого оно было вызвано и которое затем должно сохранить результаты, возможно измененные, в списке истории команд.

Расширение функциональных возможностей readline в приложениях

Точно также как пользователям в readline предлагаются различные способы настройки и расширения функциональных возможностей, используемых по умолчанию, так и приложениям предоставляется ряд механизмов, позволяющих расширить набор возможностей, предлагаемых по умолчанию. Во-первых, подключаемые функции readline получают доступ к стандартному набору аргументов и возвращают определенный набор результатов, что позволяет приложениям легко расширять возможности readline своими собственными функциями. Например, в bash добавлено более тридцати команд, используемых при связывании: от автозавершения конкретных слов и до интерфейсов к командам, встроенным в командную оболочку.

Во втором случае readline позволяет изменить свое поведение за счет использования повсеместно имеющихся указателей на функции, осуществляющие перехват управления (hook function), имена и интерфейс вызова которых хорошо известны. Приложениям разрешено подменять некоторые внутренние фрагменты кода readline, вставлять перед работой readline некоторые собственные функции и выполнять преобразования, необходимые конкретному приложению.

3.3.2. Обработка входных данных в неинтерактивном режиме

Когда командная оболочка не пользуется библиотекой readline, она для получения входных данных будет использовать либо stdio, либо свои собственные подпрограммы буферированного ввода. Если командная оболочка находится в неитерактивном режиме, то использование пакета буферированного ввода, который есть в bash, более предпочтительно, чем stdio, из-за нескольких своеобразных ограничений, которые связаны в Posix с тем, что следует делать при вводе данных: на вход командной оболочки долны поступать только данные, необходимые для анализа команд, а все остальное должно передаваться исполняемым программам. В частности это важно, когда оболочка считывает скрипт из стандартного входного потока. Командная оболочка может буферировать входные данные в том объеме, сколько это будет необходимо, и так долго, пока в файле сразу после того, как будет проанализирован последний символ, не будет выполнен откат обратно. С практической точки зрения это значит, что когда данные считываются из устройств, в которых нет возможности выполнять поиск, например, из конвейеров, командная оболочка должна считывать скрипт символ за символом, но когда чтение происходит из файла, в буфер можно записывать столько символов, сколько будет необходимо.

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

3.3.3. Многобайтовые символы

Обработка многобайтовых символов была добавлена в командную оболочку значительно позже первоначальной реализации оболочки, причем это было сделано таким образом, чтобы свести к минимуму влияние этого добавления на уже существующий код. Когда установлена локаль, в которой есть поддержка многобайтовых символов, то командная оболочка записывает свои входные данные в буфер, состоящий из байтов (тип char языка С), но трактует эти байты как потенциально возможные многобайтные символы. В readline известно, как выводить многобайтовые символы (здесь важно знать, сколько позиций на экране занимает многобайтный символ и сколько байтов нужно считать из буфера, когда символ отображается на экране), как перемещаться по строке вперед и назад в случае, когда перемещение происходит не на один байт за один раз, и так далее. Во всем другом многобайтовые символы не оказывают большого влияния на процесс ввода данных. Другие части командной оболочки, которые будут описаны ниже, должны знать о том, что используются многобайтовые символы, и учитывать это при обработке входных данных.

3.4. Анализ

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

Одна исторически сложившаяся проблема, касающаяся командной оболочки, состоит в том, как сказал Том Дафф (Tom Duff ) в своей статье о rc - командной оболочке системы Plan 9, что никто не знает, что такое грамматика оболочки Борна. Особой благодарности заслуживает Комитет по стандарту оболочки Posix, который, наконец, опубликовал окончательную редакцию грамматики для оболочки Unix, хотя и в ней есть масса контекстных зависимостей. Эта грамматика не без проблем — в ней запрещены некоторые конструкции, которые были бы без ошибок восприняты давно созданными синтаксическими анализаторами оболочки Борна, но это лучшее, что у нас есть.

Синтаксический анализатор bash был создан на основе ранней версии грамматики Posix, и, насколько я знаю, является лишь синтаксическим анализатором командной оболочки в стиле Борна, реализованной с помощью Yacc или Bison. Вследствие этого возник определенный набор трудностей — в действительности грамматика командной оболочки не очень хорошо подходит для синтаксического анализа в стиле yacc и требует более сложного лексического анализа и большего объема взаимодействий между синтаксическим и лексическим анализаторами.

В любом случае, лексический анализатор получает входные строки из readline или другого источника, разбивает их на лексемы, разделяемыми метасимволами, идентифицирует лексемы с учетом контекста и передает их в синтаксический анализатор для сборки их в инструкции и команды. Контекст может быть весьма различным — например, слово for может быть зарезервированным словом, идентификатором, часть инструкции присваивания, или другим словом, и следующая команда, которая является вполне допустимой:

for for in for; do for=for; done; echo $for

выдает на терминал слово for.

В данный момент настала очередь сделать небольшое отступление, относящееся к использованию алиасов (или синонимов — прим.пер.). Bash позволяет с помощью методики алиасов заменять произвольным текстом первое слово простой команды. Поскольку эта замена полностью лексическая, алиасы можно даже употреблять (или ими злоупотреблять) для того, чтобы изменить грамматику командной оболочки: можно написать алиас, реализующий составную команду, которой нет в bash. Анализатор bash реализует методику алиасов полностью на фазе лексического анализа, тем не менее, синтаксический анализатор должен информировать лексический анализатор, когда расрытие алиасов не допускается.

Как и во многих других языках программирования, в командной оболочке разрешается перед специальными символами указывать другие специальные символы, отменяющие особенности использования первых специальных символов (те. использовать так называемые escape-последовательности символов — прим.пер.), поэтому в командах можно использовать метасимволы, например, &. Есть три типа кавычек, каждый из которых немного отличается и позволяет несколько по иному интерпретировать выделенный текст: обратный слеш, который экранирует следующий символ, одинарные кавычки, которые предотвращают интерпретацию всех символов, находящихся внутри кавычек, и двойные кавычки, которые отключают некоторые виды интерпретации, но позволяют выполнять раскрытие некоторых слов (и иначе интерпретировать символы обратного слеша). Лексический анализатор считывает символы и строки, заключенные в кавычки, и не позволяет синтаксическому анализатору искать в них зарезервированные слова или метасимволы. Есть также два особых варианта - $'…' и $"…", в которых символы, перед которыми указан обратный слеш, раскрываются точно также, как это делается в строках языка ANSI C, и в которых можно, соответственно, транслировать символы с использованием функций, поддерживающих интернационализацию. Первый вариант используется широко, последний, возможно, из-за того, что для него приведено мало хороших примеров или вариантов его использования, применяется в меньшей степени.

Остальная часть интерфейса между синтаксическим и лексическим анализаторами сравнительно проста. Синтаксический анализатор кодирует определенное количество состояний и использует их совместно с лексическим анализатором для того, чтобы можно было с помощью грамматики выполнить контекстно-зависимый анализ. Например, лексический анализатор классифицирует слова в зависимости от типа лексемы: зарезервированное слово (в соответствующем контексте), слово, инструкция присваивания и так далее. Чтобы это сделать, синтаксический анализатор должен независимо от того, обрабатывается ли многострочный текст (иногда называемых "встроенными документами"), анализируется ли инструкция case или команда условного выполнения, обрабатывается ли расширенный шаблон или составная инструкция присваивания, сообщить некоторые сведения о том, насколько далеко вперед продвинулся синтаксический разбор команды.

Большая часть работы, связанной с нахождением конца замещения команды на стадии синтаксического анализа, инкапсулирована в одной функции (parse_comsub), которая разбирается со всеми неудобными случаями синтаксиса командной оболочки и в ней гораздо больше кода, читающего лексемы, чем это было бы в оптимальном случае. Эта функция должна знать о встроенных документах, о комментариях командной оболочки, о метасимволах и границах слов, об использовании кавычек и случаях, когда можно использовать зарезервированные слова (так что она знает, когда они должны быть в инструкции case); потребовалось время чтобы делать это правильно.

Когда в процессе раскрытия слов необходимо замещение команды, bash для того, чтобы найти правильное окончание конструкции, пользуется синтаксическим анализатором. Это аналогично преобразованию строки в команду с помощью команды eval, но в этом случае команда не завершается концом строки. Чтобы выполнить эту работу, синтаксический анализатор должен распознать правую скобку как признак завершения команды; при выводе ряда грамматических правил это приводит к частным случаям и требуется, чтобы лексический анализатор помечал правую скобку (в соответствующем контексте) как символ конца файла EOF. Прежде, чем рекурсивно обращаться к yyparse, анализатор также должен уметь сохранять и восстанавливать свое внутреннее состояние, поскольку при замещении команды в процессе ее чтения может потребоваться произвести синтаксический анализ и выполнить часть операции раскрытия. Поскольку в функциях ввода данных реализовано упреждающее чтение, то независимо от того, будет ли bash читать данные из строки файла или с терминала с помощью readline, следует, наконец, также позаботиться о перемещении указателя входного потока bash вправо в нужное место. Это важно не только чтобы не потерять входные данные, но также и для того, чтобы функции, выполняющие замещение команд, создали для исполнения правильную строку.

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

Использование кавычек также является источником несовместимости и обсуждений. Спустя двадцать лет после того, как был опубликован первый стандарт командной оболочки Posix, члены Рабочей группы по стандартам до сих пор обсуждают правильную обработку неясных случаев использования кавычек. Как и прежде, командная оболочка Bourne не поможет ничем, кроме как понаблюдать за ней как за эталонной реализацией.

Синтаксический анализатор возвращает единственную структуру на языке C, представляющую собой команду (которая, в случае составных команд, например, циклов, может, в свою очередь, состоять из нескольких других команд), и передает ее на следующий этап работы командной оболочки - этап раскрытия слов. В структуре хранятся объекты, представляющие команду, и списки слов. Большая часть списков слов будет, в зависимости от контекста, преобразовано, что описывается в следующих разделах статьи.

3.5. Раскрытие слов

После синтаксического разбора, но до этапа исполнения, многие из слов, сформированные на стадии синтаксического анализа должны быть подвергнуты одной или нескольким операциям раскрытия, так, например, слово $OSTYPE будет заменено строкой "linux-gnu".

3.5.1. Раскрытие параметров и переменных

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

Есть варианты операций раскрытия, которые действуют на значение самой переменной. Программисты могут ими пользоваться для получения подстрок из значения переменной, определения длины строки, удаления различных частей строки, соответствующих при просмотре с начала строки или с ее конца заданному шаблону, замены части строки, соответствующей заданному шаблону, новой строкой, или изменения набора символов.

В добавок есть варианты раскрытия, которые зависят от состояния переменной: в зависимости от того, задано ли переменной значение или нет, могут выбираться различные варианты раскрытия или присваивания различные значения. Например, ${parameter:-word} будет раскрыто как parameter в случае, если значение переменной задано, и как word в случае, если оно не задано или задано значение пустой строки.

3.5.2. И многое другое

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

pre{one,two,three}post

в:

preonepost pretwopost prethreepost

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

Одна из проблем, связанная с замещением команд, состоит в том, что в этом случае немедленно запускается команда, работающая изолированно, и ожидается ее завершение: в командной оболочке нет простого способа передать команде входные данные. В bash используется возможность, называющаяся замещением процессов и представляющая собой своего рода комбинацию замещения команды и конвейеров командной оболочки, благодаря чему этот недостаток компенсируется. Точно также, как и в случае замещения команд, в bash запускается команда, но она работает в фоновом режиме и bash не ожидает ее завершения. Главное то, что bash открывает конвейер к команде для чтения или записи и указывает в конвейере имя файла, который станет результатом операции раскрытия.

Далее идет раскрытие символа тильды ~. Первоначально предполагалось, что ~alan будет ссылкой на домашний каталог Алана, но за прошедшие годы этот вариант раскрытия сильно расширился и позволяет ссылаться на большое количество различных каталогов.

Наконец, имеется раскрытие арифметических выражений. В $((expression)) выражение expression должно вычисляться по тем же правилам, что выражения языка C. Результат вычисления выражения становится результатом раскрытия.

Раскрытие переменных это как раз тот случай, когда наиболее очевидной становится разница между одинарными и двойными кавычками. Одинарные кавычки отключают все раскрытия - символы, заключенные в кавычки, передаются через операцию раскрытия без всяких изменений, тогда как в случае двойных кавычек некоторые раскрытия выполняются, а другие — не выполняются. Выполняется раскрытие слов и команд, раскрытие арифметических выражений и выполняется замещение процессов - двойные кавычки только влияют то, как обрабатывается результат, раскрытие скобок и символа тильды не выполняется.

3.5.3. Разбиение на слова

Результат раскрытия слов разбивается на отдельные слова, причем в качестве разделителей слов используются символы, указанные в переменной командной оболочки IFS. Речь идет о том, как командная оболочка преобразует одно слово в несколько слов. Каждый раз, когда в результате выполнения операции раскрытия обнаруживается один из символов, указанных в переменной $IFS (смотрите в конце статьи примечание 1) bash разбивает слово на два новых слова. Одинарные и двойные кавычки отключают функцию разбиения на слова.

3.5.4. Подстановка

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

3.5.5. Реализация

Если базовая архитектура командной оболочки допускает распараллеливание конвейеров, то раскрытие слов будет небольшим конвейером, замкнутым самим на себе. На каждом этапе раскрытия слов берется некоторое слово и после того, как оно, возможно, будет преобразовано, оно передается на следующий этап раскрытия. После того, как все этапы раскрытия слова будут пройдены, будет выполнена команда.

Реализация раскрытия слов в bash строится на основе уже ранее описанных структур данных. Слова, выдаваемые синтаксическим анализатором, раскрываются по одному, в результате чего каждое отдельное слово, имеющееся на входе, будет раскрыто на выходе в виде одного или нескольких слов. Структура данных WORD_DESC оказалась достаточно универсальной и в ней может храниться вся информация, необходимая для инкапсуляции результатов раскрытия одного слова. Для кодирования информации, используемой на стадии раскрытия слов, а затем передаваемой с этой стадии на следующую, применяются флаги. Например, синтаксический анализатор использует флаг, говорящий на стадиях раскрытия слов и исполнения команд о том, что конкретное слово является инструкцией присваивания командной оболочки; а в коде, осуществляющим раскрытие слов, флаги используются для запрета разбиения на слова или пометки о присутствии заключенной в кавычки строки, имеющей значение null, ("$x", где $x не определено или имеет значение null). Использовать для раскрытия всех слов единую строку символов с какой-то кодировкой для представления дополнительной информации, оказалась бы гораздо сложнее.

Как и в синтаксическом анализаторе, в коде, осуществляющем раскрытие слов, обрабатываются символы, для представления которых требуется более одного байта. Например, длина переменной при раскрытии (${#variable}) подсчитывается в символах, а не в байтах, и код в случае использования многобайтовых символов может правильно идентифицировать завершение операций раскрытия и найти специальные символы, используемые при раскрытии.

3.6. Исполнение команд

Стадия выполнения команд внутреннего конвейера bash является тем местом, где происходит реальное действие. Как правило, набор слов, для которых было выполнение раскрытие декомпозируется на имя команды и набор аргументов и передается в операционную систему в виде файла, который читается и исполняется вместе с остальными словами, передаваемыми в остальных элементах структуры argv.

В нашем описании внимание до сих пор умышленно сосредотачивалось на том, что Posix обращается к простым командам — тем, у которых есть имя и набор аргументов. Это наиболее распространенный тип команд, но у bash намного больше возможностей.

На входе на стадию выполнения команд используется структура данных, созданная синтаксическим анализатором и заполненная возможно раскрытыми словами. Вот где в игру вступает настоящий язык программирования bash. В языке программирования, как уже указывалось ранее, используются переменные и раскрытия и также реализованы конструкции, наличие которых следовало бы ожидать в языке высокого уровня: циклы, условные инструкции, чередование, объединение в множества, выбор из множеств, условное выполнение с учетом сопоставления с шаблоном, оценка выражений и несколько конструкций более высокого уровня, характерных для командной оболочки.

3.6.1. Перенаправление

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

Недавнее дополнение к синтаксису перенаправления позволяет пользователям вместо того, чтобы давать пользователю самостоятельно выбирать дескриптор файла, дать указание командной оболочке выбрать подходящий дескриптор файла и присвоить его конкретной переменной. Программистам становится проще отслеживать дескрипторы файлов, но это требует дополнительной обработки: оболочка должна сделать в подходящем месте копии дескрипторов файлов и проверять, что они назначены конкретной переменной. Это еще один пример того, как информация передается из лексического анализатора через синтаксический анализатор на стадию выполнения команды: лексический анализатор определяет, что в слове, в котором есть перенаправление, осуществляется присваивание значения переменной; синтаксический анализатор при помощи соответствующих правил грамматического вывода создает объект, используемый при перенаправлении, у которого есть флаг, указывающий, что требуется аргумент; код, осуществляющий перенаправление, получает этот флаг и обеспечивает, чтобы номер дескриптора файла был назначен правильной переменной.

Самой трудной частью реализации перенаправления является запоминание информации, необходимой для отмены перенаправления. В командной оболочке намеренно стирается различие между командами, выполняемыми из файловой системы, что требует создания нового процесса, и командами, которые оболочка выполняет сама (встроенные команды), но, вне зависимо от того, как команда реализована, эффект перенаправления не должен сохраняться после того, как эта команда будет завершена (смотрите в конце статьи примечание 2). Поэтому командная оболочка должна следить за тем, как отменить эффект каждого перенаправления, в противном случае перенаправление выходного потока во внутренней команде изменит стандартный выходной поток самой командной оболочки. В bash известно, как отменять перенаправление каждого типа: либо с помощью закрытия дескриптора файла, который ранее был выделен, либо с помощью создания дубля дескриптора файла и позже восстановления дескриптора с помощью команды dup2. Здесь используются те же самые объекты перенаправления, которые были созданы синтаксическим анализатором, а для обработки используются те же самые функции.

Т.к. многократные перенаправления реализованы в виде простых списков объектов, перенаправления, используемые для их отмены, хранятся в отдельном списке. Этот список обрабатывается, когда выполнение команды завершено, но об этом должна позаботиться командная оболочка, поскольку перенаправления, используемые с функцией командной оболочки или со встроенной функцией ".", могут продолжать действовать до тех пор, пока функция не будет завершена. Когда происходит обращение к встроенной функции exec, причем она не вызывается как команда, то список отмены перенаправлений будет просто удаляться, поскольку перенаправления, связанные с exec, запоминаются и хранятся в среде командной оболочки без их отмены.

Другая сложность связана с самой оболочкой bash. В ранее использовавшихся версиях оболочки Bourne пользователю разрешалось обращаться только к дескрипторам 0 - 9, дескрипторы 10 и выше были зарезервированы для внутреннего использования в самой оболочке. В bash это ограничение ослаблено — пользователь может манипулировать дескриптором с любым номером вплоть до предела, обусловленного ограничением на количество открытых в процессе файлов. Это значит, что bash должен следить за дескрипторами файлов, открытых для его собственных внутренних нужд, в том числе и тех, что были открыты внешними библиотеками, а не только непосредственно самой оболочкой, и иметь возможность при необходимости переместить эти дескрипторы. Для этого требуется учитывать многое, в некоторых эвристиках нужно использовать флаг close-on-exec, и в течение всего времени, пока выполняется команда, необходимо поддерживать еще один список перенаправлений, который затем будет обработан или будет просто удален.

3.6.2. Встроенные команды

В bash есть ряд команд, которые являются частью самой оболочки. Эти команды выполняются самой командной оболочкой без создания нового процесса.

Самым распространенным мотивом сделать команду внутренней является возможность поддержки или изменения внутреннего состояния командной оболочки. Хорошим примером является команда cd; в одном из классических упражнений на вводных занятиях по Unix объясняется, почему команду cd нельзя реализовывать как внешнюю.

Встроенные команды bash используют те же самые внутренние примитивы, что и остальная часть командной оболочки. Каждая втроеннная команда реализована с помощью функций языка C, которая в качестве аргументов используется список слов. Это те слова, которые поступают со стадии раскрытия строк; встроенные команды рассматривают их как имена команд и аргументы. По большей части, встроенные команды используют те же самые стандартные правила раскрытия, что и другие команды, но с некоторыми исключениями: встроенные команды bash, в которых в качестве аргументов допускаются инструкции присваивания (например, declare и export), применяют для аргументов с инструкцией присваивания те же самые правила раскрытия, которые оболочка применяет при присваивании значений переменным. Это единственное место, где для передачи информации от одной стадии внутреннего конвейера командной оболочки к другой используется элемент flags из структуры WORD_DESC.

3.6.3. Выполнение простых команд

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

Присваивание значения переменной в командной оболочке (т.е. слова вида var=value) само является своего рода простой командой. Инструкции присваивания могут либо предшествовать имени команды, либо находиться в отдельной командной строке. Если они предшествуют команде, то переменные передаются в исполняемую команду через среду окружения команды (если они предшествуют встроенной команде или функции командной оболочки, то они сохраняются, за немногими исключениями, лишь пока исполняется встроенная команда или функция). Если за инструкциями присваивания нет имени команды, то эти инструкции изменяют состояние командной оболочки.

Если представлено имя команды, которая не является именем функции командной оболочки или встроенной команды, то bash ищет в файловой системе исполняемый файл с таким именем. Для поиска будет использована значение переменной PATH, представляющее собой список каталогов, разделенных символами двоеточия. Поиск команд с именами, в которых есть слеш (или символы других разделителей каталогов) не происходит — они сразу передаются на исполнение.

Если команда найдена с использованием переменной PATH, то bash сохраняет имя команды и соответствующий полный путь в хеш-таблице, к которой он обратится в следующий раз перед следующим поиском в переменной PATH. Если команда не найдена, то bash выполняет функцию со специальным именем, если он ее найдет, а в качестве аргументов возьмет имя команды, которая должна была быть выполнена, и список ее аргументов. В некоторых дистрибутивах Linux это свойство используется для того, чтобы устанавливать недостающие команды.

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

3.6.4. Управление заданиями

Командная оболочка может выполнять команды в приоритетном режиме, в котором она ждет, пока команда завершится и получает статус выхода из команды, или в фоновом режиме, в котором командная оболочка сразу переходит к чтению следующей команды. Благодаря механизму управления заданиями можно перемещать процессы (команды, которые выполняются) между приоритетным и фоновым режимами, а также приостанавливать и возобновлять их исполнение. Для реализации этого в bash вводится понятие задания (job), которое, по существу является командой, исполняемой одним или несколькими процессами. В конвейере, например, для каждого элемента конвейера используется один процесс. Несколько отдельных процессов можно объединить в группу в виде одного задания. Терминалу будет назначен идентификатор группы процессов, связанных с этим терминалом, так что группа процессов, работающая в приоритетном режиме, эта та группа, идентификатор которой, такой же, как и у терминала.

В командной оболочке для реализации управления заданиями используется несколько простых структур данных. Есть структура для представления дочернего процесса, в которой хранится идентификатор процесса, его состояние и состояние, которое он возвращает при завершении. Конвейер является всего лишь простым связным списком, состоящим из таких структур. Задание очень похоже: есть список процессов, некоторое состояние задания (задание исполняется, приостановлено, завершено и т.д.), и идентификатор группы процессов задания. Список процессов обычно состоит из одного процесса; с заданием связано более одного процесса только для конвейера. Каждое задание имеет уникальный идентификатор группы процессов, а процесс в задании, идентификатор процесса которого такой же, как идентификатор группы процессов задания, называется лидером группы процессов. Текущий набор заданий хранится в массиве, концептуально очень похожем на то, как это представляет пользователь. Состояние задания и состояния выхода из задания формируются путем агрегирования состояний входящих в него процессов и состояний, возвращаемых этими процессами при их завершении.

Как и со многим другим в командной оболочке, сложной частью, касающейся реализации управления заданиями, является учет. Оболочка должна позаботиться о назначении процессов правильным группам процессов, удостовериться, что синхронизировано создание дочернего процесса и его назначение группе процессов и что надлежащим образом задана группа процессов терминала, поскольку группа процессов терминала определяет задание, работающее в приоритетном режиме (и если его не вернуть его обратно группе процессов облочки, то оболочка сама не сможет прочитать ввод с терминала). Поскольку учет очень сильно ориентирован на процессы, не так легко реализовать составные команды, например, циклы while и for, поскольку весь цикл должен останавливаться и запускаться как единое целое, и это должно быть сделано в нескольких командных оболочках.

3.6.5. Составные команды

Составные команды состоят из списка одной или нескольких простых команд и начинаются с ключевого слова, например, if или while. Именно здесь видна и эффективна вся мощь программирования командной оболочки.

Реализация довольно проста. Синтаксический анализатор строит объекты, соответствующие различным составным командам, и интерпретирует их по мере обхода объектов. Каждая составная команда реализуется с помощью соответствующей функции языка C, которая отвечает за выполнение надлежащих раскрытий, исполнения команд так, как это указано, и изменения порядка исполнения в соответствие со статусом возврата команд. В качестве иллюстрации рассмотрим функцию, с помощью которой реализована команда for. Она сначала должна раскрыть список слов, следующих за зарезервированным словом in. Затем функция должна выполнить итерацию по раскрытым словам, назначая каждое слово соответствующей переменной, а затем исполнить список команд, указанных в теле команды for. Команда for не должна изменять порядок исполнения в соответствие со статусом возврата команд, но она должна обращать внимание на встроенные команды break и continue. После того, как все слова в списке будут использованы, произойдет выход из команды for. Видно, что реализация, большей частью, весьма близка к описанию команды.

3.7. Усвоенные уроки

3.7.1. Что, по моему мнению, важно

Я потратил более двадцати лет на работу над bash и, как мне бы хотелось думать, обнаружил кое-что интересное. Самое главное, что я не могу переоценить, то, что очень важно иметь подробные журналы изменений. Хорошо, когда вы можете посмотреть в журнал и вспомнить, почему было сделано конкретное изменение. Еще лучше, если вы можете дополнить это изменение конкретным сообщением об ошибке, тестом, который можно будет повторить, или советом.

Если необходимо тщательное регрессионное тестирование, то это именно то, что я бы порекомендовал делать в проекте с самого начала. В bash есть тысячи тестов, охватывающий практически все его неинтерактивные возможности. Мне надо было создавать тесты для интерактивных функций - в Posix они есть в тестовом фреймворке, но не хотелось иметь дистрибутив, в котором мне бы пришлось принять решение его использовать.

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

Внешние стандарты важны, но также хорошо иметь внутренние стандарты. Мне посчастливилось воспользоваться набором стандартов проекта GNU, в которых предлагаются достаточно хорошие практические советы по разработке и реализации программ.

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

Есть очень много хорошего программного обеспечения. Пользуйтесь всем, чем можете: например, в gnulib есть много удобных библиотечных функций (если только вы сможете извлечь их из фреймворка gnulib). Так делают в системах BSD и Mac OS X. Пикассо как-то по случаю сказал: "У великих художников воруют".

Привлекайте к участию сообщество пользователей, но будьте готовы к периодическим критическим замечаниям, некоторые из которых заставят вас сильно задуматься. Активное сообщество пользователей может быть огромным преимуществом, но одно из последствий состоит в том, что эти люди очень страстные. Не воспринимайте это как личное.

3.7.2. Что я должен был бы сделать по-другому

У bash миллионы пользователей. Я знаю о важности обратной совместимости. В некотором смысле обратная совместимость означает, что вам никогда не придется извиняться. Но мир не так прост. Время от времени я вынужден был делать несовместимые изменения, почти из-за каждого из них от пользователей поступало некоторое количество жалоб, хотя у меня всегда было то, что я считал уважительной причиной, будь то замена плохого решения, которое исправляло неправильную работу, или корректировка несоответствий между различными частями командной оболочки. Я должен был бы раньше ввести что-то вроде формальных уровней совместимости bash.

Разработка bash никогда не была особенно открытой. Я привык к использованию промежуточных релизов (например, bash-4.2) и отдельно разработанных патчей. Для этого есть причины: я подстроился под поставщиков с их сроками выпуска релизов, более длинными, чем выпуск релизов в мире бесплатного программного обеспечения и открытого исходного кода, и у меня в прошлом были проблемы с бета-версиями, которые становились все более распространенными, чем мне бы того хотелось. Хотя, если мне пришлось бы начать все заново, я бы предпочел более частые релизы и пользовался одним из вариантов публичного репозитория.

Но этот список был бы неполным, если не коснуться реализации bash. Есть вопрос, к которому я обращался несколько раз, но не приступил к его решению, - это полное переписывание синтаксического анализатора bash с использованием прямого рекурсивного спуска, а не с использованием bison. Когда-то я думал, что я должен сделать это с тем, чтобы привести подстановку команд в соответствие с POSIX, но я мог бы сделать это, если бы не изменения, которых было очень много. Если бы я начал писать bash с нуля, я бы, наверное, написал синтаксический анализатор вручную. Это, конечно, сделало бы его в некоторых местах проще.

3.8. Заключение

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

На bash повлияли многие проекты, начиная с седьмой редакции оригинальной командной оболочки Unix, написанной Стивеном Борном (Stephen Bourne). Наиболее существенное влияние оказал стандарт Posix, в котором определена значительная часть функциональных возможностей bash. Подобное сочетание обратной совместимости и соответствие стандартам добавило свои собственные проблемы.

Bash получает преимущество от того, что является частью проекта GNU, определяющего направление развития и границы, в которых существует bash. Без проекта GNU, не было бы никакого bash. Bash также выигрывает благодаря тому, что у него есть активное быстро реагирующее сообщество пользователей. Их отзывы помогли сделать bash тем, чем сегодня он стал, что свидетельствует о преимуществах свободного программного обеспечения.

Примечания:

  1. Чаще всего — последовательность, состоящая из одного из таких повторяющихся символов.
  2. Встроенная команда exec является исключением из этого правила.

Creative Commons

Перевод был сделан в соответствие с лицензией Creative Commons. С русским вариантом лицензии можно ознакомиться здесь.