7.7. Написание несложных Bourne-скриптов

Описание:  Большинство задач системного администрирования могут быть автоматизарованы с использованием shell-скриптов. Кандидат BSDA должен знать о преимуществах и недостатках использования скриптов Bourne shell более, нежели csh(1) или bash(1). Кандидат должен различать «магическую строку» (shebang), комментарии, позиционные параметры и специальные параметры, маски в шаблонах, знать как правильно использовать кавычки и обратные слеши, операторы for, while, if, case и esec. Кандидат должен знать как сделать скрипт исполнимым и как его отлаживать.

Практика: sh(1), chmod(1)

Комментарий

sh(1) используется не столько как интерактивный shell (надо признать, что sh(1) в работе не так удобен, как его более современные аналоги), сколько как язык программирования используемый для автоматизации рутинных процедур. На sh(1) написано множество скриптов используемых при старте системы, а так же скриптов обслуживающих её функционирование.

В первом приближнии sh(1) позволяет попросту перечислить внешние команды, которые необходимо выполнить. Оданко sh(1) так же обладает возможностью делать проверки, выполнять циклы, обрабатывать исключительные ситуации (перехватывать сигналы) и многими другими возможностями. Не стоит забывать, однако, что sh(1), это всего лишь удобное средство автоматизации. Несмотря на циклы и логику, несмотря на наличие собственной целочисленной арифметики, главное, для чего используется sh(1) — это вызов внешних программ, которые, собственно, и делают основную работу.

7.7.1. Магическая строка (shebang)

С чего начинается скрипт? С некоторой магической строки — shebang. Вообще, последовательность действий операционной системы с файлом при попытке пользователя запустить его на исполнение, выглядит примерно так:

  1. Сперва система пытается понять является ли вызываемый файл исполнимым бинарным файлом. Если да, то его можно сразу запустить.
  2. Если нет, то надо посмотреть на первые два символа файла. Если это #! то перед нами «магическая строка» (shebang). Вся строка от третьего символа до конца строки является указателем на интерпретатор. Надо выполнить указанную строку, подав интерпретатору на вход скрипт. Такая конструкция позволяет запускать интерпретатор с дополнительными опциями: #!/usr/local/bin/perl -w или делать сложные вызовы, указывая специальные переменные окружения: #!/usr/bin/env -i /usr/local/bin/python.
  3. Наконец, если и магической строки не найдено, файл передаётся на исполнение интерпретатору /bin/sh.

Даже несмотря на наличие такого умолчания, разумно указывать интерпретатор в явном виде. Сценарии sh должны начинаться со строки #!/bin/sh.

На вход интерпретатору подаётся весь скрипт целиком, включая первую строку с shebang. Поэтому скриптовый язык обязан воспринимать знак #как символ комментария, как минимум в первой строке. В этом легко убедится. Пусть у нас есть скрипт test.cat состоящий из двух строк:

#!/bin/cat
Hello
  

При попытке выполнить скрипт мы получим:

$ chmod 755 test.cat
$ ./test.cat
#!/bin/cat
Hello
  

Ещё одной особенностью скрипта является то, что он обязан иметь пермиссии не только на исполнение, но и на чтение. В противном случае скрипт не сможет быть направлен на стандартный вход указанному интерпретатору.

В разделе посвящённом переменным окружения обсуждается вопрос использования утилиты env(1) в shebang и вопросы связанные с безопасностью при использовании различных интерпретаторов в «магической строке».

7.7.2. Почему sh(1)?

Надо честно признать, повседневно работать в sh(1) неудобно. Да, sh(1) неудобный интерпретатор, ему есть множество альтернатив, которые, к тому же, порой полностью совместимы по синтаксису. Почему же в операционных системах BSD так упорно цепляются за него? Для начала приведу выдержку из FreeBSD FAQ.

Вопрос 7.8:  Почему возможности /bin/sh так малы? Почему бы во FreeBSD не использовать bash или какой-либо другой командный процессор?

