Bash by example, Part 1 Daniel Robbins Освоив программирование на языке сценариев bash, ваше повседневное общение с Linux станет более приятным и продуктивным, вы научитесь использовать стандартные конструкции UNIX, такие как конвейеры и перенаправления, которые вы уже успели узнать и полюбить. В этой серии из трёх статей Даниэль Роббинс (Daniel Robbins) научит вас на примерах программировать в bash. Он охватит элементарные (что делает эту серию превосходным пособием для новичков) и подойдёт к более сложным приёмам по ходу статьи. 1.3 2005-10-09 Основы программирования в Bourne again shell (bash)
Введение

Вы удивитесь, когда узнаете, почему так важно уметь программировать в bash. На это есть ряд причин:

Bash уже запущена

Обратите внимание — вы уже работаете в bash. Даже если заменена стандартная оболочка, bash по прежнему остается в системе и широко в ней используется, т. к. она является стандартной в Linux. Благодаря этому, сценарий bash не займет много памяти, поскольку будет разделять её с процессом оболочки. Зачем использовать дополнительный интерпретатор (который «съест» 500 КБ памяти), если существует bash, которая уже запущена и способна выполнить поставленную задачу?

Вы уже используете bash

Но bash не только постоянно запущена, но и используется практически ежедневно! Поэтому имеет смысл узнать, как использовать её с максимальной эффективностью, сделав работу с bash более приятной и продуктивной. Но зачем изучать программирование в bash? А все потому, что вы уже знакомы с командами, копированием файлов, использованием конвейеров и перенаправлением вывода. Так почему же не выучить язык, который позволит вам использовать эти мощные конструкции более эффективно, раз уж вы с ними знакомы? Командные оболочки раскрывают потенциал систем UNIX, а bash, в свою очередь, оболочка Linux. Она является высокоуровневой прослойкой между вами и компьютером. Повысив свои знания о bash, вы автоматически повысите производительность работы в Linux и UNIX, к тому же это так просто!

Путаница с bash

Неверный подход в изучении bash может оказаться весьма запутанным делом. Многие новички, набрав man bash, желая посмотреть справку, сталкиваются только с сухим техническим описанием функциональности оболочки. Другие, используя info bash (для просмотра документации GNU info), получают ту же страницу справки или, если улыбнется удача, слегка более дружелюбную её версию.

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

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

Переменные среды

В bash, как и в других оболочках, пользователь может определять переменные среды, которые хранятся внутри неё в виде ASCII строк. Одним из наиболее удобных свойств переменных среды является то, что они являются неотъемлимой частью модели процессов UNIX. Это означает, что переменные среды могут использоваться не только в сценариях оболочки, но и в обычных программах. Когда мы «экспортируем» переменную из bash, все запущенные впоследствии программы смогут её использовать несмотря на то, что она определена сценарием оболочки. Возьмём, к примеру, команду vipw, которая позволяет администратору редактировать системный файл паролей. Записав в переменную среды EDITOR имя своего любимого текстового редактора, вы можете настроить vipw на его использование, вместо vi (очень удобно, если вы привыкли использовать xemacs и вам не нравится vi).

Стандартный способ объявления переменной среды в bash выглядит так:

$ myvar='This is my environment variable!'

Эта команда объявляет переменную среды под именем «myvar» и содержит строку «This is my environment variable!». Стоит заметить следующее: во-первых, нет пробелов по обе стороны от «=»; поскольку это приведет к ошибке (можете проверить). Во-вторых, можно обойтись и без кавычек, если наша переменная содержит одно слово (включая пробелы или знаки табуляции).

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

