|
Close Help |
По своей исходной задумке ОС UNIX является типичной интерактивной операционной системой, или системой с разделением времени. Это означает, что каждый пользователь системы взаимодействует с системой со своего собственного терминала в интерактивном режиме, задавая системе команды и получая на экран своего терминала ответные сообщения системы. В общем, эта картина остается истинной для всех современных вариантов ОС UNIX. Но в существенных деталях она сильно отличается от того, что было несколько лет тому назад. Основное отличие заключается в способе организации интерактивного интерфейса с системой.
Когда создавались первые варианты ОС UNIX, единственным практически доступным (и сравнительно удобным) аппаратным средством интерактивного взаимодействия с вычислительной системой являлись алфавитно-цифровые терминалы, способные вводить и выводить строки символов. Поэтому исторически базовым средством взаимодействия системы с пользователем является строчный интерфейс: пользователь вводит со своего терминала некоторую строку символов, и если система понимает смысл этой строки, то она выполняет соответствующие действия и выдает на экран пользователя соответствующие результаты. (Конечно, здесь смысл терминов "строка символов", "система", "понимает" и т.д. нуждается в уточнении, что и будет сделано ниже.)
Разные алфавитно-цифровые терминалы обладали разными возможностями. Например, некоторые из них обеспечивали режимы реверсного вывода, полутонов, псевдографики и т.д. Однако система должна была работать с любым из этих терминалов. Поэтому использовались минимальные возможности. Фактически, алфавитно-цифровой терминал в базовом интерфейсе с ОС UNIX используется в режиме электронного телетайпа. В этом разделе мы рассмотрим интерфейсные возможности ОС UNIX этого уровня.
Широкое распространение дешевых цветных терминалов с развитыми графическими возможностями (и, в особенности, повсеместное использование персональных компьютеров с графическими интерфейсами) стимулировало переход ОС UNIX на использование графических интерфейсов взаимодействия с пользователем. В настоящее время невозможно найти современный вариант ОС UNIX, в котором не поддерживалась бы возможность использования графических терминалов в многооконном режиме с применением соответствующего графического интерфейса в каждом окне. Соответствующие средства мы рассмотрим в следующем разделе.
Однако сразу заметим, что нам не приходилось еще видеть графического терминала, подключенного к UNIX-системе, хотя бы одно окно которого не использовалось бы в качестве аналога традиционного алфавитно-цифрового терминала для взаимодействия с системой в традиционном режиме (это не обязательно, но экономит время). Более того, многие профессиональные программисты предпочитают использовать традиционные интерфейсы, осознавая, насколько большие возможности они обеспечивают и насколько меньшие порождают накладные расходы. Поэтому (по крайней мере, на сегодняшний день) без знания основ традиционного интерфейса с ОС UNIX обойтись все еще нельзя (если, конечно, хотеть использовать систему профессионально).
Как и в большинстве интерактивных систем, традиционный интерфейс с пользователем ОС UNIX основан на использовании командных языков. Выражаясь несколько тавтологично, можно сказать, что командный язык - это язык, на котором пользователь взаимодействует с системой в интерактивном режиме. Такой язык называется командным, поскольку каждую строку, вводимую с терминала и отправляемую системе, можно рассматривать как команду пользователя по отношению к системе. Одним из достижений ОС UNIX является то, что командные языки этой операционной системы являются хорошо определенными (не очень хороший русский термин, соответствующий совершенно однозначному английскому термину well-defined) и содержат много средств, приближающих их к языкам программирования.
Если рассматривать категорию командных языков с точки зрения общего направления языков взаимодействия человека с компьютером, то они, естественно, относятся к семейству интерпретируемых языков. Коротко охарактеризуем разницу между компилируемыми и интерпретируемыми компьютерными языками. Язык называется компилируемым, если требует, чтобы любая законченная конструкция языка была настолько замкнутой, чтобы обеспечивала возможность изолированной обработки без потребности привлечения дополнительных языковых конструкций. В противном случае понимание языковой конструкции не гарантируется. Житейским примером компилируемого языка является литературный русский язык. Ни один литературный редактор не примет от вас незаконченное сочинение, в котором имеются ссылки на еще не написанные части. Процесс компиляции (литературного редактирования в нашем примере) требует замкнутости языковых конструкций.
Основным преимуществом интерпретируемых языков является то, что в случае их использования программа пишется "инкрементально" (в пошаговом режиме), т.е. человек принимает решение о своем следующем шаге в зависимости от реакции системы на предыдущий шаг. В принципе, предпочтение компилируемых или интерпретируемых языков является предметом личного вкуса конкретного индивидуума (нам известны крупные авторитеты в области программирования - например, Д.Б. Подшивалов,- которые абсолютно уверены, что любая хорошая программа должна быть сначала написана на бумаге и отлажена за столом).
Особенностью командных языков является то, что в большинстве случаев они не используются для программирования в обычном смысле этого слова, хотя на развитом командном языке можно написать любую программу. По нашему мнению, правильным стилем использования командного языка является его применение в основном для непосредственного взаимодействия с системой с привлечением возможностей составления командных файлов (скриптов или сценариев в терминологии ОС UNIX) для экономии повторяющихся рутинных процедур.
Программы, предназначенные для обработки конструкций командных языков, называются командными интерпретаторами. В отличие от компилируемых языков программирования (таких, как Си или Паскаль), для каждого из которых обычно существует много различных компиляторов, командный язык, как правило, неразрывно связан с соответствующим интерпретатором. Когда ниже мы будем говорить о различных представителях командных языков ОС UNIX, относящихся к семейству shell, то каждый раз под одноименным названием мы будем подразумевать и соответствующий интерпретатор.
В этом пункте и далее в данном разделе мы будем более конкретно говорить о командных языках семейства shell. Основное назначение этих языков (их разновидностей существует достаточно много, но мы рассмотрим только три наиболее распространенные варианта - Bourne-shell, C-shell и Korn-shell) состоит в том, чтобы предоставить пользователям удобные средства взаимодействия с системой. Что это означает? Языки не даром называются командными. Они предназначены для того, чтобы дать пользователю возможность выполнять команды, предназначенные для исполнения некоторых действий операционной системы. Существует два вида команд.
Собственные команды shell (такие как cd, echo, exec и т.д.) выполняются непосредственно интерпретатором, т.е. их семантика встроена в соответствующий язык. Имена других команд на самом деле являются именами файлов, содержащих выполняемые программы. В случае вызова такой команды интерпретатор командного языка с использованием соответствующих системных вызовов запускает параллельный процесс, в котором выполняется нужная программа. Конечно, смысл действия подобных команд является достаточно условным, поскольку зависит от конкретного наполнения внешних файлов. Тем не менее, в описании каждого языка содержатся и характеристики "внешних команд" (например, find, grep, cc и т.д.) в расчете на то, что здравомыслящие пользователи (и их администраторы) не будут изменять содержимое соответствующих файлов.
Существенным компонентом командного языка являются средства, позволяющие разнообразными способами комбинировать простые команды, образуя на их основе составные команды. В семействе языков shell возможны следующие средства комбинирования. В одной командной строке (важный термин, означающий единицу информационного взаимодействия с командным интерпретатором) можно указать список команд, которые должны выполняться последовательно, или список команд, которые должны выполняться "параллельно" (т.е. независимо одна от другой).
Очень важной особенностью семейства языков shell являются возможности перенаправления ввода/вывода и организации конвейеров команд. Естественно, эти возможности опираются на базовые средства ОС UNIX (см. п. 2.1.8). Кратко напомним, в чем они состоят. Для каждого пользовательского процесса (а внешние команды shell выполняются в рамках отдельных пользовательских процессов) предопределены три выделенных дескриптора файлов: файла стандартного ввода (standard input), файла стандартного вывода (standard output) и файла стандартного вывода сообщений об ошибках (standard error). Хорошим стилем программирования в среде ОС UNIX является такой, при котором любая программа читает свои вводные данные из файла стандартного ввода, а выводит свои результаты и сообщения об ошибках в файлы стандартного вывода и стандартного вывода сообщений об ошибках соответственно. Поскольку любой порожденный процесс "наследует" все открытые файлы своего предка (см. п. 2.1.7), то при программировании команды рекомендуется не задумываться об источнике вводной информации программы, а также конкретном ресурсе, поддерживающим вывод основных сообщений и сообщений об ошибках. Нужно просто пользоваться стандартными файлами, за конкретное определение которых отвечает процесс-предок (заметим, что по умолчанию все три файла соответствуют вводу и выводу на тот терминал, с которого работает пользователь).
Что обеспечивает такая дисциплина работы? Прежде всего возможность создания программ, "нейтральных" по отношению к источнику своих вводных данных и назначению своих выводных данных. Собственно, на этом и основаны принципы перенаправления ввода/вывода и организации конвейера команд. Все очень просто. Если вы пишете в командной строке конструкцию
com1 par1, par2, ..., parn > file_name ,
то это означает, что для стандартного вывода команды com1 будет использоваться файл с именем file_name. Если вы пишете
file_name < com1 par1, par2, ..., parn ,
то команда com1 будет использовать файл с именем file_name в качестве источника своего стандартного ввода. Если же вы пишете
com1 par1, par2, ..., parn | com2 par1, par2, ..., parm ,
то в качестве стандартного ввода команды com2 будет использоваться стандартный вывод команды com1. (Конечно, при организации такого рода "конвейеров" используются программные каналы, см. п. 3.4.4.)
Конвейер представляет собой простое, но исключительно мощное средство языков семейства shell, поскольку позволяет во время работы динамически создавать "комбинированные" команды. Например, указание в одной командной строке последовательности связанных конвейером команд
ls -l | sort -r
приведет к тому, что подробное содержимое текущего каталога будет отсортировано по именам файлов в обратном порядке и выдано на экран терминала. Если бы не было возможности комбинирования команд, до для достижения такой возможности потребовалось бы внедрение в программу ls возможностей сортировки.
Последнее, что нам следует обсудить в этом пункте, это существо команд семейства языков shell. Различаются три вида команд. Первая разновидность состоит из команд, встроенных в командный интерпретатор, т.е. составляющих часть его программного кода. Эти команды предопределены в командном языке, и их невозможно изменить без переделки интерпретатора. Команды второго вида - это выполняемые программы ОС UNIX. Обычно они пишутся на языке Си по определенным правилам (см. п. 5.2.1). Такие команды включают стандартные утилиты ОС UNIX, состав которых может быть расширен любым пользователем (если, конечно, он еще способен программировать). Наконец, команды третьего вида (так называемые скрипты языка shell) пишутся на самом языке shell. Это то, что традиционно называлось командным файлом, поскольку на самом деле представляет собой отдельный файл, содержащий последовательность строк в синтаксисе командного языка.
Здесь под командным интерпретатором мы будем понимать не соответствующую программу, а отдельный сеанс ее выполнения. Если в случае компилируемых программ следует различать наличие готовой к выполнению программы и факт запуска такой программы, то в случае интерпретируемых программ приходится различать случаи наличия интерпретатора, программы, которая может быть проинтерпретирована, и запуска интерпретатора с предоставлением ему интерпретируемой программы. Основным свойством динамически выполняемой программы (неважно, является ли она предварительно откомпилированной или интерпретируемой) является наличие ее динамического контекста, в частности, набора определенных переменных, содержащих установленные в них значения.
В случае командных интерпретаторов семейства shell имеется два вида динамических контекстов выполнения интерпретируемой программы. Первый вид контекста состоит из предопределенного набора переменных, которые существуют (и обладают значениями) независимо от того, какая программа интерпретируется. В терминологии ОС UNIX этот набор переменных называется окружением или средой сеанса выполнения командной программы. С другой стороны, программа при своем выполнении может определить и установить дополнительные переменные, которые после этого используются точно так же, как и предопределенные переменные. Спецификой командных интерпретаторов семейства shell (связанной с их ориентацией на обеспечение интерфейса с операционной системой) является то, что все переменные shell-программы (сеанса выполнения командного интерпретатора) являются текстовыми, т.е. содержат строки символов.
Любая разновидность языка shell представляет собой развитый компьютерный язык, и объем этого курса не позволяет представить в деталях хотя бы один из них. Однако в следующих подразделах мы постараемся кратко познакомить вас с особенностями трех распространенных вариантов языка shell.
Bourne-shell является наиболее распространенным командным языком (и одновременно командным интерпретатором) системы UNIX. Вот основные определения языка Bourne-shell (конечно, мы приводим неформальные определения, хотя язык обладает вполне формализованным синтаксисом):
и т.д.
Командный язык C-shell главным образом отличается от Bourne-shell тем, что его синтаксис приближен к синтаксису языка Си (это, конечно, не означает действительной близости языков). В основном, C-shell включает в себя функциональные возможности Bourne-shell. Если не вдаваться в детали, то реальными отличиями C-shell от Bourne-shell является поддержка протокола (файла истории) и псевдонимов.
В протоколе сохраняются введенные в данном сеансе работы с интерпретатором командные строки. Размер протокола определяется установкой предопределенной переменной history, но последняя введенная командная строка сохраняется всегда. В любом месте текущей командной строки в нее может быть подставлена командная строка (или ее часть) из протокола.
Механизм псевдонимов (alias) позволяет связать с именем полностью (или частично) определенную командную строку и в дальнейшем пользоваться этим именем.
Кроме того, в C-shell по сравнению с Bourne-shell существенно расширен набор предопределенных переменных, а также введены более развитые возможности вычислений (по-прежнему, все значения представляются в текстовой форме).
Если C-shell является синтаксической вариацией командного языка семейства shell по направлению к языку программирования Си, то Korn-shell - это непосредственный последователь Bourne-shell.
Если не углубляться в синтаксические различия, то Korn-shell обеспечивает те же возможности, что и C-shell, включая использование протокола и псевдонимов.
Реально, если не стремиться использовать командный язык как язык программирования (это возможно, но по мнению автора, неоправданно), то можно пользоваться любым вариантом командного языка, не ощущая их различий.
Что действительно существенно при интерактивной работе в среде ОС UNIX, это знание и умение пользоваться разнообразными утилитами или внешними командами языка shell. Многие из этих утилит являются не менее сложными программами, чем сам командный интерпретатор (и между прочим, командный интерпретатор языка shell сам является одной из утилит, которую можно вызвать из командной строки). В этом разделе мы коротко обсудим, как устроены внешние команды shell и что нужно сделать, чтобы создать новую внешнюю команду.
Вообще-то, для создания новой команды не нужно делать почти ничего специального, нужно просто следовать правилам программирования на языке Си. Как известно, каждая правильно оформленная Си-программа начинает свое выполнение с функции main. Эта "полусистемная" функция обладает стандартным интерфейсом, являющимся основой организации команд, которые можно вызывать в среде shell. Внешние команды выполняются интерпретатором shell с помощью связки системных вызовов fork и одного из вариантов exec (см. п. 2.1.6). В число параметров системного вызова exec входит набор текстовых строк. Этот набор текстовых строк передается на вход функции main запускаемой программы.
Более точно, функция main получает два параметра - argc (число передаваемых текстовых строк) и argv (указатель на массив указателей на текстовые строки). Программа, претендующая на ее использование в качестве команды shell, должна обладать точно определенным внешним интерфейсом (параметры обычно вводятся с терминала) и должна контролировать и правильно разбирать входные параметры.
Кроме того, чтобы соответствовать стилю shell, такая программа не должна сама переопределять файлы, соответствующие стандартному вводу, стандартному выводу и стандартному выводу ошибок. Тогда команде может обычным образом перенаправляться ввод/вывод, и она может включаться в конвейеры.
Как видно из последнего предложения предыдущего пункта, для обеспечения возможностей перенаправления ввода/вывода и организации конвейера при программировании команд не требуется делать ничего специального. Достаточно просто не трогать три начальные дескриптора файлов и правильно работать с этими файлами, а именно, производить вывод в файл с дескриптором stdout, вводить данные из файла stdin и выводить сообщения об ошибках в файл stderror.
Встроенные команды представляют собой часть программного кода командного интерпретатора. Они выполняются как подпрограммы интерпретатора, и их невозможно заменить или переопределить. Синтаксис и семантика встроенных команд определены в соответствующем командном языке.
Библиотечные команды составляют часть системного программного обеспечения. Это набор выполняемых программ (утилит), поставляемых вместе с операционной системой. Большинство этих программ (таких как vi, emacs, grep, find, make и т.д.) исключительно полезно на практике, но их рассмотрение находится за пределами этого курса (по поводу редакторов vi и emacs и утилиты поддержки целостности программных файлов make существуют отдельные толстые книги).
Пользовательской командой является любая выполняемая программа, организованная в соответствии с требованиями, изложенными в п. 5.2.2. Таким образом, любой пользователь ОС UNIX может неограниченно расширять репертуар внешних команд своего командного языка (например, можно написать собственный командный интерпретатор).
Об этом мы уже говорили. Любой из упоминавшихся вариантов языка shell в принципе можно использовать как язык программирования. Среди пользователей ОС UNIX существует много людей, которые пишут на shell вполне серьезные программы. Однако по нашему мнению правильнее использовать shell для того, для чего он изначально предназначен - для непосредственного интерактивного взаимодействия с системой и для создания не очень сложных командных файлов. Для программирования лучше использовать языки программирования (Си, Си++, Паскаль и т.д.), а не командные языки. Хотя оговоримся, что это личное мнение автора, а выбор способа и стиля программирования является сугубо частным делом каждого программиста.