Виктор Хименко. Кто командует парадом II?
![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Комбинирование команд
Перейдем теперь к главной гордости интерпретаторов Unix — комбинированию команд. Вначале рассмотрим его простейшие варианты.
Как уже говорилось, из одной командной строки в bash можно запустить несколько программ последовательно, записав соответствующие команды через точку с запятой (?;?), или параллельно, разделив их амперсандами (?&?). Имеется, конечно, и так называемая «труба» (pipe), перенаправляющая стандартный выход одной программы на стандартный вход другой. Из Unix она перекочевала и в DOS, но там, впрочем, реализована «халтурно»: даже при использовании интерпретатора DOS 7.x из командной оболочки Windows 9X, где ядро ОС имеет все необходимые средства, первая команда полностью отрабатывает перед началом выполнения второй, а данные передаются через временный файл.
Для управления порядком запуска программ, очевидно, необходимы операторные скобки. И в bash они действительно есть, причем даже двух видов — фигурные и круглые. Фигурные просто разделяют команды и показывают приоритеты, а круглые полностью изолируют выполняемые операции от «внешнего мира»: так, если внутри них меняются значения переменных окружения, на последующие команды это не влияет (см. рис. 1).
[khim@localhost khim]$ echo $PATH /usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/usr/local/bin:/opt/bin:/home/khim/bin [khim@localhost khim]$ ( PATH=/bin ; echo $PATH ) /bin [khim@localhost khim]$ echo $PATH /usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/usr/local/bin:/opt/bin:/home/khim/bin [khim@localhost khim]$ ( PATH=/bin ; echo $PATH ;) /bin [khim@localhost khim]$ echo $PATH /bin [khim@localhost khim]$ Рис. 1. Если значение переменной окружения меняется внутри круглых скобок, на последующие команды это не влияет |
Команды можно скомбинировать и таким довольно неожиданным способом, как сделав одну параметром (либо иной частью) другой. Команда, заключенная в скобки, перед которыми ставится символ ?$?, или в обратные апострофы, выполняется, а результат работы (тот, что был бы выдан в стандартный выводной поток) подставляется на ее место в командную строку.
[khim@localhost khim]$ export LC_ALL=english [khim@localhost khim]$ date Fri Aug 18 19:51:24 MSD 2000 [khim@localhost khim]$ LC_ALL="`( . /etc/sysconfig/i18n ; echo \"$LOCALE\";)`" date Птн Авг 18 19:55:17 MSD 2000 [khim@localhost khim]$ echo $LOCALE [khim@localhost khim]$ Рис. 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 г. до настоящего момента).
[khim@localhost khim]$ echo $((2+2)) 4 [khim@localhost khim]$ echo "Прошло "((`date +%s` часов с \"начала времен\" Unix'а." [khim@localhost khim]$ Рис. 3. Конструкция $((...)) — встроенный калькулятор bash |
Разумеется, в bash есть и составные операторы. Но прежде чем к ним переходить, нужно рассказать о способах проверки условий, а для этого вспомнить, что в Linux, как и в DOS, каждая отработавшая команда сообщает системе код возврата.
Коды возврата и проверка условий
По общепринятому соглашению код возврата равен 0, если работа программы завершилась успешно, и другому числу в противном случае (рис. 4). Код возврата последней выполнившейся в данном сеансе команды содержится в переменной $?.
[khim@localhost khim]$ bzip2 /bin/bash bzip2: Can't create output file /bin/bash.bz2: Доступ запрещен. [khim@localhost khim]$ echo $? 1 [khim@localhost khim]$ bzip2 /bin/bash > /dev/null [khim@localhost khim]$ echo $? 0 [khim@localhost khim]$ Рис. 4. Программа bzip2 в соответствии с общепринятым соглашением возвращает 0 при успешном завершении операции и 1 при неудачном |
С этими кодами возможны разнообразные действия, из которых важнейшими являются логические операции: отрицание (?!?), конъюнкция (?&&?) и дизъюнкция (?||?), как это показано на рис. 5. (Не путайте конъюнкцию с параллельным запуском, а дизъюнкцию — с «трубой»!)
Заметьте, если результата выполнения первой команды достаточно для определения кода возврата всей конструкции, то вторая команда не вызывается: это видно из сравнения двух примеров использования ?||? на рис. 5.
[khim@localhost khim]$ ! bzip2 /bin/bash bzip2: Can't create output file /bin/bash.bz2: Доступ запрещен. [khim@localhost khim]$ echo $? 0 [khim@localhost khim]$ ! bzip2 -c /bin/bash > /dev/null [khim@localhost khim]$ echo $? 1 [khim@localhost khim]$ bzip2 -c /bin/bash > /dev/null || bzip2 /bin/bash [khim@localhost khim]$ echo $? 0 [khim@localhost khim]$ bzip2 /bin/bash || bzip2 -c /bin/bash > /dev/null bzip2: Can't create output file /bin/bash.bz2: Доступ запрещен. [khim@localhost khim]$ echo $? 0 [khim@localhost khim]$ bzip2 /bin/bash && bzip2 -c /bin/bash > /dev/null bzip2: Can't create output file /bin/bash.bz2: Доступ запрещен. [khim@localhost khim]$ echo $? 1 [khim@localhost khim]$ Рис. 5. С кодами возврата возможны логические операции |
Любое арифметическое выражение также можно записать как команду и получить для него код возврата (рис. 6). Обратите внимание на два последних примера. Не кажется ли вам, что тут замешана какая-то мистика: 0 дает в результате 1, а 2 — 0? Дело в том, что внутри арифметических операторов действуют соглашения языка Си, т. е. истиной является любое число, кроме нуля, а ложью — нуль, вовне же поступает обычный код возврата, для которого все наоборот.
[khim@localhost khim]$ ((57<2000)) ; echo $? 1←57 не больше 2000 [khim@localhost khim]$ x=3 [khim@localhost khim]$ y=8 [khim@localhost khim]$ ((x**2>=y+1)) ; echo $? 0 ←x^2 >= y+1 ; $ не обязателен [khim@localhost khim]$ ((x++,y=0)) [khim@localhost khim]$ echo "x == $x ; y == $y" x == 4 ; y == 8 ←переменные можно изменить -- поддерживаются большинство операторов языка Си [khim@localhost khim]$ ((1-1)) ; echo $? 0 ←2 как результат арифметического выражения -- истина, т. е. 0 [khim@localhost khim]$ ((1-1)) ; echo $? 1 ←0 как результат арифметического выражения -- ложь, т. е. 1 [khim@localhost khim]$ Рис. 6. Арифметические выражения могут вырабатывать код возврата |
Проверки условий в bash... нет. Вернее, есть, но не в том виде, какой привычен нам по опыту работы в DOS. Ею занимаются специальные команды (в основном внутренние), каждая из которых проверяет определенный набор условий. Все они возвращают 0, если условие выполнено, и 1 в противном случае.
[khim@localhost khim]$ [ -e /bin ] ; echo $? 0 ← /bin -- существует [khim@localhost khim]$ [ -e /bin/sh ] ; echo $? 0 ← /bin/sh -- существует [khim@localhost khim]$ [ -f /bin ] ; echo $? 1 ← /bin -- НЕ [обычный] файл [khim@localhost khim]$ [ -f /bin/sh ] ; echo $? 0 ← /bin/sh -- нормальный файл (на самом деле это символическая ссылка на обычный файл; [ эти случаи не различает) [khim@localhost khim]$ [ -L /bin/sh ] ; echo $? 0 ← /bin/sh -- символическая ссылка [khim@localhost khim]$ [ -L /bin/bash ] ; echo $? 1 ← /bin/bash -- не символическая ссылка [khim@localhost khim]$ [ "abc" == "123" ] ; echo $? 1 ← /bin/bash -- abc не равно 123 [khim@localhost khim]$ [ "abc" ">" "xyz" ] ; echo $? 1 ← /bin/bash -- abc не больше xyz [khim@localhost khim]$ [ "57" == "2000" ] ; echo $? 1 ← /bin/bash -- СТРОКА 57 меньше, чем СТРОКА 2000 [khim@localhost khim]$ [ "abc" == "123" ] ; echo $? 1 ← /bin/bash -- ЧИСЛО 57 меньше, чем ЧИСЛО 2000 [khim@localhost khim]$ Рис. 7. Команда [ |
Самыми первыми появились команды test и [, различающиеся только тем, что [ для симметрии требует последним параметром символа ]. Эти команды предназначены в основном для проверки разнообразных условий, связанных со свойствами файлов. Сравнивать с их помощью строки тоже можно, хотя и не очень удобно (рис. 7). Лучше пользоваться более мощной командой [[: она допускает сравнение с шаблоном и предусматривает более мнемоническую запись конъюнкции и дизъюнкции (рис. 8).
[khim@localhost khim]$ [[ abc == *b* ]] ; echo $? 0 ← abc подходит под шаблон *b* [khim@localhost khim]$ [[ *b* == abc ]] ; echo $? 1 ← а шаблон должен быть слева [khim@localhost khim]$ [[ abc == *x* || abc == *c* ]] ; echo $? 0 ← дизъюнкция [khim@localhost khim]$ [[abc == *x* && abc == *c* ]] ; echo $? 0 ← конъюнкция [khim@localhost khim]$ [[ -f /bin/sh ]] ; echo $? 0 ← проверки файлов тоже работают [khim@localhost khim]$ [[ 57 .lt. 2000 ]] ; echo $? 0 ← как и числовые проверки [khim@localhost khim]$ Рис. 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.
[khim@localhost khim]$ ( IFS=zyz ; echo "$+" ) 123 [khim@localhost khim]$ for i in "Strange $@ result" do echo "$i" done Strange 1 2 3 result [khim@localhost khim]$ Рис. 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-систем) так любят командные интерпретаторы. Надеюсь, мне удалось вдохновить вас на подобный эксперимент.