Программные структуры

Как во всяком языке программирования в тексте на языке shell могут быть комментарии. Для этого используется символ "#". Все, что находится в строке (в командном файле) левее этого символа, воспринимается интерпретатором как комментарий. Например,

# Это комментарий.
## И это.
### И это тоже.

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

Команда TEST ("[ ]")

Команда test проверяет выполнение некоторого условия. С использованием этой (встроенной) команды формируются операторы выбора и цикла языка shell.

Два возможных формата команды:

test условие

или

[ условие ]

мы будем пользоваться вторым вариантом, т.е. вместо того, чтобы писать перед условием слово "test", будем заключать условие в скобки, что более привычно для программистов.

На самом деле shell будет распознавать эту команду по открывающей скобке "[", как слову(!), соответствующему команде "test". Уже этого достаточно, чтобы предупредить о распространенной ошибке начинающих: Между скобками и содержащимся в них условием обязательно должны быть пробелы.

Пробелы должны быть и между значениями и символом сравнения или операции (как, кстати, и в команде "expr"). Не путать с противоположным требованием для присваивания значений переменным.

В shell используются условия различных "типов".

Условия проверки файлов:

-f fileфайл "file" является обычным файлом;
-d fileфайл "file" - каталог;
-с fileфайл "file" - специальный файл;
-r fileимеется разрешение на чтение файла "file";
-w fileимеется разрешение на запись в файл "file";
-s fileфайл "file" не пустой.

Примеры. Вводя с клавиатуры командные строки в первом случае получим подтверждение (код завершения "0"), а во втором - опровержение (код завершения "1"). "specific" - имя существующего файла.

[ -f specific ] ; echo $? 0
[ -d specific ] ; echo $? 1

Условия проверки строк:

str1 = str2строки "str1" и "str2" совпадают;
str1 != str2строки "str1" и "str2" не совпадают;
-n str1строка "str1" существует (непустая);
-z str1строка "str1" не существует (пустая).

Примеры.

x="who is who"; export x; [ "who is who" = "$x" ]; echo $? 0
x=abc ; export x ; [ abc = "$x" ] ; echo $? 0
x=abc ; export x ; [ -n "$x" ] ; echo $? 0
x="" ; export x ; [ -n "$x" ] ; echo $? 1

Замечание:
Команда "test" дает значение "истина" (т.е. код завершения "0") и просто если в скобках стоит непустое слово.

[ privet ] ; echo $? 0
[ ] ; echo $? 1

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

true ; echo $? 0
false ; echo $? 1

Условия сравнения целых чисел:

x -eq y"x" равно "y",
x -ne y"x" неравно "y",
x -gt y"x" больше "y",
x -ge y"x" больше или равно "y",
x -lt y"x" меньше "y",
x -le y"x" меньше или равно "y".

То есть, в данном случае, команда "test" воспринимает строки символов как целые (!) числа. Поэтому во всех остальных случаях "нулевому" значению соответствует пустая строка. В данном же случае, если надо обнулить переменную, скажем, "x", то это достигается присваиванием "x=0".

Примеры:

x=abc ; export x
[ abc -eq "$x" ] ; echo $?
# Ошибка: "[": integer expression expected before -eq

x=321 ; export x
[ 321 -eq "$x" ] ; echo $? 0

x=3.21 ; export x
[ 3.21 -eq "$x" ] ; echo $?
# Ошибка: "[": integer expression expected before -eq

x=321 ; export x
[ 123 -lt "$x" ] ; echo $? 0

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

!(not) инвертирует значение кода завершения.
-o(or) соответствует логическому "ИЛИ".
-a(and) соответствует логическому "И".

ПРЕДУПРЕЖДЕНИЕ. Не забывайте о пробелах.

Примеры:

[ ! privet ] ; echo $? 1

x=privet; export x
[ "$x" -a -f specific ] ; echo $? 0

x=""; export x
[ "$x" -a -f specific ] ; echo $? 1

x=""; export x
[ "$x" -a -f specific -o privet ] ; echo $? 0

x=""; export x
[ "$x" -a -f specific -o ! privet ] ; echo $? 1

Совет:
Не злоупотреблять сложными условиями.

Условный оператор "IF"

В общем случае оператор "if" имеет следующую структуру:

if условие
then
    список
elif условие
then
    список
else
    список
fi

"elif" — это сокращенный вариант от "else if", который можно использовать наряду с полным вариантом. Это позволяет создавать вложенные условные операторы. В каждом случае "список" должен быть осмысленным и допустимым в данном контексте.

Конструкции "elif условие then список" и "else список" не являются обязательными. В данном контексте квадратные скобки используются для указания на необязательность конструкций (и не имеют отношения к команде "test").

Самая усеченная структура этого оператора:

if условие
then
    список
