Написание bash-сценариев

Командный арсенал оболочки bash позволяет успешно писать простые сценарии (без этого средства автоматизации системным администраторам пришлось бы вручную вводить команды в командную строку). Ваше мастерство в использовании командных строк вы должны воплотить в искусстве создания bash-сценариев (и наоборот), что поможет вам извлечь максимальную пользу из времени, потраченного на изучение обо­лочки bash. Но когда окажется, что ваш bash-сценарий превысил в объеме сотню строк или вам потребовались средства, которыми bash не обладает, это будет означать, что настало время переходить к языку Perl или Python.

В среде bash комментарии начинаются со знака # и продолжаются до конца строки. Как и в командной строке, вы можете разбить одну логическую строку на несколько физических строк, обозначив переход на новую строку символом обратной косой черты (\). И, наоборот, можно поместить на одной строке несколько операторов, разделив их точкой с запятой.

Сценарий для оболочки bash может состоять только из последовательности командных строк. Например, следующий сценарий helloworld просто выполняет команду echo.

#! /bin/bash

echo "Hello, world!"

Первая строка содержит "банальный" описательный оператор, который сообщает, что данный текстовый файл является сценарием, предназначенным для интерпретации командной оболочкой /bin/bash. При принятии решения о том, как выполнить этот файл, ядро отыщет соответствующий синтаксис. С точки зрения оболочки, "стремящейся" выполнить этот сценарий, первая строка представляет собой просто комментарий. Если бы оболочка находилась в другом каталоге, вам пришлось бы отредактировать эту строку.

Для того чтобы подготовить этот файл к выполнению, достаточно установить его бит, "отвечающий" за выполнение.

$ chmod +х helloworld

$  ./helloworld3

Hello,  world!

Если ваша оболочка понимает команду helloworld без префикса. /, это означает, что в ва­шем пути поиска указан текущий каталог (.). И это плохо, поскольку дает другим пользователям возможность устраивать для вас "ловушки" в надежде, что вы будете пытаться выполнить опреде­ленные команды при переходе к каталогу, в котором они имеют доступ для записи. Можно также непосредственно запустить (активизировать) оболочку в качестве ин­терпретатора сценария.

$ bash helloworld

Hello, world!

$ source helloworld

Hello, world!

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

Если вы пришли сюда из мира Windows, то вам привычно использовать понятие рас­ширения файла, по которому можно судить о типе файла, а также о том, можно ли его выполнить. В мире UNIX и Linux признак того, может ли файл быть выполнен (и если да, то кем), содержится в специальных битах полномочий. При желании вы можете на­делить свои bash-сценарии суффиксом. sh, который бы напоминал вам об их типе, но тогда при выполнении соответствующей команды вам придется вводить суффикс. sh, поскольку UNIX не интерпретирует расширения специальным образом.

 

От команд к сценариям

 

Прежде чем переходить к особенностям написания bash-сценариев, остановимся на методике этого процесса. Многие пишут bash-сценарии так же, как Perl- или Python-сценарии, т.е. используя какой-нибудь текстовый редактор. Но все же удобнее рассма­тривать в качестве интерактивной среды разработки сценариев режим с приглашением на ввод команды.

Предположим, например, что у вас есть журналы регистрации, именованные с суф­фиксами .log и .LOG и разбросанные по всей иерархической системе каталогов. Допустим также, что вы хотите изменить эти суффиксы, приведя их к "прописному" виду. Прежде всего, попытаемся отыскать все эти файлы.

$   find. -name *log

.do-not-touch/important.log

admin.com-log/

foo.log

genius/spew.log

leather_flog

Так, похоже на то, что нам надо включить в шаблон точку и игнорировать при поис­ке каталоги. Нажмите комбинацию клавиш <Ctrl+P>, чтобы вернуть команду в команд­ную строку, а затем модифицируйте ее.

$   find. -type f -name '*.log

do-not-touch/important.log

foo.log

Синонимом для команды source служит команда "с точкой" (dot-команда).

Ну вот, это уже выглядит лучше. Правда, каталог .do-not-touch (т.е. "не трогать") вызывает смутное чувство опасности; но мы можем избавиться от этого неприятного холодка.

$   find. -type f -name '*.log '   | grep -v .do-not-touch

foo.log

genius/spew.log

Теперь все в порядке: мы получили абсолютно точный список файлов, которые долж­ны быть переименованы. Попробуем сгенерировать несколько новых имен.

$   find .  -type f -name '*.log '   | grep -v .do-not-touch | while read fname

do

echo mv $ fname ${ fname/. log/. LOG/}

done

mv foo.log foo.LOG

mv genius/spew.log genius/spew.LOG

Да, это именно те команды, которые позволят переименовать нужные файлы. А как мы это делаем в реальности? Мы могли бы снова вызвать уже выполненную команду и отредактировать команду echo, чтобы заставить оболочку bash выполнять команды mv, а не просто выводить их. Ведь передача команд в отдельную копию оболочки bash — более надежный вариант работы, который к тому же требует меньшего объема редакти­рования.

Нажав комбинацию клавиш <Ctrl+P>, мы обнаруживаем, что оболочка bash забот­ливо свернула наш мини-сценарий в одну-единственную строку. К этой "уплотненной" командной строке мы просто добавляем канал, передающий наши выходные данные ко­манде bash -х.

$   find .  -type f -name '*.log *   | grep -v   .do-not-touch | while read fname; do echo mv $ fname ${f name/. log/. LOG/}; done | bash -x

+ mv foo.log foo.LOG

+ mv genius/spew.log genius/spew.LOG

Ключ -x команды bash обеспечивает вывод каждой команды перед ее выполнением.

Теперь мы завершили переименование файлов, но нам хотелось бы сохранить этот сценарий, чтобы можно было использовать его снова. Встроенная в bash команда по по своему действию во многом аналогична нажатию комбинации клавиш <Ctrl+P>, но вместо возврата последней команды в командную строку она передает команду в задан­ный вами редактор. Добавьте в свой файл строку идентификационного комментария, поместите сохраненный файл в приемлемый для вас каталог (например, ~/bin или / usr/local/bin), сделайте файл исполняемым, и вы получите настоящий сценарий.

 

Итак, подытожив

  • Разрабатывайте сценарий (или его часть) в виде конвейера команд, причем пошагово и в режиме выполнения командных строк.
  • Пересылайте результат в стандартный выходной поток, проверяя правильность работы используемых вами команд.
  • На каждом этапе используйте буфер ранее выполненных команд для их появления
    в командной строке и инструменты редактирования — для их модификации.
  • Пока вы получаете неправильные результаты, можно считать, что вы, по сути, ничего не сделали, и до тех пор, пока команды не заработают так, как надо, ничего
    (из уже сделанного) не надо удалять.
  • Если результат выглядит правильным, выполните команды на реальном примере,
    чтобы убедиться, что все получилось так, как ожидалось.
  • Используйте команду fс, чтобы зафиксировать свою работу в редакторе, оформите ее соответствующим образом и сохраните.

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

 

Организация ввода и вывода данных

 

Команда echo не отличается интеллектуальностью, но зато проста в применении. Для получения большего контроля над выводом данных используйте команду printf. Она не очень удобна, поскольку предполагает, что вы должны в явном виде указывать в нужных для вас местах символы перехода на новую строку (\n), но позволяет использовать символ табуляции и другие средства форматирования результата. Сравните резуль­таты выполнения следующих двух команд.

$ echo "\taa\tbb\tcc\n"

\taa\tbb\tcc\n

$ printf "\taa\tbb\tcc\n"

аа     bb     сс

В некоторых системах работа команд echo и printf поддерживается на уровне ОС (обычно соответствующие им исполняемые файлы хранятся в каталогах /bin и /изг/ bin соответственно). Хотя эти команды и встроенные в оболочку утилиты в целом по­добны, они могут иметь незначительные отличия, и особенно это касается команды printf. Вы можете либо придерживаться bash-синтаксиса, либо вызывайте "внеш­нюю" команду printf, указывая ее полный путь.

Для того чтобы сформировать для пользователя приглашение ввести данные, можно использовать команду read.

#!/bin/bash

echo -п "Введите свое имя: " read user_name

if t -n "$user_name" ]; then

echo "Привет $user_name!"

exit 0 else

echo "Вы не назвали свое имя!"

exit 1 fi

Ключ -n в команде echo подавляет привычный переход на новую строку, но здесь была бы кстати команда printf. Мы еще рассмотрим вкратце синтаксис оператора if, Но его действие здесь, с нашей точки зрения, очевидно. Ключ -n в операторе if обеспечит значение истины, если его строковый аргумент не окажется нулевым. Вот как выглядит результат выполнения этого сценария. $ sh readexample

Введите свое имя: Ron Привет Ron!

 

Функции и аргументы командной строки

 

Аргументы командной строки служат для сценария переменными с числовыми именами: $1 — первый аргумент командной строки, $2 — второй и т.д. Аргумент $0 содер­жит имя, по которому был вызван сценарий, например. /bin/example. sh, т.е. это не фиксированное значение.

Переменная $# содержит количество переданных аргументов командной строки, а переменная $ * — все эти аргументы. Ни одна из этих переменных не учитывает аргумент $0.

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

#!/bin/bash

function show_usage {

echo "Использование: $0 source_dir dest_dir"

exit 1 } # Основная программа начинается здесь

if [ $# -ne 2 ]; then

show_usage

else # Существуют два аргумента if [ -d $1 ]; then source_dir=$l else

echo 'Недопустимый каталог-источник1 show_usage fi if t -d $2 ]; then

dest_dir=$2 else

echo 'Недопустимый каталог-приемник' show_us^ge fi fi

printf "Каталог-источник: ${source_dir}\n" printf "Каталог-приемник: ${dest_dir}\n"

Для вывода сообщения о правильном использовании данного сценария мы создали отдельную функцию showusage. Если бы позже этот сценарий был модифицирован и стал бы принимать дополнительные аргументы, это сообщение нужно было бы изме­нить только в одном месте.

$ mkdir ааа bbb

$ sh showusage ааа bbb

Каталог-источник: ааа

Каталог-приемник: bbb

 $ sh showusage foo bar

Недопустимый каталог-источник Использование: showusage source_dir dest_dir

Аргументы bash-функций обрабатываются практически так же, как аргументы командной строки: $1 — первый аргумент командной строки, $2 — второй и т.д. Как вид­но из приведенного выше примера, аргумент $0 содержит имя сценария.

Для того чтобы сделать предыдущий пример более устойчивым к ошибкам, мы могли бы заставить функцию showusage принимать в качестве аргумента код ошибки. Это позволило бы конкретизировать код возврата для каждого типа отказа. Реализация этой идеи показана в следующем фрагменте программы.

function show_usage {

echo "Использование: $0 source_dir dest_dir" if t $# -eq 0 ]; then

exit 99 # Выход с любым ненулевым кодом возврата else

exit $1 fi }

В этой версии функции добавляется анализ наличия аргументов. Внутри функции переменная $# сообщает, сколько аргументов было ей передано. Сценарий завершится с кодом возврата 99, если при его вызове не было передано никаких аргументов. Но при передаче конкретного значения, например, такого, как

show_usage 5

сценарий завершится с этим кодом после вывода сообщения об использовании сценария. (Переменная $? содержит статус завершения последней выполненной команды, независимо от того, была ли она использована в сценарии или в командной строке.)

Между функциями и командами в оболочке bash существует строгая аналогия. Вы можете определить полезные функции в своем файле ~/.bash_profile, а затем использовать их в командной строке как команды. Например, если ваш узел стандартизи­ровал в сети порт 7988 для протокола SSH (в форме "безопасность через сокрытие"), вы можете определить в своем файле ~/ .bashjprof ile функцию ssh, чтобы быть уверен­ным в том, что она всегда будет запускаться (как команда) с ключом -р 7988.

function ssh {

/usr/bin/ssh -р 7988 $*

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

 

Область видимости переменных

Переменные в рамках сценария имеют глобальный статус, но функции могут соз­давать собственные локальные переменные с помощью объявления local. Рассмотрим следующий код.

#!/bin/bash

function localizer {

echo "==> В функции localizer начальное значение а равно '$а' "

local а

echo "==> После объявления local значение а стало равным '$а' "

a="localizer version"

echo "==> При выходе из функции localizer значение а равно '$а' "

a="test"

echo "До вызова функции localizer значение а равно '$а' "

localizer

echo "После вызова функции localizer значение а равно 1$а*"

По приведенным ниже результатам выполнения сценария scope test. sh видно, что локальная версия переменной $а внутри функции localizer "забивает" глобальную переменную $а. Глобальная переменная $а видна в функции localizer до тех пор, пока не встретится объявление local, т.е. по сути, объявление local работает как команда, которая в момент выполнения создает локальную переменную.

$   sh scopetest.sh

До вызова функции localizer значение а равно  'test'

==> В функции localizer начальное значение а равно  'test'

*=> После объявления local значение а стало равным *   1

==> При выходе из функции localizer значение а равно  'localizer version'

После вызова функции localizer значение а равно  'test'

 

Поток управления

 

Выше мы уже рассмотрели несколько конструкций if-then и if-then-, else (они работают вполне ожидаемым образом). Терминатором (признаком конца) для. оператора if служит оператор fi. Для образования цепочки if-операторов можно использовать ключевое слово elif, означающее "else  if".

 

if [ $base -eq 1 ] && [ $dm -eq 1 ]; then

installDMB^se elif [ $base -ne1 ] && [ $dm -eq 1 ]; then

installBase elif [ $base -eq 1 ] && [ $dm -ne 1 ]; then

installDM else

echo '==> Installing nothing' fi

 

Как и специальный [ ] -синтаксис для выполнения операций сравнения, так и "ключеподобные" имена операторов целочисленного сравнения (например -eq) уходят "наследственными корнями" в использование утилиты /bin/test из ранней командной оболочки Стивена Борна. В действительности квадратные скобки — это не что иное, как условное обозначение вызова утилиты test; они не являются частью оператора if.

Несмотря на всю полезность формы elif, зачастую лучше (с точки зрения ясности программного кода) использовать ease-структуру выбора варианта. Ниже показан ее синтаксис на примере функции, которая централизирует процесс регистрации сообщений для сценария. Конкретные варианты описываются закрывающими скобками после каждого условия и двумя точками с запятой, завершающими блок операторов, который Должен быть выполнен при реализации заданного условия. Оператор case завершается ключевым словом esac. Сейчас эти операции встроены в оболочку и уже не запускают на выполнение утилиту /bin/ test.

Уровень протоколирования устанавливается в глобальной

переменной LOG_LEVEL. Возможные варианты перечислены в порядке

от самого строгого до наименее строгого: Error, Warning, Info и Debug.

function logMsg { message_leve1=$1 message_itself=$2

if [ $message_level -le $LOG_LEVEL ]; then case $message_level in

message_level_text=,,Error" ;;

message_level_text=,lWarning" ;;

message_level_text=nInfo" ;;

message_level_text=,,Debugn ;;
*) message_level_text="OtherM

esac

echo "${message_level_text}: $message_itself" fi }

 

Эта функция иллюстрирует общепринятую парадигму "уровня регистрации" (log level), используемую многими приложениями административного характера. Код это­го сценария позволяет генерировать сообщения на различных уровнях детализации, но действительно регистрируются или отрабатываются только те из них, которые "проходят" глобально устанавливаемый порог $LOG_LEVEL. Чтобы прояснить важность каждо­го сообщения, его текст предваряется меткой, описывающей соответствующий уровень регистрации.

 

Циклы

Конструкция f or...in предназначена для упрощения выполнения некоторых действий для группы значений или файлов, особенно при универсализации файловых имен, т.е. замене реальных символов в имени и расширении универсальными (например "*" и "?") с целью формирования целых списков имен файлов. Шаблон *. sh в приведенном ниже цикле for позволяет обработать целый список совпадающих с ним (ша­блоном) имен файлов из текущего каталога. Оператор for, проходя по этому списку, по очереди присваивает имя каждого файла переменной $file.

#!/bin/bash

suffix=BACKUP—4date +%Y%m%d-%H%M'

for script in *.sh; do

newname:="$script.$suffix"

echo "Copying $script to $newname..."

cp $script $newname done

Результат выполнения этого сценария таков.

$ sh forexaxnple

Copying rhel.sh to rhel.sh.BACKUP—20091210-1708… Copying sles.sh to sles.sh.BACKUP—20091210-1708...

В раскрытии имени файла здесь нет ничего магического; все работает в точном соответствии с тем, как написано в командной строке. Другими словами, сначала имя файла раскрывается (т.е. шаблон заменяется существующим именем), а затем уж обрабатывает­ся интерпретатором в развернутом виде.7

С таким же успехом вы могли бы ввести имена файла статически, как показано в этой командной строке.

for script in rhel.sh sles.sh; do

В действительности любой список имен, содержащих пробельные символы (включая содержимое переменной), обрабатывается как объект циклической конструкции f or...in.

Оболочке bash также присуща близость к циклу for из традиционных языков программирования, в которых задается стартовое выражение, инкрементация и условие окончания цикла.

for  ( ( i=0 ;  i < $CPU_COUNT ;  i++ ) ); do

CPU_LIST="$CPU_LIST $i" done

На примере следующего сценария иллюстрируется bash-цикл while, который часто применяется для обработки аргументов командной строки и чтения строк файла.

#!/bin/bash

exec 0<$1 counter=l while read line; do

echo "$counter:  $line"

$((counter++)) done

Вот как выглядит результат выполнения этого сценария.

ubuntu$   sh whileexaxnple /etc/passwd

1:  root:x:0:0:Superuser:/root:/bin/bash

2: bin:x:l:l:bin:/bin:/bin/bash

3:  daemon:x:2:2:Daemon:/sbin:/bin/bash

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

Оператор read в конструкции while на самом деле является встроенным в оболочку, но действует подобно внешней команде. Внешние команды можно также помещать в конструкцию while. Цикл while в такой форме завершится тогда, когда внешняя ко­манда возвратит ненулевое значение кода завершения.

Выражение $ ((counter**)) выглядит несколько странно. Обозначение $ ((...)) говорит о вычислении выражения. Кроме того, оно делает необязательным использова­ние символа $ для обозначения имен переменных. Удвоенный знак "плюс" (++) — это оператор постинкремента, знакомый, например, по языку С. Он возвращает значение переменной, с которой он связан, но также имеет побочный эффект, который состоит в приращении значения этой переменной.

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

while read line; do

echo "$((counter++)): $line" done

 

Массивы и арифметика

 

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

Все bash-переменные представляют собой строковые значения, поэтому оболочка bash не делает различия в присваиваниях между числом 1 и символьной строкой "1". Различие лежит в использовании переменных. Следующий код иллюстрирует это различие.

#!/bin/bash

а=1 Ь=$((2))

с=$а+$Ь d=$(($a+$b))

echo "$а + $b = $с \t(знак плюс как строковый литерал)"

echo "$а + $b = $d \m (знак плюс как арифметическое сложение)"

 

При выполнении этого сценария получим такой результат.

 

1 + 2 = 1+2 (знак плюс как строковый литерал)

1+2=3 (знак плюс как арифметическое сложение)

Обратите внимание на то, что знак "плюс" в присваивании переменной $с не работает как оператор конкатенации для строк. Он остается всего лишь литеральным симво­лом, и посему эта строка эквивалентна следующей.

с="$а+$b"

Для того чтобы добиться вычисления, необходимо заключить выражение в двойные скобки: $((...)), как показано выше в присваивании переменной $d. Но даже эта мера предосторожности не позволяет получить в переменной $d числового значения; это значение по-прежнему хранится в виде строки "3".

В оболочке bash реализован обычный ассортимент операторов: арифметических, логических и отношений (подробнее см. соответствующие тап-страницы).

Массивы в командной оболочке bash могут показаться немного странными объек­тами (да они и используются не очень часто). Тем не менее при необходимости их мож­но применять. Литеральные массивы ограничиваются круглыми скобками, а отдельные элементы разделяются пробельными символами. Для включения литеральных пробелов в элемент можно использовать кавычки. example=(аа  'bb сс1  dd)

Для доступа к отдельным элементам массива используйте выражение $ {имя_ массива [индекс] }. Индексация начинается с нуля. Такие индексы, как "*" и "@", относятся к массиву в целом, а специальные конструкции ${%имя_массива[*]} и $ {%имя_массива [$ ] } возвращают количество элементов в массиве. Не спутайте эти выражения с конструкцией $ {%имя_массива} — и хотя эта форма кажется более логич­ной, но в действительности она содержит указатель на первый элемент массива (эквивалент для $ {#имя_массива [ 0]}).

Можно подумать, что выражение $example [ 1 ] должно служить однозначной ссыл­кой на второй элемент массива, но оболочка bash интерпретирует эту строку так: $example (обозначение ссылки на $example [ 0 ]) плюс литеральная строка [ 1 ]. Отсю­да вывод: ссылаясь на переменные массива, всегда используйте фигурные скобки ((Ш каких-либо исключений).

Рассмотрим сценарий, который иллюстрирует некоторые особенности bash-мac­сивов и подводные камни, на которые можно наткнуться при управлении ими.

#!/bin/bash

example^(аа 'bb сс' dd) example[3]=ее

echo "example[@] = ${example [@]}"

echo "Массив example содержит ${#example[@]} элементов"

for elt in "${example[@] }";  do

echo "   Элемент = $elt" done

Вот как выглядит результат выполнения этого сценария.

$ sh arrays

example[@] = аа bb сс dd ее Массив example содержит 4 элемента

Элемент = аа

Элемент = bb сс

Элемент = dd

Элемент = ее

Этот пример кажется простым, но лишь потому, что мы организовали его "хорошее поведение". Но если потерять бдительность, вы можете попасть в ловушку. Например, сценарий с заменой for-строки вариантом

for elt in ${example[@] }; do

(т.е. без кавычек, в которых заключено выражение массива) также работает, но вместо четырех элементов он выведет пять: аа, bb, сс, dd и ее.

Важно помнить, что все bash-переменные все равно остаются строками, поэтому работа с массивами — в некотором роде иллюзия. В тонкостях, связанных с тем, когда и как строки разбиваются на элементы, можно утонуть. Для того чтобы не рисковать, лучше используйте язык Perl или Python.

 

Комментарии (0)

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.