В-третьих, хоть мы и можем использовать как апостроф, так и двойные кавычки, использование двойных кавычек в приведенном примере приведёт к ошибке. Почему? А потому, что использование апострофа отключает возможность bash делать подстановки, которые заключаются в замене специальных символов и последовательностей на определенные значения. Например, символ «!» — символ подстановки истории команд, который bash обычно заменяет на предыдущую набранную команду. (Мы не будем останавливаться на подстановке истории команд, поскольку она редко используется в bash программировании. Дополнительную информацию вы можете найти в разделе «HISTORY EXPANSION» справочной страницы bash.) Хотя эта макро-функциональность и удобна, но сейчас нас больше интересует восклицательный знак в конце значения нашей переменной, чем макрос.

Теперь давайте посмотрим, как использовать переменные среды. Например:

$ echo $myvar
This is my environment variable!

Поставив $ перед именем переменной, мы говорим bash заменить её на значение myvar. В терминологии bash это называется «подстановкой переменных». Ну а что произойдет, если мы сделаем следующее:

$ echo foo$myvarbar
foo

Мы хотели получить «fooThis is my environment variable!bar», но у нас не вышло. Что же произошло? Короче говоря, мы ввели bash в заблуждение. То есть не возможно понять какую переменную мы имели в виду — $m, $my, $myvar, $myvarbar и т.д. Как мы можем более ясно и четко сказать bash, какую переменную мы хотим получить? Попробуйте:

$ echo foo${myvar}bar
fooThis is my environment variable!bar

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

Мы уже упоминали возможность «экспортирования» переменных. Когда экспортируется переменная среды, она автоматически становится доступной всем запущенным впоследствии сценариям и программам. Сценарии оболочки могут «добраться» до переменной среды, используя встроенную поддержку, в то время как программы написанные на C должны использовать функцию getenv(). Ниже приводится пример C кода, который поможет нам разобраться в использовании переменных среды с точки зрения C:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
  char *myenvvar=getenv("EDITOR");
  printf("The editor environment variable is set to %s\n",myenvvar);
}

Сохраните приведенный выше код в файл myenv.c, и затем скомпилируйте его следующей командой:

$ gcc myenv.c -o myenv

Теперь в каталоге появится исполняемая программа, которая при запуске будет выводить значение переменной среды EDITOR, если таковая имеется. Вот что происходит, когда мы её запустим:

$ ./myenv
The editor environment variable is set to (null)

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

$ EDITOR=xemacs
$ ./myenv
The editor environment variable is set to (null)

Мы ожидали, что myenv содержит значение "xemacs", но опять ошиблись, поскольку не экспортировали переменную среды EDITOR. На этот раз все должно заработать:

$ export EDITOR
$ ./myenv
The editor environment variable is set to xemacs

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

$ export EDITOR=xemacs

Это работает аналогично двустрочному варианту. Теперь самое время показать, как уничтожить переменную среды используя unset:

$ unset EDITOR
$ ./myenv
The editor environment variable is set to (null)
Разделение строк

Разделением строк — как следует из названия, является представление первоначальной строки в виде меньших, отдельных сегментов — и это одна из задач, которую выполняют большинство сценариев оболочки. Сценариям постоянно приходиться получать полный путь, и выделять из него завершающий файл или каталог. Хоть и возможно (причем, достаточно легко!) реализовать это в bash, но стандартная UNIX-программа basenamе делает то же самое, и весьма неплохо:

$ basename /usr/local/share/doc/foo/foo.txt
foo.txt
$ basename /usr/home/drobbins
drobbins

basename является довольно удобным инструментом для разделения строк. Её «коллега» — dirname, возвращает «другую» часть пути, которую basename отбрасывает:

$ dirname /usr/local/share/doc/foo/foo.txt
/usr/local/share/doc/foo
$ dirname /usr/home/drobbins/
/usr/home
Ни dirname, ни basename не обращаются к файлам и каталогам на диске; они работают только со строками
Подстановка команд

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

$ MYDIR=`dirname /usr/local/share/doc/foo/foo.txt`
$ echo $MYDIR
/usr/local/share/doc/foo

