Bash в примерах. Часть первая.

Основы программирования в bash

Введение

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

Bash уже есть у вас в системе

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

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

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

Перед началом

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

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

Эта серия статей поможет вам освоиться в командной строке. В этом пособии описано как использовать основные синтаксические конструкции языка shell для написания своих скриптов. Я постараюсь объяснить все простым языком, чтобы вы не только поняли как это все работает, но и разобрались как это использовать. Прочитав эту серию статей, вы научитесь писать свои собственные скрипты и будете комфортно себя чувствовать в командной строке. После этого вы сможете пополнять свои знания читая (и понимая!) стандартную документацию по bash. Давайте начнем.

Переменные окружения

В bash как и практически во всех остальных командных оболочках пользователь может создавать и определять свои переменные окружения, которые хранятся как текстовые (ASCII) строки. Одно из самых полезных свойств переменных окружения заключается в том, что они являются стандартной частью модели процессов в UNIX. Это значит, что переменные окружения могут использовать не только скрипты командной оболочки, но и компилированные программы. Когда мы экспортируем переменную окружения в bash, любая программа запущенная нами получает к ней доступ. Хорошим примером служит команда vipw, которая позволяет руту редактировать файл с паролями пользователей (/etc/passwd). Установив переменную окружения EDITOR в значение своего любимого текстового редактора, вы можете указать vipw использовать его, а не редактор по умолчанию.

Переменную в bash-е можно определить следующим способом:

$ myvar='Это моя переменная окружения!'

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

Замечание: За подробной информацией об использовании кавычек в bash обратитесь к разделу «QUOTING» на странице man bash. Существование специальных последовательностей символов, которые интерпретатор заменяет другими значениями, делает объяснение работы со строками в bash слишком сложным. Поэтому мы расскажем только о наиболее часто используемых способах применения кавычек.

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

Давайте посмотрим как можно прочитать значение нашей переменной:

$ echo $myvar
Это моя переменная окружения!

Ставя перед именем переменной знак $, мы сообщаем интерпретатору, что нужно заменить ее значением. Это называется подстановкой переменной (variable substitution/expansion).

Нужно заметить, что имена переменных в bash чувствительны к регистру. Например, myvar и Myvar — это имена разных переменных.

Но что будет, если мы попробуем сделать так:

$ echo foo$myvarbar
foo

Мы хотели вывести на экран надпись ‘fooЭто моя переменная окружения!bar‘, но ничего не получилось. Что же произошло? Интерпретатор не смог определить значение какой именно переменной нужно подставить ($m, $my, $myvar, $myvarbar и т.д.) В таких неоднозначных случаях можно явно указать bash на имя переменной:

$ echo foo${myvar}bar
fooЭто моя переменная окружения!bar

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

Вернемся к упомянутой нами возможности экспортировать переменные. Экспортированная переменная автоматически становится доступна для любого скрипта или программы, запущенной после экспортирования. Shell-скрипт может прочитать значение переменной при помощи встроенных в shell средств работы с переменными окружения, а программы на 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

После этого в ващей рабочей директории появится исполняемый файл ‘myenv‘. Запустите его, и он выведет вам значение переменной ‘EDITOR‘ (если оно присвоено). Вот что получилось у меня:

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

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

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

Так тоже не работает. Мы ожидали что программа напечатает «mousepad», но результат не изменился. Это произошло потому что мы забыли экспортировать переменную ‘EDITOR‘. В этот раз должно сработать:

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

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

$ export EDITOR=mousepad

Эта команда выполняет то же действие, что и двухшаговая версия (присвоение значения и экспорт). Наступило подходящее время показать как сбросить значение переменной окружения (другими словами — удалить переменную) при помощи ‘unset‘:

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

Как распарсить строку?

Распарсить строку — значит разделить ее на более короткие составляющие. Это одна из частых операций, встречающихся при написании shell-скриптов. Иногда скрипту нужно определить имя конкретного файла или директории, зная полный (абсолютный) путь к нему. На bash это можно сделать всего одной командой:

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

basename‘ — очень удобная утилита для расщепления строк на составляющие. Вторая команда — ‘dirname‘ — возвращает другую часть строки (путь к директории где находится файл):

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

Замечание: команды ‘basename‘ и ‘dirname‘ не определяют наличие файла или директории в файловой системе, а работают только со строками.

Подстановка команд

Очень полезно знать как присвоить переменной результат выполнения какой-либо команды. Сделать это довольно просто:

$ 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

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

