Искусство программирования на языке сценариев командной оболочки

         

Пример 10-7. grep для бинарных файлов

#!/bin/bash # bin-grep.sh: Поиск строк в двоичных файлах.

# замена "grep" для бинарных файлов. # Аналогично команде "grep -a"

E_BADARGS=65 E_NOFILE=66

if [ $# -ne 2 ] then echo "Порядок использования: `basename $0` string filename" exit $E_BADARGS fi

if [ ! -f "$2" ] then echo "Файл \"$2\" не найден." exit $E_NOFILE fi

for word in $( strings "$2" | grep "$1" ) # Инструкция "strings" возвращает список строк в двоичных файлах. # Который затем передается по конвейеру команде "grep", для выполнения поиска. do echo $word done

# Как указывает S.C., вышепрведенное объявление цикла for может быть упрощено # strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'

# Попробуйте что нибудь подобное: "./bin-grep.sh mem /bin/ls"

exit 0

Еще один пример.

Пример 10-8. Список всех пользователей системы

#!/bin/bash # userlist.sh

PASSWORD_FILE=/etc/passwd n=1 # Число пользователей

for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" ) # Разделитель полей = : ^^^^^^ # Вывод первого поля ^^^^^^^^ # Данные берутся из файла паролей ^^^^^^^^^^^^^^^^^ do echo "Пользователь #$n = $name" let "n += 1" done

# Пользователь #1 = root # Пользователь #2 = bin # Пользователь #3 = daemon # ... # Пользователь #30 = bozo



exit 0

И заключительный пример использования подстановки команд при создании [списка].

Пример 10-9. Проверка авторства всех бинарных файлов в текущем каталоге

#!/bin/bash # findstring.sh: # Поиск заданной строки в двоичном файле.

directory=/usr/local/bin/ fstring="Free Software Foundation" # Поиск файлов от FSF.



for file in $( find $directory -type f -name '*' | sort ) do strings -f $file | grep "$fstring" | sed -e "s%$directory%%" # Команде "sed" передается выражение (ключ -e), #+ для того, чтобы изменить обычный разделитель "/" строки поиска и строки замены #+ поскольку "/" - один из отфильтровываемых символов. # Использование такого символа порождает сообщение об ошибке (попробуйте). done

exit 0

# Упражнение: # --------------- # Измените сценарий таким образом, чтобы он брал #+ $directory и $fstring из командной строки.

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

Пример 10-10. Список символических ссылок в каталоге

#!/bin/bash # symlinks.sh: Список символических ссылок в каталоге.

directory=${1-`pwd`} # По-умолчанию в текущем каталоге, # Блок кода, который выполняет аналогичные действия. # ---------------------------------------------------------- # ARGS=1 # Ожидается один аргумент командной строки. # # if [ $# -ne "$ARGS" ] # Если каталог поиска не задан... # then # directory=`pwd` # текущий каталог # else # directory=$1 # fi # ----------------------------------------------------------

echo "символические ссылки в каталоге \"$directory\""

for file in "$( find $directory -type l )" # -type l = символические ссылки do echo "$file" done | sort # В противном случае получится неотсортированный список.

# Как отмечает Dominik 'Aeneas' Schnitzer, #+ в случае отсутствия кавычек для $( find $directory -type l ) #+ сценарий "подавится" именами файлов, содержащими пробелы.

exit 0

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



Пример 10-11. Список символических ссылок в каталоге, сохраняемый в файле

#!/bin/bash # symlinks.sh: Список символических ссылок в каталоге.

OUTFILE=symlinks.list # файл со списком

directory=${1-`pwd`} # По-умолчанию -- текущий каталог,

echo "символические ссылки в каталоге \"$directory\"" > "$OUTFILE" echo "---------------------------" >> "$OUTFILE"

for file in "$( find $directory -type l )" # -type l = символические ссылки do echo "$file" done | sort >> "$OUTFILE" # перенаправление вывода # ^^^^^^^^^^^^^ в файл.

exit 0

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

Пример 10-12. C-подобный синтаксис оператора цикла for

#!/bin/bash # Два вапианта оформления цикла.

echo

# Стандартный синтаксис. for a in 1 2 3 4 5 6 7 8 9 10 do echo -n "$a " done

echo; echo

# +==========================================+

# А теперь C-подобный синтаксис.

LIMIT=10

for ((a=1; a <= LIMIT ; a++)) # Двойные круглые скобки и "LIMIT" без "$". do echo -n "$a " done # Конструкция заимствована из 'ksh93'.

echo; echo

# +=========================================================================+

# Попробуем и C-шный оператор "запятая".

for ((a=1, b=1; a <= LIMIT ; a++, b++)) # Запятая разделяет две операции, которые выполняются совместно. do echo -n "$a-$b " done

echo; echo

exit 0

См. так же Пример 25-10, Пример 25-11 и Пример A-7.

---

А сейчас пример сценария, который может найти "реальное" применение.

Пример 10-13. Работа с командой efax в пакетном режиме

#!/bin/bash

EXPECTED_ARGS=2 E_BADARGS=65

if [ $# -ne $EXPECTED_ARGS ] # Проверка наличия аргументов командной строки. then echo "Порядок использования: `basename $0` phone# text-file" exit $E_BADARGS fi

if [ ! -f "$2" ] then echo "Файл $2 не является текстовым файлом" exit $E_BADARGS fi



fax make $2 # Создать fax-файлы из текстовых файлов.

for file in $(ls $2.0*) # Все файлы, получившиеся в результате преобразования. # Используется шаблонный символ в списке. do fil="$fil $file" done

efax -d /dev/ttyS3 -o1 -t "T$1" $fil # отправить.

# Как указывает S.C., в цикл for может быть вставлена сама команда отправки в виде: # efax -d /dev/ttyS3 -o1 -t "T$1" $2.0* # но это не так поучительно [;-)].

exit 0

while

Оператор while проверяет условие перед началом каждой итерации и если условие истинно (если код возврата равен 0), то управление передается в тело цикла. В отличие от циклов for, циклы while используются в тех случаях, когда количество итераций заранее не известно.

while [condition]

do

 command...

done

Как и в случае с циклами for/in, при размещении ключевого слова do в одной строке с объявлением цикла, необходимо вставлять символ ";" перед do.

while [condition] ; do

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

Пример 10-14. Простой цикл while

#!/bin/bash

var0=0 LIMIT=10

while [ "$var0" -lt "$LIMIT" ] do echo -n "$var0 " # -n подавляет перевод строки. var0=`expr $var0 + 1` # допускается var0=$(($var0+1)). done

echo

exit 0

Пример 10-15. Другой пример цикла while

#!/bin/bash

echo

while [ "$var1" != "end" ] # возможна замена на while test "$var1" != "end" do echo "Введите значение переменной #1 (end - выход) " read var1 # Конструкция 'read $var1' недопустима (почему?). echo "переменная #1 = $var1" # кавычки обязательны, потому что имеется символ "#". # Если введено слово 'end', то оно тоже выводится на экран. # потому, что проверка переменной выполняется в начале итерации (перед вводом). echo done

exit 0

Оператор while может иметь несколько условий. Но только последнее из них определяет возможность продолжения цикла.


В этом случае синтаксис оператора цикла должен быть несколько иным.

Пример 10-16. Цикл while с несколькими условиями

#!/bin/bash

var1=unset previous=$var1

while echo "предыдущее значение = $previous" echo previous=$var1 # запомнить предыдущее значение [ "$var1" != end ] # В операторе "while" присутствуют 4 условия, но только последнее управляет циклом. # *последнее* условие - единственное, которое вычисляется. do echo "Введите значение переменной #1 (end - выход) " read var1 echo "текущее значение = $var1" done

# попробуйте самостоятельно разобраться в сценарии works.

exit 0

Как и в случае с for, цикл while может быть записан в C-подобной нотации, с использованием двойных круглых скобок (см. так же Пример 9-28).

Пример 10-17. C-подобный синтаксис оформления цикла while

#!/bin/bash # wh-loopc.sh: Цикл перебора от 1 до 10.

LIMIT=10 a=1

while [ "$a" -le $LIMIT ] do echo -n "$a " let "a+=1" done # Пока ничего особенного.

echo; echo

# +=================================================================+

# А теперь оформим в стиле языка C.

((a = 1)) # a=1 # Двойные скобки допускают наличие лишних пробелов в выражениях.

while (( a <= LIMIT )) # В двойных скобках символ "$" перед переменными опускается. do echo -n "$a " ((a += 1)) # let "a+=1" # Двойные скобки позволяют наращивание переменной в стиле языка C. done

echo

# Теперь, программисты, пишущие на C, могут чувствовать себя в Bash как дома.

exit 0

Стандартное устройство ввода stdin, для цикла while, можно перенаправить на файл с помощью команды перенаправления < в конце цикла.

until

Оператор цикла until проверяет условие в начале каждой итерации, но в отличие от while итерация возможна только в том случае, если условие ложно.

until [condition-is-true]

do

 command...

done

Обратите внимание: оператор until проверяет условие завершения цикла ПЕРЕД очередной итерацией, а не после, как это принято в некоторых языках программирования.



Как и в случае с циклами for/in, при размещении ключевого слова do в одной строке с объявлением цикла, необходимо вставлять символ ";" перед do.

until [condition-is-true] ; do

Пример 10-18. Цикл until

#!/bin/bash

until [ "$var1" = end ] # Проверка условия производится в начале итерации. do echo "Введите значение переменной #1 " echo "(end - выход)" read var1 echo "значение переменной #1 = $var1" done

exit 0

10.2. Вложенные циклы

Цикл называется вложенным, если он размещается внутри другого цикла. На первом проходе, внешний цикл вызывает внутренний, который исполняется до своего завершения, после чего управление передается в тело внешнего цикла. На втором проходе внешний цикл опять вызывает внутренний. И так до тех пор, пока не завершится внешний цикл. Само собой, как внешний, так и внутренний циклы могут быть прерваны командой break.

Пример 10-19. Вложенный цикл

#!/bin/bash # Вложенные циклы "for".

outer=1 # Счетчик внешнего цикла.

# Начало внешнего цикла. for a in 1 2 3 4 5 do echo "Итерация #$outer внешнего цикла." echo "---------------------" inner=1 # Сброс счетчика вложенного цикла.

# Начало вложенного цикла. for b in 1 2 3 4 5 do echo "Итерация #$inner вложенного цикла." let "inner+=1" # Увеличить счетчик итераций вложенного цикла. done # Конец вложенного цикла.

let "outer+=1" # Увеличить счетчик итераций внешнего цикла. echo # Пустая строка для отделения итераций внешнего цикла. done # Конец внешнего цикла.

exit 0

Демонстрацию вложенных циклов "while" вы найдете в Пример 25-6, а вложение цикла "while" в "until" -- в Пример 25-8.

10.3. Управление ходом выполнения цикла

break, continue

Для управления ходом выполнения цикла служат команды break и continue [23] и точно соответствуют своим аналогам в других языках программирования. Команда break прерывает исполнение цикла, в то время как continue передает управление в начало цикло, минуя все последующие команды в теле цикла.



Пример 10-20. Команды break и continue в цикле

#!/bin/bash

LIMIT=19 # Верхний предел

echo echo "Печать чисел от 1 до 20 (исключая 3 и 11)."

a=0

while [ $a -le "$LIMIT" ] do a=$(($a+1))

if [ "$a" -eq 3 ] || [ "$a" -eq 11 ] # Исключить 3 и 11 then continue # Переход в начало цикла. fi

echo -n "$a " done

# Упражнение: # Почему число 20 тоже выводится?

echo; echo

echo Печать чисел от 1 до 20, но взгляните, что происходит после вывода числа 2

##################################################################

# Тот же цикл, только 'continue' заменено на 'break'.

a=0

while [ "$a" -le "$LIMIT" ] do a=$(($a+1))

if [ "$a" -gt 2 ] then break # Завершение работы цикла. fi

echo -n "$a " done

echo; echo; echo

exit 0

Команде break может быть передан необязательный параметр. Команда break без параметра прерывает тот цикл, в который она вставлена, а break N прерывает цикл, стоящий на N уровней выше (причем 1-й уровень -- это уровень текущего цикла, прим. перев.).

Пример 10-21. Прерывание многоуровневых циклов

#!/bin/bash # break-levels.sh: Прерывание циклов.

# "break N" прерывает исполнение цикла, стоящего на N уровней выше текущего.

for outerloop in 1 2 3 4 5 do echo -n "Группа $outerloop: "

for innerloop in 1 2 3 4 5 do echo -n "$innerloop "

if [ "$innerloop" -eq 3 ] then break # Попробуйте "break 2", # тогда будут прерываться как вложенный, так и внешний циклы fi done

echo done

echo

exit 0

Команда continue, как и команда break, может иметь необязательный параметр. В простейшем случае, команда continue передает управление в начало текущего цикла, а команда continue N прерывает исполнение текущего цикла и передает управление в начало внешнего цикла, отстоящего от текущего на N уровней (причем 1-й уровень -- это уровень текущего цикла, прим. перев.).

Пример 10-22. Передача управление в начало внешнего цикла



#!/bin/bash # Команда "continue N" передает управление в начало внешнего цикла, отстоящего от текущего на N уровней.

for outer in I II III IV V # внешний цикл do echo; echo -n "Группа $outer: "

for inner in 1 2 3 4 5 6 7 8 9 10 # вложенный цикл do

if [ "$inner" -eq 7 ] then continue 2 # Передача управления в начало цикла 2-го уровня. # попробуйте убрать параметр 2 команды "continue" fi

echo -n "$inner " # 8 9 10 никогда не будут напечатаны. done

done

echo; echo

# Упражнение: # Подумайте, где реально можно использовать "continue N" в сценариях.

exit 0

Пример 10-23. Живой пример использования "continue N"

# Albert Reiner привел пример использования "continue N": # ---------------------------------------------------------

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

while true do for n in .iso.* do [ "$n" = ".iso.opts" ] && continue beta=${n#.iso.} [ -r .Iso.$beta ] && continue [ -r .lock.$beta ] && sleep 10 && continue lockfile -r0 .lock.$beta || continue echo -n "$beta: " `date` run-isotherm $beta date ls -alF .Iso.$beta [ -r .Iso.$beta ] && rm -f .lock.$beta continue 2 done break done

# Конкретная реализация цикла, особенно sleep N, зависит от конкретных применений, #+ но в общем случае он строится по такой схеме:

while true do for job in {шаблон} do {файл уже обработан или обрабатывается} && continue {пометить файл как обрабатываемый, обработать, пометить как обработанный} continue 2 done break # Или что нибудь подобное `sleep 600', чтобы избежать завершения. done

# Этот сценарий завершит работу после того как все данные будут обработаны #+ (включая данные, которые поступили во время обработки).


Использование #+ соответствующих lock- файлоа позволяет вести обработку на нескольких машинах #+ одновременно, не производя дублирующих вычислений [которые, в моем случае, #+ выполняются в течении нескольких часов, так что для меня это очень важно]. #+ Кроме того, поскольку поиск необработанных файлов всегда начинается с #+ самого начала, можно задавать приоритеты в именах файлов. Конечно, можно #+ обойтись и без `continue 2', но тогда придется ввести дополнительную #+ проверку -- действительно ли был обработан тот или иной файл #+ (чтобы перейти к поиску следующего необработанного файла).

Конструкция continue N довольно сложна в понимании и применении, поэтому, вероятно лучше будет постараться избегать ее использования.

10.4. Операторы выбора

Инструкции case и select технически не являются циклами, поскольку не предусматривают многократное исполнение блока кода. Однако, они, как и циклы, управляют ходом исполнения программы, в зависимости от начальных или конечных условий.

case (in) / esac

Конструкция case эквивалентна конструкции switch в языке C/C++. Она позволяет выполнять тот или иной участок кода, в зависимости от результатов проверки условий. Она является, своего рода, краткой формой записи большого количества операторов if/then/else и может быть неплохим инструментом при создании разного рода меню.

case "$variable" in

 "$condition1" )

 command...

 ;;

 "$condition2" )

 command...

 ;;

esac

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

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

Каждый блок команд, отрабатывающих по заданному условию, должен завершаться двумя

символами точка-с-запятой ;;.

Блок case должен завершаться ключевым словом esac (case записанное в обратном порядке).

Пример 10-24. Использование case

#!/bin/bash

echo; echo "Нажмите клавишу и затем клавишу Return." read Keypress



case "$Keypress" in [a-z] ) echo "буква в нижнем регистре";; [A-Z] ) echo "Буква в верхнем регистре";; [0-9] ) echo "Цифра";; * ) echo "Знак пунктуации, пробел или что-то другое";; esac # Допускается указыватль диапазоны символов в [квадратных скобках].

