Комбинирование команд
Перейдем теперь к главной гордости интерпретаторов Unix — комбинированию команд. Вначале рассмотрим его простейшие варианты.
Как уже говорилось, из одной командной строки в bash можно запустить несколько программ последовательно, записав соответствующие команды через точку с запятой (?;?), или параллельно, разделив их амперсандами (?&?). Имеется, конечно, и так называемая «труба» (pipe), перенаправляющая стандартный выход одной программы на стандартный вход другой. Из Unix она перекочевала и в DOS, но там, впрочем, реализована «халтурно»: даже при использовании интерпретатора DOS 7.x из командной оболочки Windows 9X, где ядро ОС имеет все необходимые средства, первая команда полностью отрабатывает перед началом выполнения второй, а данные передаются через временный файл.
Для управления порядком запуска программ, очевидно, необходимы операторные скобки. И в bash они действительно есть, причем даже двух видов — фигурные и круглые. Фигурные просто разделяют команды и показывают приоритеты, а круглые полностью изолируют выполняемые операции от «внешнего мира»: так, если внутри них меняются значения переменных окружения, на последующие команды это не влияет (см. рис. 1).
![]() |
Рис. 1. Если значение переменной окружения меняется внутри круглых скобок, на последующие команды это не влияет |
Команды можно скомбинировать и таким довольно неожиданным способом, как сделав одну параметром (либо иной частью) другой. Команда, заключенная в скобки, перед которыми ставится символ ?$?, или в обратные апострофы, выполняется, а результат работы (тот, что был бы выдан в стандартный выводной поток) подставляется на ее место в командную строку.
![]() |
Рис. 2. Можно вставить одну команду в другую |
Взгляните на рис. 2. Здесь мы сначала заносим в переменную окружения LC_ALL, определяющую параметры страны, значение english, а затем выводим дату на языке страны, указанной в файле /etc/sysconfig/i18n (там задаются стандартные настройки системы). Чтобы это сделать, мы записываем в LC_ALL новое значение. Какое же?
Давайте разберемся по порядку. Внутри кавычек (они, как обычно, поставлены на тот случай, если в значении, присваиваемом переменной LC_ALL, окажется пробел; в действительности это невозможно, но, как говорится, кашу маслом не испортишь) мы видим в обратных апострофах команду с именем ?.? и одним параметром /etc/sysconfig/i18n. Что она делает? Выполняет строки файла, указанного в качестве параметра, так, как если бы они были введены в этом месте в «натуральном виде». В частности, если в файле определяются переменные (а файл /etc/sysconfig/i18n содержит только определение переменных и комментарии), то они становятся доступны. Одну из них — LOCALE — мы выводим следующей командой, и она подставляется в командную строку в качестве значения, присваемого переменной LC_ALL.
Вместо обратных апострофов можно было использовать и конструкцию $(...). А очень похожая на нее конструкция $((...)) позволяет подставить в командную строку значение арифметического выражения (см. рис. 3; команда date с параметром +%s выдает текущую дату и время как число секунд, прошедших с 1 января 1970 г. до настоящего момента).
![]() |
Рис. 3. Конструкция $((...)) — встроенный калькулятор bash |
Разумеется, в bash есть и составные операторы. Но прежде чем к ним переходить, нужно рассказать о способах проверки условий, а для этого вспомнить, что в Linux, как и в DOS, каждая отработавшая команда сообщает системе код возврата.
Коды возврата и проверка условий
По общепринятому соглашению код возврата равен 0, если работа программы завершилась успешно, и другому числу в противном случае (рис. 4). Код возврата последней выполнившейся в данном сеансе команды содержится в переменной $?.
![]() |
Рис. 4. Программа bzip2 в соответствии с общепринятым соглашением возвращает 0 при успешном завершении операции и 1 при неудачном |
С этими кодами возможны разнообразные действия, из которых важнейшими являются логические операции: отрицание (?!?), конъюнкция (?&&?) и дизъюнкция (?||?), как это показано на рис. 5. (Не путайте конъюнкцию с параллельным запуском, а дизъюнкцию — с «трубой»!)
Заметьте, если результата выполнения первой команды достаточно для определения кода возврата всей конструкции, то вторая команда не вызывается: это видно из сравнения двух примеров использования ?||? на рис. 5.
![]() |
Рис. 5. С кодами возврата возможны логические операции |
Любое арифметическое выражение также можно записать как команду и получить для него код возврата (рис. 6). Обратите внимание на два последних примера. Не кажется ли вам, что тут замешана какая-то мистика: 0 дает в результате 1, а 2 — 0? Дело в том, что внутри арифметических операторов действуют соглашения языка Си, т. е. истиной является любое число, кроме нуля, а ложью — нуль, вовне же поступает обычный код возврата, для которого все наоборот.
![]() |
Рис. 6. Арифметические выражения могут вырабатывать код возврата |
Проверки условий в bash... нет. Вернее, есть, но не в том виде, какой привычен нам по опыту работы в DOS. Ею занимаются специальные команды (в основном внутренние), каждая из которых проверяет определенный набор условий. Все они возвращают 0, если условие выполнено, и 1 в противном случае.
![]() |
Рис. 7. Команда [ |
Самыми первыми появились команды test и [, различающиеся только тем, что [ для симметрии требует последним параметром символа ]. Эти команды предназначены в основном для проверки разнообразных условий, связанных со свойствами файлов. Сравнивать с их помощью строки тоже можно, хотя и не очень удобно (рис. 7). Лучше пользоваться более мощной командой [[: она допускает сравнение с шаблоном и предусматривает более мнемоническую запись конъюнкции и дизъюнкции (рис. 8).
![]() |
Рис. 8. Команда [[ |
Составные операторы
Теперь, наконец, можно перейти и к составным операторам. Их довольно много: условный оператор (см. листинг 1), оператор выбора (см. листинг 2), несколько операторов цикла: while (см. листинг 3), until, отличающийся от while только отрицанием условия, два цикла for (см. листинг 4 и листинг 5) и, разумеется, функции: они используются в основном в сложных скриптах, поэтому пример в листинге 6 — совсем игрушечный. Обратите, однако, внимание на конструкцию $??, позволяющую задавать спецсимволы тем же способом, что в языке Си: — табуляция, — конец строки и т. д.
Параметры и массивы
В bash, как и в командном интерпертаторе DOS, поддерживаются позиционные переменные, соответствующие параметрам скрипта, — $1, $2,.. В DOS они называются %1, %2 и т. д., и таким способом обозначаются параметры только до девятого, а если их больше, до следующих приходится добираться с помощью команды shift. Эта команда в bash тоже есть (как вы, наверное, догадались, она пришла в DOS из Unix), но кроме того, к любому параметру можно обратиться и непосредственно. Хотя в силу исторических причин интерпретатор воспримет запись $10 как переменную $1, за которой следует 0, форма ${10}, ${11} и т. д. будет понята правильно. А переменная $# содержит общее число параметров. Кстати, если нужно обратиться к последнему параметру, это удобно сделать в форме ${!#} (см. врезку «Извлечение значений переменных» в первой части статьи).
Для передачи всех параметров в другую программу в bash предусмотрены две специальные переменные: $* и $@. Переменная $* содержит список параметров; разделителем служит первый из символов, хранящихся в переменной окружения IFS (это список разделителей слов, используемый в основном командой read). А вот $@ магическим образом раскрывается в $# параметров (если используются кавычки, то результат бывает забавен — см. рис. 9). Так как перебор параметров скрипта — весьма частая операция, то in ?$@? в цикле for можно опустить, как это сделано в листинге 6.
![]() |
Рис. 9. Переменные $* и $@ |
Ввод—вывод
Работа с файлами в bash основана на потоковом вводе-выводе. Каждый поток представлен в программе уникальным целым числом — дескриптором, который передается командам чтения и записи. (Точно так же обстоит дело и в DOS.) Для чтения чаще всего применяется команда read, а для записи — команда echo; с обеими мы уже много раз встречались в примерах.
Стандартному вводному потоку соответствуют файл /dev/stdin и дескриптор 0, стандартному выводному — файл /dev/stdout и дескриптор 1. Сообщения об ошибках направляются в файл /dev/stderr, открытый с дескриптором 2, а дескрипторы, начиная с третьего, используются по усмотрению программиста.
Операции перенаправления, из которых пользователям DOS знакомы >, >> и <, в bash работают не только со стандартными вводным и выводным потоками, но и с любыми иными. Например, чтобы открыть файл для чтения с дескриптором n, следует написать n<имя_файла, а чтобы открыть его для записи или для дописывания в конец (append) — соответственно n>имя_файла или n>>имя_файла. Дескриптор стандартного вводного потока в первом случае, а также стандартного выводного во втором и в третьем разрешается опустить, и тогда конструкции начинают выглядеть и работать так же, как в DOS. Запись &>имя_файла означает перенаправление в заданный файл дескрипторов 1 и 2 (т. е. стандартного ввода и сообщений об ошибках).
Запись &n соответствует файлу, открытому с дескриптором n (еще один способ обозначить такой файл — /dev/fd/n), поэтому написав, скажем, m>&n, мы направим поток m в тот же файл, что и поток n. Написав n<&-, мы закроем файл с дескриптором n.
В bash есть и другие операции перенаправления. Конструкция n<>имя_файла позволит открыть файл и для чтения, и для записи. А конструкция <<разделитель, используемая только в скриптах, перенаправляет в стандартный вводной поток следующие за ней строки файла скрипта вплоть до той, которая состоит из заданного разделителя. Ее вариант <<-разделитель дополнительно удаляет из всех перенаправленных строк начальные символы табуляции.
Точно так же, как присваивание значений переменным, перенаправление ввода-вывода может распространяться и на одну команду, и на весь интерпретатор. Правда, синтаксис во втором случае иной, чем при присваивании: перед операцией перенаправления необходимо поставить команду exec (см. листинг 8).
О bash можно рассказывать еще и еще, но, как гласит пословица, лучше один раз увидеть, чем семь раз услышать. Попробовав поработать в командной строке и написать пару-тройку несложных скриптов, вы наверняка поймете, за что опытные пользователи Linux (и вообще Unix-систем) так любят командные интерпретаторы. Надеюсь, мне удалось вдохновить вас на подобный эксперимент.
Об авторе
Виктор Хименко, khim@rnktech.com