Готовый перевод Bash by example / Bash в примерах: Part 2: More bash programming fundamentals
Передача аргументов командной строки
Мы начнем с маленькой подсказки об использовании аргументов командной строки, а потом рассмотрим основные конструкции языка программирования Баша.
В простой программе из вступительной статьи мы использовали переменную окружения "$1", которая ссылается на первый аргумент командной строки. Аналогично вы можете использовать "$2", "$3" и т.д., чтобы указать на второй и третий аргументы, переданные вашему сценарию. Вот пример:
#!/usr/bin/env bash
echo name of script is $0
echo first argument is $1
echo second argument is ${2}
echo seventeenth argument is ${17}
echo number of arguments is $#
Пример говорит сам за себя, за исключением трех маленьких деталей. Во-первых, "$0" выведет имя сценария, которое указано при вызове из командной строки, а "$#" укажет количество аргументов переданных сценарию. Использование фигурных скобок является необязательным для однозначных номеров, но необходимо для аргументов выше "$9". Поиграйте с приведенным сценарием, подставляя различные аргументы командной строки, чтобы запомнить как это работает.
Иногда удобно обозначить сразу все аргументы командной строки. Для этого используется переменная "$@", которая объединит в себе все параметры командной строки, разделенные пробелами. Мы увидим пример ее использования чуть позже в этой статье, когда будем рассматривать циклы "for".
Управляющие конструкции в bash
Если вы программировали на процедурных языках, таких как C, Pascal, Python или Perl, то вы хорошо знакомы с такими управляющими конструкциями как операторы "if", циклы "for" и тому подобные. Bash имеет свои собственные версии большинства из этих стандартных конструкций. В следующих разделах я расскажу о нескольких управляющих конструкциях и продемонстрирую их отличия от их аналогов в других известных вам языках программирования. Если вы не имеете опыта программирования, не волнуйтесь. Я включил достаточно информации и примеров, чтобы вы смогли следить за текстом.
Любовь к условиям
Если вам когда-либо приходилось писать код для работы с файлами на С, то вы знаете, что для того, чтобы сравнить даты создания двух файлов требуется приложить значительное количество усилий. Так происходит потому, что в языке С нету встроенных средств для подобных сравнений; и вот приходится делать два вызова stat(), использовать две структуры stat и делать сравнения вручную. Напротив, в bash уже имеются встроенные средства для работы с файлами и, таким образом, проверка "имеется ли доступ на чтение к файлу /tmp/myfile" не сложнее проверки "значение переменной $myvar больше 4?".
В таблице ниже приведен список наиболее часто используемых операций сравнения в bash. Также в ней вы найдете примеры правильного использования каждой операции.
Приведенные примеры следует размещать непосредственно после "if". Например:
if [ -z "$myvar" ]
then
echo "myvar is not defined"
fi
В некоторых случаях существует несколько способов осуществления необходимой проверки. В качестве примера рассмотрим два фрагмента кода ниже, которые реализуют одну и ту же функциональность:
if [ "$myvar" -eq 3 ]
then
echo "myvar equals 3"
fi
if [ "$myvar" = "3" ]
then
echo "myvar equals 3"
fi
Хотя оба примера выше и выполяют одно и то же действие, но разница заключается в том, что в первом используется арифметическая операция сравнения, а во втором - строковая.
Замечания о сравнении строк.
В большинстве случаев можно не помещать строковые переменные в двойные кавычки, но это не слишком хорошая практика. Почему? Потому что ваш код будет работать правильно до тех пор, пока в строковой переменной не появится символы пробела или табуляции. В таком случае bash придет в некоторое замешательство. Вот пример хренового сравнения:
if [ $myvar = "foo bar oni" ]
then
echo "yes"
fi
В данном примере, если значение myvar равно "foo" - код будет работать правильно и ничего не напечатает. Однако, если значение myvar равно "foo bar oni" - при исполнении код выдаст ошибку:
[: too many arguments
В данном случае пробелы внутри значения переменной $myvar (значение которой равно "foo bar oni") приведут к тому, что bash не будет знать как сравнивать значения. После того, как bash подставит значение переменной $myvar операция сравнения будет выглядеть так:
[ foo bar oni = "foo bar oni" ]
Так как переменная не была помещена внутри двойных кавычек - bash решит, что вы передали слишком много аргументов внутри квадратных скобок. Эту проблему легко решить поместив переменную в двойные кавычки.
Запомните, если вы сразу привыкнете помещать строковые переменные в двойные кавычки - вы сможете избежать массы однообразных ошибок при программировании в bash.
Вот как пример с "foo bar oni" должен был быть написан правильно:
if [ "$myvar" = "foo bar oni" ]
then
echo "yes"
fi
Данный пример всегда будет работать так, как задумано и позволит избежать каких-либо неприятных сюрпризов.
Примечание:
Если вы хотите, чтобы bash подставлял значения переменных - помещайте их в двойные, а не одинарные кавычки. Одинарные кавычки запрещают подстановку значения переменных.
Циклические конструкции: "for"
Итак, мы рассмотрели условные операторы, а теперь пришло время разобраться с конструированием циклов в bash. Начнем мы со стандартного цикла "for".
Вот простой пример:
#!/usr/bin/env 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". После каждого из присваиваний тело цикла (код, находящийся между "do" ... "done") было выполнено один раз. В теле цикла мы использовали переменную контроля цикла "$x" используя точно такой же синтаксис, как и для любой другой переменной.
Также обратите внимание на то, что цикл "for" после конструкции "in" всегда принимает на вход какой-нибудь список слов. В данном примере мы указали четыре английских слова, но в общем случае список может включать пути к файлам на диске или даже шаблоны (символы подстановки). Взгляните на пример ниже, который демонстрирует то, как можно использовать стандартные шаблоны оболочки:
#!/usr/bin/env bash
for myfile in /etc/r*
do
if [ -d "$myfile" ]
then
echo "$myfile (dir)"
else
echo "$myfile"
fi
done
вывод:
/etc/rc.d (dir)
/etc/resolv.conf
/etc/resolv.conf~
/etc/rpc
Приведенный выше код перебирает все файлы в каталоге /etc, которые начинаются с "r". Для того, чтобы это сделать перед выполнением цикла bash сначала заменил "/etc/r*" на соответствующие этому шаблону строку /etc/rc.d /etc/resolv.conf /etc/resolv.conf~ /etc/rpc.
Внутри тела цикла условие "-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 выполняет раскрытие шаблонов относительно текущей рабочей директории, также как вы используете относительные пути в командной строке. Поэкспериментируйте немного с подстановкой по шаблону. Вы заметите, что если использовать абсолютные пути, то bash раскроет шаблон в список с абсолютными путями. В противном случае bash будет использовать относительные пути в последующем списке слов. Если вы просто ссылаетесь на файлы в своей рабочей директории (например, вы ввели for x in *), результирующий список файлов не будет начинаться с какой либо информации о пути. Помните, что информация о пути может быть удалена использованием basename, как показано ниже:
for x in /var/log/*
do
echo basename $x is a file living in /var/log
done
Конечно, часто удобно для выполнения циклов работать с аргументами командной строки сценария. Вот пример того, как использовать переменную "$@", представленную в начале этой статьи.
#!/usr/bin/env bash
for thing in "$@"
do
echo you typed ${thing}.
done
вывод:
$ allargs hello there you silly
you typed hello.
you typed there.
you typed you.
you typed silly.
Арифметические операции в оболочке
Прежде чем обратить внимание на второй тип циклических конструкций, было бы неплохо ознакомиться с выполнением арифметических операций в оболочке. Да, это правда: вы можете выполнять простые математические операции с целыми числами, используя конструкции оболочки. Просто поместите арифметическое выражение между "$((" и "))", и bash вычислит его. Вот несколько примеров:
$ echo $(( 100 / 3 ))
33
$ myvar="56"
$ echo $(( $myvar + 12 ))
68
$ echo $(( $myvar - $myvar ))
0
$ myvar=$(( $myvar + 1 ))
$ echo $myvar
57
Теперь, когда вы познакомились с выполнением математических операций, настало время представить две другие циклические конструкции в bash, "while" и "until".
Больше циклических конструкций: "while" и "until"
Оператор "while" выполняется до тех пор, пока условие истинно, и имеет следующий вид:
while [ условие ]
do
ОПЕРАТОРЫ
done
Оператор "while" используется, как правило, для выполнения цикла определенного количества раз, как в следующем примере, в котором цикл будет выполнен ровно 10 раз:
myvar=0
while [ $myvar -ne 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
Вы можете видеть, как использование арифметического вырожения в конечном итоге приводит к тому, что условие оказывается ложным, и цикл завершается.
Оператор "until" предоставляет обратную функциональность оператору "while". Он выполняется до тех пор, пока условие ложно. Здесь цикл "until" выполняет те же функции, как и предыдущий цикл "while":
myvar=0
until [ $myvar -eq 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
Оператор "case"
Оператор "case" еще одна удобная условная конструкция. Вот фрагмент примера:
case "${x##*.}" in
gz)
gzunpack ${SROOT}/${x}
;;
bz2)
bz2unpack ${SROOT}/${x}
;;
*)
echo "Archive format not recognized."
exit
;;
esac
В приведенном примере, bash сначала раскрывает "${x##*.}". В коде "$x" -- это имя файла, а "${x##*.}" удаляет весь текст за исключением того, что следует за последней точкой в имени файла. Затем bash сравнивает получившуюся строку с переменными, перечисленными слева от ")". В данном случае "${x##*.}" сравнивается с "gz", далее с "bz2" и, наконец, с "*". Если "${x##*.}" соответствует какой-нибудь из этих строк или шаблонов, то код, следующий непосредственно за ")", будет выполняться до ";;", после чего bash продолжит выполнять программу после завершающего слова "esac". Если ни шаблоны, ни строки не совпадают, то ни одной строки кода не выполняется, однако в данном фрагменте, по крайней мере, один блок кода будет выполнен, потому что шаблон "*" будет ловить всё, что не соответствует "gz" и "bz2".
Функции и пространства имен
В bash вы можете даже определять функции подобные тем, что есть в других процедурных языках программирования, таких как Pascal и C. В bash функции могут даже принимать аргументы с помощью системы, очень похожей на способ сценариев принимать аргументы командной строки. Давайте взглянем на пример определения функции:
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
}
Примечание:
Другой случай: приведенный код может быть написан с использованием оператора "case". Можете ли вы сказать каким образом?
Выше мы определяем функцию с названием "tarview", которая принимает в качестве аргумента один какой-либо архив. Когда она выполняется, то определяет, какой тип архива был передан в виде аргумента (любой несжатый, сжатый gzip или сжатый bzip), печатает однострочное информационное сообщение, а затем отображает содержимое архива. Ниже указано как должна вызываться даннуя функция (из сценария или из командной строки, после того как она была напечатана или вставлена):
$ tarview 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" (если вы запустили функцию из оболочки интерактивно) или как имя сценария, в котором вызвана функция.
Примечание:
Используйте их интерактивно: не забывайте, что функции такие, как приведенная выше, могут быть размещены в файлах ~/.bashrc или ~/.bash_profile так, что они будут доступны для использования всякий раз, когда вы находитесь в bash.
Пространство имен
Часто вам необходимо создать переменную окружения внутри функции. Если возможно, то запомните эту техническую особенность. В большинстве компилируемых языков программирования (таких как C), когда вы создаете переменную внутри функции, она будет расположена в отдельном локальном пространстве имен. Таким образом, если вы определите функцию в C с названием myfunction и в ней определите переменную с именем "x", то любая глобальная (за пределами функции) переменная с именем "x" не будет зависеть от нее, что устранит побочный эффект.
Несмотря на то, что это верно в C, в bash это не так. В bash всякий раз, когда вы создаете переменную окружения внутри функции, она добавляется в глобальное пространство имен. Это означает, что она может перезаписать любую глобальную переменную вне функции и будет продолжать существовать даже после завершения работы функции:
#!/usr/bin/env bash
myvar="hello"
myfunc() {
myvar="one two three"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
Когда этот сценарий запустится, он выведет "one two three three", показывая, как определенная в функции "$myvar" затерла глобальную переменную "$myvar", и как переменная контроля цикла "$x" продолжила существовать даже после завершения работы функции (и также затерла бы глобальную "$x", если та была определена).
В этом простом примере ошибку легко обнаружить и компенсировать с помощью альтернативных имен переменных. Однако это неправильный подход, лучший способ решения этой проблемы, предотвратить возможность перезаписи глобальных переменных, в первую очередь, с помощью команды "local". когда мы используем "local", чтобы создать переменные внутри функции, они будут храниться в локальном пространстве имен и не затрут какие-либо глобальные переменные. Вот как можно реализовать приведенный ниже код, чтобы никакие глобальные переменные не были перезаписаны:
#!/usr/bin/env 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" не была перезаписана, и "$x" не продолжает существовать вне myfunc. В первой строке функции мы создаем локальную переменную x, которую используем позднее, в то время как, во втором случае (local myvar="one two three") мы создаем локальную переменную myvar и присваиваем ей значение. Первая форма удобна для хранения локальных переменных контроля цикла, так как недопустимо писать "for local x in $myvar". Данная функция не перезапишет никаких глобальных переменных, и вам рекомендуется разрабатывать таким образом все ваши функции. Вы не должны использовать "local" только в тех случаях, когда явно хотите изменить глобальную переменную.
В заключение
Теперь, когда мы рассмотрели наиболее существенные возможности bash, пришло время посмотреть, как создать целую программу на bash. В моей следующей статье мы будем делать именно это. До встречи!
Ресурсы
Полезные ссылки
* Bash в примерах: Часть 1.
* Bash в примерах: Часть 3.
* Посетите домашнюю страницу GNU bash.
Дэниэль Роббинс
Дэниэль Роббинс — основатель сообщества Gentoo и создатель операционной системы Gentoo Linux. Дэниэль проживает в Нью-Мехико со свой женой Мэри и двумя энергичными дочерьми. Он также основатель и глава Funtoo, написал множество технических статей для IBM developerWorks, Intel Developer Services и C/C++ Users Journal.
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется
Переведено на Нотабеноиде
http://notabenoid.org/book/14299/46938
Переводчики: Daemon786, undeceived, mslasm, bugzy
Настройки
Готово:
