Углубленное руководство по программированию на языке Bash:

Полное руководство по написанию коммандных файлов для оболочки Bash

Mendel Cooper

Brindlesoft

thegrendel@theriver.com

Это пособие не требует предыдущего опыта в области написания коммандных файлов или программирования, но его курс быстро прогрессирует до уровней средней и высокой сложности (... в то же время предлагая крупицы мудрости и традиций UNIX).  Оно служит как учебник, руководство для самостоятельного изучения, а также как справочник и источник знаний о методиках написания коммандных файлов.  Упражнения и примеры программ с обильными комментариями рассчитаны на активное участие читателя, исходя из предпосылки что научиться писать коммандные файлы можно толко через написание коммандных файлов.

Dedication

   For Anita, the source of all the magic
 

Часть 1. Введение

Shell (коммандный процессор или оболочка) представляет собой переводчик комманд.  Это не только мембрана, разделяющая ядро операционной системы и пользователя, но еще и достаточно могущественный язык программирования.  Программы для оболочки, которые обычно называют коммандными файлами или скриптами, удобны для соединения воедино различных системных комманд, утилит, и бинарных файлов. Из коммандного файла можно запустить практически любую утилиту или комманду из арсенала UNIX.  В добавок к этому, такие внутренние комманды оболочки как операторы проверки условия и операторы цикла еще более увеличивают возможности скриптов.  Коммандные файлы очень хорошо подходят для решения задач связанных с системным администрированием и других часто повторяемых операций которые не требуют всех атрибутов полновесного структурного языка программирования.

Глава 1. Для чего нужно программирование в оболочке?

Практические навыки в работе с коммандными файлами важны каждому кто хочет достаточно хорошо разбираться в администрации системы, даже если вам самому никогда не потребуется написать коммандный файл.  Заметьте, что когда компьютер с Linux загружается, он выполняет коммандные файлы в /etc/rc.d для того чтобы восстановить конфигурацию системы и запустить служебные программы. Глубокое понимание таких пусковых коммандных файлов важно для анализа и внесения изменений в работу системы.

Научиться писать коммандные файлы для оболочки не сложно, так как их можно составлять из небольших частей, и для этого нужно знать лишь достаточно небольшое количество операторов и опций[1].  Синтаксис языка прост и во многом похож на порядок запуска и группирования утилит в коммандной строке, для того чтобы его освоить нужно знать не так много "правил".  Большинство коротких коммандных файлов работают правильно с первого раза, и отладить даже длинные коммандные файлы не так уж сложно.

коммандный файл можно использовать как созданный "на скорую руку" прототип сложной программы.  Получение работающего (хотя и медленно) образца основных функций будующей программы - это важный шаг в развитии проекта.  С помощью него структура будующей программы может быть опробована и протестирована, и большинство возможных проблем может быть обнаружено еще до начала написания программы в Си, Си++, Java, или Perl.

Использование коммандных файлов отлично сочетается с классической философией UNIX, философией разделения сложных задач на более мелкие, своединения в одну цепь разрозненных компонентов и утилит.  Многие признают это лучшим, или по крайней мере более эстетичным, подходом к решению многих проблем чем применение одного из могущественных всеохватывающих языков нового поколения, таких как Perl.  Эти языки призваны быть всеохватывающими и самодостаточными, но побочный эффект этого - необходимость изменить свой мыслительный процесс для того, чтобы использовать такой язык.

