2: Введение в программирование CGI-скриптов и программирование скриптов на bash

Bash CGI

2: Введение в программирование CGI-скриптов и программирование скриптов на bash

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

К наиболее популярным средствам разработки таких скриптов относятся:

  • shell (командный язык);
  • Perl;
  • С.

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

Командные языки являются тем первым инструментом программирования, который попадает в руки любого пользователя. В Windows это cmd (речь идет об Windows NT или Windows 95), в Unix — различного рода shell. cmd оставим для учебных курсов Microsoft и сосредоточимся на командных языках Unix.

Среди различных командных языков оболочек (shell) выберем тот, который является общим для большинства Unix-платформ — GNU bash (Bourne Again Shell). Прообраз bash — самый первый shell (sh), поэтому bash наследует многие его свойства.

Для программирования CGI-скриптов bash удобен тем, что наглядно демонстрирует многие свойства окружения среды Unix, которые используются и в других системах программирования. Кроме того, часто программирование на командном языке применяется для сравнительных описаний программ разработанных на С или Perl.

Структура bash-скрипта

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

#!/usr/local/bin/bash
echo Hello BASH 

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

В общем случае символ "#" рассматривается как начало комментария, который распространяется до конца строки. При программировании скриптов его чаще всего приходится употреблять для маскирования строк программы во время отладки.

Более bash-скрипт ничем не выделяется. Команды bash обычно вводятся каждая на отдельной строке. Если это по каким-то причинам затруднительно, то команды разделяются символом ";". Исключение составляют конвейеры: в них команды находятся в пределах одной строки и разделены символом "|".

При программировании на bash нужно четко различать команды, встроенные в bash, и команды операционной системы. Например, echo — это команда операционной системы, а let — встроенная команда bash.

Стандартный поток вывода

Собственно, сам командный язык bash не имеет механизма организации вывода данных. Среди встроенных в bash команд нет команды печати. Но зато можно воспользоваться командами Unix. Самой простой из них является команда echo, которая копирует свои аргументы в поток стандартного вывода. При этом объединять разные слова во фразу каким-либо образом не нужно:

bash>echo Perl meets CGI
Perl meets CGI
bash> 

В данном случае echo вывела три своих аргумента и символ перевода строки — приглашение (prompt) bash находится на новой строке.

На первый взгляд, такое простое решение для стандартного вывода кажется примитивным. На самом деле, его вполне достаточно для генерации HTML-страниц. Механизмы, которые делают echo в совокупности с bash эффективным средством генерации отчетов в HTML-формате, таковы:

  • подстановка переменных (substitution);
  • маскирующие кавычки (quoting);
  • подстановка результатов выполнения команд.

В совокупности они представляют собой мощный инструмент.

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

echo first_arg#$1 second_arg#$2

В данном случае распечатываются первый и второй аргументы командной строки скрипта. Другой пример — распечатка переменной окружения:

echo QUERY_STRING:$QUERY_STRING

Quoting используется для маскирования специальных значений некоторых символов. Такие символы называют метасимволами. Например: ">" и "<" — это символы перенаправления потоков ввода-вывода и, следовательно, их надо маскировать при выводе. Для такого маскирования проще всего использовать простые одинарные кавычки:

echo '<H1>QUOTING</H1>' 

В данном случае мы напечатаем заголовок первого уровня в HTML-документе. При маскировании следует помнить, что внутри кавычек bash не выполняет интерпретации кода скрипта, поэтому переменные внутрь одинарных кавычек вставлять нельзя:

echo '<H1>'$QUERY_STRING'</H1>' 

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

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

echo '<H1>'`date`'</H1>'

или

echo '<H1>'$(date)'</H1>' 

Таким образом можно вставлять не только отдельные команды, но и целые последовательности команд. Главное, чтобы эта последовательность что-нибудь возвращала.

Переменные окружения

Переменные окружения (оболочки) создаются в момент старта bash-скрипта. При этом существует два типа переменных — те, которые действуют только в данной оболочке, и те, которые наследуются извне. Для просмотра переменных окружения можно использовать команду set:

bash-2.01$ set
bash=/bin/bash
bash_versinfo=([0]="2" [1]="01" [2]="0" 
  [3]="1" [4]="release" 
  [5]="i386-pc-freebsd2.2.2")
bash_version='2.01.0(1)-release'
columns=106
dirstack=()
euid=1010
... 

Здесь не приводится полный список всех переменных окружения. Показано только, как этот список отображается. Каждая переменная передается парой "имя=значение". При этом каждая такая пара записывается с новой строки. Попробуем распечатать все переменные окружения скрипта в виде HTML-таблицы, используя bash:

#!/usr/freeware/bin/bash
echo Content-type: text/html
echo
echo '<HTML><HEAD></HEAD><BODY>'
echo '<H1>переменные окружения</H1>'
echo '<TABLE BORDER=1>'
echo '<TR><TD>Имя</TD><TD>значение</TD></TR>'
IFS='='
set | while read x y
do
echo '<TR><TD>'$x'</TD><TD>'$y'</TD></TR>'
done
echo '</TABLE>'
echo '<HR>'
echo '</BODY></HTML>' 

