среда, 3 февраля 2010 г.

Баш в примерах, часть 2

Подробно об основах программирования в Bash.

Daniel Robbins (drobbins@gentoo.org), президент и главный исполнительный директор, Gentoo Technologies, Inc
Оригинал: Bash by example, Part 1
Дата публикации: 01 Mar 2000
Свободный перевод Татарников Сергей.
Декабрь 2009г.

Резюме: В своей вступительной статье об основах программирования на Bash,
    Daniel Robbins познакомил Вас с некоторыми основными элементами языка
    сценариев и показал причины использования Bash. В этой, второй части,
    Daniel продолжает знакомство и рассматривает "Основные конструкции Bash,
    такие как условия (if-else), циклы, и многое другое".

Давайте начнем с краткого обзора обработки аргументов командной строки, а затем посмотрим на основные конструкции программирования в Bash.

Аргументы

В примере программы во вступительной части, мы использовали переменную окружения "$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" содержит имя скрипта, и не вызывается (как правило) из командной строки, а "$#" содержит количество аргументов, передаваемых скрипту. Поиграйте с этим сценарием, приводя различные аргументы командной строки, чтобы получить понятие, как это работает.

Иногда бывает полезно сослаться на все аргументы командной строки одновременно. Для этой цели, Bash имеет "$@" переменную, которая содержит все аргументы командной строки, разделенные пробелами. Мы увидим пример ее использования, когда посмотрим на "for" конструкцию чуть позже в этой статье.

Bash конструкции

Если Вы программировали на процедурных языках, таких как C, Pascal, Python или Perl, то Вы уже знакомы со стандартными конструкциями программирования, как "if" условия, "for" циклы, и тому подобные. Bash имеет свои собственные варианты для большинства этих стандартных конструкций. В следующих разделах, я познакомлю с тем, как это устроено в Bash и покажу различия между этими конструкциями и пр. Если Вы не программировали раньше, не беспокойтесь. Я дам Вам достаточное количество информации и примеров.

Условия

Если Вы когда-нибудь писали какой-либо файл на С, Вы знаете, что требует значительных усилий, чтобы увидеть, что этот файл новее, чем другой. Это потому, что C не имеет встроенного синтаксиса такого сравнения, вместо этого вызываются два stat() и две структуры stat должныа быть использована для сравнения вручную. В отличие от C 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 считает, что слишком много аргументов между квадратными скобками. Вы можете легко устранить эту проблему, окружающив строку с аргументами двойными кавычками. Помните, если Вы привыкните окружають строки аргументов и переменные среды в двойные кавычки, Вы устраните многие аналогичные ошибки при программировании. Вот как "foo bar oni" сравнение должно было быть написано:

if [ "$myvar" = "foo bar oni" ]
then
    echo "yes"
fi

Приведенный выше код будет работать, как ожидалось, и не создаст каких-либо неприятных сюрпризов.

Примечание:
Подробнее о специфике
Если вы хотите, чтобы Ваши переменные интерпретировались в строке, заключите строку в двойные, а не одиночные кавычки.

Циклы "for"


ОК, мы рассмотрели условные операторы, теперь пришло время изучить циклы. Мы начнем со стандартного "for" цикла. Вот самый простой пример:

#!/usr/bin/env bash

for x in one two three four
do
    echo number $x
done

output:
number one
number two
number three
number four


Что произошло? "for x" это часть нашего "for" цикла, определяющая новую переменную, называемую "$x", которой был присвоен последовательный набор "one", "two", "three", и "four". После каждого задания переменной, тело цикла (код между "do" ... "done") было выполнено один раз. В теле, мы ссылались на переменную управления циклом "$x", используя стандартные переменные расширения синтаксиса, как и любые другие переменные среды. Также обратите внимание, что "for" цикл всегда принимает какие-то слова списка после "in". В этом случае мы указали четыре английских слова, но список слов может также относиться к файлу(ам) на диске или даже подстановочным знакам в именах файлов. Посмотрите на следующий пример, который демонстрирует, как использовать стандартные подстановочные знаки для Shell:

#!/usr/bin/env bash

for myfile in /etc/r*
do
    if [ -d "$myfile" ]
    then
      echo "$myfile (dir)"
    else
      echo "$myfile"
    fi
done

output:

/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" выполняет действия, в зависимости от того, является ли myfile каталогом или нет.  Если да, до к выводу скрипта добавляется "(dir)".

Мы можем использовать одновременно и шаблоны и переменные в списке слов:

for x in /etc/r??? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
    cp $x /mnt/mydir
done

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

Хотя все наши примеры используют абсолютные пути, Вы также можете использовать пути относительные, а именно:

for x in ../* mystuff/*
do
    echo $x is a silly file
done

В этом примере, 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

output:

$ 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" циклы обеспечивают обратную функциональность, они повторяются до тех пор пока условие ложно. Вот "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, вы можете определить свои функции, аналогичные тому, как вы определяете в других процедурных Языках, как Паскаль и С. В 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 Fi
} )


Другой пример.
Приведенный выше код можно переписать, используя "case" условия. Можете ли вы выяснить, каким образом?  Выше мы определим функцию, называемую "tarview", которая принимает один аргумент - какой-то архив. При выполнении функции, она определяет тип архива (либо несжатый, gzip-сжатый или bzip2-сжатый), печатает одну строку информативное сообщение, а затем отображает содержимое архива. Вот как эта функция должна быть вызвана (независимо из сценария или из командной строки):

$ 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), когда вы создаете переменную внутри функции, она расположена в пространстве локальных имен. Если вы определите функцию в С "myfunction", и в нем определитt переменную "х", любые глобальная (вне функции) переменная "х" не будет зависеть от изменения "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 wo 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" не будет продолжать существовать вне mуfипс. В первой строке функции, мы создаем х, локальную переменную, которую будем использовать позже, а во второй (local myvar="One Two Three") мы создали  локальную myvar и присвоили ей значение. Первая форма удобна для хранения переменной управления циклом. Эта функция не изменяет глобальные переменные, и вам рекомендуется, проектировать все ваши функции таким образом. Вы не должны использовать "local", только когда вы явно хотите изменить глобальную переменную.

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

Комментариев нет:

Отправить комментарий