Ответ:  Потому что в стандарте POSIX сказано, что все командные процессоры должны вести себя так же, как shell.

Более подробный ответ заключается в следующем: многим требуется, чтобы разрабатываемые скрипты для командного процессора были переносимы между многими системами. Именно поэтому в POSIX очень подробно описан командный процессор и набор утилит. Большинство скриптов пишутся на языке процессора Bourne shell, к тому же некоторые важные программные вызовы (make(1), system(3), popen(3) и их аналоги на языках скриптов высокого уровня, таких как Perl или Tcl) предполагают для интерпретации команд использование именно Bourne shell. Так как Bourne shell используется столь широко и часто, то очень важно, чтобы он стартовал очень быстро, его поведение было строго регламентировано и при этом потребности в оперативной памяти были малы.

В имеющейся реализации мы приложили максимум усилий для воплощения в жизнь всех этих требований одновременно. Для того, чтобы сохранить /bin/sh небольшим по размеру, мы не включили многие из обычных возможностей, которые имеются в других командных процессорах. Однако в Коллекцию Портов включены командные процессоры, обладающие гораздо большими возможностями, такие, как bash, scsh, tcsh и zsh. (Вы можете сами сравнить использование памяти всеми этими оболочками, посмотрев в колонки «VSZ» и «RSS» вывода команды ps -u).

Думается, что слова эти нуждаются в некотором дополнительном пояснении. Когда UNIX запускает новый процесс, ядро осуществляет следующую сложную цепочку действий: сперва оно выполняет системный вызов fork(2) и копирует область памяти соответствующую родительскому процессу. Появляется два совершенно одинаковых родительских процесса, которые различаются только кодом возврата функции fork(2). Родитель получает PID потомка, а потомок — 0. По этому нулю потомок догадывается, что он потомок и осуществляет системный вызов exec(2), в результате чего он полностью замещается новым процессом.

Из сказанного должно быть ясно, что во-первых вызов новой программы, это очень дорогая операция (мы видели это, когда сравнивали работу аргумента -exec у программы find(1) и использование команды xargs(1) в Раздел 7.6, «Поиск файла по заданным атрибутам»), а во-вторых, чем сложнее программа вызывающая новый процесс, тем дороже эта операция. Между тем, программирование на Bourne Shell сводится именно к написанию большого количества вызовов внешних программ.

Сравним работу двух одинаковых с виду программ на sh:

#!/bin/sh

i=0
while [ $i -lt 1000 ]
do
 i=`echo $i+1|/usr/bin/bc`
done
echo $i
  

и на bash:

#!/usr/local/bin/bash

i=0
while [ $i -lt 1000 ]
do
 i=`echo $i+1|/usr/bin/bc`
done
echo $i
  

Как видите, эти программы отличаются только первой строчкой. В этих программах осуществляется в цикле тысячекратный вызов программы bc(1), крохотного калькулятора, для увеличения счётчика. И столько же раз вызвана команда test(1) (см. ниже).

$ time ./test.sh
1000

real 0m7.447s
user 0m1.428s
sys  0m5.812s

$ time ./test.bash
1000

real 0m14.223s
user 0m2.273s
sys  0m11.559s
  

Легко видеть, что тяжеловесный bash(1) ворочал этот скрипт почти вдвое дольше. На разных опробованных мною системах соотношение времени выполнения этого скрипта bash/sh колебалось от 1.3 до 1.9.

7.7.3. Программирование в Bourne Shell

7.7.3.1. Синтаксис

Таблица 7.7. Синтаксическая таблица Bourne Shell