Первой командой echo формируется предложение HTTP-заголовка. Вторая команда echo обеспечивает пропуск строки между заголовком HTTP-сообщения и его телом. Затем начинает формироваться тело HTML-документа. Обратите внимание на прямые одинарные кавычки "'". Они применяются для того, чтобы защитить от интерпретации угловые скобки "<" и ">", которые используются в bash для перенаправления стандартных потоков ввода/вывода.

Далее присваивается значение переменной окружения bash, которая не генерируется сервером HTTP — IFS. Переменная IFS хранит список символов-разделителей слов. По умолчанию это пробел и табуляция. Но нам нужно разделить имя переменной и его значения, которые на самом деле разделены символом "=".

Теперь вызываем команду set. При этом ее стандартный поток вывода перенаправляем при помощи "|" команде read, которая считывает строку из стандартного ввода, при этом присваивая переменным x и y значения последовательно от начала строки выделенных слов. А слова мы разделяем символом "=".

Читаем стандартный ввод в цикле while условие do.... done. В качестве условия все та же команда read — если считываем данные, то "истина", если нет, то — "ложь". При этом внутри цикла выводим строки таблицы "имя — значение".

B конце скрипта приводим документ к стандартному виду HTML-документа.

Обратиться к значению переменной окружения можно, конечно, гораздо проще — по имени:

#!/usr/freeware/bin/bash
echo Content-type: text/html
echo
echo '<HTML><HEAD></HEAD><BODY>'
echo '<H1>QUERY_STRING</H1>'
echo QUERY_STRING = $QUERY_STRING
echo '<HR>'
echo '</BODY></HTML>' 

Здесь по команде echo будет просто распечатано значение переменной окружения   QUERY_STRING.

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

Позиционные параметры или аргументы командной строки — это последовательность строковых констант, которые указываются в командной строке после имени скрипта. Любая встроенная в bash команда или команда Unix может запускаться с набором этих параметров. Например, для того, чтобы подсчитать число активных в данный момент процессов httpd, администратор систем выдает такую последовательность команд:

bash>ps -ax | grep httpd | wc -l 

Здесь указано три команды, организованные в конвейер. Каждая из них имеет по одному аргументу командной строки:

  • ps задана с аргументом -ax;
  • grep задана с аргументом httpd;
  • wc задана с аргументом -l.

Позиционные параметры (аргументы командной строки) задаются встроенными переменными $1$n, где n — число аргументов. Аргументы командной строки появляются при запросах типа ISINDEX. Число аргументов командной строки определяется встроенной переменной bash$#. Если мы вызовем скрипт по ссылке типа:

http://www.intuit.ru/cgi-bin/
  argv.cgi?arg1+arg2+arg3, 

то переменная $# примет значение 3, а переменные: $1arg1, $2 arg2, $3arg3. Кстати, $0 — это имя самого скрипта. Распечатка параметров в виде HTML-таблицы может выглядеть следующим образом:

#!/usr/freeware/bin/bash
echo Content-type: text/html
echo
echo '<HTML><HEAD></HEAD><BODY>'
echo '<H1>Аргументы</H1>'
echo '<TABLE BORDER=1>'
echo '<TR><TH>Номер</TH><TH>Значение</TH></TR>'
let i=0
for x in $@
do
let i=i+1
echo '<TR><TD>arg['$i']</TD><TD>'$x'</TD></TR>'
done
echo '</TABLE>'
echo '</BODY></HTML>' 

Последовательность команд echo формирует HTTP-сообщение. Команда let позволяет выполнять арифметические вычисления. Перед циклом for производим инициализацию переменной i. Цикл for "пробегает" по всем аргументам командной строки, которые объединены в переменной $@ и разделяются в ней пробелами. Фактически они представляют собой список слов, по которому и бежит переменная цикла x. Обратите внимание на отличие данного цикла от стандартного цикла for в С или Perl: в нем не используются арифметические операции, а идет работа со списком.

Внутри цикла при помощи команды let мы увеличиваем индекс аргумента командной строки (значение переменной i) и распечатываем этот индекс и значение переменной x в виде элементов HTML-таблицы.

Если аргументов мало и их местоположение известно, то к каждому из них можно просто обращаться по встроенному имени, например, первый аргумент — это $1.

Стандартный поток ввода

По большому счету, для чтения данных из стандартного потока ввода в рамках программирования CGI-скриптов bash непригоден. Дело в том, что в нем нет механизма посимвольного считывания данных. Bash-скрипт способен читать только строками и останавливает считывание лишь в случае появления в потоке символа конца файла. Как известно, HTTP-сервер такого символа в стандартный поток ввода скрипта при работе по методу POST не передает. Тем не менее чтение стандартного ввода в рамках программирования CGI-скриптов на bash применяется.