# Упражнение: # -------- # Сейчас сценарий считывает нажатую клавишу и завершается. # Измените его так, чтобы сценарий продолжал отвечать на нажатия клавиш, # но завершался бы только после ввода символа "X". # Подсказка: заключите все в цикл "while".

exit 0

Пример 10-25. Создание меню с помощью case

#!/bin/bash

# Грубый пример базы данных

clear # Очистка экрана

echo " Список" echo " ------" echo "Выберите интересующую Вас персону:" echo echo "[E]vans, Roland" echo "[J]ones, Mildred" echo "[S]mith, Julie" echo "[Z]ane, Morris" echo

read person

case "$person" in # Обратите внимание: переменная взята в кавычки.

"E" | "e" ) # Пользователь может ввести как заглавную, так и строчную букву. echo echo "Roland Evans" echo "4321 Floppy Dr." echo "Hardscrabble, CO 80753" echo "(303) 734-9874" echo "(303) 734-9892 fax" echo "revans@zzy.net" echo "Старый друг и партнер по бизнесу" ;; # Обратите внимание: блок кода, анализирующий конкретный выбор, завершается # двумя символами "точка-с-запятой".

"J" | "j" ) echo echo "Mildred Jones" echo "249 E. 7th St., Apt. 19" echo "New York, NY 10009" echo "(212) 533-2814" echo "(212) 533-9972 fax" echo "milliej@loisaida.com" echo "Подружка" echo "День рождения: 11 февраля" ;;