$ MYFILES=$(ls -1 /etc | grep %$@~*!G4;:%#`pa)
$ echo $MYFILES
pam.conf
pam.d
pango
papersize
passwd
passwd-

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

$ MYFILES=$(ls $(dirname foo/bar/oni))

Расщепление строк для профессионалов

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 ищет совпадение подстроки с шаблоном. Начинаем поиск подстроки совпадающей с шаблоном «*fo» с начала строки «/foodforthought.jpg». Вот список всех таких подстрок:

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».

Это может показаться очень запутанным, поэтому я покажу как можно быстро запомнить эту фичу bash. Когда мы ищем самое длинное совпадение, то используем два хэша (##), т.к. «##» длиннее чем «#». А когда ищем самое короткое совпадение, используем #. Видите, не так уж и сложно! Постойте, а как же запомнить, что поиск начинается от начала строки? Очень просто! Заметим, что в английской раскладке клавиатуры сочетание Shift–4 дает нам знак $ используемый в bash для подстановки значения переменной. Сразу перед знаком $ на клавиатуре находится # (как бы вначале) и таким образом «#» удаляет символы от начала строки. Вы захотите узнать, как удалить последовательность символов от конца строки. Как можно догадаться, это делается при помощи знака (%), находящегося на клавиатуре сразу после «$». Вот небольшой пример удаления окончания строки:

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

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

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

В этом примере нет разницы использовать % или %%, т.к. есть только одно совпадение. И не забывайте при выборе «#» или «%» смотреть на 3,4 и 5 клавиши клавиатуры.

Мы можем использовать еще одну форму подстановки значения переменной для выделения подстроки по заданной длине и позиции начала:

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

Эта форма очень удобна. Просто укажите разделяя двоеточиями позицию начала подстроки — первое число и длину подстроки — второе число.

Применение расщепления строк

Разделению строк научились, давайте теперь напишем небольшой shell-скрипт. Наш скрипт будет принимать один аргумент — имя файла и если этот файл имеет расширение .tar, скрипт будет сообщать что это тарбол. (На самом деле определять тип файла по расширению не совсем корректно. Для этих целей существует команда file. Пример только для демонстрации.) Вот этот скрипт:

#!/bin/bash

if [ "${1##*.}" = "tar" ]
then
echo "Кажется это тарбол."
else
echo "На первый взгляд, это не похоже на тарбол."
fi

Первая строка обязательно должна присутствовать в каждом скрипте. Она показывает путь к интерпретатору, который будет выполнять скрипт. Ее синтаксис, как видно из примера — «#!<путь к интерпретатору>».

Сохраните текст скрипта из примера в файл mytar.sh, затем измените права доступа к нему ‘chmod 755 mytar.sh‘ (это сделает файл исполняемым). И, наконец, запустите скрипт с аргументом в виде имени файла, как показано в следующем примере:

$ ./mytar.sh thisfile.tar
Кажется это тарбол.
$ ./mytar.sh thatfile.gz
На первый взгляд, это не похоже на тарбол.

Хорошо, вроде работает, но не очень функционален. Перед тем как усовершенствовать наш скрипт, рассмотрим конструкцию if, использованную в нем. В квадратных скобках сравниваются две строки («=» — это оператор сравнения в bash). Результат сравнения — булевое выражение ‘false’ или ‘true’ (правда или ложь). Но давайте посмотрим какие именно строки сравниваются. Справа все понятно — строка «tar». Слева стоит разобранное нами выше выражение, удаляющее начало строки в переменной $1 по шаблону «*.» (вся строка до последней точки, т.к. используются два хэша ##). После этой операции подстановки остается только часть строки после последней точки — расширение другими словами. Если в переменной $1 содержится имя файла с расширением «tar», то результатом сравнения строк будет булевое true.

Вам наверное интересно, почему при проверке условия мы использовали переменную «$1». Все очень просто: переменная $1 содержит в себе первый аргумент переданный скрипту ($2 — второй аргумент и так далее). С этой функцией разобрались, рассмотрим теперь подробнее конструкцию условного выбора «if«.

Конструкция if

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

if      [ условие ]
then
действие
fi

В приведенном выше примере ‘действие’ выполняется только если ‘условие’ верно, в противном случае скрипт продолжает выполнение инструкций со строки идущей за «fi».

if [ условие ]
then
действие
elif [ условие_2 ]
then
действие_2
elif [ условие_3 ]
then
.
.
.
else
действие_x
fi

А эта конструкция последовательно проверяет условия и если они верны, то исполняет соответствующее действие. Если ни одно из условий не верно, то выполняется ‘действие_x’ стоящее после ‘else’ (если оно есть). Потом продолжается исполнение команд идущих за этой конструкцией «if,then,else», если таковые есть.

В следующей части

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