Случаи когда коммандные файлы не следует применять

 * операции, использующие большое количество системных ресурсов, особенно если если важна скорость (сортирование, составление ассоциативных массивов, итд.)
 * задачи, включающие в себя сложные математические операции, в особенности арифметические операции с плавающей точкой(запятой), вычисления с произвольной точностью, или сложные числа (используйте Си++ или FORTRAN)
 * если требуется кросс-платформная переносимость (нужна возможность перенести исходный текст программы на другие платформы), используйте Си
 * сложные программы для которых необходимо структурное программирование (требуется проверка типа данных для переменных, прототипы функций, итд.)
 * жизненно важное программное обеспечение от которого зависит будующее вашей организации
 * ситуации в которых важна защищенность программы, необходимо гарантировать целостность вашей системы и предотвратить несанкционированный доступ, взлом, и вандализм
 * проект состоит из взаимозависимых компонентов
 * программа должна будет выполнять много операций с файлами (Bash имеет только последовательный доступ к файлам, и производит это достаточно неудобным построчным способом)
 * требуются многомерные массивы
 * требуются структуры данных, например связные списки или деревья
 * программа должна будет генерировать или обращаться с графическими изображениями или с графическим интерфейсом пользователя
 * требуется прямой доступ к аппаратуре системы
 * необходимо использовать ввод/вывод через порты или сокеты
 * программа должна использовать библиотеки или сопрягаться с существующими старыми программами
 * программы в частном владении, со скрытым исходным текстом (исходный текст коммандного файла невозможно скрыть)

В одном из случаев, перечисленных выше, следует применить более могущественный скриптовой язык, например Perl, Tcl, Python, или такой компиляционный язык как Си, Си++, или Java.  Но даже тогда подготовка прототипа вашей программы с использованием коммандных файлов может быть полезным шагом в ее разработке.