# Информация о Smith и Zane будет добавлена позднее.

* ) # Выбор по-умолчанию. # "Пустой" ввод тоже обрабатывается здесь. echo echo "Нет данных." ;;



esac

echo

# Упражнение: # -------- # Измените этот сценарий таким образом, чтобы он не завершал работу #+ после вывода информации о персоне, а переходил на ожидание нового #+ ввода от пользователя.

exit 0

Очень хороший пример использования case для анализа аргументов, переданных из командной строки.

#! /bin/bash

case "$1" in "") echo "Порядок использования: ${0##*/} <filename>"; exit 65;; # Параметры командной строки отсутствуют, # или первый параметр -- "пустой". # Обратите внимание на ${0##*/} это подстановка параметра ${var##pattern}. В результате получается $0.

-*) FILENAME=./$1;; # Если имя файла (аргумент $1) начинается с "-", # то заменить его на ./$1 # тогда параметр не будет восприниматься как ключ команды.

* ) FILENAME=$1;; # В противном случае -- $1. esac

Пример 10-26. Оператор case допускает использовать подстановку команд вместо анализируемой переменной

#!/bin/bash # Подстановка команд в "case".

case $( arch ) in # команда "arch" возвращает строку, описывающую аппаратную апхитектуру. i386 ) echo "Машина на базе процессора 80386";; i486 ) echo "Машина на базе процессора 80486";; i586 ) echo "Машина на базе процессора Pentium";; i686 ) echo "Машина на базе процессора Pentium2 или выше";; * ) echo "Машина на другом типе процессора";; esac

exit 0

Оператор case допускает использование шаблонных конструкций.

Пример 10-27. Простой пример сравнения строк

#!/bin/bash # match-string.sh: простое сравнение строк

match_string () { MATCH=0 NOMATCH=90 PARAMS=2 # Функция требует два входных аргумента. BAD_PARAMS=91

[ $# -eq $PARAMS ] || return $BAD_PARAMS

case "$1" in "$2") return $MATCH;; * ) return $NOMATCH;; esac

}