fi

Если условие выполнено (обычно это означает, что команда вернула код завершения "0"), то выполняется "список", иначе он пропускается.

Обратите внимание, что структура обязательно завершается служебным словом "fi". Число "fi" всегда должно соответствовать числу "if".

Примеры:

Предположим, у вас есть скрипт с именем "if-1":

if [ $1 -gt $2 ]
then
    pwd
else
    echo $0 : Hello!
fi

Тогда вызов скрипта:

if-1 12 11

вернет:

/home/sae/STUDY/SHELL

А вызов:

if-1 12 13

вернет:

if-1 : Hello!

Возможно использовать в условии то свойство shell, что команды могут выдавать различный код завершения. Это напоминает приемы программирования на Си. Рассмотрим пример "if-2":

if a=`expr "$1" : "$2"`
then
    echo then a=$a code=$?
else
    echo else a=$a code=$?
fi

Таким образом, вызов

if-2 by by

даст:

then a=2 code=0

А вызов

if-2 by be

даст:

else a=0 code=1

Ещё пример на вложенность:

echo -n "А какую оценку получил на экзамене?: "
read z
if [ $z = 5 ]
then
    echo Молодец!
elif [ $z = 4 ]
then
    echo Все равно молодец!
elif [ $z = 3 ]
then
    echo Все равно!
elif [ $z = 2 ]
then
    echo Все!
else
    echo !
fi

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

Оператор вызова ("CASE")

Оператор выбора "case" имеет структуру:

case строка in
    шаблон) список команд;;
    шаблон) список команд;;
    ...
esac

Здесь "case", "in" и "esac" - служебные слова. "Строка" (это может быть и один символ) сравнивается с "шаблоном". Затем выполняется "список команд" выбранной строки. Непривычным будет служебное слово "esac", но оно необходимо для завершения структуры.

Пример:

echo -n "А какую оценку получил на экзамене?: "
read z
case $z in
    5) echo Молодец! ;;
    4) echo Все равно молодец! ;;
    3) echo Все равно! ;;
    2) echo Все! ;;
    *) echo ! ;;
esac

Непривычно выглядят в конце строк выбора ";;", но написать здесь ";" было бы ошибкой. Для каждой альтернативы может быть выполнено несколько команд. Если эти команды будут записаны в одну строку, то символ ";" будет использоваться как разделитель команд.

Обычно последняя строка выбора имеет шаблон "*", что в структуре "case" означает "любое значение". Эта строка выбирается, если не произошло совпадение значения переменной (здесь $z) ни с одним из ранее записанных шаблонов, ограниченных скобкой ")". Значения просматриваются в порядке записи.

### # case-2: Справочник.
# Для различных фирм по имени выдается
# название холдинга, в который она входит
case $1 in
    ONE|TWO|THREE) echo Холдинг: ZERO ;;
    MMM|WWW) echo Холдинг: Not-Net ;;
    Hi|Hello|Howdoing) echo Холдинг: Привет! ;;
    *) echo Нет такой фирмы ;;
esac

При вызове "case-2 Hello" на экран будет выведено:

Холдинг: Привет!

А при вызове "case-2 HELLO" на экран будет выведено:

Нет такой фирмы

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

### # case-3: Реализация меню с помощью команды "case"
echo "Назовите файл, а затем (через пробел) наберите цифру, соответствующую требуемой обработке:
1 - отсортировать
2 - выдать на экран
3 - определить число строк"
read x y # x - имя файла, y - что сделать
case $y in
    1) sort < $x ;;
    2) cat < $x ;;
    3) wc -l < $x ;;
    *) echo " Мы не знаем такой команды ! " ;;
esac

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

Напишем команду "case-4", которая добавляет информацию к файлу, указанного первым параметром (если параметр один), со стандартного входа, либо (если 2 параметра) из файла, указанного в качестве первого параметра:

###
# case-4: Добавление в файл.
# Использование стандартной переменной.
# "$#" - число параметров при вводе расчета
# ">>" - перенаправление с добавлением в файл
case $# in
    1) cat >> $1 ;;
    2) cat >> $2 < $1 ;;
    *) echo "Формат: case-4 [откуда] куда" ;;
esac

"$1" (при "$#=1") - это имя файла, в который происходит добавление со стандартного входа.