Примером тому может служить генерация гипертекстовых ссылок на файлы текущего каталога:

#!/usr/freeware/bin/bash
echo Content-type: text/html
echo
echo '<HTML><HEAD></HEAD><BODY>'
echo '<Ul>'
ls -a | while read x
do
if test -f $x; then
echo '<LI><A HREF=./'$x'>'$x'</A>';
fi
done
echo '</BODY></HTML>' 

В данном случае команда ls доставляет в скрипт имена файлов. Один файл — это отдельная строка. Эти имена обрамляются гипертекстовыми ссылками и вставляются в HTML-страницу. При этом печатаются только обычные файлы, все остальные игнорируются.

Другой пример — фильтрация. При приеме по методу GET запрос размещается в переменной QUERY_STRING. Но он там находится в форме form-urlencoded. Для его фильтрации вызывается внешняя программа, стандартный вывод которой перенаправляется на стандартный ввод одной из команд скрипта:

echo $QUERY_STRING | tr '+' ' ' | while read x
do
for y in $x
do
echo $y
done
done 

Существуют и другие способы применения чтения из стандартного ввода при программировании CGI-скриптов на BASH.

Типы данных и переменные

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

Существует два типа переменных: встроенные переменные bash и переменные, определяемые пользователем (переменные пользователя). Не перечисляя всех встроенных переменных, назовем наиболее употребительные:

$1-$n — аргументы командной строки скрипта;
$0 — имя скрипта;
$@ — список аргументов командной строки;
$# — число аргументов командной строки;
$IFS — список разделителей;
$PATH — путь поиска команд. 

Переменные окружения, которые генерируются сервером — это переменные пользователя, импортируемые скриптом при его запуске. Пользователь внутри скрипта может установить собственные переменные:

IFS="=" 

В данном случае мы отменили значение по умолчанию для списка разделителей и назначили в качестве разделителя знак равенства "=". IFS — это глобальная переменная, поэтому она передается от скрипта к скрипту по умолчанию. Если требуется назначить собственную переменную и передать ее в другой скрипт, который вызывается из текущего скрипта, ее нужно будет экспортировать:

bash>QUERY_STRING=arg1+arg2+arg3; 
export QUERY_STRING 

В данном случае в целях отладки скрипта в командной строке bash определена переменная окружения   QUERY_STRING. Если запустить скрипт без предварительного экспорта, то значение этой переменной ($QUERY_STRING) будет неопределенным. Команда export позволяет передать это значение в тестируемый скрипт.

Управление потоком вычислений

Изо всех возможностей управления порядком выполнения команд в bash-скрипте мы рассмотрим только if, while и for. Пользуясь этими встроенными возможностями bash, следует иметь в виду, что логические выражения, которые применяются в качестве условий данных команд, строятся вокруг строк, а не чисел. Использовать числовое условие в bash крайне затруднительно.

if

Команда if имеет вид:

if list; then list; [elif list; then list;]
   ...[ else list;] fi 

Сначала выполняется список команд, который стоит после if. Если он завершился успешно, то выполняется список команд после первого then. Значение и логика выполнения других частей этой команды очевидна. Команда начинается символами "if" и должна закончиться символами "fi". Часть команды в квадратных скобках — это необязательные конструкции, которые при необходимости можно опустить.

Рассмотрим в качестве примера проверку метода доступа к скрипту. Для bash это может быть только GET:

#!/usr/freeware/bin/bash
echo Content-type: text/plain
echo
if test $REQUEST_METHOD = "POST"; then
echo POST;
elif test $REQUEST_METHOD = "GET"; then
echo GET;
else echo Unknown method $REQUEST_METHOD;
fi 

В данном случае мы используем сравнение строк (символ "="). Если нужно сравнивать арифметические выражения, то следует использовать другие операции сравнения:

-eq — равенство операндов;
-ne — неравенство операндов;
-lt — первый операнд меньше второго;
-le — первый операнд меньше либо равен второму;
-gt — первый операнд больше второго;
-ge — первый операнд больше либо равен второму. 

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

if test -r file.txt; 
  then echo file.txt is readable; fi 

Помимо проверки наличия файла и прав можно определять тип файла (-d — каталог, -f — обычный файл и т.п.).

while

Команда while позволяет выполнять список команд до тех пор, пока справедливо условие использования данного списка, которое задается аргументом while. Чаще всего в наших примерах эта команда применяется при фильтрации входного потока:

ps -axj | grep httpd | while read id pid
do
if test $id = "root"; then kill -1 $pid; fi
done 

В данном случае в системе FreeBSD просматривается список активных процессов с именем httpd (HTTP-сервера), отыскивается процесс-родитель и перезапускается.

for

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

for var; in list; do list; done 

Переменная var принимает значения из списка, указанного за in, до тех пор, пока этот список не кончится. При этом для каждого значения var выполняется список команд, заключенный между "do" и "done". Примером использования for может служить разбор входных строк:

ls -ax | while read x
do
for y in $x
do
echo $y
done
done 

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