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

phonexa.com          

Пример 22-3. Наибольшее из двух чисел

#!/bin/bash # max.sh: Наибольшее из двух целых чисел.

E_PARAM_ERR=-198 # Если функции передано меньше двух параметров. EQUAL=-199 # Возвращаемое значение, если числа равны.

max2 () # Возвращает наибольшее из двух чисел. { # Внимание: сравниваемые числа должны быть меньше 257. if [ -z "$2" ] then return $E_PARAM_ERR fi

if [ "$1" -eq "$2" ] then return $EQUAL else if [ "$1" -gt "$2" ] then return $1 else return $2 fi fi }

max2 33 34 return_val=$?

if [ "$return_val" -eq $E_PARAM_ERR ] then echo "Функции должно быть передано два аргумента." elif [ "$return_val" -eq $EQUAL ] then echo "Числа равны." else echo "Наибольшее из двух чисел: $return_val." fi

exit 0

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



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

count_lines_in_etc_passwd() { [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd)) # Если файл /etc/passwd доступен на чтение, то в переменную REPLY заносится число строк. # Возвращаются как количество строк, так и код завершения. }

if count_lines_in_etc_passwd then echo "В файле /etc/passwd найдено $REPLY строк." else echo "Невозможно подсчитать число строк в файле /etc/passwd." fi

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

Пример 22-4. Преобразование чисел в римскую форму записи

#!/bin/bash



# Преобразование чисел из арабской формы записи в римскую # Диапазон: 0 - 200

# Расширение диапазона представляемых чисел и улучшение сценария # оставляю вам, в качестве упражнения.

# Порядок использования: roman number-to-convert

LIMIT=200 E_ARG_ERR=65 E_OUT_OF_RANGE=66

if [ -z "$1" ] then echo "Порядок использования: `basename $0` number-to-convert" exit $E_ARG_ERR fi

num=$1 if [ "$num" -gt $LIMIT ] then echo "Выход за границы диапазона!" exit $E_OUT_OF_RANGE fi

to_roman () # Функция должна быть объявлена до того как она будет вызвана. { number=$1 factor=$2 rchar=$3 let "remainder = number - factor" while [ "$remainder" -ge 0 ] do echo -n $rchar let "number -= factor" let "remainder = number - factor" done

return $number # Упражнение: # -------- # Объясните -- как работает функция. # Подсказка: деление последовательным вычитанием. }

to_roman $num 100 C num=$? to_roman $num 90 LXXXX num=$? to_roman $num 50 L num=$? to_roman $num 40 XL num=$? to_roman $num 10 X num=$? to_roman $num 9 IX num=$? to_roman $num 5 V num=$? to_roman $num 4 IV num=$? to_roman $num 1 I

echo

exit 0

См. также Пример 10-28.

Наибольшее положительное целое число, которое может вернуть функция -- 255. Команда return очень тесно связана с понятием код завершения, что объясняет это специфическое ограничение. К счастью существуют различные способы

преодоления этого ограничения.

Пример 22-5. Проверка возможности возврата функциями больших значений

#!/bin/bash # return-test.sh

# Наибольшее целое число, которое может вернуть функция, не может превышать 256.

return_test () # Просто возвращает то, что ей передали. { return $1 }

return_test 27 # o.k. echo $? # Возвращено число 27.

return_test 255 # o.k. echo $? # Возвращено число 255.

return_test 257 # Ошибка! echo $? # Возвращено число 1.

return_test -151896 # Как бы то ни было, но для больших отрицательных чисел проходит! echo $? # Возвращено число -151896.

exit 0

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

Еще один способ -- использовать глобальные переменные для хранения "возвращаемого значения".

Return_Val= # Глобальная переменная, которая хранит значение, возвращаемое функцией.

alt_return_test () { fvar=$1 Return_Val=$fvar return # Возвратить 0 (успешное завершение). }

alt_return_test 1 echo $? # 0 echo "Функция вернула число $Return_Val" # 1

alt_return_test 255 echo "Функция вернула число $Return_Val" # 255

alt_return_test 257 echo "Функция вернула число $Return_Val" # 257

alt_return_test 25701 echo "Функция вернула число $Return_Val" #25701

Пример 22-6. Сравнение двух больших целых чисел

#!/bin/bash # max2.sh: Наибольшее из двух БОЛЬШИХ целых чисел.

# Это модификация предыдущего примера "max.sh", # которая позволяет выполнять сравнение больших целых чисел.

EQUAL=0 # Если числа равны. MAXRETVAL=255 # Максимально возможное положительное число, которое может вернуть функция. E_PARAM_ERR=-99999 # Код ошибки в параметрах. E_NPARAM_ERR=99999 # "Нормализованный" код ошибки в параметрах.

max2 () # Возвращает наибольшее из двух больших целых чисел. { if [ -z "$2" ] then return $E_PARAM_ERR fi

if [ "$1" -eq "$2" ] then return $EQUAL else if [ "$1" -gt "$2" ] then retval=$1 else retval=$2 fi fi

# -------------------------------------------------------------- # # Следующие строки позволяют "обойти" ограничение if [ "$retval" -gt "$MAXRETVAL" ] # Если больше предельного значения, then # то let "retval = (( 0 - $retval ))" # изменение знака числа. # (( 0 - $VALUE )) изменяет знак числа. fi # Функции имеют возможность возвращать большие *отрицательные* числа. # -------------------------------------------------------------- #

return $retval }

max2 33001 33997 return_val=$?

# -------------------------------------------------------------------------- # if [ "$return_val" -lt 0 ] # Если число отрицательное, then # то let "return_val = (( 0 - $return_val ))" # опять изменить его знак. fi # "Абсолютное значение" переменной $return_val. # -------------------------------------------------------------------------- #

if [ "$return_val" -eq "$E_NPARAM_ERR" ] then # Признак ошибки в параметрах, при выходе из функции так же поменял знак. echo "Ошибка: Недостаточно аргументов." elif [ "$return_val" -eq "$EQUAL" ] then echo "Числа равны." else echo "Наиболшее число: $return_val." fi

exit 0

См. также Пример A-8.

Упражнение:

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

<


Перенаправление

Перенаправление ввода для функций

Функции -- суть есть блок кода, а это означает, что устройство stdin для функций может быть переопределено (перенаправление stdin) (как в Пример 3-1).

Пример 22-7. Настоящее имя пользователя

#!/bin/bash

# По имени пользователя получить его "настоящее имя" из /etc/passwd.

ARGCOUNT=1 # Ожидается один аргумент. E_WRONGARGS=65

file=/etc/passwd pattern=$1

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

file_excerpt () # Производит поиск в файле по заданному шаблону, выводит требуемую часть строки. { while read line do echo "$line" | grep $1 | awk -F":" '{ print $5 }' # Указывет awk использовать ":" как разделитель полей. done } <$file # Подменить stdin для функции.

file_excerpt $pattern

# Да, этот сценарий можно уменьшить до # grep PATTERN /etc/passwd | awk -F":" '{ print $5 }' # или # awk -F: '/PATTERN/ {print $5}' # или # awk -F: '($1 == "username") { print $5 }' # Однако, это было бы не так поучительно.

exit 0

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

# Вместо: Function () { ... } < file

# Попробуйте так: Function () { { ... } < file }

# Похожий вариант,

Function () # Тоже работает. { { echo $* } | tr a b }

Function () # Этот вариант не работает. { echo $* } | tr a b # Наличие вложенного блока кода -- обязательное условие.

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

22.2. Локальные переменные

Что такое "локальная" переменная?

локальные переменные

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



Пример 22-8. Область видимости локальных переменных

#!/bin/bash