"$1" и "$2" (при $#=2) - это имена файлов, из которого ("$1") и в который ("$2") добавлять.

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

Оператор цикла с перечислением ("FOR")

Оператор цикла "for" имеет структуру:

for имя [in список значений]
do
    список команд
done

где "for" - служебное слово определяющее тип цикла, "do" и "done" - служебные слова, выделяющие тело цикла. Не забывайте про "done"! Фрагмент "in список значений" может отсутствовать.

Пусть команда "lsort" представлена командным файлом

for i in f1 f2 f3
do
    proc-sort $i
done

В этом примере имя "i" играет роль параметра цикла. Это имя можно рассматривать как shell-переменную, которой последовательно присваиваются перечисленные значения (i=f1, i=f2, i=f3), и выполняется в цикле команда "procsort".

Часто используется форма "for i in *", означающая "для всех файлов текущего каталога".

Пусть "proc-sort" в свою очередь представляется командным файлом

cat $1 | sort | tee /dev/lp > ${1}_sorted

То есть, последовательно сортируются указанные файлы, результаты сортировки выводятся на печать ("/dev/lp") и направляются в файлы f1_sorted, f2_sorted и f3_sorted.

Можно сделать более универсальной команду "lsort", если не фиксировать перечень файлов в команде, а передавать произвольное их число параметрами.

Тогда головная программа будет следующей:

for i
do
    proc-sort $i
done

Здесь отсутствие после "i" служебного слова "in" с перечислением имен говорит о том, что список поступает через параметры команды. Результат предыдущего примера можно получить, набрав:

lsort f1 f2 f3

Усложним ранее рассматривавшуюся задачу (под именем "case-2") определения холдинга фирмы. Теперь можно при вызове указывать произвольное количество фирм. При отсутствии в структуре оператора "for" фрагмента "in список значений", значения берутся из параметров вызывающей команды.

###
# holding: Справочник.
# Для различных фирм по имени выдается
# название холдинга, в который она входит
for i
do
    case $i in
        ONE|TWO|THREE) echo Холдинг: ZERO ;;
        MMM|WWW) echo Холдинг: Not-Net ;;
        Hi|Hello|Howdoing) echo Холдинг: Привет! ;;
        *) echo Нет такой фирмы ;;
    esac
done

При вызове "holding Hello HELLO ONE" на экране будет:

Холдинг: Привет!
Нет такой фирмы
Холдинг: Not-Net

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

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

Еще пример.

