Bash в примерах. Часть вторая.
Еще больше основ программирования в bash
Обработка аргументов
Давайте разберемся как передавать и обрабатывать аргументы скрипта и ознакомимся с основными управляющими конструкциями bash.
В простом скрипте из предыдущей статьи мы использовали переменную "$1", которая содержит первый аргумент командной строки при вызове скрипта. Аналогично можно использовать "$2", "$3" и так далее для доступа ко второму, третьему... аргументам командной строки. Вот пример:
#!/bin/bash
echo "Имя скрипта — $0"
echo "Первый аргумент: $1"
echo "Второй аргумент: ${2}"
echo "Семнадцатый аргумент: ${17}"
echo "Количество аргументов: $#"
Обратите внимание, что в переменной "$0" содержится имя самого скрипта, который запущен из командной строки. А переменная "$#" содержит количество переданных скрипту аргументов. Использование фигурных скобок необязательно только для переменных состоящих из одной цифры (с $0 по $9). Попробуйте позапускать этот скрипт с разным числом аргументов и посмотрите как он работает.
Иногда необходимо сослаться сразу на все аргументы командной строки. Для этого в bash есть специальная переменная "$@", которая содержит все аргументы переданные скрипту разделенные пробелами. Мы будем использовать эту переменную чуть позже при рассказе о циклах со счетчиком (конструкция "for").
Управляющие конструкции bash
Если вы раньше программировали на процедурных языках, таких как Си, Паскаль, Перл и тому подобных, вам должны быть знакомы управляющие конструкции вроде "if", "for" и другие. В bash тоже есть все эти конструкции. В следующих разделах пособия я познакомлю вас с ними и покажу чем они отличаются от подобных конструкций из других языков программирования. Если вы раньше не программировали — не волнуйтесь. Материал будет изложен подробно и дополнен примерами, так что даже новичок в программировании сможет разобраться.
Оператор условного выбора "if"
Если вы раньше программировали на языке Си, то должны знать сколько требуется усилий чтобы определить какой из двух файлов был создан первым, например. А все из-за того, что в Си нет встроенных средств для такого рода сравнения. Вместо этого приходится использовать системный вызов stat() для каждого файла и затем сравнивать результат вручную. Но в bash есть встроенный механизм сравнения файлов, Поэтому узнать "доступен ли для чтения файл /tmp/myfile" настолько же просто как и узнать "превосходит ли значение переменной 'myvar' 4".
Привожу список наиболее часто употребляемых в bash операторов сравнения
Файлы- -a file
- истинно если файл существует.
- -d file
- истинно если файл существует и является директорией.
- -f file
- истинно если файл существует и является обычным файлом.
- -r file
- истинно если файл существует и доступен для чтения.
- -s file
- истинно если файл существует и его размер больше 0.
- -w file
- истинно если файл существует и доступен для записи.
- -x file
- истинно если файл существует и является исполняемым.
- file1 -nt file2
- истинно если файл file1 новее чем file2 или file1 (в соответствии со временем последнего изменения) существует, а file2 нет.
- file1 -ot file2
- истинно если файл file1 старше чем file2 или file2 существует, а file1 нет.
- file1 -ef file2
- истинно если оба файла ссылаются на одно и то же устройство или инод.
- -z string
- истинно если строка имеет нулевую длину.
- -n string
- истинно если длина строки не нулевая.
- string1 = string2
- истинно если строки равны.
- string1 != string2
- истинно если не равны.
- string1 < string2
- истинно если строка 1 стоит в алфавитном порядке перед строкой 2.
- string1 > string2
- истинно если строка 1 стоит в алфавитном порядке после строки 2.
В следующих примерах показано как использовать оператор сравнения в конструкции "if":
if [ -z "$myvar" ]
then
echo "Переменная 'myvar' не определена."
fi
Квадратные скобки вычисляют условное выражение стоящее в них (это синоним встроенной функции bash — test). Возвращаемый результат — 1 или 0 в зависимости от того выполняется условие или нет. в скобках может стоять несколько выражений, связанных логическими операторами "и" или "или". Подробнее на странице справки help test.
В некоторых случаях одна и та же операция сравнения может быть сделана несколькими разными способами. Обе конструкции из следующего примера функционально идентичны:
if [ "$myvar" -eq 3 ]
then
echo "myvar равно 3"
fi
if [ "$myvar" = "3" ]
then
echo "myvar равно 3"
fi
В первой конструкции из предыдущего примера использована операция арифметического сравнения, а во втором — операция сравнения строк.
Тонкости при сравнении строк
В большинстве случаев, когда вы не заключаете строки и строковые переменные в двойные кавычки, это может привести к ошибке. Почему? Да потому что в строке может встретится пробел или символ табуляции, которые bash не сможет правильно обработать. Вот пример некорректного сравнения строк:
if [ $myvar = "foo bar oni" ]
then
echo "yes"
fi
В этом примере, если значение переменной "$myvar" будет равно "foo", код будет работать как и ожидается и не печатать ничего. Но если значение переменной "$myvar" будет равно "foo bar oni", скрипт вызовет следующую ошибку:
[: too many arguments
После подстановки значения переменной, bash пытается произвести следующую операцию сравнения:
[ foo bar oni = "foo bar oni" ]
В этом случае bash не может правильно обработать сравнение строк содержащих пробелы, одна из которых не заключена в двойные кавычки. Интерпретатор думает, что в квадратных скобках слишком много аргументов. После заключения переменной в двойные кавычки, ошибка не возникает и код работает так как мы задумали. Запомните, если вы возьмете в привычку заключать в двойные кавычки все строковые аргументы и переменные, то избежите множества ошибок подобных описанной выше. Вот исправленный кусок кода:
if [ "$myvar" = "foo bar oni" ]
then
echo "yes"
fi
Этот код будет работать корректно и не преподнесет нам больше никаких неприятных сюрпризов.
Конструкция создания циклов "for"
Хорошо, с условными переходами разобрались, пора перейти к циклическим конструкциям. Начнем с управляющей конструкции "for". Вот стандартный пример:
#!/bin/bash
for x in one two three four
do
echo "number $x"
done
Результат:
number one
number two
number three
number four
Что же именно произошло? Часть "for x" цикла "for" определяет переменную (называемую итератором) "$x", которая последовательно принимает значения "one", "two", "three", и "four" (по одному за один такт цикла). После присвоения каждого нового значения переменной "$x", выполняется тело цикла (код между словами "do" и "done"). В теле цикла мы выводим на печать значение переменной "$x". Заметим, что после слова "in" в конструкции "for" всегда стоит некий список. В данном примере мы указали четыре слова, но этот список может содержать имена файлов или даже шаблон (wildcard). В следующем примере показано как использовать шаблоны при инициализации итератора цикла:
#!/bin/bash
for myfile in /etc/r*
do
if [ -d "$myfile" ]
then
echo "$myfile (dir)"
else
echo "$myfile"
fi
done
результат:
/etc/rc0.d (dir)
/etc/rc1.d (dir)
/etc/rc2.d (dir)
/etc/rc3.d (dir)
/etc/rc4.d (dir)
/etc/rc5.d (dir)
/etc/rc6.d (dir)
/etc/rc.local
/etc/rcS.d (dir)
/etc/rearj.cfg
/etc/reportbug.conf
/etc/resolvconf (dir)
/etc/resolv.conf
/etc/rmt
/etc/rpc
/etc/rsyslog.conf
/etc/rsyslog.d (dir)
Код этого цикла исполнится для каждого файла из /etc/ имя которого начинается с "r". Сначала bash найдет все такие файлы и заменит шаблон строкой /etc/rc0.d /etc/rc1.d /etc/rc2.d /etc/rc3.d /etc/rc4.d ... /etc/rsyslog.d перед тем как приступить к выполнению цикла. В теле цикла для каждого файла из списка проверяется является ли этот файл директорией при помощи оператора "-d". Если файл оказался директорией, рядом с его называнием печатается "(dir)".
В списке инициализации итератора можно использовать несколько шаблонов одновременно и даже переменные окружения:
for x in /etc/r??? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
cp $x /mnt/mydira
done
Bash в этом примере подставляет значение переменной и раскрывает шаблоны. А затем копирует все файлы в заданную директорию.
До этого все примеры содержали шаблоны основанные на абсолютных путях, но можно использовать и относительные:
for x in ../* mystuff/*
do
echo "$x is a silly file"
done
В этом примере bash раскрывает шаблон относительно текущей рабочей директории (не той в которой находится скрипт, а той которую показывает команда "pwd"). Поиграйтесь с этим скриптом, позапускайте его из разных директорий и посмотрите на результат.
Иногда может потребоваться запустить цикл по списку аргументов из командной строки. Вот как это делается:
#!/bin/bash
for i in "$@"
do
echo "Вы написали: ${i}."
done
результат:
$ ./test.sh hello there you silly
Вы написали: hello.
Вы написали: there.
Вы написали: you.
Вы написали: silly.
В этом примере мы использовали переменную "$@" о которой говорили выше.
Арифметика в shell
Перед тем как приступить к разбору следующего вида циклической конструкции, научимся при помощи интерпретатора производить простые арифметические операции. Просто заключите арифметическое выражение в конструкцию "$(( ))" и bash посчитает ее значение. Вот несколько примеров:
$ echo $(( 100 / 3 ))
33
$ myvar="56"
$ echo $(( $myvar + 12 ))
68
$ echo $(( $myvar - $myvar ))
0
$ myvar=$(( $myvar + 1 ))
$ echo $myvar
57
Теперь, когда вы познакомились с вычислением арифметических выражений в shell, пришло время рассказать о циклических конструкциях "while" и "until".
Циклические конструкции с условиями ("while" и "until")
"while"–цикл исполняется пока выражение в квадратных скобках истинно. Он имеет следующий формат:
while [ условие ]
do
код
done
В следующем примере тело цикла исполняется ровно 10 раз:
myvar=0
while [ $myvar -ne 10 ]
do
echo "$myvar"
myvar=$(( $myvar + 1 ))
done
После каждого выполнения кода тела цикла переменная "myvar" увеличивается на 1. Когда значение переменной становится равным 10, условие в квадратных скобках не выполняется и цикл прерывается.
"Until"–цикл очень похож на "while"–цикл: он повторяется пока выражение в квадратных скобках ложно. Вот пример "until"–цикла по функциональности идентичного "while"–циклу из предыдущего примера:
myvar=0
until [ $myvar -eq 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
Экстренный выход из цикла
Для экстренного выхода из "for", "while" или "until" цикла используется команда break. Для выхода из нескольких вложенных циклов — break N, где N — количество вложенных циклов.
name=0
while :
do
wget http://example.com/gallery/${name}.png
[ $? -ne 0 ] && break
done
В последнем примере: "while :" — бесконечный цикл. Двоеточие — это команда bash которая не делает ничего но всегда завершается успехом. Переменная $? содержит статус с которым завершилась последняя команда (подробнее о специальных переменных смотри man bash). В нашем случае код отличный от 0 обозначает что при скачивании файла произошла ошибка. Как только условие в квадратных скобках выполнено, интерпретатор переходит к исполнению команды стоящей после логического и (&&). Break прерывает выполнение цикла.
Предпоследнюю строку предыдущего примера можно заменить на знакомую нам условную конструкцию "if" (помним, что в bash одно действие можно сделать несколькими разными способами):
[ $? -ne 0 ] && break
то же самое но через условную конструкцию:
if [ $? -ne 0 ]
then
break
fi
Или в одну строку
if [ $? -ne 0 ]; then break; fi
Да, конструкции можно записывать в одну строку, только нужно поставить несколько разделяющих знаков "точка с запятой". Но не стоит привыкать к такой форме записи — это усложняет читаемость кода.
Команда–переключатель "case"
Конструкция условного перехода "case" может оказаться очень полезной. Вот пример ее использования:
case "${x##*.}" in
gz) gzunpack ${SROOT}/${x} ;;
bz2) bz2unpack ${SROOT}/${x} ;;
*) echo "Формат архива не определен."
exit
;;
esac
В этом примере сначала происходит обработка строки в переменной "$x" — "${x##*.}". Как мы помним из первой статьи, после этой операции в переменной "$x" остается только расширение файла. Затем bash сравнивает это расширение с вариантами стоящими слева от одинарных скобок ")". Если совпадение найдено, выполняется соответствующее действие. Если совпадения не найдено, никаких действий не выполняется, но в данном конкретном коде совпадение будет всегда, потому что в последней строке стоит шаблон "*", совпадающий с любой последовательностью символов.
Функции и пространство имен
В bash вы можете определять свои функции, как и в других языках программирования (C, Pascal...). Эти функции могут принимать аргументы, используя механизм очень похожий на механизм работы с аргументами командной строки. Вот пример определения простой функции:
tarview() {
echo -n "Displaying contents of $1 "
if [ ${1##*.} = tar ]
then
echo "(uncompressed tar)"
tar tvf $1
elif [ ${1##*.} = gz ]
then
echo "(gzip-compressed tar)"
tar tzvf $1
elif [ ${1##*.} = bz2 ]
then
echo "(bzip2-compressed tar)"
cat $1 | bzip2 -d | tar tvf -
fi
}
Выше мы определили функцию с именем "tarview", которая принимает один аргумент — имя тарбола. Эта функция определяет вид тарбола (без сжатия, сжатый gzip-ом или bzip2) по расширению, затем печатает этот тип и показывает содержимое архива. Если формат определить не удалось, выводится соответствующее сообщение. Вот пример вызова функции:
$ ./tarview.sh shorten.tar.gz
Displaying contents of shorten.tar.gz (gzip-compressed tar)
drwxr-xr-x ajr/abbot 0 1999-02-27 16:17 shorten-2.3a/
-rw-r--r-- ajr/abbot 1143 1997-09-04 04:06 shorten-2.3a/Makefile
-rw-r--r-- ajr/abbot 1199 1996-02-04 12:24 shorten-2.3a/INSTALL
-rw-r--r-- ajr/abbot 839 1996-05-29 00:19 shorten-2.3a/LICENSE
....
Как вы видите, обращение к аргументам внутри функции происходит по тем же именам как и к аргументам командной строки внутри скрипта. Переменная "$#" содержит количество переданных функции аргументов. Единственное что остается по-прежнему — переменная "$0". Она содержит название скрипта при вызове функции из скрипта или строку "bash" при вызове функции напрямую из командной строки.
Вызвать функцию из командной строки можно следующим образом: сохраняем код функции в файл (например с названием "myfunc.txt") а затем даем следующую команду:
$ . myfunc.txt
или что тоже самое
$ source myfunc.txt
Эти команды строка за строкой исполняют инструкции написанные в файле в текущей командной оболочке. (Можно так же напечатать код функции строка за строкой в командной строке, но первый способ намного удобнее). После этого можем вызывать нашу функцию прямо из командной строки:
$ tarview shorten.tar.gz
Пространство имен
Часто возникает потребность создать переменную окружения внутри функции. В большинстве компилируемых языков (например Си), когда вы создаете переменную внутри функции, она попадает в отдельное пространство имен этой функции. Например, если вы напишите функцию "my function" на C и внутри этой функции создадите переменную "x", то она никак не повлияет на переменную с тем же именем "x", созданную вне функции "myfunction".
Но в bash все по-другому. В bash, когда вы создаете переменную внутри функции, она попадает в общее пространство имен. Это значит, что она может перезаписать значение глобальной переменной с таким же именем и продолжит свое существование даже после завершения исполнения функции:
#!/bin/bash
myvar="hello"
myfunc() {
myvar="one two three"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
Результатом исполнения этого кода будет строка "ne two three three", показывающая что переменная "myvar", созданная внутри функции перезаписала значение глобальной переменной "myvar" и что последнее значение итератора "x" равное "three" продолжило существование даже после завершения функции.
В этом простом примере ошибку легко заметить и устранить, переименовав переменные внутри функции. Но есть гораздо более правильное решение этой проблемы: при создании переменной можно явно указать что она является локальной при помощи инструкции "local". Созданная таким способом внутри функции переменная будет отнесена к локальному пространству имен этой функции и не сможет никак повлиять на глобальную переменную с таким же именем. Следующий пример демонстрирует возможность создания локальной переменной:
#!/bin/bash
myvar="hello"
myfunc() {
local x
local myvar="one two three"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
Результатом выполнения этого кода будет строка "hello" — значение глобальной переменной "myvar" (на которую никак не повлияла локальная переменная "myvar", созданная внутри функции), а локальная переменная "x" перестает существовать после завершения функции.
Единственное условие при котором вы не должны использовать локальные переменные внутри функций — если хотите изменить значение глобальной переменной.
Подведение итогов
Вот и все. Теперь вы имеете представление о программировании в bash и можете писать свои скрипты. За более подробной информацией обращайтесь к справке man bash или к руководству Advanced Bash-Scripting Guide
Смотри также
Оригинал статьи— Bash by example, Part 2 (eng)