func () { local loc_var=23 # Объявление локальной переменной. echo echo "\"loc_var\" в функции = $loc_var" global_var=999 # Эта переменная не была объявлена локальной. echo "\"global_var\" в функции = $global_var" }

func

# Проверим, "видна" ли локальная переменная за пределами функции.

echo echo "\"loc_var\" за пределами функции = $loc_var" # "loc_var" за пределами функции = # Итак, $loc_var не видна в глобальном контексте. echo "\"global_var\" за пределами функции = $global_var" # "global_var" за пределами функции = 999 # $global_var имеет глобальную область видимости. echo

exit 0

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

переменных.

#!/bin/bash

func () { global_var=37 # Эта переменная будет считаться необъявленной #+ до тех пор, пока функция не будет вызвана. } # КОНЕЦ ФУНКЦИИ

echo "global_var = $global_var" # global_var = # Функция "func" еще не была вызвана, #+ поэтому $global_var пока еще не "видна" здесь.

func echo "global_var = $global_var" # global_var = 37 # Переменная была инициализирована в функции.

22.2.1. Локальные переменные делают возможной рекурсию.

Хотя локальные переменные и допускают рекурсию, [52] но она сопряжена с большими накладными расходами и не рекомендуется для использования в сценариях. [53]

Пример 22-9. Использование локальных переменных при рекурсии

#!/bin/bash

# факториал # ---------

# Действительно ли bash допускает рекурсию? # Да! Но... # Нужно быть действительно дубинноголовым, чтобы использовать ее в сценариях # на языке командной оболочки.

MAX_ARG=5 E_WRONG_ARGS=65 E_RANGE_ERR=66

if [ -z "$1" ] then echo "Порядок использования: `basename $0` число" exit $E_WRONG_ARGS fi

if [ "$1" -gt $MAX_ARG ] then echo "Выход за верхний предел (максимально возможное число -- 5)." # Вернитесь к реальности. # Если вам захочется поднять верхнюю границу, # то перепишите эту программу на настоящем языке программирования.


exit $E_RANGE_ERR fi

fact () { local number=$1 # Переменная "number" должна быть объявлена как локальная, # иначе результат будет неверный. if [ "$number" -eq 0 ] then factorial=1 # Факториал числа 0 = 1. else let "decrnum = number - 1" fact $decrnum # Рекурсивный вызов функции. let "factorial = $number * $?" fi

return $factorial }

fact $1 echo "Факториал числа $1 = $?."

exit 0

Еще один пример использования рекурсии вы найдете в Пример A-18. Не забывайте, что рекурсия весьма ресурсоемкое удовольствие, к тому же она выполняется слишком медленно, поэтому не следует использовать ее в сценариях.

Глава 23. Псевдонимы

Псевдонимы в Bash -- это ни что иное, как "горячие клавиши", средство, позволяющее избежать набора длинных строк в командной строке. Если, к примеру, в файл ~/.bashrc вставить строку alias lm="ls -l | more", то потом вы сможете экономить свои силы и время, набирая команду lm, вместо более длинной ls -l | more. Установив alias rm="rm -i"

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

Псевдонимы в сценариях могут иметь весьма ограниченную область применения. Было бы здорово, если бы псевдонимы имели функциональность, присущую макроопределениям в языке C, но, к сожалению, Bash не может "разворачивать" аргументы в теле псевдонима. [54] Кроме того, попытка обратиться к псевдониму, созданному внутри "составных конструкций", таких как if/then, циклы и функции, будет приводить к появлению ошибок. Практически всегда, действия, возлагаемые на псевдоним, более эффективно могут быть выполнены с помощью функций.

Пример 23-1. Псевдонимы в сценарии

#!/bin/bash

shopt -s expand_aliases # Эта опция должна быть включена, иначе сценарий не сможет "разворачивать" псевдонимы.

alias ll="ls -l" # В определении псевдонима можно использовать как одиночные ('), так и двойные (") кавычки.



echo "Попытка обращения к псевдониму \"ll\":" ll /usr/X11R6/bin/mk* #* Работает.

echo

directory=/usr/X11R6/bin/ prefix=mk* # Определить -- не будет ли проблем с шаблонами. echo "Переменные \"directory\" + \"prefix\" = $directory$prefix" echo

alias lll="ls -l $directory$prefix"

echo "Попытка обращения к псевдониму \"lll\":" lll # Список всех файлов в /usr/X11R6/bin, чьи имена начинаются с mk. # Псевдонимы могут работать с шаблонами.

TRUE=1

echo

if [ TRUE ] then alias rr="ls -l" echo "Попытка обращения к псевдониму \"rr\", созданному внутри if/then:" rr /usr/X11R6/bin/mk* #* В результате -- сообщение об ошибке! # К псевдонимам, созданным внутри составных инструкций, нельзя обратиться. echo "Однако, ранее созданный псевдоним остается работоспособным:" ll /usr/X11R6/bin/mk* fi

echo

count=0 while [ $count -lt 3 ] do alias rrr="ls -l" echo "Попытка обращения к псевдониму \"rrr\", созданному внутри цикла \"while\":" rrr /usr/X11R6/bin/mk* #* Так же возникает ошибка. # alias.sh: line 57: rrr: command not found let count+=1 done

echo; echo

alias xyz='cat $0' # Сценарий печатает себя самого. # Обратите внимание на "строгие" кавычки. xyz # Похоже работает, #+ хотя документация Bash утверждает, что такой псевдоним не должен работать. # # Steve Jacobson отметил, что #+ параметр "$0" интерпретируется непосредственно, во время объявления псевдонима.

exit 0

Команда unalias удаляет псевдоним, объявленный ранее .

Пример 23-2. unalias: Объявление и удаление псевдонимов

#!/bin/bash

shopt -s expand_aliases # Разрешить "разворачивание" псевдонимов.

alias llm='ls -al | more' llm

echo

unalias llm # Удалить псевдоним. llm # Сообщение об ошибке, т.к. команда 'llm' больше не распознается.

exit 0

bash$ ./unalias.sh

total 6 drwxrwxr-x 2 bozo bozo 3072 Feb 6 14:04 . drwxr-xr-x 40 bozo bozo 2048 Feb 6 14:04 .. -rwxr-xr-x 1 bozo bozo 199 Feb 6 14:04 unalias.sh



./unalias.sh: llm: command not found

Глава 24. Списки команд

Средством обработки последовательности из нескольких команд служат списки: "И-списки" и "ИЛИ-списки". Они эффективно могут заменить сложную последовательность вложенных if/then или даже case.

Объединение команд в цепочки

И-список

command-1 && command-2 && command-3 && ... command-n

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

Пример 24-1. Проверка аргументов командной строки с помощью "И-списка"

#!/bin/bash # "И-список"

if [ ! -z "$1" ] && echo "Аргумент #1 = $1" && [ ! -z "$2" ] && echo "Аргумент #2 = $2" then echo "Сценарию передано не менее 2 аргументов." # Все команды в цепочке возвращают true. else echo "Сценарию передано менее 2 аргументов." # Одна из команд в списке вернула false. fi # Обратите внимание: "if [ ! -z $1 ]" тоже работает, но, казалось бы эквивалентный вариант # if [ -n $1 ] -- нет. Однако, если добавить кавычки # if [ -n "$1" ] то все работает. Будьте внимательны! # Проверяемые переменные лучше всегда заключать в кавычки.

# То же самое, только без списка команд. if [ ! -z "$1" ] then echo "Аргумент #1 = $1" fi if [ ! -z "$2" ] then echo "Аргумент #2 = $2" echo "Сценарию передано не менее 2 аргументов." else echo "Сценарию передано менее 2 аргументов." fi # Получилось менее элегантно и длиннее, чем с использованием "И-списка".

exit 0

Пример 24-2. Еще один пример проверки аргументов с помощью "И-списков"

#!/bin/bash

ARGS=1 # Ожидаемое число аргументов. E_BADARGS=65 # Код завершения, если число аргументов меньше ожидаемого.



test $# -ne $ARGS && echo "Порядок использования: `basename $0` $ARGS аргумент(а)(ов)" && exit $E_BADARGS # Если проверка первого условия возвращает true (неверное число аргументов), # то исполняется остальная часть строки, и сценарий завершается.

# Строка ниже выполняется только тогда, когда проверка выше не проходит. # обратите внимание на условие "-ne" -- "не равно" (прим. перев.) echo "Сценарию передано корректное число аргументов."

exit 0

# Проверьте код завершения сценария командой "echo $?".

Конечно же, с помощью И-списка можно присваивать переменным значения по-умолчанию.

arg1=$@ # В $arg1 записать аргументы командной строки.

[ -z "$arg1" ] && arg1=DEFAULT # Записать DEFAULT, если аргументы командной строки отсутствуют.

ИЛИ-список

command-1 || command-2 || command-3 || ... command-n

Каждая последующая команда, в таком списке, выполняется только тогда, когда предыдущая команда вернула код завершения false (не ноль). Если какая-либо из команд возвращает true (ноль), то исполнение списка команд в этом месте завершается, т.е. следующие далее команды не выполняются. Очевидно, что "ИЛИ-списки" имеют смысл обратный, по отношению к "И-спискам"

Пример 24-3. Комбинирование "ИЛИ-списков" и "И-списков"

#!/bin/bash

# delete.sh, утилита удаления файлов. # Порядок использования: delete имя_файла

E_BADARGS=65

if [ -z "$1" ] then echo "Порядок использования: `basename $0` имя_файла" exit $E_BADARGS # Если не задано имя файла. else file=$1 # Запомнить имя файла. fi

[ ! -f "$file" ] && echo "Файл \"$file\" не найден. \ Робкий отказ удаления несуществующего файла." # И-СПИСОК, выдать сообщение об ошибке, если файл не существует. # Обратите внимание: выводимое сообщение продолжается во второй строке, # благодаря экранированию символа перевода строки.

[ ! -f "$file" ] || (rm -f $file; echo "Файл \"$file\" удален.") # ИЛИ-СПИСОК, удаляет существующий файл.



# Обратите внимание на логические условия. # И-СПИСОК отрабатывает по true, ИЛИ-СПИСОК -- по false.

exit 0

Списки возвращают код завершения последней выполненной команды.

Комбинируя "И" и "ИЛИ" списки, легко "перемудрить" с логическими условиями, поэтому, в таких случаях может потребоваться детальная отладка.

false && true || echo false # false

# Тот же результат дает ( false && true ) || echo false # false # Но не эта комбинация false && ( true || echo false ) # (нет вывода на экран)

# Обратите внимание на группировку и порядок вычисления условий -- слева-направо, #+ поскольку логические операции "&&" и "||" имеют равный приоритет.

# Если вы не уверены в своих действиях, то лучше избегать таких сложных конструкций.

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

См. Пример A-8 и Пример 7-4, иллюстрирующие использование И/ИЛИ-списков для проверки переменных.

Глава 25. Массивы

Новейшие версии Bash поддерживают одномерные массивы. Инициализация элементов массива может быть произведена в виде: variable[xx]. Можно явно объявить массив в сценарии, с помощью директивы declare: declare -a variable. Обращаться к отдельным элементам массива можно с помощью фигурных скобок, т.е.: ${variable[xx]}.

Пример 25-1. Простой массив

#!/bin/bash

area[11]=23 area[13]=37 area[51]=UFOs

# Массивы не требуют, чтобы последовательность элементов в массиве была непрерывной.

# Некоторые элементы массива могут оставаться неинициализированными. # "Дыркм" в массиве не являются ошибкой.

echo -n "area[11] = " echo ${area[11]} # необходимы {фигурные скобки}

echo -n "area[13] = " echo ${area[13]}

echo "содержимое area[51] = ${area[51]}."

# Обращение к неинициализированным элементам дает пустую строку. echo -n "area[43] = " echo ${area[43]} echo "(элемент area[43] -- неинициализирован)"

echo

# Сумма двух элементов массива, записанная в третий элемент area[5]=`expr ${area[11]} + ${area[13]}` echo "area[5] = area[11] + area[13]" echo -n "area[5] = " echo ${area[5]}



area[6]=`expr ${area[11]} + ${area[51]}` echo "area[6] = area[11] + area[51]" echo -n "area[6] = " echo ${area[6]} # Эта попытка закончится неудачей, поскольку сложение целого числа со строкой не допускается.

echo; echo; echo

# ----------------------------------------------------------------- # Другой массив, "area2". # И другой способ инициализации массива... # array_name=( XXX YYY ZZZ ... )

area2=( ноль один два три четыре )

echo -n "area2[0] = " echo ${area2[0]} # Ага, индексация начинается с нуля (первый элемент массива имеет индекс [0], а не [1]).

echo -n "area2[1] = " echo ${area2[1]} # [1] -- второй элемент массива. # -----------------------------------------------------------------

echo; echo; echo

# ----------------------------------------------- # Еще один массив, "area3". # И еще один способ инициализации... # array_name=([xx]=XXX [yy]=YYY ...)

area3=([17]=семнадцать [21]=двадцать_один)

echo -n "area3[17] = " echo ${area3[17]}

echo -n "area3[21] = " echo ${area3[21]} # -----------------------------------------------

exit 0

Bash позволяет оперировать переменными, как массивами, даже если они не были явно объявлены таковыми.

string=abcABC123ABCabc echo ${string[@]} # abcABC123ABCabc echo ${string[*]} # abcABC123ABCabc echo ${string[0]} # abcABC123ABCabc echo ${string[1]} # Ничего не выводится! # Почему? echo ${#string[@]} # 1 # Количество элементов в массиве.

# Спасибо Michael Zick за этот пример.

Эти примеры еще раз подтверждают отсутствие контроля типов в Bash.

Пример 25-2. Форматирование стихотворения

#!/bin/bash # poem.sh

# Строки из стихотворения (одна строфа). Line[1]="Мой дядя самых честных правил," Line[2]="Когда не в шутку занемог;" Line[3]="Он уважать себя заставил," Line[4]="И лучше выдумать не мог." Line[5]="Его пример другим наука..."

# Атрибуты. Attrib[1]=" А.С. Пушкин" Attrib[2]="\"Евгений Онегин\""



for index in 1 2 3 4 5 # Пять строк. do printf " %s\n" "${Line[index]}" done

for index in 1 2 # Две строки дополнительных атрибутов. do printf " %s\n" "${Attrib[index]}" done

exit 0

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

array=( ноль один два три четыре пять )

echo ${array[0]} # ноль echo ${array:0} # ноль # Подстановка параметра -- первого элемента. echo ${array:1} # оль # Подстановка параметра -- первого элемента, #+ начиная с позиции #1 (со 2-го символа).

echo ${#array} # 4 # Длина первого элемента массива.

array2=( [0]="первый элемент" [1]="второй элемент" [3]="четвертый элемент" )

echo ${array2[0]} # первый элемент echo ${array2[1]} # второй элемент echo ${array2[2]} # # Элемент неинициализирован, поэтому на экран ничего не выводится. echo ${array2[3]} # четвертый элемент

При работе с массивами, некоторые встроенные команды Bash имеют несколько иной смысл. Например, unset -- удаляет отдельные элементы массива, или даже массив целиком.

Пример 25-3. Некоторые специфичные особенности массивов

#!/bin/bash

declare -a colors # Допускается объявление массива без указания его размера.

echo "Введите ваши любимые цвета (разделяя их пробелами)."

read -a colors # Введите хотя бы 3 цвета для демонстрации некоторых свойств массивов. # Специфический ключ команды 'read', #+ позволяющий вводить несколько элементов массива.

echo

element_count=${#colors[@]}

# Получение количества элементов в массиве. # element_count=${#colors[*]} -- дает тот же результат. # # Переменная "@" позволяет "разбивать" строку в кавычках на отдельные слова #+ (выделяются слова, разделенные пробелами).

index=0

while [ "$index" -lt "$element_count" ] do # Список всех элементов в массиве. echo ${colors[$index]} let "index = $index + 1" done # Каждый элемент массива выводится в отдельной строке. # Если этого не требуется, то используйте echo -n "${colors[$index]} " # # Эквивалентный цикл "for": # for i in "${colors[@]}" # do # echo "$i" # done # (Спасибо S.C.)



echo

# Еще один, более элегантный, способ вывода списка всех элементов массива. echo ${colors[@]} # ${colors[*]} дает тот же результат.

echo

# Команда "unset" удаляет элементы из массива, или даже массив целиком. unset colors[1] # Удаление 2-го элемента массива. # Тот же эффект дает команда colors[1]= echo ${colors[@]} # Список всех элементов массива -- 2-й элемент отсутствует.

unset colors # Удаление всего массива. # Тот же эффект имеют команды unset colors[*] #+ и unset colors[@]. echo; echo -n "Массив цветов опустошен." echo ${colors[@]} # Список элементов массива пуст.

exit 0

Как видно из предыдущего примера, обращение к ${array_name[@]} или ${array_name[*]} относится ко всем элементам массива. Чтобы получить количество элементов массива, можно обратиться к ${#array_name[@]} или к ${#array_name[*]}. ${#array_name} -- это длина (количество символов) первого элемента массива, т.е. ${array_name[0]}.

Пример 25-4. Пустые массивы и пустые элементы

#!/bin/bash # empty-array.sh

# Выражаю свою благодарность Stephane Chazelas за этот пример, #+ и Michael Zick за его доработку.

# Пустой массив -- это не то же самое, что массив с пустыми элементами.

array0=( первый второй третий ) array1=( '' ) # "array1" имеет один пустой элемент. array2=( ) # Массив "array2" не имеет ни одного элемента, т.е. пуст.

echo ListArray() { echo echo "Элементы массива array0: ${array0[@]}" echo "Элементы массива array1: ${array1[@]}" echo "Элементы массива array2: ${array2[@]}" echo echo "Длина первого элемента массива array0 = ${#array0}" echo "Длина первого элемента массива array1 = ${#array1}" echo "Длина первого элемента массива array2 = ${#array2}" echo echo "Число элементов в массиве array0 = ${#array0[*]}" # 3 echo "Число элементов в массиве array1 = ${#array1[*]}" # 1 (сюрприз!) echo "Число элементов в массиве array2 = ${#array2[*]}" # 0 }

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



ListArray

# Попробуем добавить новые элементы в массивы

# Добавление новых элементов в массивы. array0=( "${array0[@]}" "новый1" ) array1=( "${array1[@]}" "новый1" ) array2=( "${array2[@]}" "новый1" )

ListArray

# или array0[${#array0[*]}]="новый2" array1[${#array1[*]}]="новый2" array2[${#array2[*]}]="новый2"

ListArray

# Теперь представим каждый массив как 'стек' ('stack') # Команды выше, можно считать командами 'push' -- добавление нового значения на вершину стека # 'Глубина' стека: height=${#array2[@]} echo echo "Глубина стека array2 = $height"

# Команда 'pop' -- выталкивание элемента стека, находящегося на вершине: unset array2[${#array2[@]}-1] # Индексация массивов начинается с нуля height=${#array2[@]} echo echo "POP" echo "Глубина стека array2, после выталкивания = $height"

ListArray

# Вывести только 2-й и 3-й элементы массива array0 from=1 # Индексация массивов начинается с нуля to=2 # declare -a array3=( ${array0[@]:1:2} ) echo echo "Элементы массива array3: ${array3[@]}"

# Замена элементов по шаблону declare -a array4=( ${array0[@]/второй/2-й} ) echo echo "Элементы массива array4: ${array4[@]}"

# Замена строк по шаблону declare -a array5=( ${array0[@]//новый?/старый} ) echo echo "Элементы массива array5: ${array5[@]}"

# Надо лишь привыкнуть к такой записи... declare -a array6=( ${array0[@]#*новый} ) echo # Это может вас несколько удивить echo "Элементы массива array6: ${array6[@]}"

declare -a array7=( ${array0[@]#новый1} ) echo # Теперь это вас уже не должно удивлять echo "Элементы массива array7: ${array7[@]}"

# Выглядить очень похоже на предыдущий вариант... declare -a array8=( ${array0[@]/новый1/} ) echo echo "Элементы массива array8: ${array8[@]}"

# Итак, что вы можете сказать обо всем этом?

# Строковые операции выполняются последовательно, над каждым элементом #+ в массиве var[@]. # Таким образом, BASH поддерживает векторные операции # Если в результате операции получается пустая строка, то #+ элемент массива "исчезает".



# Вопрос: это относится к строкам в "строгих" или "мягких" кавычках?

zap='новый*' declare -a array9=( ${array0[@]/$zap/} ) echo echo "Элементы массива array9: ${array9[@]}"

# "...А с платформы говорят: "Это город Ленинград!"..." declare -a array10=( ${array0[@]#$zap} ) echo echo "Элементы массива array10: ${array10[@]}"

# Сравните массивы array7 и array10 # Сравните массивы array8 и array9

# Ответ: в "мягких" кавычках.

exit 0

Разница между ${array_name[@]} и ${array_name[*]} такая же, как между $@ и $*. Эти свойства массивов широко применяются на практике.

# Копирование массивов. array2=( "${array1[@]}" ) # или array2="${array1[@]}"

# Добавить элемент. array=( "${array[@]}" "новый элемент" ) # или array[${#array[*]}]="новый элемент"

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

Операция подстановки команд -- array=( element1 element2 ... elementN ), позволяет загружать содержимое текстовых файлов в массивы.

#!/bin/bash

filename=sample_file

# cat sample_file # # 1 a b c # 2 d e fg

declare -a array1

array1=( `cat "$filename" | tr '\n' ' '`) # Загрузка содержимого файла # $filename в массив array1. # Вывод на stdout. # с заменой символов перевода строки на пробелы.

echo ${array1[@]} # список элементов массива. # 1 a b c 2 d e fg # # Каждое "слово", в текстовом файле, отделяемое от других пробелами #+ заносится в отдельный элемент массива.

element_count=${#array1[*]} echo $element_count # 8

Пример 25-5. Копирование и конкатенация массивов

#! /bin/bash # CopyArray.sh # # Автор: Michael Zick. # Используется с его разрешения.

# "Принять из массива с заданным именем записать в массив с заданным именем" #+ или "собственный Оператор Присваивания".

CpArray_Mac() {

# Оператор Присваивания

echo -n 'eval ' echo -n "$2" # Имя массива-результата echo -n '=( ${' echo -n "$1" # Имя исходного массива echo -n '[@]} )'



# Все это могло бы быть объединено в одну команду. # Это лишь вопрос стиля. }

declare -f CopyArray # "Указатель" на функцию CopyArray=CpArray_Mac # Оператор Присваивания

Hype() {

# Исходный массив с именем в $1. # (Слить с массивом, содержащим "-- Настоящий Рок-н-Ролл".) # Вернуть результат в массиве с именем $2.

local -a TMP local -a hype=( -- Настоящий Рок-н-Ролл )

$($CopyArray $1 TMP) TMP=( ${TMP[@]} ${hype[@]} ) $($CopyArray TMP $2) }

declare -a before=( Advanced Bash Scripting ) declare -a after

echo "Массив before = ${before[@]}"

Hype before after

echo "Массив after = ${after[@]}"

# Еще?

echo "Что такое ${after[@]:4:2}?"

declare -a modest=( ${after[@]:2:1} ${after[@]:3:3} ) # ---- выделение подстроки ----

echo "Массив Modest = ${modest[@]}"

# А что в массиве 'before' ?

echo "Массив Before = ${before[@]}"

exit 0

--

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

Пример 25-6. Старая, добрая: "Пузырьковая" сортировка

#!/bin/bash # bubble.sh: "Пузырьковая" сортировка.

# На каждом проходе по сортируемому массиву, #+ сравниваются два смежных элемента, и, если необходимо, они меняются местами. # В конце первого прохода, самый "тяжелый" элемент "опускается" в конец массива. # В конце второго прохода, следующий по "тяжести" элемент занимает второе место снизу. # И так далее. # Каждый последующий проход требует на одно сравнение меньше предыдущего. # Поэтому вы должны заметить ускорение работы сценария на последних проходах.

exchange() { # Поменять местами два элемента массива. local temp=${Countries[$1]} # Временная переменная Countries[$1]=${Countries[$2]} Countries[$2]=$temp

return }

declare -a Countries # Объявление массива, #+ необязательно, поскольку он явно инициализируется ниже.

# Допустимо ли выполнять инициализацию массива в нескольки строках? # ДА!



Countries=( Нидерланды Украина Заир Турция Россия Йемен Сирия \ Бразилия Аргентина Никарагуа Япония Мексика Венесуэла Греция Англия \ Израиль Перу Канада Оман Дания Уэльс Франция Кения \ Занаду Катар Лихтенштейн Венгрия)

# "Занаду" -- это мифическое государство, где, согласно Coleridge, #+ Kubla Khan построил величественный дворец.

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

echo "0: ${Countries[*]}" # Список элементов несортированного массива.

number_of_elements=${#Countries[@]} let "comparisons = $number_of_elements - 1"

count=1 # Номер прохода.

while [ "$comparisons" -gt 0 ] # Начало внешнего цикла do

index=0 # Сбросить индекс перед началом каждого прохода.

while [ "$index" -lt "$comparisons" ] # Начало внутреннего цикла do if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ] # Если элементы стоят не по порядку... # Оператор \> выполняет сравнение ASCII-строк #+ внутри одиночных квадратных скобок.

# if [[ ${Countries[$index]} > ${Countries[`expr $index + 1`]} ]] #+ дает тот же результат. then exchange $index `expr $index + 1` # Поменять местами. fi let "index += 1" done # Конец внутреннего цикла

let "comparisons -= 1" # Поскольку самый "тяжелый" элемент уже "опустился" на дно, #+ то на каждом последующем проходе нужно выполнять на одно сравнение меньше.

echo echo "$count: ${Countries[@]}" # Вывести содержимое массива после каждого прохода. echo let "count += 1" # Увеличить счетчик проходов.

done # Конец внешнего цикла

exit 0

--

Можно ли вложить один массив в другой?

#!/bin/bash # Вложенный массив.

# Автор: Michael Zick.

AnArray=( $(ls --inode --ignore-backups --almost-all \ --directory --full-time --color=none --time=status \ --sort=time -l ${PWD} ) ) # Команды и опции.

# Пробелы важны . . .

SubArray=( ${AnArray[@]:11:1} ${AnArray[@]:6:5} ) # Массив имеет два элемента, каждый из которых, в свою очередь, является массивом.

echo "Текущий каталог и дата последнего изменения:" echo "${SubArray[@]}"



exit 0

--

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

Пример 25-7. Вложенные массивы и косвенные ссылки

#!/bin/bash # embedded-arrays.sh # Вложенные массивы и косвенные ссылки.

# Автор: Dennis Leeuw. # Используется с его разрешения. # Дополнен автором документа.

ARRAY1=( VAR1_1=value11 VAR1_2=value12 VAR1_3=value13 )

ARRAY2=( VARIABLE="test" STRING="VAR1=value1 VAR2=value2 VAR3=value3" ARRAY21=${ARRAY1[*]} ) # Вложение массива ARRAY1 в массив ARRAY2.

function print () { OLD_IFS="$IFS" IFS=$'\n' # Вывод каждого элемента массива #+ в отдельной строке. TEST1="ARRAY2[*]" local ${!TEST1} # Посмотрите, что произойдет, если убрать эту строку. # Косвенная ссылка. # Позволяет получить доступ к компонентам $TEST1 #+ в этой функции.

# Посмотрим, что получилось. echo echo "\$TEST1 = $TEST1" # Просто имя переменной. echo; echo echo "{\$TEST1} = ${!TEST1}" # Вывод на экран содержимого переменной. # Это то, что дает #+ косвенная ссылка. echo echo "-------------------------------------------"; echo echo

# Вывод переменной echo "Переменная VARIABLE: $VARIABLE"

# Вывод элементов строки IFS="$OLD_IFS" TEST2="STRING[*]" local ${!TEST2} # Косвенная ссылка (то же, что и выше). echo "Элемент VAR2: $VAR2 из строки STRING"

# Вывод элемента массива TEST2="ARRAY21[*]" local ${!TEST2} # Косвенная ссылка. echo "Элемент VAR1_1: $VAR1_1 из массива ARRAY21" }

print echo

exit 0

--

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

Пример 25-8. Пример реализации алгоритма Решето Эратосфена

#!/bin/bash # sieve.sh



# Решето Эратосфена # Очень старый алгоритм поиска простых чисел.

# Этот сценарий выполняется во много раз медленнее # чем аналогичная программа на C.

LOWER_LIMIT=1 # Начиная с 1. UPPER_LIMIT=1000 # До 1000. # (Вы можете установить верхний предел и выше... если вам есть чем себя занять.)

PRIME=1 NON_PRIME=0

declare -a Primes # Primes[] -- массив.

initialize () { # Инициализация массива.

i=$LOWER_LIMIT until [ "$i" -gt "$UPPER_LIMIT" ] do Primes[i]=$PRIME let "i += 1" done # Все числа в заданном диапазоне считать простыми, # пока не доказано обратное. }

print_primes () { # Вывод индексов элементов массива Primes[], которые признаны простыми.

i=$LOWER_LIMIT

until [ "$i" -gt "$UPPER_LIMIT" ] do

if [ "${Primes[i]}" -eq "$PRIME" ] then printf "%8d" $i # 8 пробелов перед числом придают удобочитаемый табличный вывод на экран. fi

let "i += 1"

done

}

sift () # Отсеивание составных чисел. {

let i=$LOWER_LIMIT+1 # Нам известно, что 1 -- это простое число, поэтому начнем с 2.

until [ "$i" -gt "$UPPER_LIMIT" ] do

if [ "${Primes[i]}" -eq "$PRIME" ] # Не следует проверять вторично числа, которые уже признаны составными. then

t=$i

while [ "$t" -le "$UPPER_LIMIT" ] do let "t += $i " Primes[t]=$NON_PRIME # Все числа, которые делятся на $t без остатка, пометить как составные. done

fi

let "i += 1" done

}

# Вызов функций. initialize sift print_primes # Это называется структурным программированием.

echo

exit 0

# ----------------------------------------------- # # Код, приведенный ниже, не исполняется из-за команды exit, стоящей выше.

# Улучшенная версия, предложенная Stephane Chazelas, # работает несколько быстрее.

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

UPPER_LIMIT=$1 # Из командной строки. let SPLIT=UPPER_LIMIT/2 # Рассматривать делители только до середины диапазона.



Primes=( '' $(seq $UPPER_LIMIT) )

i=1 until (( ( i += 1 ) > SPLIT )) # Числа из верхней половины диапазона могут не рассматриваться. do if [[ -n $Primes[i] ]] then t=$i until (( ( t += i ) > UPPER_LIMIT )) do Primes[t]= done fi done echo ${Primes[*]}

exit 0

Сравните этот сценарий с генератором простых чисел, не использующим массивов, Пример A-18.

--

Массивы позволяют эмулировать некоторые структуры данных, поддержка которых в Bash не предусмотрена.

Пример 25-9. Эмуляция структуры "СТЕК" ("первый вошел -- последний вышел")

#!/bin/bash # stack.sh: Эмуляция структуры "СТЕК" ("первый вошел -- последний вышел")

# Подобно стеку процессора, этот "стек" сохраняет и возвращает данные по принципу #+ "первый вошел -- последний вышел".

BP=100 # Базовый указатель на массив-стек. # Дно стека -- 100-й элемент.

SP=$BP # Указатель вершины стека. # Изначально -- стек пуст.

Data= # Содержимое вершины стека. # Следует использовать дополнительную переменную, #+ из-за ограничений на диапазон возвращаемых функциями значений.

declare -a stack

push() # Поместить элемент на вершину стека. { if [ -z "$1" ] # А вообще, есть что помещать на стек? then return fi

let "SP -= 1" # Переместить указатель стека. stack[$SP]=$1

return }

pop() # Снять элемент с вершины стека. { Data= # Очистить переменную.

if [ "$SP" -eq "$BP" ] # Стек пуст? then return fi # Это предохраняет от выхода SP за границу стека -- 100,

Data=${stack[$SP]} let "SP += 1" # Переместить указатель стека. return }

status_report() # Вывод вспомогательной информации. { echo "-------------------------------------" echo "ОТЧЕТ" echo "Указатель стека SP = $SP" echo "Со стека был снят элемент \""$Data"\"" echo "-------------------------------------" echo }

# ======================================================= # А теперь позабавимся.



echo

# Попробуем вытолкнуть что- нибудь из пустого стека. pop status_report

echo

push garbage pop status_report # Втолкнуть garbage, вытолкнуть garbage.

value1=23; push $value1 value2=skidoo; push $value2 value3=FINAL; push $value3

pop # FINAL status_report pop # skidoo status_report pop # 23 status_report # Первый вошел -- последний вышел!

# Обратите внимание как изменяется указатель стека на каждом вызове функций push и pop.

echo # =======================================================

# Упражнения: # -----------

# 1) Измените функцию "push()" таким образом, # + чтобы она позволяла помещать на стек несколько значений за один вызов.

# 2) Измените функцию "pop()" таким образом, # + чтобы она позволяла снимать со стека несколько значений за один вызов.

# 3) Попробуйте написать простейший калькулятор, выполняющий 4 арифметических действия? # + используя этот пример.

exit 0

--

Иногда, манипуляции с "индексами" массивов могут потребовать введения переменных для хранения промежуточных результатов. В таких случаях вам предоставляется лишний повод подумать о реализации проекта на более мощном языке программирования, например Perl или C.

Пример 25-10. Исследование математических последовательностей

#!/bin/bash

# Пресловутая "Q-последовательность" Дугласа Хольфштадтера *Douglas Hofstadter):

# Q(1) = Q(2) = 1 # Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), для n>2

# Это "хаотическая" последовательность целых чисел с непредсказуемым поведением. # Первые 20 членов последовательности: # 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12

# См. книгу Дугласа Хольфштадтера, "Goedel, Escher, Bach: An Eternal Golden Braid", # p. 137, ff.

LIMIT=100 # Найти первые 100 членов последовательности LINEWIDTH=20 # Число членов последовательности, выводимых на экран в одной строке

Q[1]=1 # Первые два члена последовательности равны 1. Q[2]=1

echo echo "Q-последовательность [первые $LIMIT членов]:" echo -n "${Q[1]} " # Вывести первые два члена последовательности.


echo -n "${Q[2]} "

for ((n=3; n <= $LIMIT; n++)) # C-подобное оформление цикла. do # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]] для n>2 # Это выражение необходимо разбить на отдельные действия, # поскольку Bash не очень хорошо поддерживает сложные арифметические действия над элементами массивов.

let "n1 = $n - 1" # n-1 let "n2 = $n - 2" # n-2

t0=`expr $n - ${Q[n1]}` # n - Q[n-1] t1=`expr $n - ${Q[n2]}` # n - Q[n-2]

T0=${Q[t0]} # Q[n - Q[n-1]] T1=${Q[t1]} # Q[n - Q[n-2]]

Q[n]=`expr $T0 + $T1` # Q[n - Q[n-1]] + Q[n - Q[n-2]] echo -n "${Q[n]} "

if [ `expr $n % $LINEWIDTH` -eq 0 ] # Если выведено очередные 20 членов в строке. then # то echo # перейти на новую строку. fi

done

echo

exit 0

# Этот сценарий реализует итеративный алгоритм поиска членов Q-последовательности. # Рекурсивную реализацию, как более интуитивно понятную, оставляю вам, в качестве упражнения. # Внимание: рекурсивный поиск членов последовательности будет занимать *очень* продолжительное время.

--

Bash поддерживает только одномерные массивы, но, путем небольших ухищрений, можно эмулировать многомерные массивы.

Пример 25-11. Эмуляция массива с двумя измерениями

#!/bin/bash # Эмуляция двумерного массива.

# Второе измерение представлено как последовательность строк.

Rows=5 Columns=5

declare -a alpha # char alpha [Rows] [Columns]; # Необязательное объявление массива.

load_alpha () { local rc=0 local index

for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y do local row=`expr $rc / $Columns` local column=`expr $rc % $Rows` let "index = $row * $Rows + $column" alpha[$index]=$i # alpha[$row][$column] let "rc += 1" done

# Более простой вариант # declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y ) # но при таком объявлении второе измерение массива завуалировано. }

print_alpha () { local row=0 local index

echo

while [ "$row" -lt "$Rows" ] # Вывод содержимого массива построчно do

local column=0



while [ "$column" -lt "$Columns" ] do let "index = $row * $Rows + $column" echo -n "${alpha[index]} " # alpha[$row][$column] let "column += 1" done

let "row += 1" echo

done

# Более простой эквивалент: # echo ${alpha[*]} | xargs -n $Columns

echo }

filter () # Отфильтровывание отрицательных индексов. {

echo -n " "

if [[ "$1" -ge 0 && "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]] then let "index = $1 * $Rows + $2" echo -n " ${alpha[index]}" # alpha[$row][$column] fi

}

rotate () # Поворот массива на 45 градусов { local row local column

for (( row = Rows; row > -Rows; row-- )) # В обратном порядке. do

for (( column = 0; column < Columns; column++ )) do

if [ "$row" -ge 0 ] then let "t1 = $column - $row" let "t2 = $column" else let "t1 = $column" let "t2 = $column + $row" fi

filter $t1 $t2 # Отфильтровать отрицательный индекс. done

echo; echo

done

# Поворот массива выполнен на основе примеров (стр. 143-146) # из книги "Advanced C Programming on the IBM PC", автор Herbert Mayer # (см. библиографию).

}

#-----------------------------------------------------# load_alpha # Инициализация массива. print_alpha # Вывод на экран. rotate # Повернуть на 45 градусов против часовой стрелки. #-----------------------------------------------------#

# Упражнения: # ----------- # 1) Сделайте инициализацию и вывод массива на экран # + более простым и элегантным способом. # # 2) Объясните принцип работы функции rotate().

exit 0

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

Более сложный пример эмуляции двумерного массива вы найдете в Пример A-11.

Глава 26. Файлы

сценарии начальной загрузки



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

/etc/profile

Настройки системы по-умолчанию, главным образом настраивается окружение командной оболочки (все Bourne-подобные оболочки, не только Bash [55])

/etc/bashrc

функции и псевдонимы Bash

$HOME/.bash_profile

пользовательские настройки окружения Bash, находится в домашнем каталоге у каждого пользователя (локальная копия файла /etc/profile)

$HOME/.bashrc

пользовательский файл инициализации Bash, находится в домашнем каталоге у каждого пользователя (локальная копия файла /etc/bashrc). См. Приложение Gпример файла .bashrc.

Сценарий выхода из системы (logout)

$HOME/.bash_logout

Этот сценарий отрабатывает, когда пользователь выходит из системы.

Глава 27. /dev и /proc

Как правило, Linux или UNIX система имеет два каталога специального назначения: /dev и /proc.

27.1. /dev

Каталог /dev содержит файлы физических устройств, которые могут входить в состав аппаратного обеспечения компьютера. [56] Каждому из разделов не жестком диске соответствует свой файл-устройство в каталоге /dev, информация о которых может быть получена простой командой df.

bash$ df

Filesystem 1k-blocks Used Available Use% Mounted on /dev/hda6 495876 222748 247527 48% / /dev/hda1 50755 3887 44248 9% /boot /dev/hda8 367013 13262 334803 4% /home /dev/hda5 1714416 1123624 503704 70% /usr

Кроме того, каталог /dev содержит loopback-устройства ("петлевые" устройства), например /dev/loop0. С помощью такого устройства можно представить обычный файл как блочное устройство ввода/вывода. [57] Это позволяет монтировать целые файловые системы, находящиеся в отдельных больших файлах. См. Пример 13-6 и Пример 13-5.

Отдельные псевдоустройства в /dev имеют особое назначение, к таким устройствам можно отнести /dev/null, /dev/zero и /dev/urandom.

27.2. /proc

Фактически, каталог /proc -- это виртуальная файловая система. Файлы, в каталоге /proc, содержат информацию о процессах, о состоянии и конфигурации ядра и системы.



bash$ cat /proc/devices

Character devices: 1 mem 2 pty 3 ttyp 4 ttyS 5 cua 7 vcs 10 misc 14 sound 29 fb 36 netlink 128 ptm 136 pts 162 raw 254 pcmcia

Block devices: 1 ramdisk 2 fd 3 ide0 9 md

bash$ cat /proc/interrupts

CPU0 0: 84505 XT-PIC timer 1: 3375 XT-PIC keyboard 2: 0 XT-PIC cascade 5: 1 XT-PIC soundblaster 8: 1 XT-PIC rtc 12: 4231 XT-PIC PS/2 Mouse 14: 109373 XT-PIC ide0 NMI: 0 ERR: 0

bash$ cat /proc/partitions

major minor #blocks name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq

3 0 3007872 hda 4472 22260 114520 94240 3551 18703 50384 549710 0 111550 644030 3 1 52416 hda1 27 395 844 960 4 2 14 180 0 800 1140 3 2 1 hda2 0 0 0 0 0 0 0 0 0 0 0 3 4 165280 hda4 10 0 20 210 0 0 0 0 0 210 210 ...

bash$ cat /proc/loadavg

0.13 0.42 0.27 2/44 1119

Сценарии командной оболочки могут извлекать необходимую информацию из соответствующих файлов в каталоге /proc. [58]

bash$ cat /proc/filesystems | grep iso9660

iso9660

kernel_version=$( awk '{ print $3 }' /proc/version )

CPU=$( awk '/model name/ {print $4}' < /proc/cpuinfo )

if [ $CPU = Pentium ] then выполнить_ряд_специфичных_команд ... else выполнить_ряд_других_специфичных_команд ... fi

В каталоге /proc вы наверняка заметите большое количество подкаталогов, с не совсем обычными именами, состоящими только из цифр. Каждый из них соответствует исполняющемуся процессу, а имя каталога -- это ID (идентификатор) процесса. Внутри каждого такого подкаталога находится ряд файлов, в которых содержится полезная информация о соответствующих процессах. Файлы stat и status хранят статистику работы процесса, cmdline -- команда, которой был запущен процесс, exe -- символическая ссылка на исполняемый файл программы. Здесь же вы найдете ряд других файлов, но, с точки зрения написания сценариев, они не так интересны, как эти четыре.

Пример 27-1. Поиск файла программы по идентификатору процесса

#!/bin/bash # pid-identifier.sh: Возвращает полный путь к исполняемому файлу программы по идентификатору процесса (pid).



ARGNO=1 # Число, ожидаемых из командной строки, аргументов. E_WRONGARGS=65 E_BADPID=66 E_NOSUCHPROCESS=67 E_NOPERMISSION=68 PROCFILE=exe

if [ $# -ne $ARGNO ] then echo "Порядок использования: `basename $0` PID-процесса" >&2 # Сообщение об ошибке на >stderr. exit $E_WRONGARGS fi

ps ax

pidno=$( ps ax | grep $1 | awk '{ print $1 }' | grep $1 ) # Проверка наличия процесса с заданным pid в списке, выданном командой "ps", поле #1. # Затем следует убедиться, что этот процесс не был запущен этим сценарием ('ps'). # Это делает последний "grep $1". if [ -z "$pidno" ] # Если после фильтрации получается пустая строка, then # то это означает, что в системе нет процесса с заданым pid. echo "Нет такого процесса." exit $E_NOSUCHPROCESS fi

# Альтернативный вариант: # if ! ps $1 > /dev/null 2>&1 # then # в системе нет процесса с заданым pid. # echo "Нет такого процесса." # exit $E_NOSUCHPROCESS # fi

if [ ! -r "/proc/$1/$PROCFILE" ] # Проверить право на чтение. then echo "Процесс $1 найден, однако..." echo "у вас нет права на чтение файла /proc/$1/$PROCFILE." exit $E_NOPERMISSION # Обычный пользователь не имеет прав # на доступ к некоторым файлам в каталоге /proc. fi

# Последние две проверки могут быть заменены на: # if ! kill -0 $1 > /dev/null 2>&1 # '0' -- это не сигнал, но # команда все равно проверит наличие # процесса-получателя. # then echo "Процесс с данным PID не найден, либо вы не являетесь его владельцем" >&2 # exit $E_BADPID # fi

exe_file=$( ls -l /proc/$1 | grep "exe" | awk '{ print $11 }' ) # Или exe_file=$( ls -l /proc/$1/exe | awk '{print $11}' ) # # /proc/pid-number/exe -- это символическая ссылка # на исполняемый файл работающей программы.

if [ -e "$exe_file" ] # Если файл /proc/pid-number/exe существует... then # то существует и соответствующий процесс. echo "Исполняемый файл процесса #$1: $exe_file." else echo "Нет такого процесса." fi



# В большинстве случаев, этот, довольно сложный сценарий, может быть заменен командой # ps ax | grep $1 | awk '{ print $5 }' # В большинстве, но не всегда... # поскольку пятое поле листинга,выдаваемого командой 'ps', это argv[0] процесса, # а не путь к исполняемому файлу. # # Однако, оба следующих варианта должны работать безотказно. # find /proc/$1/exe -printf '%l\n' # lsof -aFn -p $1 -d txt | sed -ne 's/^n//p'

# Автор последнего комментария: Stephane Chazelas.

exit 0

Пример 27-2. Проверка состояния соединения

#!/bin/bash

PROCNAME=pppd # демон ppp PROCFILENAME=status # Что смотреть. NOTCONNECTED=65 INTERVAL=2 # Период проверки -- раз в 2 секунды.

pidno=$( ps ax | grep -v "ps ax" | grep -v grep | grep $PROCNAME | awk '{ print $1 }' ) # Найти идентификатор процесса 'pppd', 'ppp daemon'. # По пути убрать из листинга записи о процессах, порожденных сценарием. # # Однако, как отмечает Oleg Philon, #+ Эта последовательность команд может быть заменена командой "pidof". # pidno=$( pidof $PROCNAME ) # # Мораль: #+ Когда последовательность команд становится слишком сложной, #+ это повод к тому, чтобы поискать более короткий вариант.

if [ -z "$pidno" ] # Если получилась пустая строка, значит процесс не запущен. then echo "Соединение не установлено." exit $NOTCONNECTED else echo "Соединение установлено."; echo fi

while [ true ] # Бесконечный цикл. do

if [ ! -e "/proc/$pidno/$PROCFILENAME" ] # Пока работает процесс, файл "status" существует. then echo "Соединение разорвано." exit $NOTCONNECTED fi

netstat -s | grep "packets received" # Получить некоторые сведения о соединении. netstat -s | grep "packets delivered"

sleep $INTERVAL echo; echo

done

exit 0

# Как обычно, этот сценарий может быть остановлен комбинацией клавиш Control-C.

# Упражнение: # ---------- # Добавьте возможность завершения работы сценария, по нажатии на клавишу "q". # Это сделает скрипт более жружественным к пользователю.



Будьте предельно осторожны при работе с файловой системой /proc, так как попытка записи в некоторые файлы может повредить файловую систему или привести к краху системы.

Глава 28. /dev/zero и /dev/null

/dev/null

Псевдоустройство /dev/null -- это, своего рода, "черная дыра" в системе. Это, пожалуй, самый близкий смысловой эквивалент. Все, что записывается в этот файл, "исчезает" навсегда. Попытки записи или чтения из этого файла не дают, ровным счетом, никакого результата. Тем не менее, псевдоустройство /dev/null вполне может пригодиться.

Подавление вывода на stdout.

cat $filename >/dev/null # Содержимое файла $filename не появится на stdout.

Подавление вывода на stderr (from Пример 12-2).

rm $badname 2>/dev/null # Сообщение об ошибке "уйдет в никуда".

Подавление вывода, как на stdout, так и на stderr.

cat $filename 2>/dev/null >/dev/null # Если "$filename" не будет найден, то вы не увидите сообщения об ошибке. # Если "$filename" существует, то вы не увидите его содержимое. # Таким образом, вышеприведенная команда ничего не выводит на экран. # # Такая методика бывает полезной, когда необходимо лишь проверить код завершения команды #+ и нежелательно выводить результат работы команды на экран. # # cat $filename &>/dev/null # дает тот же результат, автор примечания Baris Cicek.

Удаление содержимого файла, сохраняя, при этом, сам файл, со всеми его правами доступа (очистка файла) (из Пример 2-1 и Пример 2-2):

cat /dev/null > /var/log/messages # : > /var/log/messages дает тот же эффект, но не порождает дочерний процесс.

cat /dev/null > /var/log/wtmp

Автоматическая очистка содержимого системного журнала (logfile) (особенно хороша для борьбы с надоедливыми рекламными идентификационными файлами ("cookies")):

Пример 28-1. Удаление cookie-файлов

if [ -f ~/.netscape/cookies ] # Удалить, если имеются. then rm -f ~/.netscape/cookies fi

ln -s /dev/null ~/.netscape/cookies # Теперь, все cookie-файлы, вместо того, чтобы сохраняться на диске, будут "вылетать в трубу".



/dev/zero

Подобно псевдоустройству /dev/null, /dev/ zero так же является псевдоустройством, с той лишь разницей, что содержит нули. Информация, выводимая в этот файл, так же бесследно исчезает. Чтение нулей из этого файла может вызвать некоторые затруднения, однако это можно сделать, к примеру, с помощью команды od или шестнадцатиричного редактора. В основном, /dev/zero используется для создания заготовки файла с заданой длиной.

Пример 28-2. Создание файла подкачки (swapfile), с помощью /dev/zero

#!/bin/bash

# Создание файла подкачки. # Этот сценарий должен запускаться с правами root.

ROOT_UID=0 # Для root -- $UID 0. E_WRONG_USER=65 # Не root?

FILE=/swap BLOCKSIZE=1024 MINBLOCKS=40 SUCCESS=0

if [ "$UID" -ne "$ROOT_UID" ] then echo; echo "Этот сценарий должен запускаться с правами root."; echo exit $E_WRONG_USER fi

blocks=${1:-$MINBLOCKS} # По-умолчанию -- 40 блоков, #+ если размер не задан из командной строки. # Ниже приводится эквивалентный набор команд. # -------------------------------------------------- # if [ -n "$1" ] # then # blocks=$1 # else # blocks=$MINBLOCKS # fi # --------------------------------------------------

if [ "$blocks" -lt $MINBLOCKS ] then blocks=$MINBLOCKS # Должно быть как минимум 40 блоков. fi

echo "Создание файла подкачки размером $blocks блоков (KB)." dd if=/dev/zero of=$FILE bs=$BLOCKSIZE count=$blocks # "Забить" нулями.

mkswap $FILE $blocks # Назначить как файл подкачки. swapon $FILE # Активировать.

echo "Файл подкачки создан и активирован."

exit $SUCCESS

Еще одна область применения /dev/zero -- "очистка" специального файла заданного размера, например файлов, монтируемых как loopback-устройства (см. Пример 13-6) или для безопасного удаления файла (см. Пример 12-42).

Пример 28-3. Создание электронного диска

#!/bin/bash # ramdisk.sh

# "электронный диск" -- это область в ОЗУ компьютера #+ с которой система взаимодействует как с файловой системой. # Основное преимущество -- очень высокая скорость чтения/записи. # Недостатки -- энергозависимость, уменьшение объема ОЗУ, доступного системе, # относительно небольшой размер. # # Чем хорош электронный диск? # При хранении наборов данных, таких как таблиц баз данных или словарей, на электронном диске #+ вы получаете высокую скорость работы с этими наборами, поскольку время доступа к ОЗУ # неизмеримо меньше времени доступа к жесткому диску.



E_NON_ROOT_USER=70 # Сценарий должен запускаться с правами root. ROOTUSER_NAME=root

MOUNTPT=/mnt/ramdisk SIZE=2000 # 2K блоков (измените, если это необходимо) BLOCKSIZE=1024 # размер блока -- 1K (1024 байт) DEVICE=/dev/ram0 # Первое устройство ram

username=`id -nu` if [ "$username" != "$ROOTUSER_NAME" ] then echo "Сценарий должен запускаться с правами root." exit $E_NON_ROOT_USER fi

if [ ! -d "$MOUNTPT" ] # Проверка наличия точки монтирования, then #+ благодаря этой проверке, при повторных запусках сценария mkdir $MOUNTPT #+ ошибки возникать не будет. fi

dd if=/dev/zero of=$DEVICE count=$SIZE bs=$BLOCKSIZE # Очистить электронный диск. mke2fs $DEVICE # Создать файловую систему ext2. mount $DEVICE $MOUNTPT # Смонтировать. chmod 777 $MOUNTPT # Сделать электронный диск доступным для обычных пользователей. # Но при этом, только root сможет его отмонтировать.

echo "Электронный диск \"$MOUNTPT\" готов к работе." # Теперь электронный диск доступен для любого пользователя в системе.

# Внимание! Электронный диск -- это энергозависимое устройство! Все данные, хранящиеся на нем, #+ будут утеряны при остановке или перезагрузке системы. # Если эти данные представляют для вас интерес, то сохраняйте их копии в обычном каталоге.

# После перезагрузки, чтобы вновь создать электронный диск, запустите этот сценарий. # Простое монтирование /mnt/ramdisk, без выполнения подготовительных действий, не будет работать.

exit 0

Глава 29. Отладка сценариев

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

Пример 29-1. Сценарий, содержащий ошибку

#!/bin/bash # ex74.sh

# Этот сценарий содержит ошибку.

a=37

if [$a -gt 27 ] then echo $a fi

exit 0

В результате исполнения этого сценария вы получите такое сообщение:

./ex74.sh: [37: command not found



Что в этом сценарии может быть неправильно (подсказка: после ключевого слова if)?

Пример 29-2. Пропущено ключевое слово

#!/bin/bash # missing-keyword.sh: # Какое сообщение об ошибке будет выведено, при попытке запустить этот сценарий?

for a in 1 2 3 do echo "$a" # done # Необходимое ключевое слово 'done' закомментировано.

exit 0

На экране появится сообщение:

missing-keyword.sh: line 11: syntax error: unexpected end of file

Обратите внимание, сообщение об ошибке будет содержать номер не той строки, в которой возникла ошибка, а той, в которой Bash точно установил наличие ошибочной ситуации.

Сообщения об ошибках могут вообще не содержать номера строки, при исполнении которой эта ошибка появилась.

А что делать, если сценарий работает, но не так как ожидалось? Вот пример весьма распространенной логической ошибки.

Пример 29-3. test24

#!/bin/bash

# Ожидается, что этот сценарий будет удалять в текущем каталоге #+ все файлы, имена которых содержат пробелы. # Но он не работает. Почему?

badname=`ls | grep ' '`

# echo "$badname"

rm "$badname"

exit 0

Попробуйте найти ошибку, раскомментарив строку echo "$badname". Инструкция echo очень полезна при отладке сценариев, она позволяет узнать -- действительно ли вы получаете то, что ожидали получить.

В данном конкретном случае, команда rm "$badname" не дает желаемого результата потому, что переменная $badname взята в кавычки. В результате, rm получает единственный аргумент (т.е. команда будет считать, что получила имя одного файла). Частично эта проблема может быть решена за счет удаления кавычек вокруг $badname и установки переменной $IFS так, чтобы она содержала только символ перевода строки, IFS=$'\n'. Однако, существует более простой способ выполнить эту задачу.

# Правильный способ удаления файлов, в чьих именах содержатся пробелы. rm *\ * rm *" "* rm *' '* # Спасибо S.C.

В общих чертах, ошибочными можно считать такие сценарии, которые

"сыплют" сообщениями о "синтаксических ошибках" или



запускаются, но работают не так как ожидалось (логические ошибки).

запускаются, делают то, что требуется, но имеют побочные эффекты (логическая бомба).

Инструменты, которые могут помочь при отладке неработающих сценариев

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

команда-фильтр tee, которая поможет проверить процессы и потоки данных в критических местах.

ключи -n -v -x

sh -n scriptname

-- проверит наличие синтаксических ошибок, не запуская сам сценарий. Того же эффекта можно добиться, вставив в сценарий команду set -n или set -o noexec. Обратите внимание, некоторые из синтаксических ошибок не могут быть выявлены таким способом.

sh -v scriptname

-- выводит каждую команду прежде, чем она будет выполнена. Того же эффекта можно добиться, вставив в сценарий команду set -v или set -o verbose.

Ключи -n и -v могут употребляться совместно: sh -nv scriptname.

sh -x scriptname

-- выводит, в краткой форме, результат исполнения каждой команды. Того же эффекта можно добиться, вставив в сценарий команду set -x или set -o xtrace.

Вставив в сценарий set -u или set -o nounset, вы будете получать сообщение об ошибке unbound variable всякий раз, когда будет производиться попытка обращения к необъявленной переменной.

Функция "assert", предназначенная для проверки переменных или условий, в критических точках сценария. (Эта идея заимствована из языка программирования C.)

Пример 29-4. Проверка условия с помощью функции "assert"

#!/bin/bash # assert.sh

assert () # Если условие ложно, { #+ выход из сценария с сообщением об ошибке. E_PARAM_ERR=98 E_ASSERT_FAILED=99

if [ -z "$2" ] # Недостаточное количество входных параметров. then return $E_PARAM_ERR fi

lineno=$2

if [ ! $1 ] then echo "Утверждение ложно: \"$1\"" echo "Файл: \"$0\", строка: $lineno" exit $E_ASSERT_FAILED # else # return # и продолжить исполнение сценария.


fi }

a=5 b=4 condition="$a -lt $b" # Сообщение об ощибке и завершение сценария. # Попробуйте поменять условие "condition" #+ на что нибудь другое и #+ посмотреть -- что получится.

assert "$condition" $LINENO # Сценарий продолжит работу только в том случае, если утверждение истинно.

# Прочие команды. # ... echo "Эта строка появится на экране только если утверждение истинно." # ... # Прочие команды. # ...

exit 0

Ловушка на выхто в этом сценарии может быть неправильно (подсказка: после ключевого словоде.

Команда exit, в сценарии, порождает сигнал 0, по которому процесс завершает работу, т.е. -- сам сценарий. [60] Часто бывает полезным по выходу из сценария выдать "распечатку" переменных.

Установка ловушек на сигналы

trap

Определяет действие при получении сигнала; так же полезна при отладке.

Сигнал (signal) -- это просто сообщение, передается процессу либо ядром, либо другим процессом, чтобы побудить процесс выполнить какие либо действия (обычно -- завершить работу). Например, нажатие на Control-C, вызывает передачу сигнала SIGINT, исполняющейся программе.

trap '' 2 # Игнорировать прерывание 2 (Control-C), действие по сигналу не указано.

trap 'echo "Control-C disabled."' 2 # Сообщение при нажатии на Control-C.