a=one b=two c=three d=two

match_string $a # неверное число аргументов echo $? # 91

match_string $a $b # не равны echo $? # 90

match_string $b $d # равны echo $? # 0

exit 0

Пример 10-28.


Проверка ввода

#!/bin/bash # isalpha.sh: Использование "case" для анализа строк.

SUCCESS=0 FAILURE=-1

isalpha () # Проверка - является ли первый символ строки символом алфавита. { if [ -z "$1" ] # Вызов функции без входного аргумента? then return $FAILURE fi

case "$1" in [a-zA-Z]*) return $SUCCESS;; # Первый символ - буква? * ) return $FAILURE;; esac } # Сравните с функцией "isalpha ()" в языке C.

isalpha2 () # Проверка - состоит ли вся строка только из символов алфавита. { [ $# -eq 1 ] || return $FAILURE

case $1 in *[!a-zA-Z]*|"") return $FAILURE;; *) return $SUCCESS;; esac }

isdigit () # Проверка - состоит ли вся строка только из цифр. { # Другими словами - является ли строка целым числом. [ $# -eq 1 ] || return $FAILURE

case $1 in *[!0-9]*|"") return $FAILURE;; *) return $SUCCESS;; esac }

check_var () # Интерфейс к isalpha { if isalpha "$@" then echo "\"$*\" начинается с алфавитного символа." if isalpha2 "$@" then # Дальнейшая проверка не имеет смысла, если первй символ не буква. echo "\"$*\" содержит только алфавитные символы." else echo "\"$*\" содержит по меньшей мере один не алфавитный символ." fi else echo "\"$*\" начинсется с не алфавитного символа ." # Если функция вызвана без входного параметра, #+ то считается, что строка содержит "не алфавитной" символ. fi

echo

}

digit_check () # Интерфейс к isdigit (). { if isdigit "$@" then echo "\"$*\" содержит только цифры [0 - 9]." else echo "\"$*\" содержит по меньшей мере один не цифровой символ." fi

echo

}

a=23skidoo b=H3llo c=-What? d=What? e=`echo $b` # Подстановка команды. f=AbcDef g=27234 h=27a34 i=27.34

check_var $a check_var $b check_var $c check_var $d check_var $e check_var $f check_var # Вызов без параметра, что произойдет? # digit_check $g digit_check $h digit_check $i

exit 0 # Сценарий дополнен S.C.



# Упражнение: # -------- # Напишите функцию 'isfloat ()', которая проверяла бы вещественные числа. # Подсказка: Эта функция подобна функции 'isdigit ()', #+ надо лишь добавить анализ наличия десятичной точки.

select

Оператор select был заимствован из Korn Shell, и является еще одним инструментом, используемым при создании меню.

select variable [in list]

do

 command...

 break

done