### subdir: Выдает имена всех поддиректориев 
# директория с именем $dir 
for i in $dir/* 
do 
    if [ -d $i ] 
    then 
        echo $i 
    fi 
done

Следующий расчет иллюстрирует полезный, хотя и с долей трюкачества, способ повторения одних и тех же действий несколько раз. Переменная "i" принимает здесь пять значений: 1, 2, 3, 4, 5, но внутри цикла эта переменная отсутствует и поэтому ее значение никакой роли не играет и ни чего не меняет. С таким же успехом переменная "i" могла принимать значения, скажем Ф О К У С , а в результате точно также было бы пять раз повторено одно и то же вычисление содержимого цикла без изменений.

### print-5: Организации пятикратного выполнения команды 
for i in 1 2 3 4 5 
do 
    cat file-22 > /dev/lp 
done

Расчет "print-n" иллюстрирует еще одну полезную возможность в использовании цикла "for". Здесь, после "for i ...", отсутствуют "in ..." и перечень имен, т.е. перечнем имен для "i" становится перечень параметров, а следовательно количество печатаемых экземпляров можно менять.

### print-n: Задание числа копий 
# через параметры 
for i 
do 
    cat file-22 > /dev/lp 
done

Смысл не изменится, если первую строку расчета записать как

for i in $*

поскольку значение "$*" - есть список значений параметров.

Отметим различие в специальных переменных "$*" и "$@", представляющих перечень параметров. Первый представляет параметры, как строку, а второй, как совокупность слов.

Пусть командный файл "cmp" имеет вид:

for i in "$*" 
do 
    echo $i 
done 
echo 
for i in "$@" 
do 
    echo $i 
done

При вызове

cmp aa bb cc

на экран будет выведено

aa bb cc 
aa bb cc

Оператор цикла с истинным условием ("WHILE")

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

Оператор цикла "while" имеет структуру:

while условие
do
   список команд
done

Где "while" - служебное слово, определяющее тип цикла с истинным условием. Список команд в теле цикла (между "do" и "done") повторяется до тех пор, пока сохраняется истинность условия (т.е. код завершения последней команды в теле цикла равен "0") или цикл не будет прерван изнутри специальными командами ("break", "continue" или "exit"). При первом входе в цикл условие должно выполняться.

### # print-50: Структура "while"
# Расчет позволяет напечатать 50
# экземпляров файла "file-22"
n=0
while [ $n -lt 50 ] # пока n < 50
do
   n=`expr $n + 1`
   cat file-22 > /dev/lp
done

Обратим внимание на то, что переменной "n" в начале присваивается значение 0, а не пустая строка, так как команда "expr" работает с shell-переменными как с целыми числами, а не как со строками.

n=`expr $n + 1`

То есть, при каждом выполнении значение "n" увеличивается на 1.

### # рr-br: Структура "while"break"
# Расчет позволяет напечатать 50
# экземпляров файла "file-22"
n=0
while true
do
   if [ $n -lt 50 ] # если n < 50
   then
      n=`expr $n + 1`
   else
      break
   fi
   cat file-22 > /dev/lp
done

Команда "break [n]" позволяет выходить из цикла. Если "n" отсутствует, то это эквивалентно "break 1". "n" указывает число вложенных циклов, из которых надо выйти, например, "break 3" - выход из трех вложенных циклов.

В отличие от команды "break", команда "continue [n]" лишь прекращает выполнение текущего цикла и возвращает на НАЧАЛО цикла. Она также может быть с параметром. Например, "continue 2" означает выход на начало второго (если считать из глубины) вложенного цикла.

Команда "exit [n]" позволяет выйти вообще из процедуры с кодом возврата "0" или "n" (если параметр "n" указан). Эта команда может использоваться не только в циклах. Даже в линейной последовательности команд она может быть полезна при отладке, чтобы прекратить выполнение (текущего) расчета в заданной точке.

Оператор цикла с ложным условием ("UNTIL")

Оператор цикла "until" имеет структуру:

until условие
do
    список команд
done

Где "until" - служебное слово, определяющее тип цикла с ложным условием. Список команд в теле цикла (между "do" и "done") повторяется до тех пор, пока сохраняется ложность условия или цикл не будет прерван изнутри специальными командами ("break", "continue" или "exit"). При первом входе в цикл условие не должно выполняться.

Отличие от оператора "while" состоит в том, что условие цикла проверяется на ложность (на ненулевой код завершения последней команды тела цикла) проверяется ПОСЛЕ каждого (в том числе и первого!) выполнения команд тела цикла.

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

Примеры:

until false
do
    read x
    if [ $x = 5 ]
    then
        echo enough ; break
    else
        echo some more
    fi
done

Здесь программа с бесконечным циклом ждет ввода слов (повторяя на экране фразу "some more"), пока не будет введено "5". После этого выдается "enough" и команда "break" прекращает выполнение цикла.

Другой пример ("Ожидание полдня") иллюстрирует возможность использовать в условии вычисления:

until date | grep 12:00:
do
    sleep 30
done

Здесь каждые 30 секунд выполняется командная строка условия. Команда "date" выдает текущую дату и время. Команда "grep" получает эту информацию через конвейер и пытается совместить заданный шаблон "12:00:" с временем, выдаваемым командой "date". При несовпадении "grep" выдает код возврата "1", что соответствует значению "ложь", и цикл "выполняет ожидание" в течение 30 секунд, после чего повторяется выполнение условия. В полдень (возможно с несколькими секундами) произойдет сравнение, условие станет истинным, "grep" выдаст на экран соответствующую строку и работа цикла закончится.

Пустой оператор

Пустой оператор имеет формат:

:

Ничего не делает. Возвращает значение "0". Например, в конструкции "while :" или ставить в начале командного файла, чтобы гарантировать, что файл не будет принят за выполняемый файл для "csh".

Функции в SHELL

Функция позволяет подготовить список команд shell для последующего выполнения.

Описание функции имеет вид:

имя() { список команд }

после чего обращение к функции происходит по имени. При выполнении функции не создается нового процесса. Она выполняется в среде соответствующего процесса. Аргументы функции становятся ее позиционными параметрами; имя функции - ее нулевой параметр. Прервать выполнение функции можно оператором "return [n]", где (необязательное) "n" - код возврата.

echo $$ fn() # описание функции
{ echo xx=$xx echo $# echo $0: $$ $1 $2 xx=yy ; echo xx=$xx return 5 }
xx=xx ; echo xx=$xx
fn a b # вызов функции "fn" с параметрами
echo $? echo xx=$xx

содержащего описание и вызов функции "fn", выдаст на экран:

749 xx=xx xx=xx 2 fun: 749 a b xx=yy 5 xx=yy

Обработка прерываний ("TRAP")

Бывает необходимо защитить выполнение программы от прерывания.

Наиболее часто приходится встречаться со следующими прерываниями, соответствующими сигналам:

0выход из интерпретатора,
1отбой (отключение удаленного абонента),
2прерывание от <Del>,
9уничтожение (не перехватывается),
15окончание выполнения.

Для защиты от прерываний существует команда "trap", имеющая формат:

trap 'список команд' сигналы

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

tarp 'rm /tmp/* ; exit 1' 1 2 15

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

Команда "trap" позволяет и просто игнорировать прерывания, если "список команд" пустой. Так например, если команда "cmd" выполняется очень долго, а пользователь решил отключиться от системы, то для продолжения выполнения этой команды можно написать, запустив команду в фоновом режиме:

( trap '' 1; cmd )&