Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки; Версия 2.5 (15 февраля 2004) | ||
---|---|---|
Назад | Вперед |
В этом приложении собраны сценарии, которые не попали в основной текст документа. Однако, они определенно стоят того, что бы вы потратили время на их изучение.
Пример A-1. manview: Просмотр страниц руководств man
#!/bin/bash # manview.sh: Просмотр страниц руководств man в форматированном виде. # Полезен писателям страниц руководств, позволяет просмотреть страницы в исходном коде #+ как они будут выглядеть в конечном виде. E_WRONGARGS=65 if [ -z "$1" ] then echo "Порядок использования: `basename $0` имя_файла" exit $E_WRONGARGS fi groff -Tascii -man $1 | less # Если страница руководства включает в себя таблицы и/или выражения, # то этот сценарий "стошнит". # Для таких случаев можно использовать следующую строку. # # gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man # # Спасибо S.C. exit 0
Пример A-2. mailformat: Форматирование электронных писем
#!/bin/bash # mail-format.sh: Форматирование электронных писем. # Удаляет символы "^", табуляции и ограничивает чрезмерно длинные строки. # ================================================================= # Стандартная проверка аргументов ARGS=1 E_BADARGS=65 E_NOFILE=66 if [ $# -ne $ARGS ] # Проверка числа аргументов then echo "Порядок использования: `basename $0` имя_файла" exit $E_BADARGS fi if [ -f "$1" ] # Проверка наличия файла. then file_name=$1 else echo "Файл \"$1\" не найден." exit $E_NOFILE fi # ================================================================= MAXWIDTH=70 # Максимальная длина строки. # Удаление символов "^" начиная с первого символа строки, #+ и ограничить длину строки 70-ю символами. sed ' s/^>// s/^ *>// s/^ *// s/ *// ' $1 | fold -s --width=$MAXWIDTH # ключ -s команды "fold" разрывает, если это возможно, строку по пробельному символу. # Этот сценарий был написан после прочтения статьи, в котором расхваливалась #+ утилита под Windows, размером в 164K, с подобной функциональностью. # # Хороший набор утилит для обработки текста и эффективный #+ скриптовый язык -- это все, что необходимо, чтобы составить серьезную конкуренцию #+ чрезмерно "раздутым" программам. exit 0
Пример A-3. rn: Очень простая утилита для переименования файлов
Этот сценарий является модификацией Пример 12-18.
#! /bin/bash # # Очень простая утилита для переименования файлов # # Утилита "ren", автор Vladimir Lanin (lanin@csd2.nyu.edu), #+ выполняет эти же действия много лучше. ARGS=2 E_BADARGS=65 ONE=1 # Единственное или множественное число (см. ниже). if [ $# -ne "$ARGS" ] then echo "Порядок использования: `basename $0` старый_шаблон новый_шаблон" # Например: "rn gif jpg", поменяет расширения всех файлов в текущем каталоге с gif на jpg. exit $E_BADARGS fi number=0 # Количество переименованных файлов. for filename in *$1* # Проход по списку файлов в текущем каталоге. do if [ -f "$filename" ] then fname=`basename $filename` # Удалить путь к файлу из имени. n=`echo $fname | sed -e "s/$1/$2/"` # Поменять старое имя на новое. mv $fname $n # Переименовать. let "number += 1" fi done if [ "$number" -eq "$ONE" ] # Соблюдение правил грамматики. then echo "$number файл переименован." else echo "Переименовано файлов: $number." fi exit 0 # Упражнения: # ---------- # С какими типами файлов этот сценарий не будет работать? # Как это исправить? # # Переделайте сценарий таким образом, чтобы он мог обрабатывать все файлы в каталоге, #+ в именах которых содержатся пробелы, заменяя пробелы символом подчеркивания.
Пример A-4. blank-rename: переименование файлов, чьи имена содержат пробелы
Это даже более простая версия предыдущего примера.
#! /bin/bash # blank-rename.sh # # Заменяет пробелы символом подчеркивания в именах файлов в текущем каталоге. ONE=1 # единственное или множественное число (см. ниже). number=0 # Количество переименованных файлов. FOUND=0 # Код завершения в случае успеха. for filename in * # Перебор всех файлов в текущем каталоге. do echo "$filename" | grep -q " " # Проверить -- содержит ли имя файла if [ $? -eq $FOUND ] #+ пробелы. then fname=$filename # Удалить путь из имени файла. n=`echo $fname | sed -e "s/ /_/g"` # Заменить пробелы символом подчеркивания. mv "$fname" "$n" # Переименование. let "number += 1" fi done if [ "$number" -eq "$ONE" ] then echo "$number файл переименован." else echo "Переименовано файлов: $number" fi exit 0
Пример A-5. encryptedpw: Передача файла на ftp-сервер, с использованием пароля
#!/bin/bash # Модификация примера "ex72.sh", добавлено шифрование пароля. # Обратите внимание: этот вариант все еще нельзя считать безопасным, #+ поскольку в сеть пароль уходит в незашифрованном виде. # Используйте "ssh", если вас это беспокоит. E_BADARGS=65 if [ -z "$1" ] then echo "Порядок использования: `basename $0` имя_файла" exit $E_BADARGS fi Username=bozo # Измените на свой. pword=/home/bozo/secret/password_encrypted.file # Файл, содержащий пароль в зашифрованном виде. Filename=`basename $1` # Удалить путь из имени файла Server="XXX" Directory="YYY" # Подставьте фактические имя сервера и каталога. Password=`cruft <$pword` # Расшифровка. # Используется авторская программа "cruft", #+ основанная на алгоритме "onetime pad", #+ ее можно скачать с : #+ Primary-site: ftp://ibiblio.org/pub/Linux/utils/file #+ cruft-0.2.tar.gz [16k] ftp -n $Server <<End-Of-Session user $Username $Password binary bell cd $Directory put $Filename bye End-Of-Session # ключ -n, команды "ftp", запрещает автоматический вход. # "bell" -- звонок (звуковой сигнал) после передачи каждого файла. exit 0
Пример A-6. copy-cd: Копирование компакт-дисков с данными
#!/bin/bash # copy-cd.sh: copying a data CD CDROM=/dev/cdrom # устройство CD ROM OF=/home/bozo/projects/cdimage.iso # промежуточный файл # /xxxx/xxxxxxx/ измените для своей системы. BLOCKSIZE=2048 SPEED=2 # Можно задать более высокую скорость, если поддерживается. echo; echo "Вставьте исходный CD, но *НЕ* монтируйте его." echo "Нажмите ENTER, когда будете готовы. " read ready # Ожидание. echo; echo "Создается промежуточный файл $OF." echo "Это может занять какое-то время. Пожалуйста подождите." dd if=$CDROM of=$OF bs=$BLOCKSIZE # Копирование. echo; echo "Выньте исходный CD." echo "Вставьте чистую болванку CDR." echo "Нажмите ENTER, когда будете готовы. " read ready # Ожидание. echo "Копируется файл $OF на болванку." cdrecord -v -isosize speed=$SPEED dev=0,0 $OF # Используется пакет Joerg Schilling -- "cdrecord" . # http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html echo; echo "Копирование завершено." echo "Желаете удалить промежуточный файл (y/n)? " # Наверняка большой файл получился. read answer case "$answer" in [yY]) rm -f $OF echo "Файл $OF удален." ;; *) echo "Файл $OF не был удален.";; esac echo # Упражнение: # Добавьте в оператор "case" возможность обработки, введенных пользователем, "yes" и "Yes". exit 0
Пример A-7. Последовательности Коллаца (Collatz)
#!/bin/bash # collatz.sh # Широко известная последовательность Коллаца (Collatz) (гипотеза Коллаца). # ------------------------------------------- # 1) Принимает из командной строки "начальное" целое число. # 2) ЧИСЛО <--- НАЧАЛЬНОЕ ЗНАЧЕНИЕ # 3) Вывести ЧИСЛО. # 4) Если ЧИСЛО четное, разделить на 2, # 5)+ Если не четное -- умножить на 3 и прибавить 1. # 6) ЧИСЛО <--- РЕЗУЛЬТАТ # 7) Повторить, начиная с п. 3, заданное число раз. # # Теоретически, такая последовательность должна сходиться, #+ не зависимо от величины начального значения, #+ к повторению циклов "4,2,1...", #+ даже после значительных флуктуаций в самом начале. MAX_ITERATIONS=200 # Для больших начальных значений (>32000), это значение придется увеличить. h=${1:-$$} # Начальное значение # если из командной строки ничего не задано, то берется $PID, echo echo "C($h) --- $MAX_ITERATIONS итераций" echo for ((i=1; i<=MAX_ITERATIONS; i++)) do echo -n "$h " # ^^^^^ # табуляция let "remainder = h % 2" if [ "$remainder" -eq 0 ] # Четное? then let "h /= 2" # Разделить на 2. else let "h = h*3 + 1" # Умножить на 3 и прибавить 1. fi COLUMNS=10 # Выводить по 10 значений в строке. let "line_break = i % $COLUMNS" if [ "$line_break" -eq 0 ] then echo fi done echo exit 0
Пример A-8. days-between: Подсчет числа дней между двумя датами
#!/bin/bash # days-between.sh: Подсчет числа дней между двумя датами. # Порядок использования: ./days-between.sh [M]M/[D]D/YYYY [M]M/[D]D/YYYY ARGS=2 # Ожидается два аргумента из командной строки. E_PARAM_ERR=65 # Ошибка в числе ожидаемых аргументов. REFYR=1600 # Начальный год. CENTURY=100 DIY=365 ADJ_DIY=367 # Корректировка на високосный год + 1. MIY=12 DIM=31 LEAPCYCLE=4 MAXRETVAL=255 # Максимально возможное возвращаемое значение # для положительных чисел. diff= # Количество дней между датами. value= # Абсолютное значение. day= # день, месяц, год. month= year= Param_Error () # Ошибка в пвраметрах командной строки. { echo "Порядок использования: `basename $0` [M]M/[D]D/YYYY [M]M/[D]D/YYYY" echo " (даты должны быть после 1/3/1600)" exit $E_PARAM_ERR } Parse_Date () # Разбор даты. { month=${1%%/**} dm=${1%/**} # День и месяц. day=${dm#*/} let "year = `basename $1`" # Хотя это и не имя файла, но результат тот же. } check_date () # Проверка даты. { [ "$day" -gt "$DIM" ] || [ "$month" -gt "$MIY" ] || [ "$year" -lt "$REFYR" ] && Param_Error # Выход из сценария при обнаружении ошибки. # Используется комбинация "ИЛИ-списка / И-списка". # # Упражнение: Реализуйте более строгую проверку даты. } strip_leading_zero () # Удалить ведущий ноль { val=${1#0} # иначе Bash будет считать числа return $val # восьмеричными (POSIX.2, sect 2.9.2.1). } day_index () # Формула Гаусса: { # Количество дней от 3 Янв. 1600 до заданной даты. day=$1 month=$2 year=$3 let "month = $month - 2" if [ "$month" -le 0 ] then let "month += 12" let "year -= 1" fi let "year -= $REFYR" let "indexyr = $year / $CENTURY" let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM" # Более подробное объяснение алгоритма вы найдете в # http://home.t-online.de/home/berndt.schwerdtfeger/cal.htm if [ "$Days" -gt "$MAXRETVAL" ] # Если больше 255, then # то поменять знак let "dindex = 0 - $Days" # чтобы функция смогла вернуть полное значение. else let "dindex = $Days" fi return $dindex } calculate_difference () # Разница между двумя датами. { let "diff = $1 - $2" # Глобальная переменная. } abs () # Абсолютное значение { # Используется глобальная переменная "value". if [ "$1" -lt 0 ] # Если число отрицательное then # то let "value = 0 - $1" # изменить знак, else # иначе let "value = $1" # оставить как есть. fi } if [ $# -ne "$ARGS" ] # Требуется два аргумента командной строки. then Param_Error fi Parse_Date $1 check_date $day $month $year # Проверка даты. strip_leading_zero $day # Удалить ведущие нули day=$? # в номере дня и/или месяца. strip_leading_zero $month month=$? day_index $day $month $year date1=$? abs $date1 # Абсолютное значение date1=$value Parse_Date $2 check_date $day $month $year strip_leading_zero $day day=$? strip_leading_zero $month month=$? day_index $day $month $year date2=$? abs $date2 # Абсолютное значение date2=$value calculate_difference $date1 $date2 abs $diff # Абсолютное значение diff=$value echo $diff exit 0 # Сравните этот сценарий с реализацией формулы Гаусса на C # http://buschencrew.hypermart.net/software/datedif
Пример A-9. Создание "словаря"
#!/bin/bash # makedict.sh [создание словаря] # Модификация сценария /usr/sbin/mkdict. # Авторские права на оригинальный сценарий принадлежат Alec Muffett. # # Этот модифицированный вариант включен в документ на основе #+ документа "LICENSE" из пакета "Crack" #+ с которым распространяется оригинальный сценарий. # Этот скрипт обрабатывает текстовые файлы и создает отсортированный список #+ слов, найденных в этих файлах. # Он может оказаться полезным для сборки словарей #+ и проведения лексикографического анализа. E_BADARGS=65 if [ ! -r "$1" ] # Необходим хотя бы один аргумент -- then #+ имя файла. echo "Порядок использования: $0 имена_файлов" exit $E_BADARGS fi # SORT="sort" # Необходимость задания ключей сортировки отпала. #+ Изменено, по отношению к оригинальному сценарию. cat $* | # Выдать содержимое файлов на stdout. tr A-Z a-z | # Преобразовать в нижний регистр. tr ' ' '\012' | # Новое: заменить пробелы символами перевода строки. # tr -cd '\012[a-z][0-9]' | # В оригинальном сценарии: удалить все символы, #+ которые не являются буквами или цифрами. tr -c '\012a-z' '\012' | # Вместо удаления #+ неалфавитно-цифровые символы заменяются на перевод строки. sort | uniq | # Удалить повторяющиеся слова. grep -v '^#' | # Удалить строки, начинающиеся с "#". grep -v '^$' # Удалить пустые строки. exit 0
Пример A-10. Расчет индекса "созвучности"
#!/bin/bash # soundex.sh: Расчет индекса "созвучности" # ======================================================= # Сценарий Soundex # Автор # Mendel Cooper # thegrendel@theriver.com # 23 Января 2002 г. # # Условия распространения: Public Domain. # # Несколько отличающаяся версия этого сценария была опубликована #+ Эдом Шэфером (Ed Schaefer) в Июле 2002 года в колонке "Shell Corner" #+ "Unix Review" on-line, #+ http://www.unixreview.com/documents/uni1026336632258/ # ======================================================= ARGCOUNT=1 # Требуется аргумент командной строки. E_WRONGARGS=70 if [ $# -ne "$ARGCOUNT" ] then echo "Порядок использования: `basename $0` имя" exit $E_WRONGARGS fi assign_value () # Присвоить числовые значения { #+ символам в имени. val1=bfpv # 'b,f,p,v' = 1 val2=cgjkqsxz # 'c,g,j,k,q,s,x,z' = 2 val3=dt # и т.п. val4=l val5=mn val6=r # Попробуйте разобраться в том, что здесь происходит. value=$( echo "$1" \ | tr -d wh \ | tr $val1 1 | tr $val2 2 | tr $val3 3 \ | tr $val4 4 | tr $val5 5 | tr $val6 6 \ | tr -s 123456 \ | tr -d aeiouy ) # Символам в имени присваиваются числовые значения. # Удаляются повторяющиеся числа, если они не разделены гласными. # Гласные игнорируются, если они не являются разделителями, которые удаляются в последнюю очередь. # Символы 'w' и 'h' удаляются в первую очередь. } input_name="$1" echo echo "Имя = $input_name" # Перевести все символы в имени в нижний регистр. # ------------------------------------------------ name=$( echo $input_name | tr A-Z a-z ) # ------------------------------------------------ # Начальный символ в индекса "созвучия": первая буква в имени. # -------------------------------------------- char_pos=0 # Начальная позиция в имени. prefix0=${name:$char_pos:1} prefix=`echo $prefix0 | tr a-z A-Z` # Первую букву в имени -- в верхний регистр. let "char_pos += 1" # Передвинуть "указатель" на один символ. name1=${name:$char_pos} # ++++++++++++++++++++++++++++ Исключение отдельных ситуаций +++++++++++++++++++++++++++++++ # Теперь мы передвинулись на один символ вправо. # Если второй символ в имени совпадает с первым #+ то его нужно отбросить. # Кроме того, мы должны проверить -- не является ли первый символ #+ гласной, 'w' или 'h'. char1=`echo $prefix | tr A-Z a-z` # Первый символ -- в нижний регистр. assign_value $name s1=$value assign_value $name1 s2=$value assign_value $char1 s3=$value s3=9$s3 # Если первый символ в имени -- гласная буква #+ или 'w' или 'h', #+ то ее "значение" нужно отбросить. #+ Поэтому ставим 9, или другое #+ неиспользуемое значение, которое можно будет проверить. if [[ "$s1" -ne "$s2" || "$s3" -eq 9 ]] then suffix=$s2 else suffix=${s2:$char_pos} fi # ++++++++++++++++++++++++ Конец исключения отдельных ситуаций +++++++++++++++++++++++++++++++ padding=000 # Дополнить тремя нулями. soun=$prefix$suffix$padding # Нули добавить в конец получившегося индекса. MAXLEN=4 # Ограничить длину индекса 4-мя символами. soundex=${soun:0:$MAXLEN} echo "Индекс созвучия = $soundex" echo # Индекс "созвучия" - это метод индексации и классификации имен #+ по подобию звучания. # Индекс "созвучия" начинается с первого символа в имени, #+ за которым следуют 3-значный расчетный код. # Имена, которые произносятся примерно одинаково, имеют близкие индексы "созвучия". # Например: # Smith и Smythe -- оба имеют индекс "созвучия" "S530". # Harrison = H625 # Hargison = H622 # Harriman = H655 # Как правило эта методика дает неплохой результат, но имеются и аномалии. # # # Дополнительную информацию вы найдете на #+ "National Archives and Records Administration home page", #+ http://www.nara.gov/genealogy/soundex/soundex.html # Упражнение: # ---------- # Упростите блок "Исключение отдельных ситуаций" . exit 0
Пример A-11. "Игра "Жизнь""
#!/bin/bash # life.sh: Игра "Жизнь" # ##################################################################### # # Это Bash-версия известной игры Джона Конвея (John Conway) "Жизнь". # # --------------------------------------------------------------------- # # Прямоугольное игровое поле разбито на ячейки, в каждой ячейке может # #+ располагаться живая особь. # # Соответственно, ячейка с живой особью отмечается точкой, # #+ не занятая ячейка -- остается пустой. # # Изначально, ячейки заполняются из файла -- # #+ это первое поколение, или "поколение 0" # # Воспроизводство особей, в каждом последующем поколении, # #+ определяется следующими правилами # # 1) Каждая ячейка имеет "соседей" # #+ слева, справа, сверху, снизу и 4 по диагоналям. # # 123 # # 4*5 # # 678 # # # # 2) Если живая особь имеет 2 или 3 живых соседей, то она остается жить.# # 3) Если пустая ячейка имеет 3 живых соседей -- # #+ в ней "рождается" новая особь # SURVIVE=2 # BIRTH=3 # # 4) В любом другом случае, живая особь "погибает" # # ##################################################################### # startfile=gen0 # Начальное поколение из файла по-умолчанию -- "gen0". # если не задан другой файл, из командной строки. # if [ -n "$1" ] # Проверить аргумент командной строки -- файл с "поколениемn 0". then if [ -e "$1" ] # Проверка наличия файла. then startfile="$1" fi fi ALIVE1=. DEAD1=_ # Представление "живых" особей и пустых ячеек в файле с "поколением 0". # Этот сценарий работает с игровым полем 10 x 10 grid (может быть увеличено, #+ но большое игровое поле будет обрабатываться очень медленно). ROWS=10 COLS=10 GENERATIONS=10 # Максимальное число поколений. NONE_ALIVE=80 # Код завершения на случай, #+ если не осталось ни одной "живой" особи. TRUE=0 FALSE=1 ALIVE=0 DEAD=1 avar= # Текущее поколение. generation=0 # Инициализация счетчика поколений. # ================================================================= let "cells = $ROWS * $COLS" # Количество ячеек на игровом поле. declare -a initial # Массивы ячеек. declare -a current display () { alive=0 # Количество "живых" особей. # Изначально -- ноль. declare -a arr arr=( `echo "$1"` ) # Преобразовать аргумент в массив. element_count=${#arr[*]} local i local rowcheck for ((i=0; i<$element_count; i++)) do # Символ перевода строки -- в конец каждой строки. let "rowcheck = $i % ROWS" if [ "$rowcheck" -eq 0 ] then echo # Перевод строки. echo -n " " # Выравнивание. fi cell=${arr[i]} if [ "$cell" = . ] then let "alive += 1" fi echo -n "$cell" | sed -e 's/_/ /g' # Вывести массив, по пути заменяя символы подчеркивания на пробелы. done return } IsValid () # Проверка корректности координат ячейки. { if [ -z "$1" -o -z "$2" ] # Проверка наличия входных аргументов. then return $FALSE fi local row local lower_limit=0 # Запрет на отрицательные координаты. local upper_limit local left local right let "upper_limit = $ROWS * $COLS - 1" # Номер последней ячейки на игровом поле. if [ "$1" -lt "$lower_limit" -o "$1" -gt "$upper_limit" ] then return $FALSE # Выход за границы массива. fi row=$2 let "left = $row * $ROWS" # Левая граница. let "right = $left + $COLS - 1" # Правая граница. if [ "$1" -lt "$left" -o "$1" -gt "$right" ] then return $FALSE # Выхол за нижнюю строку. fi return $TRUE # Координаты корректны. } IsAlive () # Проверка наличия "живой" особи в ячейке. # Принимает массив и номер ячейки в качестве входных аргументов. { GetCount "$1" $2 # Подсчитать кол-во "живых" соседей. local nhbd=$? if [ "$nhbd" -eq "$BIRTH" ] # "Живая". then return $ALIVE fi if [ "$3" = "." -a "$nhbd" -eq "$SURVIVE" ] then # "Живая" если перед этим была "живая". return $ALIVE fi return $DEAD # По-умолчанию. } GetCount () # Подсчет "живых" соседей. # Необходимо 2 аргумента: # $1) переменная-массив # $2) cell номер ячейки { local cell_number=$2 local array local top local center local bottom local r local row local i local t_top local t_cen local t_bot local count=0 local ROW_NHBD=3 array=( `echo "$1"` ) let "top = $cell_number - $COLS - 1" # Номера соседних ячеек. let "center = $cell_number - 1" let "bottom = $cell_number + $COLS - 1" let "r = $cell_number / $ROWS" for ((i=0; i<$ROW_NHBD; i++)) # Просмотр слева-направо. do let "t_top = $top + $i" let "t_cen = $center + $i" let "t_bot = $bottom + $i" let "row = $r" # Пройти по соседям в средней строке. IsValid $t_cen $row # Координаты корректны? if [ $? -eq "$TRUE" ] then if [ ${array[$t_cen]} = "$ALIVE1" ] # "Живая"? then # Да! let "count += 1" # Нарастить счетчик. fi fi let "row = $r - 1" # По верхней строке. IsValid $t_top $row if [ $? -eq "$TRUE" ] then if [ ${array[$t_top]} = "$ALIVE1" ] then let "count += 1" fi fi let "row = $r + 1" # По нижней строке. IsValid $t_bot $row if [ $? -eq "$TRUE" ] then if [ ${array[$t_bot]} = "$ALIVE1" ] then let "count += 1" fi fi done if [ ${array[$cell_number]} = "$ALIVE1" ] then let "count -= 1" # Убедиться, что сама проверяемая ячейка fi #+ не была подсчитана. return $count } next_gen () # Обновить массив, в котором содержится информация о новом "поколении". { local array local i=0 array=( `echo "$1"` ) # Преобразовать в массив. while [ "$i" -lt "$cells" ] do IsAlive "$1" $i ${array[$i]} # "Живая"? if [ $? -eq "$ALIVE" ] then # Если "живая", то array[$i]=. #+ записать точку. else array[$i]="_" # Иначе -- символ подчеркивания fi #+ (который позднее заменится на пробел). let "i += 1" done # let "generation += 1" # Увеличить счетчик поколений. # Подготовка переменных, для передачи в функцию "display". avar=`echo ${array[@]}` # Преобразовать массив в строку. display "$avar" # Вывести его. echo; echo echo "Поколение $generation -- живых особей $alive" if [ "$alive" -eq 0 ] then echo echo "Преждеверменное завершение: не осталось ни одной живой особи!" exit $NONE_ALIVE # Нет смысла продолжать fi #+ если не осталось ни одной живой особи } # ========================================================= # main () # Загрузить начальное поколение из файла. initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '\n' |\ sed -e 's/\./\. /g' -e 's/_/_ /g'` ) # Удалить строки, начинающиеся с символа '#' -- комментарии. # Удалить строки перевода строки и вставить пробелы между элементами. clear # Очистка экрана. echo # Заголовок echo "=======================" echo " $GENERATIONS поколений" echo " в" echo " игре \" ЖИЗНЬ\"" echo "=======================" # -------- Вывести первое поколение. -------- Gen0=`echo ${initial[@]}` display "$Gen0" # Тлько вывод. echo; echo echo "Поколение $generation -- живых особей $alive" # ------------------------------------------- let "generation += 1" # Нарастить счетчик поколений. echo # ------- Вывести второе поколение. ------- Cur=`echo ${initial[@]}` next_gen "$Cur" # Обновить и вывести. # ------------------------------------------ let "generation += 1" # Нарастить счетчик поколений. # ------ Основной цикл игры ------ while [ "$generation" -le "$GENERATIONS" ] do Cur="$avar" next_gen "$Cur" let "generation += 1" done # ============================================================== echo exit 0 # -------------------------------------------------------------- # Этот сценарий имеет недоработку. # Граничные ячейки сверху, снизу и сбоков остаются пустыми. # Упражнение: Доработайте сценарий таким образом, чтобы , # + левая и правая стороны как бы "соприкасались", # + так же и верхняя и нижняя стороны.
Пример A-12. Файл с первым поколением для игры "Жизнь"
# Это файл-пример, содержащий "поколение 0", для сценария "life.sh". # -------------------------------------------------------------- # Игровое поле имеет размер 10 x 10, точкой обозначается "живая" особь, #+ символом подчеркивания -- пустая ячейка. Мы не можем использовать пробелы, #+ для обозначения пустых ячеек, из-за особенностей строения массивов в Bash. # [Упражнение для читателей: объясните, почему?.] # # Строки, начинающиеся с символа '#' считаются комментариями, сценарий их игнорирует. __.__..___ ___._.____ ____.___.. _._______. ____._____ ..__...___ ____._____ ___...____ __.._..___ _..___..__
+++
Следующие два сценария предоставил Mark Moraes, из университета в Торонто. См. файл "Moraes-COPYRIGHT", который содержит указание на авторские права.
Пример A-13. behead: Удаление заголовков из электронных писем и новостей
#! /bin/sh # Удаление заголовков из электронных писем и новостей т.е. до первой # пустой строки # Mark Moraes, Университет в Торонто # ==> Такие комментарии добавлены автором документа. if [ $# -eq 0 ]; then # ==> Если входной аргумент не задан (файл), то выводить результат на stdin. sed -e '1,/^$/d' -e '/^[ ]*$/d' # --> Удалить пустые строки и все строки предшествующие им else # ==> Если аргумент командной строки задан, то использовать его как имя файла. for i do sed -e '1,/^$/d' -e '/^[ ]*$/d' $i # --> То же, что и выше. done fi # ==> Упражнение: Добавьте проверку на наличие ошибок. # ==> # ==> Обратите внимание -- как похожи маленькие сценарии sed, за исключением передачи аргумента. # ==> Можно ли его оформит в виде функции? Почему да или почему нет?
Пример A-14. ftpget: Скачивание файлов по ftp
#! /bin/sh # $Id: ftpget,v 1.2 91/05/07 21:15:43 moraes Exp $ # Сценарий устанавливает анонимное соединение с ftp-сервером. # Простой и быстрый - написан как дополнение к ftplist # -h -- удаленный сервер (по-умолчанию prep.ai.mit.edu) # -d -- каталог на сервере - вы можете указать последовательность из нескольких ключей -d # Если вы используете относительные пути, # будьте внимательны при задании последовательности. # (по-умолчанию -- каталог пользователя ftp) # -v -- "многословный" режим, будет показывать все ответы ftp-сервера # -f -- file[:localfile] скачивает удаленный file и записывает под именем localfile # -m -- шаблон для mget. Не забудьте взять в кавычки! # -c -- локальный каталог # Например, # ftpget -h expo.lcs.mit.edu -d contrib -f xplaces.shar:xplaces.sh \ # -d ../pub/R3/fixes -c ~/fixes -m 'fix*' # Эта команда загрузит файл xplaces.shar из ~ftp/contrib с expo.lcs.mit.edu # и сохранит под именем xplaces.sh в текущем каталоге, затем заберет все исправления (fixes) # из ~ftp/pub/R3/fixes и поместит их в каталог ~/fixes. # Очевидно, что последовательность ключей и аргументов очень важна, поскольку # она определяет последовательность операций, выполняемых с удаленным ftp-сервером # # Mark Moraes (moraes@csri.toronto.edu), Feb 1, 1989 # # ==> Эти комментарии добавлены автором документа. # PATH=/local/bin:/usr/ucb:/usr/bin:/bin # export PATH # ==> Первые две строки в оригинальном сценарии вероятно излишни. TMPFILE=/tmp/ftp.$$ # ==> Создан временный файл SITE=`domainname`.toronto.edu # ==> 'domainname' подобен 'hostname' usage="Порядок использования: $0 [-h удаленный_сервер] [-d удаленный_каталог]... [-f удаленный_файл:локальный_файл]... \ [-c локальный_каталог] [-m шаблон_имен_файлов] [-v]" ftpflags="-i -n" verbflag= set -f # разрешить подстановку имен файлов (globbing) для опции -m set x `getopt vh:d:c:m:f: $*` if [ $? != 0 ]; then echo $usage exit 65 fi shift trap 'rm -f ${TMPFILE} ; exit' 0 1 2 3 15 echo "user anonymous ${USER-gnu}@${SITE} > ${TMPFILE}" # ==> Добавлены кавычки (рекомендуется). echo binary >> ${TMPFILE} for i in $* # ==> Разбор командной строки. do case $i in -v) verbflag=-v; echo hash >> ${TMPFILE}; shift;; -h) remhost=$2; shift 2;; -d) echo cd $2 >> ${TMPFILE}; if [ x${verbflag} != x ]; then echo pwd >> ${TMPFILE}; fi; shift 2;; -c) echo lcd $2 >> ${TMPFILE}; shift 2;; -m) echo mget "$2" >> ${TMPFILE}; shift 2;; -f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`; echo get ${f1} ${f2} >> ${TMPFILE}; shift 2;; --) shift; break;; esac done if [ $# -ne 0 ]; then echo $usage exit 65 # ==> В оригинале было "exit 2", изменено в соответствии со стандартами. fi if [ x${verbflag} != x ]; then ftpflags="${ftpflags} -v" fi if [ x${remhost} = x ]; then remhost=prep.ai.mit.edu # ==> Здесь можете указать свой ftp-сервер по-умолчанию. fi echo quit >> ${TMPFILE} # ==> Все команды сохранены во временном файле. ftp ${ftpflags} ${remhost} < ${TMPFILE} # ==> Теперь обработать пакетный файл. rm -f ${TMPFILE} # ==> В заключение, удалить временный файл (можно скопировать его в системный журнал). # ==> Упражнения: # ==> ---------- # ==> 1) Добавьте обработку ошибок. # ==> 2) Добавьте уведомление звуковым сигналом.
Пример A-15. Указание на авторские права
Следующее соглащение об авторских правах относится к двум, включенным в книгу, сценариям от Mark Moraes: "behead.sh" и "ftpget.sh" /* * Copyright University of Toronto 1988, 1989. * Автор: Mark Moraes * * Автор дает право на использование этого программного обеспечения * его изменение и рапространение со следующими ограничениями: * * 1. Автор и Университет Торонто не отвечают за * последствия использования этого программного * обеспечения, какими ужасными бы они ни были, * даже если эти последствия вызваны ошибками * в данном программном обеспечении. * * 2. Указание на происхождение программного обеспечения * не должно подвергаться изменениям, явно или по * оплошности. Так как некоторые пользователи обращаются * к исходным текстам, они обязательно должны быть * включены в состав документа. * * 3. Измененная версия должна содержать явное упоминание * об этом и не должна выдаваться за оригинал. * Так как некоторые пользователи обращаются к исходным текстам, * они обязательно должны быть включены в состав документа. * * 4. Это соглашение не может удаляться и/или изменяться. */
+
Antek Sawicki предоставил следующий сценарий, который демонстрирует операцию подстановки параметров, обсуждавшуюся в Section 9.3.
Пример A-16. password: Генератор случайного 8-ми символьного пароля
#!/bin/bash # Для старых систем может потребоваться указать #!/bin/bash2. # # Генератор случайных паролей для bash 2.x # Автор: Antek Sawicki <tenox@tenox.tc>, # который великодушно позволил использовать его в данном документе. # # ==> Комментарии, добавленные автором документа ==> MATRIX="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" LENGTH="8" # ==> 'LENGTH' можно увеличить, для генерации более длинных паролей. while [ "${n:=1}" -le "$LENGTH" ] # ==> Напоминаю, что ":=" -- это оператор "подстановки значения по-умолчанию". # ==> Таким образом, если 'n' не инициализирована, то в нее заносится 1. do PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}" # ==> Хитро, хитро.... # ==> Начнем с самых внутренних скобок... # ==> ${#MATRIX} -- возвращает длину массива MATRIX. # ==> $RANDOM%${#MATRIX} -- возвращает случайное число # ==> в диапазоне 1 .. ДЛИНА_МАССИВА(MATRIX) - 1. # ==> ${MATRIX:$(($RANDOM%${#MATRIX})):1} # ==> возвращает символ из MATRIX, из случайной позиции (найденной выше). # ==> См. подстановку параметров {var:pos:len} в Разделе 3.3.1 # ==> и примеры в этом разделе. # ==> PASS=... -- добавление символа к строке PASS, полученной на предыдущих итерациях. # ==> Чтобы детальнее проследить ход работы цикла, раскомментируйте следующую строку # ==> echo "$PASS" # ==> Вы увидите, как на каждом проходе цикла, # ==> к строке PASS добавляется по одному символу. let n+=1 # ==> Увеличить 'n' перед началом следующей итерации. done echo "$PASS" # ==> Или перенаправьте в файл, если пожелаете. exit 0
+
James R. Van Zandt предоставил следующий сценарий, который демонстрирует применение именованных каналов, по его словам, "на самом деле -- упражнение на применение кавычек и на экранирование".
Пример A-17. fifo: Создание резервных копий с помощью именованных каналов
#!/bin/bash # ==> Автор:James R. Van Zandt # ==> используется с его разрешения. # ==> Комментарии, добавленные автором документа. HERE=`uname -n` # ==> hostname THERE=bilbo echo "начало создания резервной копии на $THERE, за `date +%r`" # ==> `date +%r` возвращает время в 12-ти часовом формате, т.е. "08:08:34 PM". # убедиться в том, что /pipe -- это действительно канал, а не простой файл rm -rf /pipe mkfifo /pipe # ==> Создание "именованного канала", с именем "/pipe". # ==> 'su xyz' -- запускает команду от имени порльзователя "xyz". # ==> 'ssh' -- вызов secure shell (вход на удаленную систему). su xyz -c "ssh $THERE \"cat >/home/xyz/backup/${HERE}-daily.tar.gz\" < /pipe"& cd / tar -czf - bin boot dev etc home info lib man root sbin share usr var >/pipe # ==> Именованный канал /pipe, используется для передачи данных между процессами: # ==> 'tar/gzip' пишет в /pipe, а 'ssh' -- читает из /pipe. # ==> В результате будет получена резервная копия всех основных каталогов. # ==> В чем состоит преимущество именованного канала, в данной ситуации, # ==> перед неименованным каналом "|" ? # ==> Будет ли работать неименованный канал в данной ситуации? exit 0
+
Stephane Chazelas предоставил следующий сценарий, который демонстрирует возможность генерации простых чисел без использования массивов.
Пример A-18. Генерация простых чисел, с использованием оператора деления по модулю (остаток от деления)
#!/bin/bash # primes.sh: Генерация простых чисел, без использования массивов. # Автор: Stephane Chazelas. # Этот сценарий не использует класический алгоритм "Решето Эратосфена", #+ вместо него используется более понятный метод проверки каждого кандидата в простые числа #+ путем поиска делителей, с помощью оператора нахождения остатка от деления "%". LIMIT=1000 # Простые от 2 до 1000 Primes() { (( n = $1 + 1 )) # Перейти к следующему числу. shift # Следующий параметр в списке. # echo "_n=$n i=$i_" if (( n == LIMIT )) then echo $* return fi for i; do # "i" устанавливается в "@", предыдущее значение $n. # echo "-n=$n i=$i-" (( i * i > n )) && break # Оптимизация. (( n % i )) && continue # Отсечь составное число с помощью оператора "%". Primes $n $@ # Рекурсивный вызов внутри цикла. return done Primes $n $@ $n # Рекурсивный вызов за пределами цикла. # Последовательное накопление позиционных параметров. # в "$@" накапливаются простые числа. } Primes 1 exit 0 # Раскомментарьте строки 16 и 24, это поможет понять суть происходящего. # Сравните скоростные характеристики этого сценария и сценария (ex68.sh), # реализующего алгоритм "Решето Эратосфена". # Упражнение: Попробуйте реализовать этот сценарий без использования рекурсии. # Это даст некоторый выигрыш в скорости.
+
Jordi Sanfeliu дал согласие на публикацию своего сценария tree.
Пример A-19. tree: Вывод дерева каталогов
#!/bin/sh # @(#) tree 1.1 30/11/95 by Jordi Sanfeliu # email: mikaku@fiwix.org # # Начальная версия: 1.0 30/11/95 # Следующая версия: 1.1 24/02/97 Now, with symbolic links # Исправления : Ian Kjos, поддержка недоступных каталогов # email: beth13@mail.utexas.edu # # Tree -- средство просмотра дерева каталогов (очевидно :-) ) # # ==> Используется в данном документе с разрешения автора сценария, Jordi Sanfeliu. # ==> Комментарии, добавленные автором документа. # ==> Добавлено "окавычивание" аргументов. search () { for dir in `echo *` # ==> `echo *` список всех файлов в текущем каталоге, без символов перевода строки. # ==> Тот же эффект дает for dir in * # ==> но "dir in `echo *`" не обрабатывет файлы, чьи имена содержат пробелы. do if [ -d "$dir" ] ; then # ==> Если это каталог (-d)... zz=0 # ==> Временная переменная, для сохранения уровня вложенности каталога. while [ $zz != $deep ] # Keep track of inner nested loop. do echo -n "| " # ==> Показать символ вертикальной связи, # ==> с 2 пробелами и без перевода строки. zz=`expr $zz + 1` # ==> Нарастить zz. done if [ -L "$dir" ] ; then # ==> Если символическая ссылка на каталог... echo "+---$dir" `ls -l $dir | sed 's/^.*'$dir' //'` # ==> Показать горизонтальный соединитель и имя связянного каталога, но... # ==> без указания даты/времени. else echo "+---$dir" # ==> Вывести горизонтальный соединитель... # ==> и название каталога. if cd "$dir" ; then # ==> Если можно войти в каталог... deep=`expr $deep + 1` # ==> Нарастить уровень вложенности. search # рекурсия ;-) numdirs=`expr $numdirs + 1` # ==> Нарастить счетчик каталогов. fi fi fi done cd .. # ==> Подняться на один уровень вверх. if [ "$deep" ] ; then # ==> Если depth = 0 (возвращает TRUE)... swfi=1 # ==> выставить признак окончания поиска. fi deep=`expr $deep - 1` # ==> Уменьшить уровень вложенности. } # - Main - if [ $# = 0 ] ; then cd `pwd` # ==> Если аргумент командной строки отсутствует, то используется текущий каталог. else cd $1 # ==> иначе перейти в заданный каталог. fi echo "Начальный каталог = `pwd`" swfi=0 # ==> Признак завершения поиска. deep=0 # ==> Уровень вложенности. numdirs=0 zz=0 while [ "$swfi" != 1 ] # Пока поиск не закончен... do search # ==> Вызвать функцию поиска. done echo "Всего каталогов = $numdirs" exit 0 # ==> Попробуйте разобраться в том как этот сценарий работает.
Noah Friedman дал разрешение на публикацию своей библиотеки функций для работы со строками, которая, по сути, воспроизводит некоторые библиотечные функции языка C.
Пример A-20. Функции для работы со строками
#!/bin/bash # string.bash --- эмуляция библиотеки функций string(3) # Автор: Noah Friedman <friedman@prep.ai.mit.edu> # ==> Используется с его разрешения. # Дата создания: 1992-07-01 # Дата последней модификации: 1993-09-29 # Public domain # Преобразование в синтаксис bash v2 выполнил Chet Ramey # Комментарий: # Код: #:docstring strcat: # Порядок использования: strcat s1 s2 # # Strcat добавляет содержимое переменной s2 к переменной s1. # # Пример: # a="foo" # b="bar" # strcat a b # echo $a # => foobar # #:end docstring: ###;;;autoload function strcat () { local s1_val s2_val s1_val=${!1} # косвенная ссылка s2_val=${!2} eval "$1"=\'"${s1_val}${s2_val}"\' # ==> eval $1='${s1_val}${s2_val}' во избежание проблем, # ==> если одна из переменных содержит одиночную кавычку. } #:docstring strncat: # Порядок использования: strncat s1 s2 $n # # Аналог strcat, но добавляет не более n символов из # переменной s2. Результат выводится на stdout. # # Пример: # a=foo # b=barbaz # strncat a b 3 # echo $a # => foobar # #:end docstring: ###;;;autoload function strncat () { local s1="$1" local s2="$2" local -i n="$3" local s1_val s2_val s1_val=${!s1} # ==> косвенная ссылка s2_val=${!s2} if [ ${#s2_val} -gt ${n} ]; then s2_val=${s2_val:0:$n} # ==> выделение подстроки fi eval "$s1"=\'"${s1_val}${s2_val}"\' # ==> eval $1='${s1_val}${s2_val}' во избежание проблем, # ==> если одна из переменных содержит одиночную кавычку. } #:docstring strcmp: # Порядок использования: strcmp $s1 $s2 # # Strcmp сравнивает две строки и возвращает число меньше, равно # или больше нуля, в зависимости от результатов сравнения. #:end docstring: ###;;;autoload function strcmp () { [ "$1" = "$2" ] && return 0 [ "${1}" '<' "${2}" ] > /dev/null && return -1 return 1 } #:docstring strncmp: # Порядок использования: strncmp $s1 $s2 $n # # Подобна strcmp, но сравнивает не более n символов #:end docstring: ###;;;autoload function strncmp () { if [ -z "${3}" -o "${3}" -le "0" ]; then return 0 fi if [ ${3} -ge ${#1} -a ${3} -ge ${#2} ]; then strcmp "$1" "$2" return $? else s1=${1:0:$3} s2=${2:0:$3} strcmp $s1 $s2 return $? fi } #:docstring strlen: # Порядок использования: strlen s # # возвращает количество символов в строке s. #:end docstring: ###;;;autoload function strlen () { eval echo "\${#${1}}" # ==> Возвращает длину переменной, # ==> чье имя передается как аргумент. } #:docstring strspn: # Порядок использования: strspn $s1 $s2 # # Strspn возвращает максимальную длину сегмента в строке s1, # который полностью состоит из символов строки s2. #:end docstring: ###;;;autoload function strspn () { # Сброс содержимого переменной IFS позволяет обрабатывать пробелы как обычные символы. local IFS= local result="${1%%[!${2}]*}" echo ${#result} } #:docstring strcspn: # Порядок использования: strcspn $s1 $s2 # # Strcspn возвращает максимальную длину сегмента в строке s1, # который полностью не содержит символы из строки s2. #:end docstring: ###;;;autoload function strcspn () { # Сброс содержимого переменной IFS позволяет обрабатывать пробелы как обычные символы. local IFS= local result="${1%%[${2}]*}" echo ${#result} } #:docstring strstr: # Порядок использования: strstr s1 s2 # # Strstr выводит подстроку первого вхождения строки s2 # в строке s1, или ничего не выводит, если подстрока s2 в строке s1 не найдена. # Если s2 содержит строку нулевой длины, то strstr выводит строку s1. #:end docstring: ###;;;autoload function strstr () { # Если s2 -- строка нулевой длины, то вывести строку s1 [ ${#2} -eq 0 ] && { echo "$1" ; return 0; } # не выводить ничего, если s2 не найдена в s1 case "$1" in *$2*) ;; *) return 1;; esac # использовать шаблон, для удаления всех несоответствий после s2 в s1 first=${1/$2*/} # Затем удалить все несоответствия с начала строки echo "${1##$first}" } #:docstring strtok: # Порядок использования: strtok s1 s2 # # Strtok рассматривает строку s1, как последовательность из 0, или более, # лексем (токенов), разделенных символами строки s2 # При первом вызове (с непустым аргументом s1) # выводит первую лексему на stdout. # Функция запоминает свое положение в строке s1 от вызова к вызову, # так что последующие вызовы должны производиться с пустым первым аргументом, # чтобы продолжить выделение лексем из строки s1. # После вывода последней лексемы, все последующие вызовы будут выводить на stdout # пустое значение. Строка-разделитель может изменяться от вызова к вызову. #:end docstring: ###;;;autoload function strtok () { : } #:docstring strtrunc: # Порядок использования: strtrunc $n $s1 {$s2} {$...} # # Используется многими функциями, такими как strncmp, чтобы отсечь "лишние" символы. # Выводит первые n символов в каждой из строк s1 s2 ... на stdout. #:end docstring: ###;;;autoload function strtrunc () { n=$1 ; shift for z; do echo "${z:0:$n}" done } # provide string # string.bash конец библиотеки # ========================================================================== # # ==> Все, что находится ниже, добавлено автором документа. # ==> Чтобы этот сценарий можно было использовать как "библиотеку", необходимо # ==> удалить все, что находится ниже и "source" этот файл в вашем сценарии. # strcat string0=one string1=two echo echo "Проверка функции \"strcat\" :" echo "Изначально \"string0\" = $string0" echo "\"string1\" = $string1" strcat string0 string1 echo "Теперь \"string0\" = $string0" echo # strlen echo echo "Проверка функции \"strlen\" :" str=123456789 echo "\"str\" = $str" echo -n "Длина строки \"str\" = " strlen str echo # Упражнение: # --------- # Добавьте проверку остальных функций. exit 0
Michael Zick предоставил очень сложный пример работы с массивами и утилитой md5sum, используемой для кодирования сведений о каталоге.
От переводчика:
К своему стыду вынужден признаться, что перевод комментариев оказался мне не "по зубам", поэтому оставляю этот сценарий без перевода.
Пример A-21. Directory information
#! /bin/bash # directory-info.sh # Parses and lists directory information. # NOTE: Change lines 273 and 353 per "README" file. # Michael Zick is the author of this script. # Used here with his permission. # Controls # If overridden by command arguments, they must be in the order: # Arg1: "Descriptor Directory" # Arg2: "Exclude Paths" # Arg3: "Exclude Directories" # # Environment Settings override Defaults. # Command arguments override Environment Settings. # Default location for content addressed file descriptors. MD5UCFS=${1:-${MD5UCFS:-'/tmpfs/ucfs'}} # Directory paths never to list or enter declare -a \ EXCLUDE_PATHS=${2:-${EXCLUDE_PATHS:-'(/proc /dev /devfs /tmpfs)'}} # Directories never to list or enter declare -a \ EXCLUDE_DIRS=${3:-${EXCLUDE_DIRS:-'(ucfs lost+found tmp wtmp)'}} # Files never to list or enter declare -a \ EXCLUDE_FILES=${3:-${EXCLUDE_FILES:-'(core "Name with Spaces")'}} # Here document used as a comment block. : << LSfieldsDoc # # # # # List Filesystem Directory Information # # # # # # # ListDirectory "FileGlob" "Field-Array-Name" # or # ListDirectory -of "FileGlob" "Field-Array-Filename" # '-of' meaning 'output to filename' # # # # # String format description based on: ls (GNU fileutils) version 4.0.36 Produces a line (or more) formatted: inode permissions hard-links owner group ... 32736 -rw------- 1 mszick mszick size day month date hh:mm:ss year path 2756608 Sun Apr 20 08:53:06 2003 /home/mszick/core Unless it is formatted: inode permissions hard-links owner group ... 266705 crw-rw---- 1 root uucp major minor day month date hh:mm:ss year path 4, 68 Sun Apr 20 09:27:33 2003 /dev/ttyS4 NOTE: that pesky comma after the major number NOTE: the 'path' may be multiple fields: /home/mszick/core /proc/982/fd/0 -> /dev/null /proc/982/fd/1 -> /home/mszick/.xsession-errors /proc/982/fd/13 -> /tmp/tmpfZVVOCs (deleted) /proc/982/fd/7 -> /tmp/kde-mszick/ksycoca /proc/982/fd/8 -> socket:[11586] /proc/982/fd/9 -> pipe:[11588] If that isn't enough to keep your parser guessing, either or both of the path components may be relative: ../Built-Shared -> Built-Static ../linux-2.4.20.tar.bz2 -> ../../../SRCS/linux-2.4.20.tar.bz2 The first character of the 11 (10?) character permissions field: 's' Socket 'd' Directory 'b' Block device 'c' Character device 'l' Symbolic link NOTE: Hard links not marked - test for identical inode numbers on identical filesystems. All information about hard linked files are shared, except for the names and the name's location in the directory system. NOTE: A "Hard link" is known as a "File Alias" on some systems. '-' An undistingushed file Followed by three groups of letters for: User, Group, Others Character 1: '-' Not readable; 'r' Readable Character 2: '-' Not writable; 'w' Writable Character 3, User and Group: Combined execute and special '-' Not Executable, Not Special 'x' Executable, Not Special 's' Executable, Special 'S' Not Executable, Special Character 3, Others: Combined execute and sticky (tacky?) '-' Not Executable, Not Tacky 'x' Executable, Not Tacky 't' Executable, Tacky 'T' Not Executable, Tacky Followed by an access indicator Haven't tested this one, it may be the eleventh character or it may generate another field ' ' No alternate access '+' Alternate access LSfieldsDoc ListDirectory() { local -a T local -i of=0 # Default return in variable # OLD_IFS=$IFS # Using BASH default ' \t\n' case "$#" in 3) case "$1" in -of) of=1 ; shift ;; * ) return 1 ;; esac ;; 2) : ;; # Poor man's "continue" *) return 1 ;; esac # NOTE: the (ls) command is NOT quoted (") T=( $(ls --inode --ignore-backups --almost-all --directory \ --full-time --color=none --time=status --sort=none \ --format=long $1) ) case $of in # Assign T back to the array whose name was passed as $2 0) eval $2=\( \"\$\{T\[@\]\}\" \) ;; # Write T into filename passed as $2 1) echo "${T[@]}" > "$2" ;; esac return 0 } # # # # # Is that string a legal number? # # # # # # # IsNumber "Var" # # # # # There has to be a better way, sigh... IsNumber() { local -i int if [ $# -eq 0 ] then return 1 else (let int=$1) 2>/dev/null return $? # Exit status of the let thread fi } # # # # # Index Filesystem Directory Information # # # # # # # IndexList "Field-Array-Name" "Index-Array-Name" # or # IndexList -if Field-Array-Filename Index-Array-Name # IndexList -of Field-Array-Name Index-Array-Filename # IndexList -if -of Field-Array-Filename Index-Array-Filename # # # # # : << IndexListDoc Walk an array of directory fields produced by ListDirectory Having suppressed the line breaks in an otherwise line oriented report, build an index to the array element which starts each line. Each line gets two index entries, the first element of each line (inode) and the element that holds the pathname of the file. The first index entry pair (Line-Number==0) are informational: Index-Array-Name[0] : Number of "Lines" indexed Index-Array-Name[1] : "Current Line" pointer into Index-Array-Name The following index pairs (if any) hold element indexes into the Field-Array-Name per: Index-Array-Name[Line-Number * 2] : The "inode" field element. NOTE: This distance may be either +11 or +12 elements. Index-Array-Name[(Line-Number * 2) + 1] : The "pathname" element. NOTE: This distance may be a variable number of elements. Next line index pair for Line-Number+1. IndexListDoc IndexList() { local -a LIST # Local of listname passed local -a -i INDEX=( 0 0 ) # Local of index to return local -i Lidx Lcnt local -i if=0 of=0 # Default to variable names case "$#" in # Simplistic option testing 0) return 1 ;; 1) return 1 ;; 2) : ;; # Poor man's continue 3) case "$1" in -if) if=1 ;; -of) of=1 ;; * ) return 1 ;; esac ; shift ;; 4) if=1 ; of=1 ; shift ; shift ;; *) return 1 esac # Make local copy of list case "$if" in 0) eval LIST=\( \"\$\{$1\[@\]\}\" \) ;; 1) LIST=( $(cat $1) ) ;; esac # Grok (grope?) the array Lcnt=${#LIST[@]} Lidx=0 until (( Lidx >= Lcnt )) do if IsNumber ${LIST[$Lidx]} then local -i inode name local ft inode=Lidx local m=${LIST[$Lidx+2]} # Hard Links field ft=${LIST[$Lidx+1]:0:1} # Fast-Stat case $ft in b) ((Lidx+=12)) ;; # Block device c) ((Lidx+=12)) ;; # Character device *) ((Lidx+=11)) ;; # Anything else esac name=Lidx case $ft in -) ((Lidx+=1)) ;; # The easy one b) ((Lidx+=1)) ;; # Block device c) ((Lidx+=1)) ;; # Character device d) ((Lidx+=1)) ;; # The other easy one l) ((Lidx+=3)) ;; # At LEAST two more fields # A little more elegance here would handle pipes, #+ sockets, deleted files - later. *) until IsNumber ${LIST[$Lidx]} || ((Lidx >= Lcnt)) do ((Lidx+=1)) done ;; # Not required esac INDEX[${#INDEX[*]}]=$inode INDEX[${#INDEX[*]}]=$name INDEX[0]=${INDEX[0]}+1 # One more "line" found # echo "Line: ${INDEX[0]} Type: $ft Links: $m Inode: \ # ${LIST[$inode]} Name: ${LIST[$name]}" else ((Lidx+=1)) fi done case "$of" in 0) eval $2=\( \"\$\{INDEX\[@\]\}\" \) ;; 1) echo "${INDEX[@]}" > "$2" ;; esac return 0 # What could go wrong? } # # # # # Content Identify File # # # # # # # DigestFile Input-Array-Name Digest-Array-Name # or # DigestFile -if Input-FileName Digest-Array-Name # # # # # # Here document used as a comment block. : <<DigestFilesDoc The key (no pun intended) to a Unified Content File System (UCFS) is to distinguish the files in the system based on their content. Distinguishing files by their name is just, so, 20th Century. The content is distinguished by computing a checksum of that content. This version uses the md5sum program to generate a 128 bit checksum representative of the file's contents. There is a chance that two files having different content might generate the same checksum using md5sum (or any checksum). Should that become a problem, then the use of md5sum can be replace by a cyrptographic signature. But until then... The md5sum program is documented as outputting three fields (and it does), but when read it appears as two fields (array elements). This is caused by the lack of whitespace between the second and third field. So this function gropes the md5sum output and returns: [0] 32 character checksum in hexidecimal (UCFS filename) [1] Single character: ' ' text file, '*' binary file [2] Filesystem (20th Century Style) name Note: That name may be the character '-' indicating STDIN read. DigestFilesDoc DigestFile() { local if=0 # Default, variable name local -a T1 T2 case "$#" in 3) case "$1" in -if) if=1 ; shift ;; * ) return 1 ;; esac ;; 2) : ;; # Poor man's "continue" *) return 1 ;; esac case $if in 0) eval T1=\( \"\$\{$1\[@\]\}\" \) T2=( $(echo ${T1[@]} | md5sum -) ) ;; 1) T2=( $(md5sum $1) ) ;; esac case ${#T2[@]} in 0) return 1 ;; 1) return 1 ;; 2) case ${T2[1]:0:1} in # SanScrit-2.0.5 \*) T2[${#T2[@]}]=${T2[1]:1} T2[1]=\* ;; *) T2[${#T2[@]}]=${T2[1]} T2[1]=" " ;; esac ;; 3) : ;; # Assume it worked *) return 1 ;; esac local -i len=${#T2[0]} if [ $len -ne 32 ] ; then return 1 ; fi eval $2=\( \"\$\{T2\[@\]\}\" \) } # # # # # Locate File # # # # # # # LocateFile [-l] FileName Location-Array-Name # or # LocateFile [-l] -of FileName Location-Array-FileName # # # # # # A file location is Filesystem-id and inode-number # Here document used as a comment block. : <<StatFieldsDoc Based on stat, version 2.2 stat -t and stat -lt fields [0] name [1] Total size File - number of bytes Symbolic link - string length of pathname [2] Number of (512 byte) blocks allocated [3] File type and Access rights (hex) [4] User ID of owner [5] Group ID of owner [6] Device number [7] Inode number [8] Number of hard links [9] Device type (if inode device) Major [10] Device type (if inode device) Minor [11] Time of last access May be disabled in 'mount' with noatime atime of files changed by exec, read, pipe, utime, mknod (mmap?) atime of directories changed by addition/deletion of files [12] Time of last modification mtime of files changed by write, truncate, utime, mknod mtime of directories changed by addtition/deletion of files [13] Time of last change ctime reflects time of changed inode information (owner, group permissions, link count -*-*- Per: Return code: 0 Size of array: 14 Contents of array Element 0: /home/mszick Element 1: 4096 Element 2: 8 Element 3: 41e8 Element 4: 500 Element 5: 500 Element 6: 303 Element 7: 32385 Element 8: 22 Element 9: 0 Element 10: 0 Element 11: 1051221030 Element 12: 1051214068 Element 13: 1051214068 For a link in the form of linkname -> realname stat -t linkname returns the linkname (link) information stat -lt linkname returns the realname information stat -tf and stat -ltf fields [0] name [1] ID-0? # Maybe someday, but Linux stat structure [2] ID-0? # does not have either LABEL nor UUID # fields, currently information must come # from file-system specific utilities These will be munged into: [1] UUID if possible [2] Volume Label if possible Note: 'mount -l' does return the label and could return the UUID [3] Maximum length of filenames [4] Filesystem type [5] Total blocks in the filesystem [6] Free blocks [7] Free blocks for non-root user(s) [8] Block size of the filesystem [9] Total inodes [10] Free inodes -*-*- Per: Return code: 0 Size of array: 11 Contents of array Element 0: /home/mszick Element 1: 0 Element 2: 0 Element 3: 255 Element 4: ef53 Element 5: 2581445 Element 6: 2277180 Element 7: 2146050 Element 8: 4096 Element 9: 1311552 Element 10: 1276425 StatFieldsDoc # LocateFile [-l] FileName Location-Array-Name # LocateFile [-l] -of FileName Location-Array-FileName LocateFile() { local -a LOC LOC1 LOC2 local lk="" of=0 case "$#" in 0) return 1 ;; 1) return 1 ;; 2) : ;; *) while (( "$#" > 2 )) do case "$1" in -l) lk=-1 ;; -of) of=1 ;; *) return 1 ;; esac shift done ;; esac # More Sanscrit-2.0.5 # LOC1=( $(stat -t $lk $1) ) # LOC2=( $(stat -tf $lk $1) ) # Uncomment above two lines if system has "stat" command installed. LOC=( ${LOC1[@]:0:1} ${LOC1[@]:3:11} ${LOC2[@]:1:2} ${LOC2[@]:4:1} ) case "$of" in 0) eval $2=\( \"\$\{LOC\[@\]\}\" \) ;; 1) echo "${LOC[@]}" > "$2" ;; esac return 0 # Which yields (if you are lucky, and have "stat" installed) # -*-*- Location Discriptor -*-*- # Return code: 0 # Size of array: 15 # Contents of array # Element 0: /home/mszick 20th Century name # Element 1: 41e8 Type and Permissions # Element 2: 500 User # Element 3: 500 Group # Element 4: 303 Device # Element 5: 32385 inode # Element 6: 22 Link count # Element 7: 0 Device Major # Element 8: 0 Device Minor # Element 9: 1051224608 Last Access # Element 10: 1051214068 Last Modify # Element 11: 1051214068 Last Status # Element 12: 0 UUID (to be) # Element 13: 0 Volume Label (to be) # Element 14: ef53 Filesystem type } # And then there was some test code ListArray() # ListArray Name { local -a Ta eval Ta=\( \"\$\{$1\[@\]\}\" \) echo echo "-*-*- List of Array -*-*-" echo "Size of array $1: ${#Ta[*]}" echo "Contents of array $1:" for (( i=0 ; i<${#Ta[*]} ; i++ )) do echo -e "\tElement $i: ${Ta[$i]}" done return 0 } declare -a CUR_DIR # For small arrays ListDirectory "${PWD}" CUR_DIR ListArray CUR_DIR declare -a DIR_DIG DigestFile CUR_DIR DIR_DIG echo "The new \"name\" (checksum) for ${CUR_DIR[9]} is ${DIR_DIG[0]}" declare -a DIR_ENT # BIG_DIR # For really big arrays - use a temporary file in ramdisk # BIG-DIR # ListDirectory -of "${CUR_DIR[11]}/*" "/tmpfs/junk2" ListDirectory "${CUR_DIR[11]}/*" DIR_ENT declare -a DIR_IDX # BIG-DIR # IndexList -if "/tmpfs/junk2" DIR_IDX IndexList DIR_ENT DIR_IDX declare -a IDX_DIG # BIG-DIR # DIR_ENT=( $(cat /tmpfs/junk2) ) # BIG-DIR # DigestFile -if /tmpfs/junk2 IDX_DIG DigestFile DIR_ENT IDX_DIG # Small (should) be able to parallize IndexList & DigestFile # Large (should) be able to parallize IndexList & DigestFile & the assignment echo "The \"name\" (checksum) for the contents of ${PWD} is ${IDX_DIG[0]}" declare -a FILE_LOC LocateFile ${PWD} FILE_LOC ListArray FILE_LOC exit 0
Stephane Chazelas демонстрирует возможность объектно ориентированного подхода к программированию в Bash-сценариях.
Пример A-22. Объектно ориентированная база данных
#!/bin/bash # obj-oriented.sh: Объектно ориентрованный подход к программированию в сценариях. # Автор: Stephane Chazelas. person.new() # Очень похоже на объявление класса в C++. { local obj_name=$1 name=$2 firstname=$3 birthdate=$4 eval "$obj_name.set_name() { eval \"$obj_name.get_name() { echo \$1 }\" }" eval "$obj_name.set_firstname() { eval \"$obj_name.get_firstname() { echo \$1 }\" }" eval "$obj_name.set_birthdate() { eval \"$obj_name.get_birthdate() { echo \$1 }\" eval \"$obj_name.show_birthdate() { echo \$(date -d \"1/1/1970 0:0:\$1 GMT\") }\" eval \"$obj_name.get_age() { echo \$(( (\$(date +%s) - \$1) / 3600 / 24 / 365 )) }\" }" $obj_name.set_name $name $obj_name.set_firstname $firstname $obj_name.set_birthdate $birthdate } echo person.new self Bozeman Bozo 101272413 # Создается экземпляр класса "person.new" (фактически -- вызов функции с аргументами). self.get_firstname # Bozo self.get_name # Bozeman self.get_age # 28 self.get_birthdate # 101272413 self.show_birthdate # Sat Mar 17 20:13:33 MST 1973 echo # typeset -f # чтобы просмотреть перечень созданных функций. exit 0
Пример сценария, имеющего практическую ценность: установка и монтирование USB-устройств "жесткиз дисков."
Пример A-23. Монтирование USB-устройств флэш-памяти.
#!/bin/bash # ==> usb.sh # ==> Сценарий монтирует и устанавливает USB-устройства флэш-памяти. # ==> Запускается с правами root во время загрузки системы (см. ниже). # Этот сценарий распространяется на условиях GNU GPL license версии 2 или выше. # Полный текст лицензии вы найдете на http://www.gnu.org/. # # Часть кода заимствована из сценария usb-mount, автор Michael Hamilton (LGPL) #+ см. http://users.actrix.co.nz/michael/usbmount.html # # УСТАНОВКА # --------- # Поместите сценарий в каталог /etc/hotplug/usb/diskonkey. # Затем скопируйте все описания устройств usb-storage из /etc/hotplug/usb.distmap #+ в /etc/hotplug/usb.usermap, заменяя "usb-storage" на "diskonkey". # # TODO # ---- # Обслуживание более одного устройства diskonkey (например /dev/diskonkey1 #+ и /mnt/diskonkey1), и т.д. Наибольшая проблема здесь состоит в работе с #+ devlabel. # # АВТОР и ПОДДЕРЖКА # ------------------- # Konstantin Riabitsev, <icon linux duke edu>. # Сообщения об обнаруженных ошибках отправляйте мне на электронный адрес. # # ==> Комментарии добавленные автором книги. SYMLINKDEV=/dev/diskonkey MOUNTPOINT=/mnt/diskonkey DEVLABEL=/sbin/devlabel DEVLABELCONFIG=/etc/sysconfig/devlabel IAM=$0 ## # Функция заимствована из usb-mount. # function allAttachedScsiUsb { find /proc/scsi/ -path '/proc/scsi/usb-storage*' -type f | xargs grep -l 'Attached: Yes' } function scsiDevFromScsiUsb { echo $1 | awk -F"[-/]" '{ n=$(NF-1); print "/dev/sd" substr("abcdefghijklmnopqrstuvwxyz", n+1, 1) }' } if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]; then ## # заимствовано из usbcam. # if [ -f /var/run/console.lock ]; then CONSOLEOWNER=`cat /var/run/console.lock` elif [ -f /var/lock/console.lock ]; then CONSOLEOWNER=`cat /var/lock/console.lock` else CONSOLEOWNER= fi for procEntry in $(allAttachedScsiUsb); do scsiDev=$(scsiDevFromScsiUsb $procEntry) # Это ошибка в usb-storage? # Разделы не появляются в /proc/partitions до тех пор #+ пока к ним не было хотябы одного обращения. /sbin/fdisk -l $scsiDev >/dev/null ## # Большинство устройств имеет информацию о разделах, например #+ /dev/sd?1. Однако, некоторые устройства не имеют разделов #+ выделяя под данные весь объем памяти. Здесь делается #+ попытка определить такие устройства, т.е. имеется ли /dev/sd?1 #+ или нет. # if grep -q `basename $scsiDev`1 /proc/partitions; then part="$scsiDev""1" else part=$scsiDev fi ## # Изменение владельца устройства, чтобы пользователь #+ мог смонтировать его. # if [ ! -z "$CONSOLEOWNER" ]; then chown $CONSOLEOWNER:disk $part fi ## # Здесь проверяется -- зарегистрирован ли UUID с помощью devlabel. # Если нет, то устройство добавляется в список. # prodid=`$DEVLABEL printid -d $part` if ! grep -q $prodid $DEVLABELCONFIG; then # скрестим пальцы, надеюсь это поможет $DEVLABEL add -d $part -s $SYMLINKDEV 2>/dev/null fi ## # Если точка монтирования отсутствует, то она создается. # if [ ! -e $MOUNTPOINT ]; then mkdir -p $MOUNTPOINT fi ## # Позаботиться о создании соответствующей записи в /etc/fstab. # if ! grep -q "^$SYMLINKDEV" /etc/fstab; then # Добавить запись в fstab echo -e \ "$SYMLINKDEV\t\t$MOUNTPOINT\t\tauto\tnoauto,owner,kudzu 0 0" \ >> /etc/fstab fi done if [ ! -z "$REMOVER" ]; then ## # Обеспечить запуск сценария при извлечении устройства. # mkdir -p `dirname $REMOVER` ln -s $IAM $REMOVER fi elif [ "${ACTION}" = "remove" ]; then ## # Если устройство смонтировано -- отмонтировать его. # if grep -q "$MOUNTPOINT" /etc/mtab; then # отмонтировать umount -l $MOUNTPOINT fi ## # Удалить запись из /etc/fstab, если она там имеется. # if grep -q "^$SYMLINKDEV" /etc/fstab; then grep -v "^$SYMLINKDEV" /etc/fstab > /etc/.fstab.new mv -f /etc/.fstab.new /etc/fstab fi fi
А этот сценарий согреет душу и сердце и веб-мастера, и владельца сайта: он создает резервную копию файлов журналов WEB-сервера.
Пример A-24. Резервное копирование файлов журналов
#!/bin/bash # archiveweblogs.sh v1.0 # Troy Engel <tengel@fluid.com> # С небольшими изменениями, внесенными автором документа. # Используется с разрешения автора. # # Этот сценарий выполняет резервное копирование #+ файлов журналов из стандартного каталога установки RedHat/Apache. # Вставляет дату создания копии в имя файла-архива, #+ сжимает (bzip), и помещает сжатые файлы в заданный каталог. # # Запускается из crontab ночью в 0 часов, #+ так как bzip2 потребляет значительную часть ресурсов процессора, #+ если журналы достаточно велики: # 0 2 * * * /opt/sbin/archiveweblogs.sh PROBLEM=66 # Здесь укажите ваш каталог для архивации. BKP_DIR=/opt/backups/weblogs # Настройки Apache/RedHat по-умолчанию LOG_DAYS="4 3 2 1" LOG_DIR=/var/log/httpd LOG_FILES="access_log error_log" # Расположение программ в RedHat по-умолчанию LS=/bin/ls MV=/bin/mv ID=/usr/bin/id CUT=/bin/cut COL=/usr/bin/column BZ2=/usr/bin/bzip2 # Проверка прав пользователя? USER=`$ID -u` if [ "X$USER" != "X0" ]; then echo "PANIC: Только root может запускать этот сценарий!" exit $PROBLEM fi # Каталог для резервной копии существует и доступен на запись? if [ ! -x $BKP_DIR ]; then echo "PANIC: Каталог $BKP_DIR не найден или не доступен для записи!" exit $PROBLEM fi # Переместить, переименовать и сжать for logday in $LOG_DAYS; do for logfile in $LOG_FILES; do MYFILE="$LOG_DIR/$logfile.$logday" if [ -w $MYFILE ]; then DTS=`$LS -lgo --time-style=+%Y%m%d $MYFILE | $COL -t | $CUT -d ' ' -f7` $MV $MYFILE $BKP_DIR/$logfile.$DTS $BZ2 $BKP_DIR/$logfile.$DTS else # Выдать сообщение об ошибке, если файл недоступен на запись. if [ -f $MYFILE ]; then echo "ERROR: $MYFILE not writable. Skipping." fi fi done done exit 0
Как предотвратить интерпретацию строки в сценарии?
Пример A-25. Предотвращение интерпретации строк символов
#! /bin/bash # protect_literal.sh # set -vx :<<-'_Protect_Literal_String_Doc' Copyright (c) Michael S. Zick, 2003; All Rights Reserved Ограничения: Допускается использовать без каких либо ограничений в любой форме. Гарантии: Никаких Издание: $ID$ Этот встроенный документ Bash отправит на устройство '/dev/null'. (Раскомментарьте команду set, стоящую выше, чтобы убедиться в этом.) Удалите первую строку (Sha-Bang), если вы собираетесь использовать этот сценарий в качестве библиотеки. Не забудьте при этом закомментарить примеры использования процедур (там где это указано). Порядок использования: _protect_literal_str 'Whatever string meets your ${fancy}' Какая бы строка ни была передана функции, она просто будет выведена на stdout, включая "строгие" кавычки. $(_protect_literal_str 'Whatever string meets your ${fancy}') как правосторонняя часть операции присваивания. Назначение: В операциях присваивания, предотвращают дополнительную интерпретацию содержимого строки, путем добавления "строгих" кавычек. Примечание: Имена функций (_*) выбраны таким образом, чтобы избежать конфликтов имен, при подключении данного сценария к пользовательским сценариям, в качестве библиотеки. _Protect_Literal_String_Doc _protect_literal_str() { # Выберем неиспользуемый, непечатный символ в качестве разделителя полей для IFS. # В этом нет необходимости, но делается это для демогстрации # того, что разделитель полей игнорируется. local IFS=$'\x1B' # Символ \ESC # Заключим Все-Элементы в "строгие" кавычки. local tmp=$'\x27'$@$'\x27' local len=${#tmp} # Исключительно для демонстрации. echo $tmp, длина:$len. # Вывод строки и дополнительной информации. } # Версия с более коротким именем. _pls() { local IFS=$'x1B' # Символ \ESC (не обязательно) echo $'\x27'$@$'\x27' # Заключить в "строгие" кавычки } # :<<-'_Protect_Literal_String_Test' # # # Раскомментарьте вышестоящую строку, чтобы запретить исполнение нижеследующего кода. # # # # Посмотрим как выглядит простой вывод. echo echo "- - Тест #1 - -" _protect_literal_str 'Hello $user' _protect_literal_str 'Hello "${username}"' echo # В результате должно получиться: # - - Тест #1 - - # 'Hello $user', длина: 13. # 'Hello "${username}"', длина: 21. # Собственно получили то, что и ожидали, тогда в чем проблема? # Проблема скрыта внутри Bash, в порядке выполнения операций. # Она проявляется, когда функции учавствуют в операциях присваивания. # Объявим массив тестовых значений. declare -a arrayZ # Запишем в массив элементы с разного рода кавычками и экранирующими символами. arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" ) # Теперь выведем массив на экран и посмотрим, что там лежит. echo "- - Тест #2 - -" for (( i=0 ; i<${#arrayZ[*]} ; i++ )) do echo Элемент $i: ${arrayZ[$i]}, длина: ${#arrayZ[$i]}. done echo # В результате должно получиться: # - - Тест #2 - - # Элемент 0: zero, длина: 4. # Маркировочный (ничем не примечательный) элемент # Элемент 1: 'Hello ${Me}', длина: 13. # Результат "$(_pls '...' )" # Элемент 2: Hello ${You}, длина: 12. # Кавычки исчезли # Элемент 3: \'Pass: \', длина: 10. # ${pw} -- была интерпретирована, # # а на ее место подставлена пустая строка # Выполним присвоение одного массива другому. declare -a array2=( ${arrayZ[@]} ) # И выведем его содержимое. echo "- - Тест #3 - -" for (( i=0 ; i<${#array2[*]} ; i++ )) do echo Элемент $i: ${array2[$i]}, длина: ${#array2[$i]}. done echo # В результате должно получиться: # - - Тест #3 - - # Элемент 0: zero, длина: 4. # Наш маркер. # Элемент 1: Hello ${Me}, длина: 11. # Вполне предсказуемый результат. # Элемент 2: Hello, длина: 5. # ${You} -- была интерпретирована. # # а на ее место подставлена пустая строка # Элемент 3: 'Pass:, длина: 6. # Элемент был "разбит" на два по пробелу. # Элемент 4: ', длина: 1. # Завершающая кавычка попала в отдельный элемент. # В Элементе 1 были удалены начальная и завершающая "строгие" кавычки. # Хотя здесь и не показано, но начальные и звершающие пробелы также удаляются. # Теперь, когда содержимое строки установлено, Bash всегда, внутри, будет # "строго" окавычивать содержимое строки, на протяжении всей операции # Зачем это нужно? # В нашем случае, в конструкции "$(_pls 'Hello ${Me}')": # " ... " -> Требуется интерпретация (экспансия), кавычки удаляются. # $( ... ) -> Замещается результатом выполнения ..., пустая строка. # _pls ' ... ' -> вызов функции со строковым аргументом, кавычки удаляются. # Возвращаемый результат включает в себя "строгие" кавычки; НО обработка команды #+ уже была завершена выше, так что теперь они становятся частью присваиваемого #+ значения. # # Таким образом, ${Me} оказывается частью результата #+ и сохраняется в первоначальном виде # (До тех пор, пока явно не будет указано на необходимость ее интерпретации). # Дополнительно: Взгляните, что произойдет, если в этих функциях #+ "строгие" кавычки ($'\x27') заменить на "мягкие" ($'\x22'). # Интересный результат получится если вообще убрать кавычки. # _Protect_Literal_String_Test # # # Раскомментарьте вышестоящую строку, чтобы запретить исполнение вышестоящего кода. # # # exit 0
А что делать, если необходимо заставить командный интерпретатор интерпретировать строку?
Пример A-26. Принудительная интерпретация строк
#! /bin/bash # unprotect_literal.sh # set -vx :<<-'_UnProtect_Literal_String_Doc' Copyright (c) Michael S. Zick, 2003; All Rights Reserved Ограничения: Допускается использовать без каких либо ограничений в любой форме. Гарантии: Никаких Издание: $ID$ Этот встроенный документ Bash отправит на устройство '/dev/null'. (Раскомментарьте команду set, стоящую выше, чтобы убедиться в этом.) Удалите первую строку (Sha-Bang), если вы собираетесь использовать этот сценарий в качестве библиотеки. Не забудьте при этом закомментарить примеры использования процедур (там где это указано). Порядок использования: Противоположная по смыслу функции "$(_pls 'Literal String')". (см. пример protect_literal.sh) StringVar=$(_upls ProtectedSringVariable) Назначение: Выполняет подстановку (интерпретацию) строк в операциях присваивания. Примечание: Имена функций (_*) выбраны таким образом, чтобы избежать конфликтов имен, при подключении данного сценария к пользовательским сценариям, в качестве библиотеки. _UnProtect_Literal_String_Doc _upls() { local IFS=$'x1B' # Символ \ESC character (не обязательно) eval echo $@ # Принудительная интерпретация. } # :<<-'_UnProtect_Literal_String_Test' # # # Раскомментарьте вышестоящую строку, чтобы запретить исполнение нижеследующего кода. # # # _pls() { local IFS=$'x1B' # Символ \ESC character (не обязательно) echo $'\x27'$@$'\x27' # Заключить в "строгие" кавычки } # Объявим массив тестовых значений. declare -a arrayZ # Запишем в массив элементы с разного рода кавычками и экранирующими символами. arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" ) # Выполним присвоение одного массива другому. declare -a array2=( ${arrayZ[@]} ) # В результате должно получиться: # - - Тест #3 - - # Элемент 0: zero, длина: 4. # Наш маркер. # Элемент 1: Hello ${Me}, длина: 11. # Вполне предсказуемый результат. # Элемент 2: Hello, длина: 5. # ${You} -- была интерпретирована. # # а на ее место подставлена пустая строка # Элемент 3: 'Pass:, длина: 6. # Элемент был "разбит" на два по пробелу. # Элемент 4: ', длина: 1. # Завершающая кавычка попала в отдельный элемент. # set -vx # Инициализируем переменную 'Me' каким нибудь значением #+ чтобы увидеть последующую ее интерпретацию. Me="to the array guy." # Присвоим результат принудительной интерпретации другой переменной. newVar=$(_upls ${array2[1]}) # И посмотрим, что получилось. echo $newVar # Так ли необходима эта функция? newerVar=$(eval echo ${array2[1]}) echo $newerVar # Оказывается совсем не обязательно, но функция _upls делает сценарий #+ более понятным. # Она поможет в том случае, если вдруг забудется смысл конструкции #+ $(eval echo ... ). # Что произойдет, если часть строки, #+ которая требует дополнительной интерпретации, окажется неинициализированной? unset Me newestVar=$(_upls ${array2[1]}) echo $newestVar # Просто и со вкусом! Никаких сообщений, никаких предупреждений, никаких ошибок. # Для чего все это? # Одна из основных проблем в Bash -- невозможность записать в переменные #+ некоторые последовательности символов # # Теперь эта проблема разрешается восемью строчками кода #+ (и четырьмя страницами описания). # Где это можно использовать? # Для динамической генерации содержимого Web-страниц, #+ в виде массивов строк. # Содержимое таких страниц может генерироваться командой Bash 'eval' # Я совсем не призываю заменить PHP, просто высказал интересную мысль. ### # _UnProtect_Literal_String_Test # # # Раскомментарьте вышестоящую строку, чтобы запретить исполнение вышестоящего кода. # # # exit 0
В завершение этого раздела, бросим краткий взгляд назад ... .
Пример A-27. Повторение основ
#!/bin/bash # basics-reviewed.bash # Расширение файла == *.bash == сценарий, использующий особенности Bash # Copyright (c) Michael S. Zick, 2003; All rights reserved. # License: Use in any form, for any purpose. # Издание: $ID$ # # Правка, с целью улучшения оформления, выполнена автором книги. # ("Advanced Bash Scripting Guide") # Этот сценарий тестировался в Bash версий 2.04, 2.05a и 2.05b. # Он может не работать в более ранних версиях. # Этот сценарий умышленно генерирует ошибку #+ "command not found". См. строку 394. # Ведущий разработчик Bash, Chet Ramey, обязался исправить эту проблему #+ в следующих версиях Bash. ###-------------------------------------------### ### Сценарий выводит много информации на ### ###+ экран, поэтому запускайте его в конвейере### ###+ с командой more ### ### ### ### Кроме того, вы можете перенаправить ### ###+ вывод в файл, с целью последующего ### ###+ изучения ### ###-------------------------------------------### # Большая часть из приводимых здесь моментов описывается #+ в вышеупомянутой книге "Advanced Bash Scripting Guide." # Этот сценарий, по сути можно расценивать как своего рода презентацию. # -- msz # Переменные не типизированы, если не указано обратное. # Соглашения по именованию переменных. # Имена переменных не должны начинаться с цифровых символов. # Имена дескрипторов файлов (как например: 2>&1) #+ должны содержать ТОЛЬКО цифры. # Параметры и элементы массивов Bash -- пронумерованы. # (Параметры, в этом смысле, очень похожи на массивы.) # Переменные в Bash могут иметь неопределенное значение. unset VarNull # Переменные в Bash могут быть опеределены, но содержать "пустое" (null) значение. VarEmpty='' # Переменные могут быть определены и содержать некоторое, непустое значение VarSomething='Literal' # Переменные могут хранить: # * Целое 32-битовое число со знаком # * Строку символов # Переменные могут быть массивом. # Строки могут содержать пробелы и интерпретироваться #+ как вызов функции с аргументами. # Имена переменных и имена функций #+ находятся в разных пространствах имен (namespaces). # Переменные могут быть объявлены массивами как явно, так и неявно #+ в зависимости от семантики операции присваивания. # Явно: declare -a ArrayVar # Команда echo -- внутренняя команда. echo $VarSomething # Команда printf -- внутренняя команда. # здесь %s интерпретируется как строка формата printf %s $VarSomething # Перевод строки отсутствует, ничего не выводится. echo # Выводит только перевод строки. # Интерпретатор Bash различает отдельные слова по символу пробела между ними. # Поэтому наличие или отсутствие пробела -- очень важный признак. # (В общем случае это так, но есть и исключения из правил.) # Символ "доллара" ($) интерпретируется как: Content-Of (содержимое для ...). # Расширенный синтаксис, с использованием символа "доллара": echo ${VarSomething} # Здесь, конструкция ${ ... }, позволяет указывать #+ не только имена переменных. # Как правило, запись $VarSomething #+ всегда может быть представлена в виде : ${VarSomething}. # Чтобы увидеть следующие операции в действии -- вызовите сценарий #+ с несколькими входными аргументами. # За пределами двойных кавычек, специальные символы @ и * #+ имеют идентичное назначение. # Может произноситься как: All-Elements-Of (Все-Элементы-Для). # Если имя переменной не указано, то эти специальные символы #+ применяются к предопределенным переменным Bash. echo $* # Все входные параметры сценария или функции echo ${*} # То же самое # Bash запрещает подстановку имен файлов в вышеупомянутых конструкциях. # Ссылка на Все-Элементы-Для echo $@ # то же самое, что и выше echo ${@} # то же самое # Внутри двойных кавычек, поведение символов @ и * #+ зависит от установки переменной IFS (Input Field Separator -- Разделитель Полей). # Ссылка на Все-Элементы-Для, внутри двойных кавычек, работает точно так же. # Обращение к имени переменной означает получение #+ всех элементов (символов) строки. # Для обращения к отдельным элементам (символам) строки, #+ может использоваться расширенный синтаксис (см. ниже). # Обращение к имени переменной-массива в Bash #+ означает обращение к нулевому элементу массива, #+ а НЕ к ПЕРВОМУ ОПРЕДЕЛЕННОМУ или к ПЕРВОМУ ИНИЦИАЛИЗИРОВАННОМУ элементу. # Для обращения к другим элементам массива, необходимо явное указание элемента, #+ это означает, что ДОЛЖЕН использоваться расширенный синтаксис. # В общем случае: ${name[subscript]}. # Для строк может использоваться такая форма записи: ${name:subscript}, #+ а также для обращения к нулевому элементу массива. # Массивы в Bash реализованы как связанные списки, #+ а не как фиксированная область памяти, что характерно для некоторых #+ языков программирования. # Характеристики массивов в Bash: # ------------------------------- # Если не определено иначе, индексация массивов в Bash #+ начинается с нуля: [0] # Это называется "индексация с нуля". ### # Если не указано иначе, массивы в Bash являются упакованными #+ (т.е. массивы просто не содержат элементов с отсутствующими индексами). ### # Отрицательные индексы недопустимы. ### # Элементы массива не обязательно должны быть одного и того же типа. ### # Элементы массива могут быть неинициализированы. # Т.е. массив может быть "разреженным" ### # Элементы массива могут быть инициализированы пустым значением. ### # Элементы массива могут содержать: # * Целое 32-битовое число со знаком # * Строку # * Форматированную строку, которая выглядит # + как вызов к функции с параметрами ### # Инициализированные элементы массива могут быть деинициализированы (unset). # Т.е. массив может быть переупакован так, # + что он не будет содержать элемента с данным индексом. ### # К массиву могут добавляться дополнительные элементы, #+ не определенные ранее. ### # По этим причинам я называю массивы Bash -- "Bash-массивами" ("Bash-Arrays"). # # -- msz # Демонстрация вышесказанного -- инициализируем ранее объявленный массив ArrayVar #+ как "разреженный" массив. # (команда 'unset ... ' используется для демонстрации вышесказанного.) unset ArrayVar[0] # Для демонстрации ArrayVar[1]=one # Без кавычек ArrayVar[2]='' # Инициализация пустым значением unset ArrayVar[3] # Для демонстрации ArrayVar[4]='four' # В кавычках # Строка формата %q -- трактуется как: Quoted-Respecting-IFS-Rules #+ (в соответствии с установками IFS). echo echo '- - Вне двойных кавычек - -' ### printf %q ${ArrayVar[*]} # Шаблон "Все-Элементы-Для" echo echo 'команда echo:'${ArrayVar[*]} ### printf %q ${ArrayVar[@]} # "Все-Элементы-Для" echo echo 'команда echo:'${ArrayVar[@]} # Двойные кавычки используются для разрешения операции подстановки #+ внутри кавычек. # Существует пять самых распространенных случаев, #+ зависящих от установок переменной IFS. echo echo '- - В двойных кавычках - По-умолчанию IFS содержит пробел-табуляцию-перевод строки- -' IFS=$'\x20'$'\x09'$'\x0A' # Три байта, #+ и именно в таком порядке. printf %q "${ArrayVar[*]}" # Шаблон "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[*]}" ### printf %q "${ArrayVar[@]}" # "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[@]}" echo echo '- - В двойных кавычках - Первый символ в IFS: ^ - -' # Любой печатаемый, непробельный символ, дает тот же эффект. IFS='^'$IFS # ^ + пробел табуляция перевод строки ### printf %q "${ArrayVar[*]}" # Шаблон "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[*]}" ### printf %q "${ArrayVar[@]}" # "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[@]}" echo echo '- - В двойных кавычках - IFS не содержит пробела - -' IFS='^:%!' ### printf %q "${ArrayVar[*]}" # Шаблон "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[*]}" ### printf %q "${ArrayVar[@]}" # "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[@]}" echo echo '- - В двойных кавычках - переменная IFS пуста - -' IFS='' ### printf %q "${ArrayVar[*]}" # Шаблон "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[*]}" ### printf %q "${ArrayVar[@]}" # "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[@]}" echo echo '- - В двойных кавычках - переменная IFS не определена - -' unset IFS ### printf %q "${ArrayVar[*]}" # Шаблон "Все-Элементы-Для" All-Elements-Of echo echo 'команда echo:'"${ArrayVar[*]}" ### printf %q "${ArrayVar[@]}" # "Все-Элементы-Для" echo echo 'команда echo:'"${ArrayVar[@]}" # Вернем переменную IFS в первоначальное состояние, # записав в нее значение по-умолчанию. IFS=$'\x20'$'\x09'$'\x0A' # точно в таком порядке. # Интерпретация результатов, полученных выше: # Форма ввыода по шаблону "Все-Элементы-Для" зависит от содержимого переменной IFS. ### # Простой вывод "Всех-Элементов-Для" не зависит от содержимого переменной IFS. ### # Обратите внимание на различия, имеющиеся в выводе #+ от команд echo и printf с форматом %q. # Давайте вспомним: # Параметры очень похожи на массивы и имеют сходное поведение. ### # Примеры выше показывают, что для того, чтобы вывести разреженный #+ массив полностью, необходимо писать дополнительный код. ### # Длина строки равна количеству ненулевых элементов (символов): echo echo '- - Имя переменной употребляется вне кавычек - -' echo 'Количество ненулевых символов: '${#VarSomething}'.' # test='Lit'$'\x00''eral' # $'\x00' -- нулевой (null) символ. # echo ${#test} # Что получится? # Длина массива равна количеству инициализированных элементов, #+ включая элементы, инициализированные пустыми значениями. echo echo 'Количество инициализированных элементов в массиве: '${#ArrayVar[@]}'.' # Это НЕ максимальный индекс массива (4). # Это НЕ ширина диапазона (1 . . 4 включительно). # Это длина связного списка. ### # Максимальный номер индекса массива и диапазон индексов #+ могут быть найдены, но для этого потребуется дополнительный код. # Длина строки равна количеству ненулевых элементов (символов): echo echo '- - Имя переменной употребляется в кавычках - -' echo 'Количество непустых символов: '"${#VarSomething}"'.' # Длина массива равна количеству инициализированных элементов, #+ включая элементы, инициализированные пустыми значениями. echo echo 'Количество инициализированных элементов в массиве: '"${#ArrayVar[*]}"'.' # Вывод: Конструкция ${# ... } не производит подстановку. # Совет: # Всегда используйте символ All-Elements-Of (Все-Элементы-Для) #+ если желаете получить результат, не зависящий от содержимого переменной IFS. # Определим простую функцию. # Я включил в имя функции символ подчеркивания #+ чтобы как-то обозначить, что это функция, а не переменная. ### # Bash различает имена функций и переменных, #+ размещая из в различных пространствах имен. ### _simple() { echo -n 'SimpleFunc'$@ # Символ перевода строки в любом случае } #+ будет "съеден". # Конструкция ( ... ) вызывает команду или функцию. # Форма записи $( ... ) произносится как: Result-Of (Результат-Выполнения). # Вызовем функцию _simple echo echo '- - Результат работы функции _simple - -' _simple # Попробуйте передать несколько аргументов. echo # или (_simple) # Попробуйте передать несколько аргументов. echo echo '- Существует ли переменная с таким именем? -' echo $_simple not defined # Нет переменной с таким именем. # Обращение к результату выполнения функции _simple # (будет получено сообщение об ошибке) ### $(_simple) # Генерирует сообщение об ошибке: # line 394: SimpleFunc: command not found # --------------------------------------- echo ### # Причина ошибки вполне очевидна: результат работы функции _simple не есть #+ ни команда Bash, ни имя определенной ранее функции. ### # Этот пример показывает, что вывод от функции _simple подвергается #+ дополнительной интерпретации. ### # Вывод: # Функция может использоваться для генерации команд Bash. # Простая функция, которая выводит команду bash: ### _print() { echo -n 'printf %q '$@ } echo '- - Результат работы функции _print - -' _print parm1 parm2 # Простой вывод -- НЕ команда. echo $(_print parm1 parm2) # Исполняет команду printf %q parm1 parm2 # См. пример с IFS выше #+ на предмет дополнительных возможнойстей. echo $(_print $VarSomething) # Вполне предсказуемый результат. echo # Переменные-функции # ------------------ echo echo '- - Переменные-функции - -' # Переменная может хранить целое число, строку или массив. # Строка может интерпретироваться как вызов функции. # set -vx # Раскомментарьте при желании declare -f funcVar # в пространстве имен функций! funcVar=_print # Записать имя функции. $funcVar parm1 # Аналогично вызову функции _print. echo funcVar=$(_print ) # Результат работы функции. $funcVar # Нет ни ввода, ни вывода. $funcVar $VarSomething # Предсказуемый результат. echo funcVar=$(_print $VarSomething) # Здесь выполняется подстановка #+ значения переменной $VarSomething. $funcVar # Содержимое переменной $VarSomething echo #+ стало частью переменной $funcVar funcVar="$(_print $VarSomething)" # Здесь выполняется подстановка #+ значения переменной $VarSomething. $funcVar # Содержимое переменной $VarSomething echo #+ стало частью переменной $funcVar # Различия в применении или неприменении двойных кавычек #+ объясняются в примере "protect_literal.sh". # В первом случае Bash обрабатывает строку как два отдельных слова, # во втором -- как одно слово в кавычках с пробелом внутри слова. # Отложенная подстановка # ---------------------- echo echo '- - Отложенная подстановка - -' funcVar="$(_print '$VarSomething')" # Подстановка значения переменной не производится. eval $funcVar # Подстановка производится ЗДЕСЬ. echo VarSomething='NewThing' eval $funcVar # Подстановка производится ЗДЕСЬ. echo # Восстановим прежнее значение переменной VarSomething. VarSomething=Literal # В примерах "protect_literal.sh" и "unprotect_literal.sh" #+ вы найдете две функции, которые выполняют отложенную подстановку #+ значений переменных. # ОБЗОР: # ----- # Строки могут рассматриваться как классический массив элементов-символов. # Строковые операции воздействуют на все элементы (символы) строки ### # Запись: ${array_name[@]} представляет все элементы #+ Bash-Массива: array_name. ### # Строковые операции, в расширенном синтаксисе, могут манипулировать #+ всеми элементами массива сразу. ### # Эта способность может рассматриваться как операция For-Each над вектором строк. ### # Параметры подобны массивам. # Различия в параметрах для функции и сценария касаются только параметра #+ ${0}, который никогда не изменяется. ### # Нулевой параметр сценария содержит имя файла сценария ### # Нулевой параметр функции НЕ СОДЕРЖИТ имени функции. # Имя функции хранится в служебной переменной $FUNCNAME. ### echo echo '- - Тест (без изменения содержимого переменной) - -' echo '- неинициализированная переменная -' echo -n ${VarNull-'NotSet'}' ' # NotSet echo ${VarNull} # только перевод строки echo -n ${VarNull:-'NotSet'}' ' # NotSet echo ${VarNull} # только перевод строки echo '- "пустая" переменная -' echo -n ${VarEmpty-'Empty'}' ' # только пробел echo ${VarEmpty} # только перевод строки echo -n ${VarEmpty:-'Empty'}' ' # Empty echo ${VarEmpty} # только перевод строки echo '- непустая переменная -' echo ${VarSomething-'Content'} # Literal echo ${VarSomething:-'Content'} # Literal echo '- Разреженный массив -' echo ${ArrayVar[@]-'not set'} # ASCII-Art time # State Y==yes, N==no # - :- # Unset Y Y ${# ... } == 0 # Empty N Y ${# ... } == 0 # Contents N N ${# ... } > 0 # Либо первая, либо вторая часть операции подстановки параметра по-умолчанию #+ может быть командой или вызовом функции. echo echo '- - Тест 1, не определенная переменная - -' declare -i t _decT() { t=$t-1 } # Для не определенной переменной: t == -1 t=${#VarNull} # t == 0 ${VarNull- _decT } # Вызов функции, теперь t == -1. echo $t # "Пустая" переменная: t == 0 t=${#VarEmpty} # t == 0 ${VarEmpty- _decT } # Функция _decT НЕ вызывается. echo $t # Непустая переменная: t == число непустых символов VarSomething='_simple' # Записать имя функции в переменную. t=${#VarSomething} # ненулевая длина ${VarSomething- _decT } # Вызывается функция _simple. echo $t # Обратите внимание на вывод. # Упражнение: Разберитесь в этом примере. unset t unset _decT VarSomething=Literal echo echo '- - Тест (с изменением содержимого переменной) - -' echo '- Присвоить, если переменная не определена -' echo -n ${VarNull='NotSet'}' ' # NotSet NotSet echo ${VarNull} unset VarNull echo '- Присвоить, если переменная не определена -' echo -n ${VarNull:='NotSet'}' ' # NotSet NotSet echo ${VarNull} unset VarNull echo '- Не присваивать, если переменная пуста -' echo -n ${VarEmpty='Empty'}' ' # только пробел echo ${VarEmpty} VarEmpty='' echo '- Присвоить, если переменная пуста -' echo -n ${VarEmpty:='Empty'}' ' # Empty Empty echo ${VarEmpty} VarEmpty='' echo '- Не изменять, если переменная не пуста -' echo ${VarSomething='Content'} # Literal echo ${VarSomething:='Content'} # Literal # "Разреженные" Bash-Массивы ### # Bash-Массивы не содержат пустых элементов, и индексация их, #+ если не оговорено иное, начинается с нуля. ### # Инициализируем массив ArraySparse как раз тем способом, #+ когда "оговорено иное". Ниже приведен один из вариантов: ### echo declare -a ArraySparse ArraySparse=( [1]=one [2]='' [4]='four' ) # [0]=нет элемента, [2]=пустой элемент, [3]=нет элемента echo '- - Разреженный массив - -' # В двойных кавычках, значение IFS -- по-умолчанию, Все-Элементы-Для IFS=$'\x20'$'\x09'$'\x0A' printf %q "${ArraySparse[*]}" echo # Обратите внимание на отсутствие различий в том, как выводятся отсутствующий #+ и пустой элементы массива. # Оба выводятся как экранированные пробелы. ### # Не упустите из виду и то, что все неопределенные элементы массива, #+ предшествующие первому инициализированному элементу, не выводятся ### # Замечание о таком "поведении" Bash, версий 2.04, 2.05a и 2.05b, #+ было передано разработчикам has been reported #+ и возможно будет изменено в последующих версиях Bash. # Чтобы вывести содержимое такого разреженного массива без изменений #+ требуется некоторое количество усилий. # Вот один из возможных вариантов вывода такого массива: ### # local l=${#ArraySparse[@]} # Количество инициализированных элементов # local f=0 # Количество найденных индексов # local i=0 # текущий индекс ( # Анонимная функция for (( l=${#ArraySparse[@]}, f = 0, i = 0 ; f < l ; i++ )) do # 'if defined then...' ${ArraySparse[$i]+ eval echo '\ ['$i']='${ArraySparse[$i]} ; (( f++ )) } done ) # Важно: # Команда "read -a array_name" начинает заполнять массив #+ array_name с нулевого элемента. # ArraySparse -- не содержит нулевого элемента. ### # Для выполнения операций над разреженными массивами, #+ такими как чтение/запись массива из/в файла #+ программист должен сам создать программный код, который #+ будет удовлетворять его потребности. ### # Упражнение: разберитесь в следующем примере самостоятельно. unset ArraySparse echo echo '- - Замена по условию (замена не производится)- -' echo '- Не изменять если переменная не определена -' echo -n ${VarNull+'NotSet'}' ' echo ${VarNull} unset VarNull echo '- Не изменять если переменная не определена -' echo -n ${VarNull:+'NotSet'}' ' echo ${VarNull} unset VarNull echo '- Изменить если переменная пуста -' echo -n ${VarEmpty+'Empty'}' ' # Empty echo ${VarEmpty} VarEmpty='' echo '- Не изменять если переменная пуста -' echo -n ${VarEmpty:+'Empty'}' ' # Только пробел echo ${VarEmpty} VarEmpty='' echo '- Изменить, если переменная не пуста -' echo -n ${VarSomething+'Content'}' ' # Content Literal echo ${VarSomething} # Вызов функции echo -n ${VarSomething:+ $(_simple) }' ' # SimpleFunc Literal echo ${VarSomething} echo echo '- - Разреженный массив - -' echo ${ArrayVar[@]+'Empty'} # An array of 'Empty'(ies) echo echo '- - Тест 2, неопределенные переменные - -' declare -i t _incT() { t=$t+1 } # Обратите внимание: # Тот же самый тест, что и в случае с разреженными массивами # Неопределенная переменная: t == -1 t=${#VarNull}-1 # t == -1 ${VarNull+ _incT } # Функция не вызывается. echo $t' Переменная не определена' # Пустая переменная: t == 0 t=${#VarEmpty}-1 # t == -1 ${VarEmpty+ _incT } # Вызов функции. echo $t' Переменная пуста' # Переменная не пуста: t == (количество непустых символов) t=${#VarSomething}-1 # количество_непустых_символов минус один ${VarSomething+ _incT } # Вызов функции. echo $t' Переменная не пуста' # Операции над элементами массива # ------------------------------- echo echo '- - Выборка элементов - -' # Строки, массивы и позиционные параметры # Чтобы увидеть работу сценария с позиционными параметрами, #+ вызовите его с несколькими аргументами echo '- Все -' echo ${VarSomething:0} # все не пустые символы echo ${ArrayVar[@]:0} # все не пустые элементы echo ${@:0} # все не пустые параметры; # параметр [0] игнорируется echo echo '- Все после -' echo ${VarSomething:1} # все не пустые символы, стоящие после [0] echo ${ArrayVar[@]:1} # все не пустые элементы, стоящие после [0] echo ${@:2} # все не пустые параметры, стоящие после [1] echo echo '- Диапазон символов -' echo ${VarSomething:4:3} # ral # Три символа, следующие # за символом [3] echo '- Разреженный массив -' echo ${ArrayVar[@]:1:2} # four - Единственный непустой элемент. # Чтобы убедиться в том, что Bash в данной ситуации рассматривает только #+ непустые элементы # printf %q "${ArrayVar[@]:0:3}" # Попробуйте раскомментарить эту строку # Версии Bash 2.04, 2.05a и 2.05b, #+ работают с разреженными массивами не так как ожидается. # # Chet Ramey обещал исправить это в последующих версиях Bash. echo '- Неразреженный массив -' echo ${@:2:2} # За параметром [1] следуют еще два параметра # Простые примеры со строками и массивами строк: stringZ=abcABC123ABCabc arrayZ=( abcabc ABCABC 123123 ABCABC abcabc ) sparseZ=( [1]='abcabc' [3]='ABCABC' [4]='' [5]='123123' ) echo echo ' - - Простая строка - -'$stringZ'- - ' echo ' - - Простой массив - -'${arrayZ[@]}'- - ' echo ' - - Разреженный массив - -'${sparseZ[@]}'- - ' echo ' - [0]==нет элемента, [2]==нет элемента, [4]==пустой элемент - ' echo ' - [1]=abcabc [3]=ABCABC [5]=123123 - ' echo ' - количество инициализированных элементов: '${#sparseZ[@]} echo echo '- - Префиксы - -' echo '- - Шаблон должен совпадать с первым символом строки. - -' echo '- - Шаблон может быть строкой или результатом работы функции. - -' echo # Функция, результатом работы которой является обычная строка _abc() { echo -n 'abc' } echo '- Кратчайший префикс -' echo ${stringZ#123} # Без изменения -- не префикс. echo ${stringZ#$(_abc)} # ABC123ABCabc echo ${arrayZ[@]#abc} # Применяется к каждому элементу массива. # Chet Ramey обещал исправить в последующих версиях Bash. # echo ${sparseZ[@]#abc} # В версии 2.05b -- core dumps. # -Это было бы здорово- First-Subscript-Of (Первый-Индекс-Массива) # echo ${#sparseZ[@]#*} # line 805: ${#sparseZ[@]#*}: bad substitution. echo echo '- Наибольший префикс -' echo ${stringZ##1*3} # Без изменения -- не префикс. echo ${stringZ##a*C} # abc echo ${arrayZ[@]##a*c} # ABCABC 123123 ABCABC # Chet Ramey обещал исправить в последующих версиях Bash. # echo ${sparseZ[@]##a*c} # В версии 2.05b -- core dumps. echo echo '- - Суффиксы - -' echo '- - Шаблон должен совпадать с последним символом строки. - -' echo '- - Шаблон может быть строкой или результатом работы функции. - -' echo echo '- Кратчайший суффикс -' echo ${stringZ%1*3} # Без изменения -- не суффикс. echo ${stringZ%$(_abc)} # abcABC123ABC echo ${arrayZ[@]%abc} # Применяется к каждому элементу массива. # Chet Ramey обещал исправить в последующих версиях Bash. # echo ${sparseZ[@]%abc} # В версии 2.05b -- core dumps. # -Это было бы здорово- Last-Subscript-Of (Последний-Индекс-Массива) # echo ${#sparseZ[@]%*} # line 830: ${#sparseZ[@]%*}: bad substitution echo echo '- Наибольший суффикс -' echo ${stringZ%%1*3} # Без изменения -- не суффикс. echo ${stringZ%%b*c} # a echo ${arrayZ[@]%%b*c} # a ABCABC 123123 ABCABC a # Chet Ramey обещал исправить в последующих версиях Bash. # echo ${sparseZ[@]%%b*c} # В версии 2.05b -- core dumps. echo echo '- - Замена подстроки - -' echo '- - Подстрока может находиться в любом месте в строке. - -' echo '- - Первый описатель задает шаблон поиска - -' echo '- - Шаблон может быть строкой или результатом работы функции. - -' echo '- - Второй описатель может быть строкой или результатом работы функции. - -' echo '- - Второй описатель может быть опущен. В результате получится:' echo ' Заменить-Ничем (Удалить) - -' echo # Функция, результатом работы которой является обычная строка _123() { echo -n '123' } echo '- Замена первого вхождения -' echo ${stringZ/$(_123)/999} # Подстрока 123 заменена на 999). echo ${stringZ/ABC/xyz} # xyzABC123ABCabc echo ${arrayZ[@]/ABC/xyz} # Применяется ко всем элементам массива. echo ${sparseZ[@]/ABC/xyz} # Работает так как и ожидается. echo echo '- Удаление первого вхождения -' echo ${stringZ/$(_123)/} echo ${stringZ/ABC/} echo ${arrayZ[@]/ABC/} echo ${sparseZ[@]/ABC/} # Замещающий элемент необязательно должен быть строкой, #+ допускается употреблять результат функции. # Это применимо ко всем формам замены. echo echo '- Замена первого вхождения результатом работы функции -' echo ${stringZ/$(_123)/$(_simple)} # Работает так как и ожидается. echo ${arrayZ[@]/ca/$(_simple)} # Применяется ко всем элементам массива. echo ${sparseZ[@]/ca/$(_simple)} # Работает так как и ожидается. echo echo '- Замена всех вхождений -' echo ${stringZ//[b2]/X} # все символы b и 2 заменяются символом X echo ${stringZ//abc/xyz} # xyzABC123ABCxyz echo ${arrayZ[@]//abc/xyz} # Применяется ко всем элементам массива. echo ${sparseZ[@]//abc/xyz} # Работает так как и ожидается. echo echo '- Удаление всех вхождений -' echo ${stringZ//[b2]/} echo ${stringZ//abc/} echo ${arrayZ[@]//abc/} echo ${sparseZ[@]//abc/} echo echo '- - Замена префикса - -' echo '- - Шаблон должен совпадать с первым символом строки. - -' echo echo '- - Замена префикса - -' echo ${stringZ/#[b2]/X} # Без изменения -- не префикс. echo ${stringZ/#$(_abc)/XYZ} # XYZABC123ABCabc echo ${arrayZ[@]/#abc/XYZ} # Применяется ко всем элементам массива. echo ${sparseZ[@]/#abc/XYZ} # Работает так как и ожидается. echo echo '- Удаление префикса -' echo ${stringZ/#[b2]/} echo ${stringZ/#$(_abc)/} echo ${arrayZ[@]/#abc/} echo ${sparseZ[@]/#abc/} echo echo '- - Замена суффикса - -' echo '- - Шаблон должен совпадать с последним символом строки. - -' echo echo '- - Замена суффикса - -' echo ${stringZ/%[b2]/X} # Без изменения -- не суффикс. echo ${stringZ/%$(_abc)/XYZ} # abcABC123ABCXYZ echo ${arrayZ[@]/%abc/XYZ} # Применяется ко всем элементам массива. echo ${sparseZ[@]/%abc/XYZ} # Работает так как и ожидается. echo echo '- Удаление суффикса -' echo ${stringZ/%[b2]/} echo ${stringZ/%$(_abc)/} echo ${arrayZ[@]/%abc/} echo ${sparseZ[@]/%abc/} echo echo '- - Специальный случай -- пустой шаблон поиска - -' echo echo '- Префикс -' # пустой шаблон означает простую вставку в начало строки echo ${stringZ/#/NEW} # NEWabcABC123ABCabc echo ${arrayZ[@]/#/NEW} # Применяется ко всем элементам массива. echo ${sparseZ[@]/#/NEW} # И к неинициализированным элементам тоже echo echo '- Суффикс -' # пустой шаблон означает простое добавление в конец строки echo ${stringZ/%/NEW} # abcABC123ABCabcNEW echo ${arrayZ[@]/%/NEW} # Применяется ко всем элементам массива. echo ${sparseZ[@]/%/NEW} # И к неинициализированным элементам тоже echo echo '- - Специальный случай оператора For-Each - -' echo '- - - - This is a nice-to-have dream - - - -' echo _GenFunc() { echo -n ${0} # Только как иллюстрация. # Фактически здесь может стоять любое другое выражение. } # Все вхождения, совпадающие с шаблоном "*" # В настоящее время, шаблон //*/ не совпадает с пустыми #+ и неинициализированными элементами. # В то время как шаблоны /#/ и /%/ совпадают с пустыми и не совпадают #+ с неинициализированными элементами. echo ${sparseZ[@]//*/$(_GenFunc)} exit 0
Назад | В начало документа | Вперед |
Библиография | Справочная информация |