Этот оператор предлагает пользователю выбрать один из представленных вариантов. Примечательно, что select по-умолчанию использует в качестве приглашения к вводу (prompt) -- PS3 (#? ), который легко изменить.

Пример 10-29. Создание меню с помощью select

#!/bin/bash

PS3='Выберите ваш любимый овощ: ' # строка приглашения к вводу (prompt)

echo

select vegetable in "бобы" "морковь" "картофель" "лук" "брюква" do echo echo "Вы предпочитаете $vegetable." echo ";-))" echo break # если 'break' убрать, то получится бесконечный цикл. done

exit 0

Если в операторе select список in list

не задан, то в качестве списка будет использоваться список аргументов ($@), передаваемый сценарию или функции.

Сравните это с поведением оператора цикла

for variable [in list]

в котором не задан список аргументов.

Пример 10-30. Создание меню с помощью select в функции

#!/bin/bash

PS3='Выберите ваш любимый овощ: '

echo

choice_of() { select vegetable # список выбора [in list] отсутствует, поэтому 'select' использует входные аргументы функции. do echo echo "Вы предпочитаете $vegetable." echo ";-))" echo break done }

choice_of бобы рис морковь редис томат шпинат # $1 $2 $3 $4 $5 $6 # передача списка выбора в функцию choice_of()

exit 0

См. так же Пример 34-3.

Глава 11. Внутренние команды

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



Действие, когда какая либо команда или сама командная оболочка инициирует (порождает) новый подпроцесс, что бы выполнить какую либо работу, называется ветвлением (forking) процесса. Новый процесс называется "дочерним" (или "потомком"), а породивший его процесс -- "родительским" (или "предком"). В результате и потомок и предок продолжают исполняться одновременно -- параллельно друг другу.

В общем случае, встроенные команды

Bash, при исполнении внутри сценария, не порождают новый подпроцесс, в то время как вызов внешних команд, как правило, приводит к созданию нового подпроцесса.
Внутренние команды могут иметь внешние аналоги. Например, внутренняя команда Bash -- echo имеет внешний аналог /bin/echo и их поведение практически идентично.

#!/bin/bash

echo "Эта строка выводится внутренней командой \"echo\"." /bin/echo "А эта строка выводится внешней командой the /bin/echo."

Ключевое слово (keyword) -- это зарезервированное слово, синтаксический элемент (token) или оператор. Ключевые слова имеют специальное назначение для командного интерпретатора, и фактически являются элементами синтаксиса языка командной оболочки. В качестве примера можно привести "for", "while", "do", "!", которые являются ключевыми (или зарезервированными) словами. Подобно встроенным командам, ключевые слова жестко зашиты в Bash, но в отличие от встроенных команд, ключевые слова не являются командами как таковыми, хотя при этом могут являться их составной частью. [24]

Ввод/вывод

echo

выводит (на stdout) выражение или содержимое переменной (см. Пример 4-1).

echo Hello echo $a

Для вывода экранированных символов, echo требует наличие ключа -e. См. Пример 5-2.

Обычно, командв echo выводит в конце символ перевода строки. Подавить вывод это символа можно ключом -n.

Команда echo может использоваться для передачи информации по конвейеру другим командам.

if echo "$VAR" | grep -q txt # if [[ $VAR = *txt* ]] then echo "$VAR содержит подстроку \"txt\"" fi

<


Кроме того, команда echo, в комбинации с подстановкой команд

может учавствовать в операции присвоения значения переменной.

a=`echo "HELLO" | tr A-Z a-z`

См. так же Пример 12-15, Пример 12-2, Пример 12-32 и Пример 12-33.

Следует запомнить, что команда echo `command` удалит все символы перевода строки, которые будут выведены командой command.

Переменная $IFS обычно содержит символ перевода строки \n, как один из вариантов пробельного символа. Bash разобьет вывод команды command, по пробельным символам, на аргументы и передаст их команде echo, которая выведет эти аргументы, разделенные пробелами.

bash$ ls -l /usr/share/apps/kjezz/sounds

-rw-r--r-- 1 root root 1407 Nov 7 2000 reflect.au -rw-r--r-- 1 root root 362 Nov 7 2000 seconds.au

bash$ echo `ls -l /usr/share/apps/kjezz/sounds`

total 40 -rw-r--r-- 1 root root 716 Nov 7 2000 reflect.au -rw-r--r-- 1 root root 362 Nov 7 2000 seconds.au

Это встроенная команда Bash и имеет внешний аналог /bin/echo.

bash$ type -a echo

echo is a shell builtin echo is /bin/echo

printf

printf -- команда форматированного вывода, расширенный вариант команды echo и ограниченный вариант библиотечной функции printf() в языке C, к тому же синтаксис их несколько отдичается друг от друга.

printf format-string... parameter...

Это встроенная команда Bash. Имеет внешний аналог /bin/printf или /usr/bin/printf. За более подробной информацией обращайтесь к страницам справочного руководства man 1 printf по системным командам.

Старые версии Bash могут не поддерживать команду printf.

Пример 11-1. printf в действии

#!/bin/bash # printf demo

# От переводчика: # Считаю своим долгом напомнить, что в качестве разделителя дробной и целой # частей в вещественных числах, может использоваться символ "запятая" # (в русских локалях), поэтому данный сценарий может выдавать сообщение # об ошибке (у меня так и произошло) при выводе числа PI. # Тогда попробуйте заменить в определении числа PI десятичную точку # на запятую -- это должно помочь. ;-)



PI=3,14159265358979 DecimalConstant=31373 Message1="Поздравляю," Message2="Землянин."

echo

printf "Число пи с точностью до 2 знака после запятой = %1.2f" $PI echo printf "Число пи с точностью до 9 знака после запятой = %1.9f" $PI # Даже округляет правильно.

printf "\n" # Перевод строки,

printf "Константа = \t%d\n" $DecimalConstant # Вставлен символ табуляции (\t)

printf "%s %s \n" $Message1 $Message2

echo

# ==========================================# # Эмуляция функции 'sprintf' в языке C. # Запись форматированной строки в переменную.

echo

Pi12=$(printf "%1.12f" $PI) echo "Число пи с точностью до 12 знака после запятой = $Pi12"

Msg=`printf "%s %s \n" $Message1 $Message2` echo $Msg; echo $Msg

exit 0

Одно из полезных применений команды printf -- форматированный вывод сообщений об ошибках

E_BADDIR=65

var=nonexistent_directory

error() { printf "$@" >&2 # Форматированный вывод аргументов на stderr. echo exit $E_BADDIR }

cd $var || error $"Невозможно перейти в каталог %s." "$var"

# Спасибо S.C.

read

"Читает" значение переменной с устройства стандартного ввода -- stdin, в интерактивном режиме это означает клавиатуру. Ключ -a позволяет записывать значения в массивы (см. Пример 25-3).

Пример 11-2. Ввод значений переменных с помощью read

#!/bin/bash

echo -n "дите значение переменной 'var1': " # Ключ -n подавляет вывод символа перевода строки.

read var1 # Обратите внимание -- перед именем переменной отсутствует символ '$'.

echo "var1 = $var1"

echo

# Одной командой 'read' можно вводить несколько переменных. echo -n "дите значения для переменных 'var2' и 'var3' (через пробел или табуляцию): " read var2 var3 echo "var2 = $var2 var3 = $var3" # Если было введено значение только одной переменной, то вторая останется "пустой".

exit 0

Если команде read не была передано ни одной переменной, то ввод будет осуществлен в переменную $REPLY.



Пример 11-3. Пример использования команды read без указания переменной для ввода

#!/bin/bash

echo

# -------------------------- # # Первый блок кода. echo -n "Введите значение: " read var echo "\"var\" = "$var"" # Здесь нет ничего неожиданного. # -------------------------- #

echo

echo -n "Введите другое значение: " read # Команда 'read' употребляется без указания переменной для ввода, #+ тем не менее... #+ По-умолчанию ввод осуществляется в переменную $REPLY. var="$REPLY" echo "\"var\" = "$var"" # Эта часть сценария эквивалентна первому блоку, выделенному выше.

echo

exit 0

Обычно, при вводе в окне терминала с помощью команды "read", символ \ служит для экранирования символа перевода строки. Ключ -r заставляет интерпретировать символ \ как обычный символ.

Пример 11-4. Ввод многострочного текста с помощью read

#!/bin/bash

echo

echo "Введите строку, завершающуюся символом \\, и нажмите ENTER." echo "Затем введите вторую строку, и снова нажмите ENTER." read var1 # При чтении, символ "\" экранирует перевод строки. # первая строка \ # вторая строка

echo "var1 = $var1" # var1 = первая строка вторая строка

# После ввода каждой строки, завершающейся символом "\", # вы можете продолжать ввод на другой строке.

echo; echo

echo "Введите другую строку, завершающуюся символом \\, и нажмите ENTER." read -r var2 # Ключ -r заставляет команду "read" воспринимать "\" # как обычный символ. # первая строка \

echo "var2 = $var2" # var2 = первая строка \

# Ввод данных прекращается сразу же после первого нажатия на клавишу ENTER.

echo

exit 0

Команда read имеет ряд очень любопытных опций, которые позволяют выводить подсказку - приглашение ко вводу (prompt), и даже читать данные не дожидаясь нажатия на клавишу ENTER.

# Чтение данных, не дожидаясь нажатия на клавишу ENTER.

read -s -n1 -p "Нажмите клавишу " keypress echo; echo "Была нажата клавиша "\"$keypress\""."



# -s -- подавляет эхо-вывод, т.е. ввод с клавиатуры не отображается на экране. # -n N -- ввод завершается автоматически, сразу же после ввода N-го символа. # -p -- задает вид строки подсказки - приглашения к вводу (prompt).

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

Ключ -n, кроме всего прочего, позволяет команде read обнаруживать нажатие курсорных и некоторых других служебных клавиш.

Пример 11-5. Обнаружение нажатия на курсорные клавиши

#!/bin/bash # arrow-detect.sh: Обнаружение нажатия на курсорные клавиши, и не только... # Спасибо Sandro Magi за то что показал мне -- как.

# -------------------------------------------- # Коды клавиш. arrowup='\[A' arrowdown='\[B' arrowrt='\[C' arrowleft='\[D' insert='\[2' delete='\[3' # --------------------------------------------

SUCCESS=0 OTHER=65

echo -n "Нажмите на клавишу... " # Может потребоваться нажать на ENTER, если была нажата клавиша # не входящая в список выше. read -n3 key # Прочитать 3 символа.

echo -n "$key" | grep "$arrowup" #Определение нажатой клавиши. if [ "$?" -eq $SUCCESS ] then echo "Нажата клавиша \"." exit $SUCCESS fi

echo -n "$key" | grep "$arrowdown" if [ "$?" -eq $SUCCESS ] then echo "Нажата клавиша \" exit $SUCCESS fi

echo -n "$key" | grep "$arrowrt" if [ "$?" -eq $SUCCESS ] then echo "Нажата клавиша \"О\"." exit $SUCCESS fi

echo -n "$key" | grep "$arrowleft" if [ "$?" -eq $SUCCESS ] then echo "Нажата клавиша \"." exit $SUCCESS fi

echo -n "$key" | grep "$insert" if [ "$?" -eq $SUCCESS ] then echo "Нажата клавиша \"Insert\"." exit $SUCCESS fi

echo -n "$key" | grep "$delete" if [ "$?" -eq $SUCCESS ] then echo "Нажата клавиша \"Delete\"." exit $SUCCESS fi



echo " Нажата какая-то другая клавиша."

exit $OTHER

# Упражнения: # --------- # 1) Упростите сценарий, заменив множество if-ов #+ одной конструкцией 'case'. # 2) Добавьте определение нажатий на клавиши "Home", "End", "PgUp" и "PgDn".

Ключ -t позволяет ограничивать время ожидания ввода командой read (см. Пример 9-4).

Команда read может считывать значения для переменных из файла, перенаправленного на stdin. Если файл содержит не одну строку, то переменной будет присвоена только первая строка. Если команде read будет передано несколько переменных, то первая строка файла будет разбита, по пробелам, на несколько подстрок, каждая из которых будет записана в свою переменную. Будьте осторожны!

Пример 11-6. Чтение командой read из файла через перенаправление

#!/bin/bash

read var1 <data-file echo "var1 = $var1" # Первая строка из "data-file" целиком записывается в переменную var1

read var2 var3 <data-file echo "var2 = $var2 var3 = $var3" # Обратите внимание! # Поведение команды "read" далеко от ожидаемого! # 1) Произошел возврат к началу файла. # 2) Вместо того, чтобы последовательно читать строки из файла, # по числу переменных, первая строка файла была разбита на подстроки, # разделенные пробелами, которые и были записаны в переменные. # 3) В последнюю переменную была записана вся оставшаяся часть строки. # 4) Если команде "read" будет передано большее число переменных, чем подстрок # в первой строке файла, то последние переменные останутся "пустыми".

echo "------------------------------------------------"

# Эта проблема легко разрешается с помощью цикла: while read line do echo "$line" done <data-file # Спасибо Heiner Steven за разъяснения.

echo "------------------------------------------------"

# Разбор строки, разделенной на поля # Для задания разделителя полей, используется переменная $IFS,

echo "Список всех пользователей:" OIFS=$IFS; IFS=: # В файле /etc/passwd, в качестве разделителя полей # используется символ ":" .


while read name passwd uid gid fullname ignore do echo "$name ($fullname)" done </etc/passwd # перенаправление ввода. IFS=$OIFS # Восстановление предыдущего состояния переменной $IFS. # Эту часть кода написал Heiner Steven.

# Если переменная $IFS устанавливается внутри цикла, #+ то отпадает необходимость сохранения ее первоначального значения #+ во временной переменной. # Спасибо Dim Segebart за разъяснения. echo "------------------------------------------------" echo "Список всех пользователей:"

while IFS=: read name passwd uid gid fullname ignore do echo "$name ($fullname)" done </etc/passwd # перенаправление ввода.

echo echo "Значение переменной \$IFS осталось прежним: $IFS"

exit 0

Передача информации, выводимой командой echo, по конвейеру команде read, будет вызывать ошибку.

Тем не менее, передача данных по конвейеру от cat, кажется срабатывает.

cat file1 file2 | while read line do echo $line done

Файловая система

cd

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

(cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)

[взято из упоминавшегося ранее примера]

Команда cd с ключом -P (physical) игнорирует символические ссылки.

Команда "cd -" выполняет переход в каталог $OLDPWD -- предыдущий рабочий каталог.

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

bash$ cd //

bash$ pwd

//

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

pwd

Выводит название текущего рабочего каталога (Print Working Directory) (см. Пример 11-7). Кроме того, имя текущего каталога хранится во внутренней переменной $PWD.

pushd, popd, dirs

Этот набор команд является составной частью механизма "закладок" на каталоги и позволяет перемещаться по каталогам вперед и назад в заданном порядке.


Для хранения имен каталогов используется стек (LIFO -- "последний вошел, первый вышел").

pushd dir-name

-- помещает имя текущего каталога в стек и осуществляет переход в каталог dir-name.

popd -- выталкивает, находящееся на вершине стека, имя каталога и одновременно осуществляет переход в каталог, оказавшийся на врешине стека.

dirs -- выводит содержимое стека каталогов (сравните с переменной $DIRSTACK). В случае успеха, обе команды -- pushd и popd автоматически вызывают dirs.

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

Пример 11-7. Смена текущего каталога

#!/bin/bash

dir1=/usr/local dir2=/var/spool

pushd $dir1 # Команда 'dirs' будет вызвана автоматически (на stdout будет выведено содержимое стека). echo "Выполнен переход в каталог `pwd`." # Обратные одиночные кавычки.

# Теперь можно выполнить какие либо действия в каталоге 'dir1'. pushd $dir2 echo "Выполнен переход в каталог `pwd`."

# Теперь можно выполнить какие либо действия в каталоге 'dir2'. echo "На вершине стека находится: $DIRSTACK." popd echo "Возврат в каталог `pwd`."

# Теперь можно выполнить какие либо действия в каталоге 'dir1'. popd echo "Возврат в первоначальный рабочий каталог `pwd`."

exit 0

Переменные

let

Команда let производит арифметические операции над переменными. В большинстве случаев, ее можно считать упрощенным вариантом команды expr.

Пример 11-8. Команда let, арифметические операции.

#!/bin/bash

echo

let a=11 # То же, что и 'a=11' let a=a+5 # Эквивалентно "a = a + 5" # (Двойные кавычки и дополнительные пробелы делают код более удобочитаемым) echo "11 + 5 = $a"

let "a <<= 3" # Эквивалентно let "a = a << 3" echo "\"\$a\" (=16) после сдвига влево на 3 разряда = $a"



let "a /= 4" # Эквивалентно let "a = a / 4" echo "128 / 4 = $a"

let "a -= 5" # Эквивалентно let "a = a - 5" echo "32 - 5 = $a"

let "a = a * 10" # Эквивалентно let "a = a * 10" echo "27 * 10 = $a"

let "a %= 8" # Эквивалентно let "a = a % 8" echo "270 mod 8 = $a (270 / 8 = 33, остаток = $a)"

echo

exit 0

eval

eval arg1 [arg2] ... [argN]

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

Пример 11-9. Демонстрация команды eval

#!/bin/bash

y=`eval ls -l` # Подобно y=`ls -l` echo $y # но символы перевода строки не выводятся, поскольку имя переменной не в кавычках. echo echo "$y" # Если имя переменной записать в кавычках -- символы перевода строки сохраняются.

echo; echo

y=`eval df` # Аналогично y=`df` echo $y # но без символов перевода строки.

# Когда производится подавление вывода символов LF (перевод строки), то анализ #+ результатов различными утилитами, такими как awk, можно сделать проще.

exit 0

Пример 11-10. Принудительное завершение сеанса

#!/bin/bash

y=`eval ps ax | sed -n '/ppp/p' | awk '{ print $1 }'` # Выяснить PID процесса 'ppp'.

kill -9 $y # "Прихлопнуть" его

# Предыдущие строки можно заменить одной строкой # kill -9 `ps ax | awk '/ppp/ { print $1 }'

chmod 666 /dev/ttyS3 # Завершенный, по сигналу SIGKILL, ppp изменяет права доступа # к последовательному порту. Вернуть их в первоначальное состояние.

rm /var/lock/LCK..ttyS3 # Удалить lock-файл последовательного порта.

exit 0

Пример 11-11. Шифрование по алгоритму "rot13"

#!/bin/bash # Реализация алгоритма шифрования "rot13" с помощью 'eval'. # Сравните со сценарием "rot13.sh".

setvar_rot_13() # Криптование по алгоритму "rot13" { local varname=$1 varvalue=$2 eval $varname='$(echo "$varvalue" | tr a-z n-za-m)' }

setvar_rot_13 var "foobar" # Пропустить слово "foobar" через rot13.


echo $var # sbbone

echo $var | tr a-z n-za-m # foobar # Расшифровывание.

# Пример предоставил Stephane Chazelas.

exit 0

Rory Winston представил следующий пример, как образец практического использования команды eval.

Пример 11-12. Замена имени переменной на ее значение, в исходном тексте программы на языке Perl, с помощью eval

В программе "test.pl", на языке Perl: ... my $WEBROOT = <WEBROOT_PATH>; ...

Эта попытка подстановки значения переменной вместо ее имени: $export WEBROOT_PATH=/usr/local/webroot $sed 's/<WEBROOT_PATH>/$WEBROOT_PATH/' < test.pl > out

даст такой результат: my $WEBROOT = $WEBROOT_PATH;

Тем не менее: $export WEBROOT_PATH=/usr/local/webroot $eval sed 's/<WEBROOT_PATH>/$WEBROOT_PATH/' < test.pl > out # ====

Этот вариант дал желаемый результат -- имя переменной, в тексте программы, благополучно было заменено на ее значение: my $WEBROOT = /usr/local/webroot

Команда eval может быть небезопасна. Если существует приемлемая альтернатива, то желательно воздерживаться от использования eval. Так, eval $COMMANDS исполняет код, который записан в переменную COMMANDS, которая, в свою очередь, может содержать весьма неприятные сюрпризы, например rm -rf *. Использование команды eval, для исполнения кода неизвестного происхождения, крайне опасно.

set

Команда set изменяет значения внутренних переменных сценария. Она может использоваться для переключения опций (ключей, флагов), определяющих поведение скрипта. Еще одно применение -- сброс/установка позиционных параметров (аргументов), значения которых будут восприняты как результат работы команды (set `command`).

Пример 11-13. Установка значений аргументов с помощью команды set

#!/bin/bash

# script "set-test"

# Вызовите сценарий с тремя аргументами командной строки, # например: "./set-test one two three".

echo echo "Аргументы перед вызовом set \`uname -a\` :" echo "Аргумент #1 = $1" echo "Аргумент #2 = $2" echo "Аргумент #3 = $3"



set `uname -a` # Изменение аргументов # значения которых берутся из результата работы `uname -a`

echo $_

echo "Аргументы после вызова set \`uname -a\` :" # $1, $2, $3 и т.д. будут переустановлены в соответствии с выводом #+ команды `uname -a` echo "Поле #1 'uname -a' = $1" echo "Поле #2 'uname -a' = $2" echo "Поле #3 'uname -a' = $3" echo --- echo $_ # --- echo

exit 0

Вызов set без параметров просто выводит список инициализированных переменных окружения.

bash$ set

AUTHORCOPY=/home/bozo/posts BASH=/bin/bash BASH_VERSION=$'2.05.8(1)-release' ... XAUTHORITY=/home/bozo/.Xauthority _=/etc/bashrc variable22=abc variable23=xzy

Если команда set используется с ключом "--", после которого следует переменная, то значение переменной переносится в позиционные параметры (аргументы). Если имя переменной отсутствует, то эта команда приводит к сбросу позиционных параметров.

Пример 11-14. Изменение значений позиционных параметров (аргументов)

#!/bin/bash

variable="one two three four five"

set -- $variable # Значения позиционных параметров берутся из "$variable".

first_param=$1 second_param=$2 shift; shift # сдвиг двух первых параметров. remaining_params="$*"

echo echo "первый параметр = $first_param" # one echo "второй параметр = $second_param" # two echo "остальные параметры = $remaining_params" # three four five

echo; echo

# Снова. set -- $variable first_param=$1 second_param=$2 echo "первый параметр = $first_param" # one echo "второй параметр = $second_param" # two

# ======================================================

set -- # Позиционные параметры сбрасываются, если не задано имя переменной.

first_param=$1 second_param=$2 echo "первый параметр = $first_param" # (пустое значение) echo "второй параметр = $second_param" # (пустое значение)

exit 0

См. так же Пример 10-2 и Пример 12-40.

unset

Команда unset удаляет переменную, фактически -- устанавливает ее значение в null.


ldd

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

bash$ ldd /bin/ls

libc.so.6 => /lib/libc.so.6 (0x4000c000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)

watch

Периодически запускает указанную программу с заданным интервалом времени.

По-умолчанию интервал между запусками принимается равным 2 секундам, но может быть изменен ключом -n.

watch -n 5 tail /var/log/messages # Выводит последние 10 строк из системного журнала, /var/log/messages, каждые пять секунд.

strip

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

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

nm

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

rdist

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

А теперь, используя полученные нами знания, попробуем разобраться с одним из системных сценариев. Один из самых коротких и простых -- это killall, который вызывается для остановки процессов при перезагрузке или выключении компьютера.

Пример 13-8. Сценарий killall, из каталога /etc/rc.d/init.d

#!/bin/sh

# --> Комментарии, начинающиеся с "# -->", добавлены автором документа.

# --> Этот сценарий является частью пакета 'rc'-сценариев # --> Автор: Miquel van Smoorenburg, <miquels@drinkel.nl.mugnet.org>

# --> Этот сценарий характерен для дистрибутива Red Hat # --> (в других дистрибутивах может отсутствовать).

# Остановить все ненужные сервисы которые еще работают (собственно, # их уже не должно быть, это лишь формальная проверка, на всякий случай)



for i in /var/lock/subsys/*; do # --> Стандартный заголовок цикла for/in, но, поскольку "do" # --> находится в той же самой строке, что и for, # --> необходимо разделить их символом ";". # Проверяется наличие сценария. [ ! -f $i ] && continue # --> Очень интересное использование "И-списка", эквивалентно: # --> if [ ! -f "$i" ]; then continue

# Получить имя подсистемы. subsys=${i#/var/lock/subsys/} # --> В данном случае совпадает с именем файла. # --> Это точный эквивалент subsys=`basename $i`.

# --> Таким образом получается имя файла блокировки (если он присутствует, # -->+ то это означает, что процесс запущен). # --> См. описание команды "lockfile" выше.

# Остановить службу. if [ -f /etc/rc.d/init.d/$subsys.init ]; then /etc/rc.d/init.d/$subsys.init stop else /etc/rc.d/init.d/$subsys stop # --> Останавливает задачу или демона # --> посредством встроенной команды 'stop'. fi done

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

Упражнение 1. Просмотрите сценарий halt в каталоге /etc/rc.d/init.d. Он по размеру немного больше, чем killall, но придерживается той же концепции. Создайте копию этого сценария в своем домашнем каталоге и поэкспериментируйте с ним (НЕ запускайте его с привилегиями суперпользователя). Попробуйте запустить его с ключами -vn (sh -vn scriptname). Добавьте свои комментарии. Замените действующие команды на "echo".

Упражнение 2. Просмотрите другие, более сложные сценарии из /etc/rc.d/init.d. Попробуйте разобраться в их работе. Проверьте их работу, следуя рекомендациям, приведенным выше. За дополнительной информацией вы можете обратиться к документу sysvinitfiles в каталоге /usr/share/doc/initscripts-?.??, который входит в пакет документации к "initscripts".

Глава 14. Подстановка команд

Подстановка команд -- это подстановка результатов выполнения команды [43] или даже серии команд; буквально, эта операция позволяет вызвать команду в другом окружении.



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

script_name=`basename $0` echo "Имя этого файла-сценария: $script_name."

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

rm `cat filename` # здесь "filename" содержит список удаляемых файлов. # # S. C. предупреждает, что в данном случае может возникнуть ошибка "arg list too long". # Такой вариант будет лучше: xargs rm -- < filename # ( -- подходит для случая, когда "filename" начинается с символа "-" )

textfile_listing=`ls *.txt` # Переменная содержит имена всех файлов *.txt в текущем каталоге. echo $textfile_listing

textfile_listing2=$(ls *.txt) # Альтернативный вариант. echo $textfile_listing2 # Результат будет тем же самым.

# Проблема записи списка файлов в строковую переменную состоит в том, # что символы перевода строки заменяются на пробел. # # Как вариант решения проблемы -- записывать список файлов в массив. # shopt -s nullglob # При несоответствии, имя файла игнорируется. # textfile_listing=( *.txt ) # # Спасибо S.C.

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

COMMAND `echo a b` # 2 аргумента: a и b

COMMAND "`echo a b`" # 1 аргумент: "a b"

COMMAND `echo` # без аргументов

COMMAND "`echo`" # один пустой аргумент

# Спасибо S.C.

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

# cd "`pwd`" # Должна выполняться всегда. # Однако...

mkdir 'dir with trailing newline '

cd 'dir with trailing newline '

cd "`pwd`" # Ошибка: # bash: cd: /tmp/dir with trailing newline: No such file or directory

cd "$PWD" # Выполняется без ошибки.

old_tty_setting=$(stty -g) # Сохранить настройки терминала. echo "Нажмите клавишу " stty -icanon -echo # Запретить "канонический" режим терминала. # Также запрещает эхо-вывод. key=$(dd bs=1 count=1 2> /dev/null) # Поймать нажатие на клавишу. stty "$old_tty_setting" # Восстановить настройки терминала. echo "Количество нажатых клавиш = ${#key}." # ${#variable} = количество символов в переменной $variable # # Нажмите любую клавишу, кроме RETURN, на экране появится "Количество нажатых клавиш = 1." # Нажмите RETURN, и получите: "Количество нажатых клавиш = 0." # Символ перевода строки будет "съеден" операцией подстановки команды.

Спасибо S.C.

<


При выводе значений переменных, полученных в результате подстановки команд, командой echo, без кавычек, символы перевода строки будут удалены. Это может оказаться неприятным сюрпризом.

dir_listing=`ls -l` echo $dir_listing # без кавычек

# Вы наверно ожидали увидеть удобочитаемый список каталогов.

# Однако, вы получите: # total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo # bozo 51 May 15 20:57 t2.sh -rwxr-xr-x 1 bozo bozo 217 Mar 5 21:13 wi.sh

# Символы перевода строки были заменены пробелами.

echo "$dir_listing" # в кавычках # -rw-rw-r-- 1 bozo 30 May 13 17:15 1.txt # -rw-rw-r-- 1 bozo 51 May 15 20:57 t2.sh # -rwxr-xr-x 1 bozo 217 Mar 5 21:13 wi.sh

Подстановка команд позволяет даже записывать в переменные содержимое целых файлов, с помощью перенаправления или команды cat.

variable1=`<file1` # Записать в переменную "variable1" содержимое файла "file1". variable2=`cat file2` # Записать в переменную "variable2" содержимое файла "file2".

# Замечание 1: # Удаляются символы перевода строки. # # Замечание 2: # В переменные можно записать даже управляющие символы.

# Выдержки из системного файла /etc/rc.d/rc.sysinit #+ (Red Hat Linux)

if [ -f /fsckoptions ]; then fsckoptions=`cat /fsckoptions` ... fi # # if [ -e "/proc/ide/${disk[$device]}/media" ] ; then hdmedia=`cat /proc/ide/${disk[$device]}/media` ... fi # # if [ ! -n "`uname -r | grep -- "-"`" ]; then ktag="`cat /proc/version`" ... fi # # if [ $usb = "1" ]; then sleep 5 mouseoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=02"` kbdoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=01"` ... fi

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

Пример 14-1. Глупая выходка

#!/bin/bash # stupid-script-tricks.sh: Люди! Будьте благоразумны! # Из "Глупые выходки", том I.

dangerous_variable=`cat /boot/vmlinuz` # Сжатое ядро Linux.

echo "длина строки \$dangerous_variable = ${#dangerous_variable}" # длина строки $dangerous_variable = 794151 # ('wc -c /boot/vmlinuz' даст другой результат.)

# echo "$dangerous_variable" # Даже не пробуйте раскомментарить эту строку! Это приведет к зависанию сценария.

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

exit 0

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

<


Подстановка команд, позволяет записать в переменную результаты выполнения цикла. Ключевым моментом здесь является команда echo, в теле цикла.