Мы будем работать с оболочкой Bash. Ее полное название, "Bourne-Again Shell", является каламбуром на название оболочки Стивена Барни, считающейся классической - Bourne Shell.  Bash фактически стала стандартной оболочкой для написания коммандных файлов на всех разновидностях UNIX.  Большинство основных понятий которые будут обсуждены в этой книге относятся и к написанию коммандных файлов для других видов оболочек, в том числе Korn Shell, некоторые из особенностей которой унаследовала Bash[2], а также C Shell и ее вариантов.  (Следует сказать что программирование с оболочкой C Shell не рекомендуется из за некоторых проблем которые ей присущи, как отметчено в сообщении для новостной группы которое сделал Том Крисченсен (Tom Christiansen) в Октябре 1993 года.

Данный документ является учебным пособием по написанию коммандных файлов.  Для того, чтобы продемонстрировать свойства и особенности оболочки, в нем часто используются примеры написания коммандных файлов.  На сколько это было возможно, все коммандные файлы используемые как примеры были проверены, и некоторые из них даже могут оказаться полезны в реальной жизни.  Для того чтобы опробовать действие коммандного файла использованного в каком-либо примере, читателю следует использовать соответствующий файл в архиве коммандных файлов (название_коммандного_файла.sh), сделать этот файл выполняемым (chmod u+rx название_коммандного_файла.sh)[3] и выполнить его.  Если коммандного файла нет в архиве, скопируйте его текст прямо из этого документа.  Некоторые из примеров будут включать в себя еще не пройденный материал, по этому читателю возможно прийдется время от времени забегать вперед чтобы разобраться в действии этих коммандных файлов.

Образцы скриптов, автор которых не указан, написаны автором этой книги.

Примечания

 [1]
Это так называемые встроенные операторы, внутренние функции оболочки.

 [2]
В Bash было включено многое из ksh88 и даже кое-что из новой ksh93.

 [3]
Обычно коммандным файлам, соответствующим стандартам оболочки Bourne, дают расширение .sh.  Это не относится, однако, к системным коммандным файлам вроде тех, что находятся в каталоге /etc/rc.d.
 

Глава 2. Все начинается с шэбэнга.

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

   Пример 2-1. чистка: Коммандный файл, очищающий лог (журнальные) файлы в /var/log
# чистка
# Конечно, выполнять этот коммандный файл следует зарегистрировавшись как администратор

cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "Лог файлы очищены."

Ничего особенного, просто набор комманд, которые также легко можно было выполнить одну за другой с коммандной строки на консоли или в xterm.  Преимущества размещения команд в коммандном файле не только в том что нам не нужно будет перепечатывать их снова и снова.  Коммандный файл легко можно изменить, обобщить, или приспособить для какой-либо специфической области применения.

   Пример 2-2. чистка: Улучшенная и обобщенная версия коммандного файла представленного выше
#!/bin/bash
# чистка, версия 2
# Выполняйте зарегистрировавшись как администратор.

LOG_DIR=/var/log
ROOT_UID=0     # Только пользователи $UID которых равен 0 имеют права администратора (root).
LINES=50       # Сколько строк будет сохранено по умолчанию.
E_XCD=66       # Не удалось переменить каталог?
E_NOTROOT=67   # Код завершения, возвращаемый при выполнении не-администратором.
 

if [ "$UID" -ne "$ROOT_UID" ]
then
  echo "Только администратор может выполнять этот коммандный файл."
  exit $E_NOTROOT
fi

if [ -n "$1" ]
# Проверяем, дан-ли был параметр с коммандной строки (т.е. параметр не пустой).
then
  lines=$1
else
  lines=$LINES # Количество строк по умолчанию, если не указано на коммандной строке.
fi

#Стефане Чазелас предлагает улучшенный
#способ проверки параметров с коммандной
#строки (представленный ниже), но это слишком
#углубленный материал для того, чтобы это
#использовать в начале пособия.
#
#    E_WRONGARGS=65  # Не цифровой параметр (неправильная форма параметра)
#
#    case "$1" in
#    ""      ) lines=50;;
#    *[!0-9]*) echo "Выполнение: `basename $0` очистить-этот-файл"; exit $E_WRONGARGS;;
#    *       ) lines=$1;;
#    esac
#
#* Чтобы разобраться в этом, загляните в секцию "Операторы цикла".

cd $LOG_DIR

if [ `pwd` != "$LOG_DIR" ]  # или   if [ "$PWD" != "LOG_DIR" ]
                            # Находимся не в /var/log?
then
  echo "Не удалось перейти в $LOG_DIR."
  exit $E_XCD
fi  # Проверяем что находимся в правильном каталоге перед тем как начать "портачить" лог файл.

# еще лучше это можно сделать так:
# ---
# cd /var/log || {
#   echo "Не удалось перейти в нужный каталог." >&2
#   exit $E_XCD;
# }
 

tail -$lines messages > mesg.temp # Сохраняем последнюю часть лог файла сообщений.
mv mesg.temp messages             # Становится новым лог файлом.
 

# cat /dev/null > messages
#* Это больше не нужно, так как способ, указанный выше, более безопасен.

cat /dev/null > wtmp  # > wtemp   приводит к тому-же результату.
echo "Лог файлы очищены."

exit 0
#  Код завершения, равный нолю, оповещает оболочку
#  об успешном завершении коммандного файла.

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

Шэбэнг (#!) в начале коммандного файла говорит вашей системе что этот файл представляет собой набор комманд которые нужно переправить в указанный коммандный процессор. Вообще-то #! это "волшебное число" размером в два байта[1] который используется как специальная отметка по которой можно узнать тип файла, и в данном случае это выполняемый коммандный файл (чтобы узнать об этом больше, загляните в справочных страницы о "волшебных числах" (man magic)).  Сразу после шэбэнга идет имя файла.  Это имя программы которая будет выполнять комманды в этом коммандном файле, будет ли это оболочка, программный язык, или утилита.  Этот коммандный процессор затем выполняет каждую из комманд в этом файле, начиная с самого верха (с первой строки) игнорируя комментарии[2].

#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/usr/awk -f

Заглавия для коммандных файлов, приведенные выше, вызывают различные коммандные процессоры, например оболочку /bin/sh, используемую по умолчанию (bash на системах с Linux) или что-то другое[3].  Указывая #!/bin/sh, т.е. оболочку Bourne Shell, которая используется по умолчанию на большинстве коммерческих версий UNIX, делает коммандный файл переносимым на компьютеры на которых работает не Linux, хотя вам возможно прийдется пожертвовать некоторыми элементами которые работают только в Bash (тогда коммандный файл будет соответствовать стандарту POSIX[4] для оболочки sh).  Заметьте что полное имя файла, указанное после "шэбэнга" должно быть правильным, иначе сообщение об ошибке, чаще всего "Комманда не найдена" ("Command not found") будет единственным результатом выполнения вашего коммандного файла.

#! можно не употреблять если коммандный файл содержит только обычные системные комманды и не использует внутренние директивы оболочки.  Пример 2 приведенный выше, требует использования #! так как строка на которой переменной присваивается значение, lines=50, использует оператор доступный только оболочке.  Заметьте, что #!/bin/sh вызывает оболочку, используемую по умолчанию, чем на компьютере с Linux является /bin/bash.

Важное замечание

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

if [ $# -ne ожидаем_колич_парам ]
then
  echo "Выполнение: `basename $0` ваши параметры"
  exit $WRONG_ARGS
fi

Примечания

[1]

Некоторые разновидности UNIX используют "волшебное число" из четырех байтов, требуя пробел после !, #! /bin/sh.

[2]

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

[3]

Это открывает дорогу для некоторых хитростей.

#!/bin/rm
# Самоуничтожающийся коммандный файл.

# Когда вы выполняете этот файл, ничего особенного не происходит... за исключением того, что сам файл исчезает.

WHATEVER=65

echo "Спорим что эта строка не будет выдана на экран?"

exit $WHATEVER  # Не имеет значения.  Выполнение все равно не завершится здесь.

Еще попробуйте проставить #!/bin/more на первую строку информационного файла (README) и сделать его выполняемым. Вы получите информационный файл который может автоматически выводить себя на экран.
 

[4]

Portable Operating System Interface (Переносимый Интерфейс Операционной Системы), попытка стандартизировать UNIXоподобные операционные системы.

2.1. Запуск коммандных файлов.

Когда коммандный файл готов, вы можете выполнить его напечатав sh название_вашего_коммандного_файла [1] или bash название_вашего_коммандного_файла.  (Не рекомендуется использовать sh <название_вашего_коммандного_файла, так как в этом случае коммандный файл не может получать информацию со стандартного ввода(stdin).)  Но гораздо удобнее сделать сам коммандный файл выполняемым с помощью chmod.

Используйте одну из комманд:
 
 chmod 555 название_вашего_коммандного_файла (дает всем право просматривать и выполнять файл) [2]
 
 chmod +rx название_вашего_коммандного_файла (дает всем право просматривать и выполнять файл)

 chmod u+rx название_вашего_коммандного_файла (дает только пользователю которому принадлежит файл право просматривать и выполнять его)

Сделав коммандный файл выполняемым, вы можете опробовать его введя ./название_вашего_коммандного_файла[3].  Если он начинается со строки с "шэбэнгом" то его запуск вызывает коммандный процессор который сможет его правильно выполнить.

И наконец после тестирования и отладки вы возможно захотите поместить ваш коммандный файл в /usr/local/bin (конечно, зарегистрировавшись как администратор) чтобы дать себе и другим пользователям возможность использовать его как всесистемную программу.  Тогда его можно будет запускать просто напечатав название_вашего_коммандного_файла[ВВОД] на коммандной строке.

Примечания

 [1]
Осторожно: запуск коммандного файла написанного для Bash через sh название_вашего_коммандного_файла отключает операторы и элементы которые присутствуют только в Bash, по этому коммандный файл может быть не выполнен.

 [2]
Кф требует право просмотра (read) как и выполнения для того чтобы его запустить, так как оболочка должна иметь возможность прочитать его.

 [3]
Почему нельзя запустить коммандный файл указав только его название?  Если коммандный файл находится в вашем текущем каталоге ($PWD), почему это не получится?  Потому что из соображений безопасности название текущего каталога не присутствует в переменной $PATH пользователя.  По этому необходимо вызывать коммандный файл в текущем каталоге используя его полное имя - ./название_коммандного_файла.

2.2. Начальные Упражнения

    1. Системные администраторы часто создают коммандные файлы для автоматизации выполнения повседневных задач.  Приведите примеры задач для выполнения которых не помешало бы написать коммандные файлы.
    2. Напишите коммандный файл который при выполнении показывает время и дату, перечисляет пользователей, находящихся в системе, и показдвает время работы системы с момента последнего старта.  Под конец он должен сохранить всю эту информацию в журнальный файл. (Используйте перенаправление ввода/вывода и комманды date, who, и uptime.) 1