Проделанное выше называется подстановкой команд. Обратим внимание на следующие моменты. В первой строчке мы просто заключили нужную нам команду в обратные кавычки. Это не обычный апостроф и расположен он на клавиатуре выше клавиши «TAB». Мы можем проделать то же самое при помощи альтернативного синтаксиса подстановки команд:

$ MYDIR=$(dirname /usr/local/share/doc/foo/foo.txt)
$ echo $MYDIR
/usr/local/share/doc/foo

Как видите, bash предоставляет множество способов реализации одних и тех же вещей. Используя подстановку команд, мы можем поместить любую команду или конвейер команд между ` ` или $( ) и ассоциировать с переменной среды. Удобная штука! Вот еще пример того, как использовать подстановку конвейера команд:

$ MYFILES=$(ls /etc | grep pa)
$ echo $MYFILES
pam.d passwd
Профессиональный подход к разделению строк

Безусловно basename и dirname великолепные инструменты, но иногда требуется более сложное, по сравнению со стандартным, разделение строк. Если мы хотим нечто большего, то можно воспользоваться расширенной функциональностью подстановки переменных bash. Мы уже использовали стандартные приемы подстановки переменных, которые выглядели, например, так: ${MYVAR}. Но у bash в запасе есть еще несколько приемов. Посмотрим на примеры:

$ MYVAR=foodforthought.jpg
$ echo ${MYVAR##*fo}
rthought.jpg
$ echo ${MYVAR#*fo}
odforthought.jpg

В первом примере мы набрали ${MYVAR##*fo}. И что же это означает? В принципе, внутри ${ }, мы поместили имя переменной, два ## и шаблон («*fo»). Затем, bash взяла переменную MYVAR, нашла в ней самую длинную подстроку, считая от начала строки «foodforthought.jpg», которая совпадает с шаблоном «*fo» и отделила её. С первого раза сложновато понять, как работает специальная опция «##», так что рассмотрим по шагам, как bash обрабатывает это выражение. Сперва, bash осуществляет поиск подстроки с начала «foodforthought.jpg», что соответствует шаблону «*fo». Вот проверенные подстроки:

f       
fo              СОВПАДЕНИЕ *fo
foo     
food
foodf           
foodfo          СОВПАДЕНИЕ *fo
foodfor
foodfort        
foodforth
foodfortho      
foodforthou
foodforthoug
foodforthought
foodforthought.j
foodforthought.jp
foodforthought.jpg

Проверив все строки на совпадение, bash нашла две. Она выделила самую длинную подстроку, убрала её из первоначальной строки и вернула результат.

Второй вид подстановки переменных совпадает с первым, за исключением того, что используется одиночный символ «#». Bash выполняет практически те же действия — она проверяет тот же набор подстрок, что и в первом примере, но убирает не самую длинную, а самую короткую подстроку из первоначальной строки, и возвращает результат. Поэтому, как только она обнаружит «fo» подстроку, она уберёт её из строки и вернет «odforthought.jpg»

Всё это может казаться запутанным, поэтому я покажу Вам простое правило, чтобы запоминать, какие символы что делают. Когда ищете самое длинное вхождение, используйте ## (поскольку ## длиннее, чем #). Когда ищете самую короткую подстроку — используйте #. Как видите, совсем не трудно запомнить! Хотя постойте, а как запомнить то, что использование символа «#» означает удаление с начала строки? Элементарно! Обратите внимание, что на латинской раскладке, «SHIFT+4» является символом «$», который, в свою очередь, является символом подстановки переменной в bash. А сразу слева от «$» находится «#». Таким образом «#» находится перед «$» и, следовательно (опираясь на нашу мнемонику), «#» удаляет символы от начала строки. Вы будете удивлены, когда узнаете как удаляются подстроки от конца строки. Если вы предполагаете, что для этого используется символ, который находится справа от «$» в латинской раскладке («%»), то будете абсолютно правы! Вот пример, как убрать подстроку с конца строки:

$ MYFOO="chickensoup.tar.gz"
$ echo ${MYFOO%%.*}
chickensoup
$ echo ${MYFOO%.*}
chickensoup.tar

Как видите, опции подстановки переменных % и %% работают идентично # и ##, за исключением того, что они удаляют совпавшую подстроку с конца строки. Заметьте, что Вам не нужно использовать символ «*» при удалении определенной подстроки с конца:

MYFOOD="chickensoup"
$ echo ${MYFOOD%%soup}
chicken

В этом примере не имеет значения, что использовать — «%%» или «%», поскольку только одно совпадение возможно. И запомните: если запутаетесь в значениях «#» и «%», то просто посмотрите на клавиши 3, 4, 5 и сразу все поймете.

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

$ EXCLAIM=cowabunga
$ echo ${EXCLAIM:0:3}
cow
$ echo ${EXCLAIM:3:7}
abunga

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

Применение разделения строк

Теперь, когда мы все знаем о разделении строк, давайте напишем простой сценарий оболочки. Он будет получать в качестве аргумента имя файла и выводить, является ли этот файл архивом. Для определения этого, надо искать шаблон «.tar» в конце файла. Вот он:

#!/bin/bash

if [ "${1##*.}" = "tar" ]
then
       echo This appears to be a tarball.
else
       echo At first glance, this does not appear to be a tarball.
fi

Чтобы получить этот сценарий, сохраните его в файле с именем mytar.sh и выполните chmod 755 mytar.sh, чтобы сделать его исполняемым. Затем запустите его, в качестве аргумента используя какой-нибудь архив:

$ ./mytar.sh thisfile.tar
This appears to be a tarball.
$ ./mytar.sh thatfile.gz
At first glance, this does not appear to be a tarball.

Хоть он и работает, но все же ему не хватает функциональности. Однако, перед тем, как мы сделаем его более полезным, давайте посмотрим на выражение «if», использованное выше. Внутри него располагается логическое выражение. В bash оператор сравнения «=» проверяет строки на равенство. Все логические выражения в bash заключаются в квадратные скобки. Но что же в нашем примере проверяет выражение? Давайте посмотрим на его левую часть. В соответствии с тем, что мы знаем о разделении строк, «${1##*.}» отделит самое длинное вхождение, совпадающее с «*.» от начала строки, содержащейся в переменной «1», и вернет результат. Таким образом, все что находится за последним символом "." в имени файла, будет возвращено. Очевидно, что если файл заканчивается на «.tar», мы получим «tar» в качестве результата, и условие будет истинным.

Вас может смутить происхождение переменной «1» в самом начале выражения. Однако, все очень просто — $1 является первым аргументом сценария, $2 — вторым и т.д. Что ж, разобравшись с логическим выражением, мы можем рассмотреть конструкцию «if».

Выражение if

Как и в большинстве языков, в bash есть собственная форма представления условных выражений. Когда будете использовать её, придерживайтесь формата, приведённого выше. То есть ставьте «if» и «then» на разные строки, а «else» и завершающий (и обязательный) «fi» горизонтально выровненными с ними. Это делает код более удобным для чтения и отладки. В дополнение к конструкции «if,else», есть еще несколько видов выражения «if»:

if      [ condition ]
then
        action
fi

Здесь действие action будет выполнено только в том случае, если условие истинно. В противном случае ничего не будет сделано, а выполнение сценария продолжится со строки, следующей за «fi».

if [ condition ]
then 
        action
elif [ condition2 ]
then
        action2
.
.
.
elif [ condition3 ]
then

else
        actionx
fi

Приведенная выше форма «elif» будет последовательно проверять каждое условие и выполнит действие, соответствующее первому истинному выражению. Если таковых не окажется, она запустит действие, находящееся в else, если таковое имеется, а затем продолжит выполнение со строки, следующей после всего выражения «if,elif,else».

Что дальше?

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

Resources
Useful links
  • Read Bash by example: Part 2.
  • Read Bash by example: Part 3.
  • Visit GNU's bash home page