ЭлементОписание
#Комментарий
;Конец команды. Тождественен концу строки. Команда будет выполнена в foreground, т.е. sh(1) будет ждать, пока она выполнится (ср. с & ниже).
&Конец команды. Команда будет выполнена в фоновом режиме, т.е. sh(1) не будет ждать, пока она выполнится (ср. с ; выше). Не путать с &&
&&Логическое И. если до && стоит команда, которая выдала нулевой код возврата (истина), то выполняется команда следующая за &&, суммарным кодом возврата будет код возврата второй команды. Если нет, то вторая команда не выполняется, а код возврата берётся от первой операции (т.е. ложь).
||Логическое ИЛИ. если до || стоит команда, которая выдала ненулевой код возврата (ложь), то выполняется команда следующая за ||, суммарным кодом возврата будет код возврата второй команды. Если нет, то вторая команда не выполняется, а код возврата берётся от первой операции (т.е. истина).
|Символ pipe (труба). STDOUT команды перед | перенапрвляется на STDIN следующей команде. Подробно о перенаправлении говорится в Раздел 7.1, «Перенаправление вывода и использование tee(1)». Команды объединённые через | вместе называются конвейером (pipeline). Код возврата конвейера равен коду возврата последней команды в конвейере.
[n]>&mПеренаправление STDOUT или файлового дескриптора n в файловый дескриптор m. Подробно о перенаправлении говорится в Раздел 7.1, «Перенаправление вывода и использование tee(1)».
[n]> fileПеренаправление STDOUT или файлового дескриптора n в файл. Подробно о перенаправлении говорится в Раздел 7.1, «Перенаправление вывода и использование tee(1)».
[n]< fileПеренаправление STDIN, либо файлового дескриптора n из файла. Подробно о перенаправлении говорится в Раздел 7.1, «Перенаправление вывода и использование tee(1)».
[n]<&-Закрыть STDIN, либо файловый дескриптор n
[n]>&-Закрыть STDOUT, либо файловый дескриптор n
var=...Присваивание переменной var
$varВызов значения переменной var
Экранирование следующего символа. Например, если find(1) ожидает получить в качестве аргумента знак ;, то, для того, чтобы этот знак не был интерпретирован sh(1), а был-бы благополучно доставлен в find(1), в командной строке следует написать ;
'Внутри одинарных кавычек решительно все символы не имеют никакого специального значения. Полное экранирование.
`Команда внутри обратных кавычек будет выполнена, а её STDOUT будет подставлен в командную строку вместо кавычек. При этом концы строк будут заменены на пробелы.
"Неполное экранирование. Внутри двойных кавычек не экранируются обратные кавычки и знак $ (ссылка на переменную).
!Инвертирует код возврата последующей команды (или конвейера).
$(...)То же, что и обратные кавычки, но может быть вложенным.
$((...))Ожидается, что внутри будет арифметическое выражение. Оно будет вычислено и подставлено в командную строку вместо скобок. Внимание! Аналогичные скобки $[...], это расширение bash(1) и sh(1) его не поддерживает.
(...)команды в круглых скобках будут выполнены в отдельном подпроцессе. Область видимости переменных, определённых в скобках, не выйдет за их пределы.
name () {...}Определение функции (см. ниже).
. filenameВыполнение набора команд из файла filename. (См. ниже про «модули»)
:Встроенная команда ничего не делающая, но возвращающая истину.

7.7.3.2. Работа с переменными в sh(1)

Присваивание переменной осуществляется при помощи оператора =. Вызов определённой ранее переменной, при помощи префикса $. Например:

$ PI=3.1415926535897931
$ echo $PI
3.1415926535897931
   

Следует подчеркнуть: переменные и переменные окружения это не одно и то же. Любая переменная окружения видна как переменная, но не любая переменная видна как переменная окружения. Чтобы переменная стала переменной окружения её надо экспортировать:

$ PI=3.1415926535897931
$ echo $PI
3.1415926535897931
$ printenv PI
$ echo $?
1
$ export PI
$ printenv PI
3.1415926535897931
   

Т.е. до вызова команды export, Переменная PI была, а переменной окружения PI не было. (Подробно о команде printenv(1) можно узнать в разделе Раздел 7.2.1.1, «env(1), printenv(1)») Здесь использована так же переменная $? в которой хранится код возврата последней операции. Подробно специальные переменные перечислены в таблице ниже.

Таблица 7.8. Специальные переменные в Bourne Shell

ПеременнаяОписание
$*Список аргументов, с которыми был вызван скрипт.
$@Список аргументов, с которыми был вызван скрипт.
$#Число аргументов, с которыми был вызван скрипт.
$?Код возврата последней команды (последнего конвейера).
$-Список аргументов, с которыми вызван sh(1)
$$PID родительской оболочки
$!PID последнего отправленного в фон процесса.

7.7.3.3. Условные операторы

В качестве условия в sh(1) выполняется некоторая команда, и изучается её код возврата. Если код возврата равен нулю, и, следовательно, программа завершилась успешно, sh(1) трактует это как истину, если программа вернула код возврата больше нуля, sh(1) трактует это как ложь.

Проверки можно комбинировать при помощи знаков && (логическое «и») и || (логическое «или»).

В простейшем случае можно вообще обойтись без явного условного оператора, используя лишь комбинацию этих знаков:

uname | grep -q BSD && echo "Это какая-то BSD!" || echo "Не знаю что это."
   

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

if uname | grep -q BSD
then echo "Это какая-то BSD!"
else echo "Не знаю, что это."
fi
   

Если эти операторы понадобится написать в одну строку, то надо понимать, что ключевые слова if, then, else и fi, это самостоятельные команды и они в строке они должны предваряться знаком ;.

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

if uname | grep -q FreeBSD
then
 echo "Это FreeBSD, на ней может получиться удобная рабочая станция"
elif uname | grep -q OpenBSD
then
 echo "Это OpenBSD, знаменитая своей безопасностью."
 echo "Прекрасный выбор для сервера"
elif uname | grep -q NetBSD
then
 echo "Это NetBSD, она поддерживает самые немыслимые архитектуры."
 echo "Хороший выбор для тостера или холодильника."
else
 echo "Не знаю, что это."
fi
   

Недостатком данной конструкции является то, что здесь шесть раз вызываются программы uname(1) и grep(1). Существует более очевидная конструкция для проверки на соответвие строки списку значений.

case "`uname`" in
FreeBSD)
 echo "Это FreeBSD, на ней может получиться удобная рабочая станция";;
OpenBSD)
 echo "Это OpenBSD, знаменитая своей безопасностью."
 echo "Прекрасный выбор для сервера";;1
NetBSD)
 echo "Это NetBSD, она поддерживает самые немыслимые архитектуры."
 echo "Хороший выбор для тостера или холодильника.";;
BSD|[Dd][Aa][Rr][Vv][Ii][Nn])2
 echo "Есть основания полагать, что это тоже BSD";;
*)3
 echo "Не знаю, что это.";;
esac
   
1 Каждый список команд должен заканчиваться двумя следующими друг за другом точками с запятой — ;;.
2 Перед закрывающей круглой скобкой должен следовать шаблон набранный по правилам shell. (Т.е. допустимы классы в [] и символ *.) Если условию может удовлетворять несколько альтернатив, то их можно объединить через «или» — |. На прмере слова Darvin (так идентифицирует себя ядро MacOS X), показано как сделать проверку нечувствительной к регистру.
3 Это пример того, как при помощи конструкции case...esac сделать аналог ветви else, которая будет срабатывать всегда, когда не сработали все перечисленные варианты.

Специально для условного оператора sh(1) существует программа осуществляющая математические проверки, проверки на существование файловых объектов и равенство строк. В зависимости от результата сравнения эта программа возвращает либо ноль, либо единицу. Речь идёт о программе test(1).

Таблица 7.9. Опции команды test(1)

ОпцияОписание
Проверки файловых объектов
-e <file>Истина, если <file> существует независимо от того, чем он является
-r <file>Истина, если <file> существует и из него можно читать
-w <file>Истина, если <file> существует и в него можно писать
-x <file>Истина, если <file> существует и его можно выполнить
-s <file>Истина, если <file> существует и не пуст
-b <file>Истина, если <file> существует и является блочным устройством
-c <file>Истина, если <file> существует и является символьным устройством устройством
-d <file>Истина, если <file> существует и является каталогом
-f <file>Истина, если <file> существует и является обычным файлом
-h <file> -L <file>Истина, если <file> существует и является символьной ссылкой. Опция -h оставлена для совместимости и не рекомендуется к использованию.
-p <file>Истина, если <file> существует и является именованным каналом (FIFO)
-S <file>Истина, если <file> существует и является сокетом
-k <file>Истина, если <file> существует и на нём установлен stiсky-бит
-t <num>Истина, если файловый дескриптор <num> существует и направлен на терминал. С помощью этой проверки можно убедиться направлен ли вывод скрипта на терминал или перенаправлен в файл
-O <file>Истина, если <file> существует и его владелец тот же, что и EUID данного процесса
-G <file>Истина, если <file> существует и его группа та же, что и EGID данного процесса
<file1> -nt <file2>Истина, если файл <file1> новее (newer then) чем файл <file2>
<file1> -ot <file2>Истина, если файл <file1> старше (older then) чем файл <file2>
<file1> -et <file2>Истина, если файл <file1> и файл <file2> указывают на один и тот же файл
Проверки строк
-n <string>Истина, если строка <string> не пуста
<string>Истина, если строка <string> не пуста
-z <string>Истина, если строка <string> пуста
<s1> = <s2>Истина, если строки <s1> и <s2> одинаковы
<s1> != <s2>Истина, если строки <s1> и <s2> отличаются
<s1> < <s2>Истина, если строка <s1> должна идти перед <s2> по кодам ASCII. Например "abc" < "abd"
<s1> > <s2>Истина, если строка <s1> должна идти после <s2> по кодам ASCII
Проверки чисел
<n1> -eq <n2>Истина, если числа <n1> и <n2> равны (equal)
<n1> -ne <n2>Истина, если числа <n1> и <n2> не равны (not equal)
<n1> -ge <n2>Истина, если число <n1> больше либо равно <n2> (grater or equal)
<n1> -gt <n2>Истина, если число <n1> строго больше <n2> (grater then)
<n1> -le <n2>Истина, если число <n1> меньше либо равно <n2> (less or equal)
<n1> -lt <n2>Истина, если число <n1> строго меньше <n2> (less then)
Объединение условий
-aИ (and)
-oИли (or)
!инвертирование проверки
(...)группирование для операторов «и» или «или»

Для команды test(1) существует альтернативное имя [. Если она вызывается по имени [, то она разбирает командную строку вплоть до того, пока не встретит закрывающую квадратную скобку. Таким образом, следующие четыре конструкции эквивалентны:

$ test -d /usr/ports && echo "найдено дерево портов" || echo "Дерево портов не найдено"
$ [ -d /usr/ports ] && echo "найдено дерево портов" || echo "Дерево портов не найдено"
$ if test -d /usr/ports
> then echo "найдено дерево портов"
> else echo "Дерево портов не найдено"
> fi
$ if [ test -d /usr/ports ]
> then echo "найдено дерево портов"
> else echo "Дерево портов не найдено"
> fi
   

Обратите внимание: вокруг квадратных скобок обязательно должны быть пробелы, потому что [ это не синтаксическая конструкция sh(1), а обычная команда на подобии test(1).

7.7.3.4. Циклы

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

7.7.3.4.1. while — цикл с условием

Следующая программа выводит список квадратов натуральных чисел от 1 до 100, используя цикл с условием while:

i=1
while [ $i -le 100 ]
do
 echo $(($i*$i))
 i=$(($i+1))
done
   

Ключевые слова do и done ограничивают тело цикла, при написании в одной строке они должны предваряться ;.

i=1; while [ $i -le 100 ]; do echo $(($i*$i)); i=$(($i+1)); done
   

Цикл может быть прерван встроенной командой break:

i=1
while :
do
 echo $(($i*$i))
 i=$(($i+1))
 if [ $i -gt 100 ]; then break; fi
done
   

Встроенная команда : всегда возвращает истину (см. Таблица 7.7, «Синтаксическая таблица Bourne Shell»). Вместо неё можно было бы употребить команду /usr/bin/true.

Встроенная команда continue предназначена для прерывания текущей итерации. Т.е. выполнение цикла будет продолжено, но текущая итерация будет прервана. Если после команд break или continue указано число, то оно означает глубину цикла, который будет ими прерван.

7.7.3.4.2. for — цикл с перебором списка

Следующая программа конвертирует все картинки в каталоге image из формата GIF в формат PNG

for filename in image/*.jpg
do
 # Действительно ли это JPEG?
 if ! file $filename | grep -q "JPEG image data"
 then continue
 fi

 # Конвертируем JPEG в PNG
 if convert $filename ${filename%jpg}png
 then echo "$filename -> ${filename%jpg}png"
 else echo "$filename don't converted"
 fi

done
   

Утилита convert(1) — сторонняя утилита. Не входит ни в одну операционную систему BSD по-умолчанию и доставляется отдельно из портов или пакетов.

Конструкция image/*.jpg превратится в список файлов в каталоге image имя которых оканчивается на .jpg.

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

Обратите внимание на использование кавычек: внутри двойных кавычек переменные раскрываются в свои значения. Кавычки мы использовали для того, чтобы в одном случае строка трактовалась бы утилитой grep(1) как один аргумент, несмотря на наличие в ней пробелов, в другом случае, чтобы защитить символ > и в третьем случае защитить одинарную кавычку.

Конструкция ${filename%jpg} указывает sh(1), что надо взять значение переменной $filename и отрезать с конца фрагмент jpg.

7.7.3.5. Функции

sh(1) позволяет определять функции и вызывать их. Переменные $1, $2 и т.д. внутри функций ссылаются не на аргументы скрипта, а на аргументы с которыми функция вызывалась. Ниже определена функция для вычисления квадрата натурального числа и переписан цикл выводящий список квадратов натуральных чисел:

sqrt () {
 echo $(($1*$1))
}

i=1
while [ $i -le 100 ]
do
 sqrt $i
 i=$(($i+1))
done
   

При помощи директивы return функция может вернуть свой собственный код возврата.

7.7.3.6. «Модули»

В sh(1) существует конструкция, позволяющая подгружать внешние файлы с определёнными в них функциями и переменными. Допустим у нас есть файл math в котором имеются следующие определения:

# Описываем константы
PI=3.1415926535897931

# Функция для возведения в квадрат
sqrt () {
 echo $(($1*$1))
}

# Длина окружности
circlen () {
 echo "$PI*$1*2" | /usr/bin/bc
}

# Площадь круга
circarea () {
 sq=`sqrt $1`
 echo "$PI*$sq" | /usr/bin/bc
}
   

Теперь, если мы захотим вычислить длину окружности, или её площадь, нам достаточно внутри скрипта подгрузить данный «модуль». Это делается при помощи оператора . (точка). После того, как модуль подгружен, мы можем использовать все функции и константы, которые в нём определены.

#!/bin/sh

. math

echo "Длина окружности радиуса 3 см равна `circlen 3` см"
echo "А площадь круга того же радиуса равна `circarea 3` см^2"
echo "Причина этого явления в том, что число пи, по прежнему"
echo "равно $PI, и со времён древних греков"
echo "существенно не изменилось..."
   

В этом скрипте мы подгружаем «модуль» math и вызываем функцию подсчёта длины окружности и площади круга, которые в нём определены, а так же ссылаемся на определённую в нём переменную $PI. Этот приём часто используется при написании системных скриптов. В частности, именно так реализованы файлы /etc/defaults/rc.conf и /etc/rc.conf в FreeBSD. Оба являются модулями, в первом определяются константы, вроде:

.................................
inetd_enable="NO"    # Run the network daemon dispatcher (YES/NO).
inetd_program="/usr/sbin/inetd" # path to inetd, if you want a different one.
inetd_flags="-wW -C 60"   # Optional flags to inetd
.................................
   

Во втором они могут частично переопределяться, например:

.................................
inetd_enable="YES"
.................................
   

При этом оба файла последовательно подгружаются в файле /etc/rc.subr в функции load_rc_config().

7.7.3.7. Некоторые приёмы используемые при программировании на sh(1)

7.7.3.7.1. Чтение конфигурационных файлов
7.7.3.7.2. Разбор командной строки

Команда getopts входит в стандарт POSIX и является встроенной командой sh(1). У команды getopts имеется два аргумента: 1) — строка с перечнем возможных опций. После опций у которых возможно значение, ставится двоеточие. 2) — имя переменной, в которую будет сохраняться имя опции. Значение переменной будет сохраняться в переменной $OPTARG. Пример:

#!/bin/sh

while getopts e:h option
do
 case $option in
  h)
  echo "Usage: `/usr/bin/basename $0` [-h|-e text]";;
  e)
  echo Hello, $OPTARG;;
  ?)
  $0 -h;;
 esac
done
   

Теперь вызовем этот скрипт (назовём его getopts.sh).

$ ./getopts.sh -h
Usage: getopts.sh [-h|-e text]
$ ./getopts.sh -a
Illegal option -a
Usage: getopts.sh [-h|-e text]
$ ./getopts.sh -e BSD
Hello, BSD
$ ./getopts.sh -h -e BSD
Usage: getopts.sh [-h|-e text]
Hello, BSD
   
7.7.3.7.3. Конструирование скрипта «на лету», раскрытие переменных

Команда eval просто выполняет свой аргумент. Это позволяет «сконструировать скрипт» складывая команды и аргументы в некоторую переменную, а потом подставить её на выполнение команде eval.

Ниже приведено другое остроумное применение данной команды. Пример взят из файла /etc/rc.subr, являющегося «модулем» для некоторых системных скриптов FreeBSD. Рассмотрим функцию проверяющую значение переменной. Этой функции передаётся имя переменной. Функция проверяет значение этой переменной и, если там стоит слово «no» возвращает единицу, если «yes» — 0, в противном случае предупреждает об ошибке. Для того, чтобы манипулировать и с именем переменной и с её значением применяется команда eval. С её помощью значение переменной помещается в отдельную переменную _value, а имя переменной доступно как аргумент функции, т.е. $1:

#
# checkyesno var
#  Test $1 variable, and warn if not set to YES or NO.
#  Return 0 if it's "yes" (et al), nonzero otherwise.
#
checkyesno()
{
eval _value=$${1}
debug "checkyesno: $1 is set to $_value."
case $_value in

 #  "yes", "true", "on", or "1"
[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
 return 0
 ;;

 #  "no", "false", "off", or "0"
[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0)
 return 1
 ;;
*)
 warn "$${1} is not set properly - see rc.conf(5)."
 return 1
 ;;
esac
}
   
7.7.3.7.4. Обработка сигналов

Встроенная в sh(1) команда trap позволяет зарегистрировать обработчик сигнала. Если в процессе выполнения скрипт получит указанный в команде trap сигнал, то вместо обычного поведения, он вызовет указанный обработчик:

terminator () {
echo "Не умру ни за что" >&2
}
trap terminator 15
   

Теперь, если скрипт получит сигнал SIGTERM (номер 15), то вместо того, чтобы нормально завершиться, он напечатает на стандартный вывод ошибок сообщение и продолжит работу.

Более разумным применением этого механизма было бы стирание временных файлов и корректное завершение работы.

7.7.3.7.5. Объединение вывода нескольких команд в общий конвейер

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

$ (
> for i in Apple Microsoft "Free Software Foundation"
> do
>  echo $i
> done
> ) | grep Free
Free Software Foundation
   

В заключение следует сказать, что после того, как вы написали могучий сценарий sh(1), ему не мешает дать пермиссии на выполнение, командой chmod(1).


Обновлено: 12.03.2015