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

Еще об основах программирования в bash

Соглашение об аргументах

Начнем с кратких сведений о работе с аргументами командной строки, а затем посмотрим на основные конструкции программирования на 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" раскроется в имя скрипта, как он был вызван в командной строке, а "$#" -- в число переданных скрипту аргументов. Использование фигурных скобок не обязательно для чисел, состоящих из одной цифры, но требуется для аргументов больших "$9". Поиграйте с этим скриптом, передавая ему аргументы командной строки различного рода, чтобы освоиться с тем, как это работает.

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

Конструкции программирования bash

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

Условная любовь

Если вы когда-нибудь программировали на C что-то связанной с файлами, вы знаете, что требуются значительные усилия, чтобы посмотреть является ли какой-либо отдельный файл новее, чем другой. Причина в том, что в C нет никакого встроенного синтаксиса для выполнения такого сравнения; вместо этого нужно использовать два вызова stat() и две структуры stat для выполнения такого сравнения вручную. Напротив, bash имеет стандартные встроенные операции сравнения файлов, так что определить "является ли /tmp/myfile читаемым" так же легко, как проверить "больше ли 4 '$myvar'".

В следующей таблице приведен список наиболее часто используемых операторов сравнения bash. Вы найдете также пример, как правильно использовать каждую опцию. Подразумевается, что пример следует непосредственно за "if". Например:

if [ -z "$myvar" ]
then
     echo "myvar не определена"
fi

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

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 "да"
fi

В этом примере, если myvar равно "foo", код будет работать, как ожидается, и не напечатает ничего. Однако, если myvar равна "foo bar oni", код завершится неудачей со следующей ошибкой:

[: too many arguments

В этом случае пробелы в "$myvar" (которая равна "foo bar oni") запутывают bash. После того как "$myvar" раскроется, он завершается следующим сравнением:

[ foo bar oni = "foo bar oni" ]

Поскольку переменная среды не помещена в квадратные скобки, bash думает, что вы задали слишком много аргументов в квадратных скобках. Вы легко может исключить эту проблему, окружив строковые аргументы двойными кавычками. Помните, если у вас будет привычка окружать все строковые аргументы и переменные среды двойными кавычками, вы исключите много подобных ошибок программирования. Вот как нужно написать сравнение с "foo bar oni":

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

Этот код будет работать, как и ожидается, и не создаст неприятных сюрпризов.

Замечание:

Если вы хотите, чтобы ваши переменные среды были раскрыты, вы должны заключить их в двойные кавычки, а не в одинарные. Одинарные кавычки отменяют раскрытие переменных (также как и истории).

Конструкции цикла: "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", чтобы выполнить два различных действия, в зависимости от того, является ли myfile директорией или нет. Если это директория, к выходной строке добавляется " (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 - глупый файл
done

В этом примере bash выполняет раскрытие образцов относительно текущей рабочей директории, так же как вы используете относительные пути в командной строке. Поиграйте немного с образцами. Вы заметите, что если вы используете абсолютный путь в образцах, bash раскроет образцы в список абсолютных путей. В противном случае, в последующем списке слов bash будет использовать относительные пути. Если вы просто сошлетесь на файлы в текущей рабочей директории (например, если вы наберете 'for x in *'), в результирующем списке файлов не будет спереди приставлена информация о путях. Помните, что стоящую впереди информацию о путях можно отрезать с помощью команды 'basename', например:

for x in /var/log/*
do
    echo `basename $x` - файл, живущий в /var/log
done

Конечно, часто удобно выполнить цикл, который обрабатывает аргументы командной строки. Вот пример использования переменной "$@", введенной в начале этой статьи:

#!/usr/bin/env bash

for thing in "$@"
do
    echo вы набрали ${thing}.
done

output:

$ allargs hello there you silly
вы набрали hello.
вы набрали there.
вы набрали you.
вы набрали 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 "Архивный формат не распознан."
           exit
           ;;
esac

Здесь bash сначала раскрывает "${x##*.}". В коде "$x" это имя файла, а "${x##*.}" в результате удаляет весь текст за исключением того, что следует в имени файла после последней точки. Далее bash сравнивает получившийся результат со значением, стоящим слева от закрывающих круглых скобок ")". В нашем случае "${x##*.}" сравнивается с "gz", затем с "bz2" и наконец с "*". Если "${x##*.}" соответствует любому из этих строк или образцов, выполняется часть скрипта, непосредственно следующая за ")" до ";;", после чего bash продолжает выполнение скрипта со строки после завершающего "esac". Если соответствующих строк или образцов нет, коды конструкции "case" не выполняются; однако в нашем конкретном отрезке кода будет выполняться по крайней мере один блок кода, поскольку образец "*" отловит все, что не соответствует "gz" или "bz2".

Функции и пространства имен

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

tarview() {
    echo -n "Displaying contents of $1 "
    if [ ${1##*.} = tar ]
    then
        echo "(несжатый tar)"
        tar tvf $1
    elif [ ${1##*.} = gz ]
    then
        echo "(tar, сжатый gzip)"
        tar tzvf $1
    elif [ ${1##*.} = bz2 ]
    then
        echo "(tar, сжатый bzip2)"
        cat $1 | bzip2 -d | tar tvf -
    fi
}

Замечание:

Другой вариант: этот код можно написать, используя предложение "case". Можете вы догадаться, как?

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

$ tarview shorten.tar.gz
Вывод содержимого shorten.tar.gz (tar, сжатый gzip)
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="привет"

myfunc() {

    myvar="один два три"
    for x in $myvar
    do
        echo $x
    done
}

myfunc

echo $myvar $x

Будучи запущенным этот скрипт выдаст "один два три три", показывая, что "$myvar", определенная в функции, полностью заменяет глобальную переменную "$myvar" и что переменная цикла "$x" продолжает существовать даже после выхода из функции (и даже заменила бы любую глобальную переменную "$x", если бы она была определена).

В этом простом примере баг легко увидеть и компенсировать, используя альтернативные имена переменных. Однако это неправильный подход; лучший путь решения этой проблемы заключается в том, чтобы предотвратить возможность замены глобальных переменных с самого начала с помощью команды "local". Если мы используем "local" для создания переменных в функции, они будут размещаться в локальном пространстве имен и не заменят никаких глобальных переменных. Вот как реализовать наш код, чтобы глобальные переменные не заменялись:

#!/usr/bin/env bash

myvar="привет"

myfunc() {
    local x
    local myvar="один два три"
    for x in $myvar
    do
        echo $x
    done
}

myfunc

echo $myvar $x

Эта функция выведет "привет" -- глобальная "$myvar" не заменяется, а "$x" не существует после выхода из myfunc. В первой строке функции мы создали x, локальную переменную, которую в дальнейшем используем, а во втором примере (local myvar="one two three"") мы создали локальную myvar и присвоили ей значение. Первая форма удобна, чтобы сделать переменные цикла локальными, так как сказать "for local x in $myvar" нельзя. Эта функция не заменяет никаких глобальных переменных, и вы призываетесь конструировать все ваши функции таким способом. Единственный случай, когда не следует использовать "local", это когда вы явно хотите модифицировать глобальную переменную.

В заключение

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

Ресурсы

Полезные ссылки


Daniel Robbins

silly photo
This is not really me

Daniel Robbins is the founder of the Gentoo community and creator of the Gentoo Linux operating system. Daniel resides in New Mexico with his wife Mary and two energetic daughters, and is founder and lead of Funtoo. Daniel has also written many technical articles for IBM developerWorks, Intel Developer Services and C/C++ Users Journal.